1
0
Fork 0
mirror of synced 2024-10-03 19:53:33 +13:00

Merge pull request #6023 from appwrite/feat-mailgun-provider

Feat provider controllers
This commit is contained in:
Jake Barnby 2023-10-12 21:41:21 +13:00 committed by GitHub
commit 736b76cbfa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 3113 additions and 197 deletions

11
.env
View file

@ -98,4 +98,13 @@ _APP_VCS_GITHUB_CLIENT_SECRET=
_APP_VCS_GITHUB_WEBHOOK_SECRET=
_APP_MIGRATIONS_FIREBASE_CLIENT_ID=
_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=
_APP_ASSISTANT_OPENAI_API_KEY=
_APP_ASSISTANT_OPENAI_API_KEY=
_APP_MESSAGE_SMS_PROVIDER_MSG91_SENDER_ID=
_APP_MESSAGE_SMS_PROVIDER_MSG91_AUTH_KEY=
_APP_MESSAGE_SMS_PROVIDER_MSG91_FROM=
_APP_MESSAGE_SMS_PROVIDER_MSG91_TO=
_APP_MESSAGE_SMS_PROVIDER_MAILGUN_API_KEY=
_APP_MESSAGE_SMS_PROVIDER_MAILGUN_DOMAIN=
_APP_MESSAGE_SMS_PROVIDER_MAILGUN_FROM=
_APP_MESSAGE_SMS_PROVIDER_MAILGUN_RECEIVER_EMAIL=
_APP_MESSAGE_SMS_PROVIDER_MAILGUN_IS_EU_REGION=

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ debug/
app/sdks
dev/yasd_init.php
.phpunit.result.cache
Makefile

View file

@ -2,7 +2,7 @@
<br />
<p align="center">
<a href="https://appwrite.io" target="_blank"><img width="260" height="39" src="https://appwrite.io/images/appwrite.svg" alt="Appwrite Logo"></a>
<a href="https://appwrite.io" target="_blank"><img src="./public/images/banner.png" alt="Appwrite Logo"></a>
<br />
<br />
<b>适用于[Flutter/Vue/Angular/React/iOS/Android/* 等等平台 *]的完整后端服务</b>

View file

@ -1449,6 +1449,17 @@ $commonCollections = [
'array' => false,
'filters' => ['json', 'encrypt'],
],
[
'$id' => ID::custom('options'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => [],
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
@ -1534,6 +1545,28 @@ $commonCollections = [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('description'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => 'processing',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('data'),
'type' => Database::VAR_STRING,
@ -1567,6 +1600,17 @@ $commonCollections = [
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('deliveredAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('deliveryErrors'),
'type' => Database::VAR_STRING,
@ -1589,17 +1633,6 @@ $commonCollections = [
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('delivered'),
'type' => Database::VAR_BOOLEAN,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => false,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
@ -1912,6 +1945,20 @@ $commonCollections = [
'attributes' => ['providerInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_identifier'),
'type' => Database::INDEX_KEY,
'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_TARGET_NOT_FOUND => [
'name' => Exception::USER_TARGET_NOT_FOUND,
'description' => 'The target could not be found.',
'code' => 404,
],
Exception::USER_TARGET_ALREADY_EXISTS => [
'name' => Exception::USER_TARGET_ALREADY_EXISTS,
'description' => 'A target with the same ID already exists.',
'code' => 409,
],
/** Teams */
Exception::TEAM_NOT_FOUND => [
@ -728,4 +738,28 @@ return [
'description' => 'Migration is already in progress. You can check the status of the migration in your Appwrite Console\'s "Settings" > "Migrations".',
'code' => 409,
],
/** Provider Errors */
Exception::PROVIDER_NOT_FOUND => [
'name' => Exception::PROVIDER_NOT_FOUND,
'description' => 'Provider with the requested ID could not be found.',
'code' => 404,
],
Exception::PROVIDER_ALREADY_EXISTS => [
'name' => Exception::PROVIDER_ALREADY_EXISTS,
'description' => 'Provider with the requested ID already exists.',
'code' => 409,
],
Exception::PROVIDER_INCORRECT_TYPE => [
'name' => Exception::PROVIDER_INCORRECT_TYPE,
'description' => 'Provider with the requested ID is of incorrect type: ',
'code' => 400,
],
/** Message Errors */
Exception::MESSAGE_NOT_FOUND => [
'name' => Exception::MESSAGE_NOT_FOUND,
'description' => 'Message with the requested ID could not be found.',
'code' => 404,
]
];

View file

@ -258,6 +258,9 @@ return [
'create' => [
'$description' => 'This event triggers when a message is created.',
],
'update' => [
'$description' => 'This event triggers when a message is updated.',
],
'topics' => [
'$model' => Response::MODEL_TOPIC,
'$resource' => true,

View file

@ -251,4 +251,17 @@ return [
'optional' => true,
'icon' => '/images/services/migrations.png',
],
'messaging' => [
'key' => 'messaging',
'name' => 'Messaging',
'subtitle' => 'The Messaging service allows you to send messages to any provider type (SMTP, push notification, SMS, etc.).',
'description' => '/docs/services/messaging.md',
'controller' => 'api/messaging.php',
'sdk' => true,
'docs' => true,
'docsUrl' => 'https://appwrite.io/docs/server/messaging',
'tests' => true,
'optional' => true,
'icon' => '/images/services/messaging.png',
]
];

View file

@ -8,7 +8,6 @@ use Appwrite\Auth\Validator\Phone;
use Appwrite\Detector\Detector;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Phone as EventPhone;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Host;
@ -45,6 +44,7 @@ use Utopia\Validator\WhiteList;
use Appwrite\Auth\Validator\PasswordHistory;
use Appwrite\Auth\Validator\PasswordDictionary;
use Appwrite\Auth\Validator\PersonalData;
use Appwrite\Event\Messaging;
$oauthDefaultSuccess = '/auth/oauth2/success';
$oauthDefaultFailure = '/auth/oauth2/failure';
@ -1238,9 +1238,12 @@ App::post('/v1/account/sessions/phone')
->inject('events')
->inject('messaging')
->inject('locale')
->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $events, EventPhone $messaging, Locale $locale) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $events, Messaging $messaging, Locale $locale) {
$provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [
Query::equal('default', [true]),
Query::equal('type', ['sms'])
]));
if ($provider === false || $provider->isEmpty()) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@ -1326,9 +1329,34 @@ 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([
'$id' => $token->getId(),
'to' => [$target->getId()],
'data' => [
'content' => $message,
],
'providerId' => $provider->getId(),
'providerInternalId' => $provider->getInternalId(),
]));
$messaging
->setRecipient($phone)
->setMessage($message)
->setMessageId($messageDoc->getId())
->setProject($project)
->trigger();
$events->setPayload(
@ -2885,10 +2913,13 @@ App::post('/v1/account/verification/phone')
->inject('messaging')
->inject('project')
->inject('locale')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events, EventPhone $messaging, Document $project, Locale $locale) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED);
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $events, Messaging $messaging, Document $project, Locale $locale) {
$provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [
Query::equal('default', [true]),
Query::equal('type', ['sms'])
]));
if ($provider === false || $provider->isEmpty()) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
if (empty($user->getAttribute('phone'))) {
@ -2933,11 +2964,35 @@ 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([
'$id' => $verification->getId(),
'to' => [$target->getId()],
'data' => [
'content' => $message,
],
'providerId' => $provider->getId(),
'providerInternalId' => $provider->getInternalId(),
]));
$messaging
->setRecipient($user->getAttribute('phone'))
->setMessage($message)
->trigger()
;
->setMessageId($messageDoc->getId())
->setProject($project)
->trigger();
$events
->setParam('userId', $user->getId())

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@ use Appwrite\Detector\Detector;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Phone as EventPhone;
use Appwrite\Event\Messaging;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Email;
use Utopia\Validator\Host;
@ -380,6 +380,7 @@ App::post('/v1/teams/:teamId/memberships')
->param('roles', [], new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.')
->param('url', '', fn($clients) => new Host($clients), 'URL to redirect the user back to your app from the invitation email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients']) // TODO add our own built-in confirm page
->param('name', '', new Text(128), 'Name of the new team member. Max length: 128 chars.', true)
->param('from', '', new Text(128), 'Sender of the message. It can be alphanumeric (Ex: MyCompany20). Restrictions may apply depending of the destination.', true)
->inject('response')
->inject('project')
->inject('user')
@ -388,7 +389,7 @@ App::post('/v1/teams/:teamId/memberships')
->inject('mails')
->inject('messaging')
->inject('events')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, EventPhone $messaging, Event $events) {
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, string $from, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $mails, Messaging $messaging, Event $events) {
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@ -632,6 +633,15 @@ App::post('/v1/teams/:teamId/memberships')
->trigger()
;
} elseif (!empty($phone)) {
$provider = Authorization::skip(fn () => $dbForProject->findOne('providers', [
Query::equal('default', [true, false]),
Query::equal('type', ['sms'])
]));
if ($provider === false || $provider->isEmpty()) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? [];
@ -642,9 +652,29 @@ 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(),
'to' => [$target->getId()],
'data' => [
'content' => $message,
],
'providerId' => $provider->getId(),
'providerInternalId' => $provider->getInternalId(),
'deliveryTime' => Datetime::now(),
]));
$messaging
->setRecipient($phone)
->setMessage($message)
->setMessageId($messageDoc->getId())
->setProject($project)
->trigger();
}
}

View file

@ -380,6 +380,64 @@ App::post('/v1/users/scrypt-modified')
->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/:userId/targets')
->desc('Create User Target')
->groups(['api', 'users'])
->label('audits.event', 'users.targets.create')
->label('audits.resource', 'target/response.$id')
->label('scope', 'targets.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createTarget')
->label('sdk.description', '/docs/references/users/create-target.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET)
->param('userId', '', new UID(), 'User ID.')
->param('targetId', '', new UID(), 'Target ID.')
->param('providerId', '', new UID(), 'Provider ID.')
->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
->inject('response')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $targetId, string $providerId, string $identifier, Response $response, Database $dbForProject, Event $events) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
}
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $dbForProject->getDocument('targets', $targetId);
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
try {
$target = $dbForProject->createDocument('targets', new Document([
'$id' => $targetId,
'providerId' => $providerId,
'providerInternalId' => $provider->getInternalId(),
'userId' => $userId,
'userInternalId' => $user->getInternalId(),
'identifier' => $identifier,
]));
} catch (Duplicate) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
$dbForProject->deleteCachedDocument('users', $user->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($target, Response::MODEL_TARGET);
});
App::get('/v1/users')
->desc('List users')
->groups(['api', 'users'])
@ -483,6 +541,38 @@ App::get('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::get('/v1/users/:userId/targets/:targetId')
->desc('Get User Target')
->groups(['api', 'users'])
->label('scope', 'targets.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'getTarget')
->label('sdk.description', '/docs/references/users/get-user-target.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET)
->param('userId', '', new UID(), 'User ID.')
->param('targetId', '', new UID(), 'Target ID.')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $user->find('$id', $targetId, 'targets');
if (empty($target)) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
$response->dynamic($target, Response::MODEL_TARGET);
});
App::get('/v1/users/:userId/sessions')
->desc('List user sessions')
->groups(['api', 'users'])
@ -647,6 +737,35 @@ App::get('/v1/users/:userId/logs')
]), Response::MODEL_LOG_LIST);
});
App::get('/v1/users/:userId/targets')
->desc('List User Targets')
->groups(['api', 'users'])
->label('scope', 'targets.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'listTargets')
->label('sdk.description', '/docs/references/users/list-user-targets.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET_LIST)
->param('userId', '', new UID(), 'User ID.')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$targets = $user->getAttribute('targets', []);
$response->dynamic(new Document([
'targets' => $targets,
'total' => \count($targets),
]), Response::MODEL_TARGET_LIST);
});
App::get('/v1/users/identities')
->desc('List Identities')
->groups(['api', 'users'])
@ -1081,6 +1200,56 @@ 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')
->groups(['api', 'users'])
->label('audits.event', 'users.targets.update')
->label('audits.resource', 'target/{response.$id}')
->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.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.)')
->inject('response')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $targetId, string $identifier, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $dbForProject->getDocument('targets', $targetId);
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
if ($user->getId() !== $target->getAttribute('userId')) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
$target->setAttribute('identifier', $identifier);
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
$dbForProject->deleteCachedDocument('users', $user->getId());
$events
->setParam('userId', $userId)
->setParam('targetId', $targetId);
$response
->dynamic($target, Response::MODEL_TARGET);
});
App::delete('/v1/users/:userId/sessions/:sessionId')
->desc('Delete user session')
->groups(['api', 'users'])
@ -1211,6 +1380,48 @@ App::delete('/v1/users/:userId')
$response->noContent();
});
App::delete('/v1/users/:userId/targets/:targetId')
->desc('Delete user target')
->groups(['api', 'users'])
->label('audits.event', 'users.targets.delete')
->label('audits.resource', 'target/{request.$targetId}')
->label('scope', 'targets.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'deleteTarget')
->label('sdk.description', '/docs/references/users/delete-target.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_NONE)
->param('userId', '', new UID(), 'User ID.')
->param('targetId', '', new UID(), 'Target ID.')
->inject('response')
->inject('dbForProject')
->inject('events')
->action(function (string $userId, string $targetId, Response $response, Database $dbForProject, Event $events) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $dbForProject->getDocument('targets', $targetId);
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
if ($user->getId() !== $target->getAttribute('userId')) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
$target = $dbForProject->deleteDocument('targets', $target->getId());
$dbForProject->deleteCachedDocument('users', $user->getId());
$response->noContent();
});
App::delete('/v1/users/identities/:identityId')
->desc('Delete Identity')
->groups(['api', 'users'])

View file

@ -25,7 +25,7 @@ use Appwrite\Event\Audit;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Event;
use Appwrite\Event\Mail;
use Appwrite\Event\Phone;
use Appwrite\Event\Messaging;
use Appwrite\Event\Delete;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Validator\Email;
@ -539,7 +539,6 @@ Database::addFilter(
return Authorization::skip(fn() => $database
->find('targets', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
]));
}
);
@ -555,7 +554,6 @@ Database::addFilter(
$database
->find('subscribers', [
Query::equal('topicInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY),
])
));
if (\count($targetIds) > 0) {
@ -926,7 +924,7 @@ App::setResource('audits', fn() => new Audit());
App::setResource('mails', fn() => new Mail());
App::setResource('deletes', fn() => new Delete());
App::setResource('database', fn() => new EventDatabase());
App::setResource('messaging', fn() => new Phone());
App::setResource('messaging', fn() => new Messaging());
App::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
}, ['pools']);

View file

@ -1,17 +1,30 @@
<?php
use Appwrite\Resque\Worker;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\DSN\DSN;
use Utopia\Messaging\Adapter;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Messaging\Adapters\SMS as SMSAdapter;
use Utopia\Messaging\Adapters\SMS\Mock;
use Utopia\Messaging\Adapters\SMS\Msg91;
use Utopia\Messaging\Adapters\SMS\Telesign;
use Utopia\Messaging\Adapters\SMS\TextMagic;
use Utopia\Messaging\Adapters\SMS\Twilio;
use Utopia\Messaging\Adapters\SMS\Vonage;
use Utopia\Messaging\Adapters\Push as PushAdapter;
use Utopia\Messaging\Adapters\Push\APNS;
use Utopia\Messaging\Adapters\Push\FCM;
use Utopia\Messaging\Adapters\Email as EmailAdapter;
use Utopia\Messaging\Adapters\Email\Mailgun;
use Utopia\Messaging\Adapters\Email\SendGrid;
use Utopia\Messaging\Messages\Email;
use Utopia\Messaging\Messages\Push;
use Utopia\Messaging\Messages\SMS;
use Appwrite\Extend\Exception;
use function Swoole\Coroutine\batch;
require_once __DIR__ . '/../init.php';
@ -20,59 +33,199 @@ Console::success(APP_NAME . ' messaging worker v1 has started' . "\n");
class MessagingV1 extends Worker
{
protected ?Adapter $sms = null;
protected ?string $from = null;
protected ?SMSAdapter $sms = null;
protected ?PushAdapter $push = null;
protected ?EmailAdapter $email = null;
protected ?Database $dbForProject = null;
public function getName(): string
{
return "mails";
return "messaging";
}
public function init(): void
{
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$user = $dsn->getUser();
$secret = $dsn->getPassword();
$this->sms = match ($dsn->getHost()) {
'mock' => new Mock($user, $secret), // used for tests
'twilio' => new Twilio($user, $secret),
'text-magic' => new TextMagic($user, $secret),
'telesign' => new Telesign($user, $secret),
'msg91' => new Msg91($user, $secret),
'vonage' => new Vonage($user, $secret),
default => null
};
$this->from = App::getEnv('_APP_SMS_FROM');
}
public function run(): void
{
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
Console::info('Skipped sms processing. No Phone provider has been set.');
return;
$project = new Document($this->args['project']);
$this->dbForProject = $this->getProjectDB($project);
$message = $this->dbForProject->getDocument('messages', $this->args['messageId']);
$provider = $this->dbForProject->getDocument('providers', $message->getAttribute('providerId'));
$this->processMessage($message, $provider);
}
private function processMessage(Document $message, Document $provider): void
{
$adapter = match ($provider->getAttribute('type')) {
'sms' => $this->sms($provider),
'push' => $this->push($provider),
'email' => $this->email($provider),
default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
};
$recipientsId = $message->getAttribute('to');
/**
* @var Document[] $recipients
*/
$recipients = [];
$topics = $this->dbForProject->find('topics', [Query::equal('$id', $recipientsId)]);
foreach ($topics as $topic) {
$recipients = \array_merge($recipients, $topic->getAttribute('targets'));
}
if (empty($this->from)) {
Console::info('Skipped sms processing. No phone number has been set.');
return;
$users = $this->dbForProject->find('users', [Query::equal('$id', $recipientsId)]);
foreach ($users as $user) {
$recipients = \array_merge($recipients, $user->getAttribute('targets'));
}
$message = new SMS(
to: [$this->args['recipient']],
content: $this->args['message'],
from: $this->from,
);
$targets = $this->dbForProject->find('targets', [Query::equal('$id', $recipientsId)]);
$recipients = \array_merge($recipients, $targets);
$recipients = \array_filter($recipients, function (Document $recipient) use ($provider) {
return $recipient->getAttribute('providerId') === $provider->getId();
});
try {
$this->sms->send($message);
} catch (\Exception $error) {
throw new Exception('Error sending message: ' . $error->getMessage(), 500);
$identifiers = \array_map(function (Document $recipient) {
return $recipient->getAttribute('identifier');
}, $recipients);
$maxBatchSize = $adapter->getMaxMessagesPerRequest();
$batches = \array_chunk($identifiers, $maxBatchSize);
$batchIndex = 0;
$results = batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) {
return function () use ($batch, $message, $provider, $adapter, $batchIndex) {
$deliveredTo = 0;
$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);
$deliveredTo += \count($batch);
} catch (\Exception $e) {
$deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage();
} finally {
$batchIndex++;
return [
'deliveredTo' => $deliveredTo,
'deliveryErrors' => $deliveryErrors,
];
}
};
}, $batches));
$deliveredTo = 0;
$deliveryErrors = [];
foreach ($results as $result) {
$deliveredTo += $result['deliveredTo'];
$deliveryErrors = \array_merge($deliveryErrors, $result['deliveryErrors']);
}
$message->setAttribute('deliveryErrors', $deliveryErrors);
if (\count($message->getAttribute('deliveryErrors')) > 0) {
$message->setAttribute('status', 'failed');
} else {
$message->setAttribute('status', 'sent');
}
$message->setAttribute('to', $recipientsId);
$message->setAttribute('deliveredTo', $deliveredTo);
$message->setAttribute('deliveredAt', DateTime::now());
$this->dbForProject->updateDocument('messages', $message->getId(), $message);
}
public function shutdown(): void
{
}
private function sms(Document $provider): ?SMSAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'mock' => new Mock('username', 'password'),
'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']),
'text-magic' => new TextMagic($credentials['username'], $credentials['apiKey']),
'telesign' => new Telesign($credentials['username'], $credentials['password']),
'msg91' => new Msg91($credentials['senderId'], $credentials['authKey']),
'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']),
default => null
};
}
private function push(Document $provider): ?PushAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'apns' => new APNS(
$credentials['authKey'],
$credentials['authKeyId'],
$credentials['teamId'],
$credentials['bundleId'],
$credentials['endpoint']
),
'fcm' => new FCM($credentials['serverKey']),
default => null
};
}
private function email(Document $provider): ?EmailAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'mailgun' => new Mailgun($credentials['apiKey'], $credentials['domain'], $credentials['isEuRegion']),
'sendgrid' => new SendGrid($credentials['apiKey']),
default => null
};
}
private function buildEmailMessage(Document $message, Document $provider): Email
{
$from = $provider['options']['from'];
$to = $message['to'];
$subject = $message['data']['subject'];
$content = $message['data']['content'];
$html = $message['data']['html'];
return new Email($to, $subject, $content, $from, null, $html);
}
private function buildSMSMessage(Document $message, Document $provider): SMS
{
$to = $message['to'];
$content = $message['data']['content'];
$from = $provider['options']['from'];
return new SMS($to, $content, $from);
}
private function buildPushMessage(Document $message): Push
{
$to = $message['to'];
$title = $message['data']['title'];
$body = $message['data']['body'];
$data = $message['data']['data'];
$action = $message['data']['action'];
$sound = $message['data']['sound'];
$icon = $message['data']['icon'];
$color = $message['data']['color'];
$tag = $message['data']['tag'];
$badge = $message['data']['badge'];
return new Push($to, $title, $body, $data, $action, $sound, $icon, $color, $tag, $badge);
}
}

View file

@ -56,7 +56,7 @@
"utopia-php/image": "0.5.*",
"utopia-php/locale": "0.4.*",
"utopia-php/logger": "0.3.*",
"utopia-php/messaging": "0.1.*",
"utopia-php/messaging": "0.2.*",
"utopia-php/migration": "0.3.*",
"utopia-php/orchestration": "0.9.*",
"utopia-php/platform": "0.4.*",

42
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "13a3bdc7c1dec5756bf58ec73a49753d",
"content-hash": "34cb0b1c81424d1858df197aed030793",
"packages": [
{
"name": "adhocore/jwt",
@ -1050,16 +1050,16 @@
},
{
"name": "matomo/device-detector",
"version": "6.1.5",
"version": "6.1.6",
"source": {
"type": "git",
"url": "https://github.com/matomo-org/device-detector.git",
"reference": "40ca2990dba2c1719e5c62168e822e0b86c167d4"
"reference": "5cbea85106e561c7138d03603eb6e05128480409"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/matomo-org/device-detector/zipball/40ca2990dba2c1719e5c62168e822e0b86c167d4",
"reference": "40ca2990dba2c1719e5c62168e822e0b86c167d4",
"url": "https://api.github.com/repos/matomo-org/device-detector/zipball/5cbea85106e561c7138d03603eb6e05128480409",
"reference": "5cbea85106e561c7138d03603eb6e05128480409",
"shasum": ""
},
"require": {
@ -1115,7 +1115,7 @@
"source": "https://github.com/matomo-org/matomo",
"wiki": "https://dev.matomo.org/"
},
"time": "2023-08-17T16:17:41+00:00"
"time": "2023-10-02T10:01:54+00:00"
},
{
"name": "mongodb/mongodb",
@ -2152,16 +2152,16 @@
},
{
"name": "utopia-php/database",
"version": "0.43.4",
"version": "0.43.5",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/database.git",
"reference": "cabdd02e8dc1732eb0b22007c511e7bb3caa5c8c"
"reference": "5f7b05189cfbcc0506090498c580c5765375a00a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/database/zipball/cabdd02e8dc1732eb0b22007c511e7bb3caa5c8c",
"reference": "cabdd02e8dc1732eb0b22007c511e7bb3caa5c8c",
"url": "https://api.github.com/repos/utopia-php/database/zipball/5f7b05189cfbcc0506090498c580c5765375a00a",
"reference": "5f7b05189cfbcc0506090498c580c5765375a00a",
"shasum": ""
},
"require": {
@ -2202,9 +2202,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/database/issues",
"source": "https://github.com/utopia-php/database/tree/0.43.4"
"source": "https://github.com/utopia-php/database/tree/0.43.5"
},
"time": "2023-09-28T09:00:05+00:00"
"time": "2023-10-06T06:49:47+00:00"
},
{
"name": "utopia-php/domains",
@ -2516,16 +2516,16 @@
},
{
"name": "utopia-php/messaging",
"version": "0.1.1",
"version": "0.2.0",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/messaging.git",
"reference": "a75d66ddd59b834ab500a4878a2c084e6572604a"
"reference": "2d0f474a106bb1da285f85e105c29b46085d3a43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/a75d66ddd59b834ab500a4878a2c084e6572604a",
"reference": "a75d66ddd59b834ab500a4878a2c084e6572604a",
"url": "https://api.github.com/repos/utopia-php/messaging/zipball/2d0f474a106bb1da285f85e105c29b46085d3a43",
"reference": "2d0f474a106bb1da285f85e105c29b46085d3a43",
"shasum": ""
},
"require": {
@ -2534,8 +2534,8 @@
},
"require-dev": {
"laravel/pint": "^1.2",
"phpmailer/phpmailer": "6.6.*",
"phpunit/phpunit": "9.5.*"
"phpmailer/phpmailer": "6.8.*",
"phpunit/phpunit": "9.6.*"
},
"type": "library",
"autoload": {
@ -2558,9 +2558,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/messaging/issues",
"source": "https://github.com/utopia-php/messaging/tree/0.1.1"
"source": "https://github.com/utopia-php/messaging/tree/0.2.0"
},
"time": "2023-02-07T05:42:46+00:00"
"time": "2023-09-14T20:48:42+00:00"
},
{
"name": "utopia-php/migration",
@ -6019,5 +6019,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View file

@ -188,6 +188,15 @@ services:
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
- _APP_ASSISTANT_OPENAI_API_KEY
- _APP_MESSAGE_SMS_PROVIDER_MSG91_SENDER_ID
- _APP_MESSAGE_SMS_PROVIDER_MSG91_AUTH_KEY
- _APP_MESSAGE_SMS_PROVIDER_MSG91_FROM
- _APP_MESSAGE_SMS_PROVIDER_MSG91_TO
- _APP_MESSAGE_SMS_PROVIDER_MAILGUN_API_KEY
- _APP_MESSAGE_SMS_PROVIDER_MAILGUN_DOMAIN
- _APP_MESSAGE_SMS_PROVIDER_MAILGUN_FROM
- _APP_MESSAGE_SMS_PROVIDER_MAILGUN_RECEIVER_EMAIL
- _APP_MESSAGE_SMS_PROVIDER_MAILGUN_IS_EU_REGION
appwrite-realtime:
entrypoint: realtime
@ -571,8 +580,11 @@ services:
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG

View file

@ -32,6 +32,7 @@
<directory>./tests/e2e/Services/Teams</directory>
<directory>./tests/e2e/Services/Users</directory>
<directory>./tests/e2e/Services/Webhooks</directory>
<directory>./tests/e2e/Services/Messaging</directory>
<file>./tests/e2e/Services/Functions/FunctionsBase.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomServerTest.php</file>
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>

View file

@ -0,0 +1,76 @@
<?php
namespace Appwrite\Event;
use ResqueScheduler;
use Utopia\Database\DateTime;
class Messaging extends Event
{
protected ?string $messageId = null;
private ?string $deliveryTime = null;
public function __construct()
{
parent::__construct(Event::MESSAGING_QUEUE_NAME, Event::MESSAGING_CLASS_NAME);
}
/**
* Sets message ID for the messaging event.
*
* @param string $message
* @return self
*/
public function setMessageId(string $messageId): self
{
$this->messageId = $messageId;
return $this;
}
/**
* Returns set message ID for the messaging event.
*
* @return string
*/
public function getMessageId(): string
{
return $this->messageId;
}
/**
* Sets Delivery time for the messaging event.
*
* @param string $deliveryTime
* @return self
*/
public function setDeliveryTime(string $deliveryTime): self
{
$this->deliveryTime = $deliveryTime;
return $this;
}
/**
* Returns set Delivery Time for the messaging event.
*
* @return string
*/
public function getDeliveryTime(): string
{
return $this->deliveryTime;
}
/**
* Executes the event and sends it to the messaging worker.
*/
public function trigger(): string | bool
{
ResqueScheduler::enqueueAt(!empty($this->deliveryTime) ? $this->deliveryTime : DateTime::now(), $this->queue, $this->class, [
'project' => $this->project,
'user' => $this->user,
'messageId' => $this->messageId,
]);
return true;
}
}

View file

@ -1,80 +0,0 @@
<?php
namespace Appwrite\Event;
use Resque;
class Phone extends Event
{
protected string $recipient = '';
protected string $message = '';
public function __construct()
{
parent::__construct(Event::MESSAGING_QUEUE_NAME, Event::MESSAGING_CLASS_NAME);
}
/**
* Sets recipient for the messaging event.
*
* @param string $recipient
* @return self
*/
public function setRecipient(string $recipient): self
{
$this->recipient = $recipient;
return $this;
}
/**
* Returns set recipient for this messaging event.
*
* @return string
*/
public function getRecipient(): string
{
return $this->recipient;
}
/**
* Sets url for the messaging event.
*
* @param string $message
* @return self
*/
public function setMessage(string $message): self
{
$this->message = $message;
return $this;
}
/**
* Returns set url for the messaging event.
*
* @return string
*/
public function getMessage(): string
{
return $this->message;
}
/**
* Executes the event and sends it to the messaging worker.
*
* @return string|bool
* @throws \InvalidArgumentException
*/
public function trigger(): string|bool
{
return Resque::enqueue($this->queue, $this->class, [
'project' => $this->project,
'user' => $this->user,
'payload' => $this->payload,
'recipient' => $this->recipient,
'message' => $this->message,
'events' => Event::generateEvents($this->getEvent(), $this->getParams())
]);
}
}

View file

@ -84,7 +84,8 @@ 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_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';
@ -224,6 +225,14 @@ class Exception extends \Exception
public const MIGRATION_ALREADY_EXISTS = 'migration_already_exists';
public const MIGRATION_IN_PROGRESS = 'migration_in_progress';
/** Provider */
public const PROVIDER_NOT_FOUND = 'provider_not_found';
public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists';
public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type';
/** Message */
public const MESSAGE_NOT_FOUND = 'message_not_found';
protected $type = '';
protected $errors = [];

View file

@ -0,0 +1,23 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Providers extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'provider',
'type',
'default',
'enabled'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('providers', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -32,11 +32,18 @@ class Message extends Any
])
->addRule('deliveryTime', [
'type' => self::TYPE_DATETIME,
'description' => 'Time the message is delivered at.',
'description' => 'The scheduled time for message.',
'required' => false,
'default' => DateTime::now(),
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('deliveredAt', [
'type' => self::TYPE_DATETIME,
'description' => 'The time when the message was delivered.',
'required' => false,
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('deliveryErrors', [
'type' => self::TYPE_STRING,
'description' => 'Delivery errors if any.',
@ -51,11 +58,18 @@ class Message extends Any
'default' => 0,
'example' => 1,
])
->addRule('delivered', [
'type' => self::TYPE_BOOLEAN,
->addRule('status', [
'type' => self::TYPE_STRING,
'description' => 'Status of delivery.',
'default' => false,
'example' => true,
'default' => 'processing',
'example' => 'Message status can be one of the following: processing, sent, failed.',
])
->addRule('description', [
'type' => self::TYPE_STRING,
'description' => 'Message description.',
'required' => false,
'default' => '',
'example' => 'Welcome Email.',
]);
}

View file

@ -45,6 +45,23 @@ class Provider extends Model
'description' => 'Type of provider.',
'default' => '',
'example' => 'sms',
])
->addRule('credentials', [
'type' => self::TYPE_JSON,
'description' => 'Provider credentials.',
'default' => [],
'example' => [
'key' => '123456789'
],
])
->addRule('options', [
'type' => self::TYPE_JSON,
'description' => 'Provider options.',
'default' => [],
'required' => false,
'example' => [
'from' => 'sender-email@mydomain'
],
]);
}

View file

@ -81,6 +81,12 @@ trait ProjectCustom
'locale.read',
'avatars.read',
'health.read',
'targets.read',
'targets.write',
'providers.read',
'providers.write',
'messages.read',
'messages.write',
],
]);

View file

@ -2,13 +2,12 @@
namespace Tests\E2E\Services\Account;
use Appwrite\Extend\Exception;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\Tests\Retry;
use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Utopia\App;
use Utopia\Database\DateTime;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
@ -744,8 +743,29 @@ class AccountCustomClientTest extends Scope
public function testCreatePhone(): array
{
$number = '+123456789';
$to = App::getEnv('_APP_MESSAGE_SMS_PROVIDER_MSG91_TO');
$from = App::getEnv('_APP_MESSAGE_SMS_PROVIDER_MSG91_FROM');
$authKey = App::getEnv('_APP_MESSAGE_SMS_PROVIDER_MSG91_AUTH_KEY');
$senderId = App::getEnv('_APP_MESSAGE_SMS_PROVIDER_MSG91_SENDER_ID');
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,
'default' => true,
'from' => $from,
]);
$this->assertEquals(201, $response['headers']['status-code']);
/**
* Test for SUCCESS
*/
@ -756,6 +776,7 @@ class AccountCustomClientTest extends Scope
]), [
'userId' => ID::unique(),
'phone' => $number,
'from' => $from,
]);
$this->assertEquals(201, $response['headers']['status-code']);
@ -764,6 +785,7 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire']));
$userId = $response['body']['userId'];
$messageId = $response['body']['$id'];
/**
* Test for FAILURE
@ -780,17 +802,19 @@ class AccountCustomClientTest extends Scope
\sleep(5);
$smsRequest = $this->getLastRequest();
$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'],
]);
$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']);
$this->assertEquals(200, $message['headers']['status-code']);
$this->assertEquals(1, $message['body']['deliveredTo']);
$this->assertEquals(0, \count($message['body']['deliveryErrors']));
$data['token'] = $smsRequest['data']['message'];
$data['token'] = $message['body']['data']['content'];
$data['id'] = $userId;
$data['number'] = $number;
@ -999,19 +1023,28 @@ class AccountCustomClientTest extends Scope
'x-appwrite-project' => $this->getProject()['$id'],
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $session,
]));
]), ['from' => App::getEnv('_APP_MESSAGE_SMS_PROVIDER_MSG91_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(2);
\sleep(3);
$smsRequest = $this->getLastRequest();
$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']['deliveredTo']);
$this->assertEquals(0, \count($message['body']['deliveryErrors']));
return \array_merge($data, [
'token' => $smsRequest['data']['message']
'token' => $message['body']['data']['content']
]);
}

View file

@ -6,6 +6,7 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\App;
use Utopia\Database\Helpers\ID;
class AccountTest extends Scope
@ -122,7 +123,35 @@ class AccountTest extends Scope
*/
public function testCreatePhoneVerification(): array
{
$to = App::getEnv('_APP_MESSAGE_SMS_PROVIDER_MSG91_TO');
$from = App::getEnv('_APP_MESSAGE_SMS_PROVIDER_MSG91_FROM');
$authKey = App::getEnv('_APP_MESSAGE_SMS_PROVIDER_MSG91_AUTH_KEY');
$senderId = App::getEnv('_APP_MESSAGE_SMS_PROVIDER_MSG91_SENDER_ID');
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,
'default' => true,
],
];
$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 = [
'query' => $query,

View file

@ -197,6 +197,29 @@ trait Base
public static string $GET_QRCODE = 'get_qrcode';
public static string $GET_USER_INITIALS = 'get_user_initials';
// Providers
public static string $CREATE_MAILGUN_PROVIDER = 'create_mailgun_provider';
public static string $CREATE_SENDGRID_PROVIDER = 'create_sendgrid_provider';
public static string $CREATE_TWILIO_PROVIDER = 'create_twilio_provider';
public static string $CREATE_TELESIGN_PROVIDER = 'create_telesign_provider';
public static string $CREATE_TEXTMAGIC_PROVIDER = 'create_textmagic_provider';
public static string $CREATE_MSG91_PROVIDER = 'create_msg91_provider';
public static string $CREATE_VONAGE_PROVIDER = 'create_vonage_provider';
public static string $CREATE_FCM_PROVIDER = 'create_fcm_provider';
public static string $CREATE_APNS_PROVIDER = 'create_apns_provider';
public static string $LIST_PROVIDERS = 'list_providers';
public static string $GET_PROVIDER = 'get_provider';
public static string $UPDATE_MAILGUN_PROVIDER = 'update_mailgun_provider';
public static string $UPDATE_SENDGRID_PROVIDER = 'update_sendgrid_provider';
public static string $UPDATE_TWILIO_PROVIDER = 'update_twilio_provider';
public static string $UPDATE_TELESIGN_PROVIDER = 'update_telesign_provider';
public static string $UPDATE_TEXTMAGIC_PROVIDER = 'update_textmagic_provider';
public static string $UPDATE_MSG91_PROVIDER = 'update_msg91_provider';
public static string $UPDATE_VONAGE_PROVIDER = 'update_vonage_provider';
public static string $UPDATE_FCM_PROVIDER = 'update_fcm_provider';
public static string $UPDATE_APNS_PROVIDER = 'update_apns_provider';
public static string $DELETE_PROVIDER = 'delete_provider';
// Complex queries
public static string $COMPLEX_QUERY = 'complex_query';
@ -1686,6 +1709,235 @@ trait Base
status
}
}';
case self::$CREATE_MAILGUN_PROVIDER:
return 'mutation createMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $from: String!) {
messagingCreateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, from: $from) {
_id
name
provider
type
default
enabled
}
}';
case self::$CREATE_SENDGRID_PROVIDER:
return 'mutation createSendgridProvider($providerId: String!, $name: String!, $apiKey: String!) {
messagingCreateSendgridProvider(providerId: $providerId, name: $name, apiKey: $apiKey) {
_id
name
provider
type
default
enabled
}
}';
case self::$CREATE_TWILIO_PROVIDER:
return 'mutation createTwilioProvider($providerId: String!, $name: String!, $accountSid: String!, $authToken: String!) {
messagingCreateTwilioProvider(providerId: $providerId, name: $name, accountSid: $accountSid, authToken: $authToken) {
_id
name
provider
type
default
enabled
}
}';
case self::$CREATE_TELESIGN_PROVIDER:
return 'mutation createTelesignProvider($providerId: String!, $name: String!, $username: String!, $password: String!) {
messagingCreateTelesignProvider(providerId: $providerId, name: $name, username: $username, password: $password) {
_id
name
provider
type
default
enabled
}
}';
case self::$CREATE_TEXTMAGIC_PROVIDER:
return 'mutation createTextmagicProvider($providerId: String!, $name: String!, $username: String!, $apiKey: String!) {
messagingCreateTextmagicProvider(providerId: $providerId, name: $name, username: $username, apiKey: $apiKey) {
_id
name
provider
type
default
enabled
}
}';
case self::$CREATE_MSG91_PROVIDER:
return 'mutation createMsg91Provider($providerId: String!, $name: String!, $from: String!, $senderId: String!, $authKey: String!, $default: Boolean, $enabled: Boolean) {
messagingCreateMsg91Provider(providerId: $providerId, name: $name, from: $from, senderId: $senderId, authKey: $authKey, default: $default, enabled: $enabled) {
_id
name
provider
type
default
enabled
}
}';
case self::$CREATE_VONAGE_PROVIDER:
return 'mutation createVonageProvider($providerId: String!, $name: String!, $apiKey: String!, $apiSecret: String!) {
messagingCreateVonageProvider(providerId: $providerId, name: $name, apiKey: $apiKey, apiSecret: $apiSecret) {
_id
name
provider
type
default
enabled
}
}';
case self::$CREATE_FCM_PROVIDER:
return 'mutation createFcmProvider($providerId: String!, $name: String!, $serverKey: String!) {
messagingCreateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) {
_id
name
provider
type
default
enabled
}
}';
case self::$CREATE_APNS_PROVIDER:
return 'mutation createApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) {
messagingCreateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) {
_id
name
provider
type
default
enabled
}
}';
case self::$LIST_PROVIDERS:
return 'query listProviders {
messagingListProviders {
total
providers {
_id
name
provider
type
default
enabled
}
}
}';
case self::$GET_PROVIDER:
return 'query getProvider($providerId: String!) {
messagingGetProvider(providerId: $providerId) {
_id
name
provider
type
default
enabled
}
}';
case self::$UPDATE_MAILGUN_PROVIDER:
return 'mutation updateMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $isEuRegion: Boolean, $enabled: Boolean) {
messagingUpdateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, isEuRegion: $isEuRegion, enabled: $enabled) {
_id
name
provider
type
default
enabled
}
}';
case self::$UPDATE_SENDGRID_PROVIDER:
return 'mutation messagingUpdateSendgridProvider($providerId: String!, $name: String!, $apiKey: String!) {
messagingUpdateSendgridProvider(providerId: $providerId, name: $name, apiKey: $apiKey) {
_id
name
provider
type
default
enabled
}
}';
case self::$UPDATE_TWILIO_PROVIDER:
return 'mutation updateTwilioProvider($providerId: String!, $name: String!, $accountSid: String!, $authToken: String!) {
messagingUpdateTwilioProvider(providerId: $providerId, name: $name, accountSid: $accountSid, authToken: $authToken) {
_id
name
provider
type
default
enabled
}
}';
case self::$UPDATE_TELESIGN_PROVIDER:
return 'mutation updateTelesignProvider($providerId: String!, $name: String!, $username: String!, $password: String!) {
messagingUpdateTelesignProvider(providerId: $providerId, name: $name, username: $username, password: $password) {
_id
name
provider
type
default
enabled
}
}';
case self::$UPDATE_TEXTMAGIC_PROVIDER:
return 'mutation updateTextmagicProvider($providerId: String!, $name: String!, $username: String!, $apiKey: String!) {
messagingUpdateTextmagicProvider(providerId: $providerId, name: $name, username: $username, apiKey: $apiKey) {
_id
name
provider
type
default
enabled
}
}';
case self::$UPDATE_MSG91_PROVIDER:
return 'mutation updateMsg91Provider($providerId: String!, $name: String!, $senderId: String!, $authKey: String!) {
messagingUpdateMsg91Provider(providerId: $providerId, name: $name, senderId: $senderId, authKey: $authKey) {
_id
name
provider
type
default
enabled
}
}';
case self::$UPDATE_VONAGE_PROVIDER:
return 'mutation updateVonageProvider($providerId: String!, $name: String!, $apiKey: String!, $apiSecret: String!) {
messagingUpdateVonageProvider(providerId: $providerId, name: $name, apiKey: $apiKey, apiSecret: $apiSecret) {
_id
name
provider
type
default
enabled
}
}';
case self::$UPDATE_FCM_PROVIDER:
return 'mutation updateFcmProvider($providerId: String!, $name: String!, $serverKey: String!) {
messagingUpdateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) {
_id
name
provider
type
default
enabled
}
}';
case self::$UPDATE_APNS_PROVIDER:
return 'mutation updateApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) {
messagingUpdateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) {
_id
name
provider
type
default
enabled
}
}';
case self::$DELETE_PROVIDER:
return 'mutation deleteProvider($providerId: String!) {
messagingDeleteProvider(providerId: $providerId) {
status
}
}';
case self::$COMPLEX_QUERY:
return 'mutation complex($databaseId: String!, $databaseName: String!, $collectionId: String!, $collectionName: String!, $documentSecurity: Boolean!, $collectionPermissions: [String!]!) {
databasesCreate(databaseId: $databaseId, name: $databaseName) {

View file

@ -0,0 +1,260 @@
<?php
namespace Tests\E2E\Services\GraphQL;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\Helpers\ID;
class MessagingTest extends Scope
{
use ProjectCustom;
use SideServer;
use Base;
public function testCreateProviders()
{
$providersParams = [
'Sendgrid' => [
'providerId' => ID::unique(),
'name' => 'Sengrid1',
'apiKey' => 'my-apikey',
],
'Mailgun' => [
'providerId' => ID::unique(),
'name' => 'Mailgun1',
'apiKey' => 'my-apikey',
'domain' => 'my-domain',
'from' => 'sender-email@my-domain',
],
'Twilio' => [
'providerId' => ID::unique(),
'name' => 'Twilio1',
'accountSid' => 'my-accountSid',
'authToken' => 'my-authToken',
],
'Telesign' => [
'providerId' => ID::unique(),
'name' => 'Telesign1',
'username' => 'my-username',
'password' => 'my-password',
],
'Textmagic' => [
'providerId' => ID::unique(),
'name' => 'Textmagic1',
'username' => 'my-username',
'apiKey' => 'my-apikey',
],
'Msg91' => [
'providerId' => ID::unique(),
'name' => 'Ms91-1',
'senderId' => 'my-senderid',
'authKey' => 'my-authkey',
'from' => '+123456789'
],
'Vonage' => [
'providerId' => ID::unique(),
'name' => 'Vonage1',
'apiKey' => 'my-apikey',
'apiSecret' => 'my-apisecret',
],
'Fcm' => [
'providerId' => ID::unique(),
'name' => 'FCM1',
'serverKey' => 'my-serverkey',
],
'Apns' => [
'providerId' => ID::unique(),
'name' => 'APNS1',
'authKey' => 'my-authkey',
'authKeyId' => 'my-authkeyid',
'teamId' => 'my-teamid',
'bundleId' => 'my-bundleid',
'endpoint' => 'my-endpoint',
],
];
$providers = [];
foreach (\array_keys($providersParams) as $key) {
$query = $this->getQuery('create_' . \strtolower($key) . '_provider');
$graphQLPayload = [
'query' => $query,
'variables' => $providersParams[$key],
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), $graphQLPayload);
\array_push($providers, $response['body']['data']['messagingCreate' . $key . 'Provider']);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($providersParams[$key]['name'], $response['body']['data']['messagingCreate' . $key . 'Provider']['name']);
}
return $providers;
}
/**
* @depends testCreateProviders
*/
public function testUpdateProviders(array $providers): array
{
$providersParams = [
'Sendgrid' => [
'providerId' => $providers[0]['_id'],
'name' => 'Sengrid2',
'apiKey' => 'my-apikey',
],
'Mailgun' => [
'providerId' => $providers[1]['_id'],
'name' => 'Mailgun2',
'apiKey' => 'my-apikey',
'domain' => 'my-domain',
],
'Twilio' => [
'providerId' => $providers[2]['_id'],
'name' => 'Twilio2',
'accountSid' => 'my-accountSid',
'authToken' => 'my-authToken',
],
'Telesign' => [
'providerId' => $providers[3]['_id'],
'name' => 'Telesign2',
'username' => 'my-username',
'password' => 'my-password',
],
'Textmagic' => [
'providerId' => $providers[4]['_id'],
'name' => 'Textmagic2',
'username' => 'my-username',
'apiKey' => 'my-apikey',
],
'Msg91' => [
'providerId' => $providers[5]['_id'],
'name' => 'Ms91-2',
'senderId' => 'my-senderid',
'authKey' => 'my-authkey',
],
'Vonage' => [
'providerId' => $providers[6]['_id'],
'name' => 'Vonage2',
'apiKey' => 'my-apikey',
'apiSecret' => 'my-apisecret',
],
'Fcm' => [
'providerId' => $providers[7]['_id'],
'name' => 'FCM2',
'serverKey' => 'my-serverkey',
],
'Apns' => [
'providerId' => $providers[8]['_id'],
'name' => 'APNS2',
'authKey' => 'my-authkey',
'authKeyId' => 'my-authkeyid',
'teamId' => 'my-teamid',
'bundleId' => 'my-bundleid',
'endpoint' => 'my-endpoint',
],
];
foreach (\array_keys($providersParams) as $index => $key) {
$query = $this->getQuery('update_' . \strtolower($key) . '_provider');
$graphQLPayload = [
'query' => $query,
'variables' => $providersParams[$key],
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], $graphQLPayload);
$providers[$index] = $response['body']['data']['messagingUpdate' . $key . 'Provider'];
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($providersParams[$key]['name'], $response['body']['data']['messagingUpdate' . $key . 'Provider']['name']);
}
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'query' => $this->getQuery('update_mailgun_provider'),
'variables' => [
'providerId' => $providers[1]['_id'],
'name' => 'Mailgun2',
'apiKey' => 'my-apikey',
'domain' => 'my-domain',
'isEuRegion' => true,
'enabled' => false,
]
]);
$providers[1] = $response['body']['data']['messagingUpdateMailgunProvider'];
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('Mailgun2', $response['body']['data']['messagingUpdateMailgunProvider']['name']);
$this->assertEquals(false, $response['body']['data']['messagingUpdateMailgunProvider']['enabled']);
return $providers;
}
/**
* @depends testUpdateProviders
*/
public function testListProviders(array $providers)
{
$query = $this->getQuery(self::$LIST_PROVIDERS);
$graphQLPayload = [
'query' => $query,
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], $graphQLPayload);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(\count($providers), \count($response['body']['data']['messagingListProviders']['providers']));
}
/**
* @depends testUpdateProviders
*/
public function testGetProvider(array $providers)
{
$query = $this->getQuery(self::$GET_PROVIDER);
$graphQLPayload = [
'query' => $query,
'variables' => [
'providerId' => $providers[0]['_id'],
]
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], $graphQLPayload);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($providers[0]['name'], $response['body']['data']['messagingGetProvider']['name']);
}
/**
* @depends testUpdateProviders
*/
public function testDeleteProvider(array $providers)
{
foreach ($providers as $provider) {
$query = $this->getQuery(self::$DELETE_PROVIDER);
$graphQLPayload = [
'query' => $query,
'variables' => [
'providerId' => $provider['_id'],
]
];
$response = $this->client->call(Client::METHOD_POST, '/graphql', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], $graphQLPayload);
$this->assertEquals(204, $response['headers']['status-code']);
}
}
}

View file

@ -0,0 +1,211 @@
<?php
namespace Tests\E2E\Services\Messaging;
use Tests\E2E\Client;
use Utopia\Database\Helpers\ID;
trait MessagingBase
{
public function testCreateProviders(): array
{
$providersParams = [
'sendgrid' => [
'providerId' => ID::unique(),
'name' => 'Sengrid1',
'apiKey' => 'my-apikey',
],
'mailgun' => [
'providerId' => ID::unique(),
'name' => 'Mailgun1',
'apiKey' => 'my-apikey',
'domain' => 'my-domain',
'from' => 'sender-email@my-domain',
],
'twilio' => [
'providerId' => ID::unique(),
'name' => 'Twilio1',
'accountSid' => 'my-accountSid',
'authToken' => 'my-authToken',
],
'telesign' => [
'providerId' => ID::unique(),
'name' => 'Telesign1',
'username' => 'my-username',
'password' => 'my-password',
],
'textmagic' => [
'providerId' => ID::unique(),
'name' => 'Textmagic1',
'username' => 'my-username',
'apiKey' => 'my-apikey',
],
'msg91' => [
'providerId' => ID::unique(),
'name' => 'Ms91-1',
'senderId' => 'my-senderid',
'authKey' => 'my-authkey',
'from' => '+123456789'
],
'vonage' => [
'providerId' => ID::unique(),
'name' => 'Vonage1',
'apiKey' => 'my-apikey',
'apiSecret' => 'my-apisecret',
],
'fcm' => [
'providerId' => ID::unique(),
'name' => 'FCM1',
'serverKey' => 'my-serverkey',
],
'apns' => [
'providerId' => ID::unique(),
'name' => 'APNS1',
'authKey' => 'my-authkey',
'authKeyId' => 'my-authkeyid',
'teamId' => 'my-teamid',
'bundleId' => 'my-bundleid',
'endpoint' => 'my-endpoint',
],
];
$providers = [];
foreach (\array_keys($providersParams) as $key) {
$response = $this->client->call(Client::METHOD_POST, '/messaging/providers/' . $key, \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), $providersParams[$key]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals($providersParams[$key]['name'], $response['body']['name']);
\array_push($providers, $response['body']);
}
return $providers;
}
/**
* @depends testCreateProviders
*/
public function testUpdateProviders(array $providers): array
{
$providersParams = [
'sendgrid' => [
'name' => 'Sengrid2',
'apiKey' => 'my-apikey',
],
'mailgun' => [
'name' => 'Mailgun2',
'apiKey' => 'my-apikey',
'domain' => 'my-domain',
],
'twilio' => [
'name' => 'Twilio2',
'accountSid' => 'my-accountSid',
'authToken' => 'my-authToken',
],
'telesign' => [
'name' => 'Telesign2',
'username' => 'my-username',
'password' => 'my-password',
],
'textmagic' => [
'name' => 'Textmagic2',
'username' => 'my-username',
'apiKey' => 'my-apikey',
],
'msg91' => [
'name' => 'Ms91-2',
'senderId' => 'my-senderid',
'authKey' => 'my-authkey',
],
'vonage' => [
'name' => 'Vonage2',
'apiKey' => 'my-apikey',
'apiSecret' => 'my-apisecret',
],
'fcm' => [
'name' => 'FCM2',
'serverKey' => 'my-serverkey',
],
'apns' => [
'name' => 'APNS2',
'authKey' => 'my-authkey',
'authKeyId' => 'my-authkeyid',
'teamId' => 'my-teamid',
'bundleId' => 'my-bundleid',
'endpoint' => 'my-endpoint',
],
];
foreach (\array_keys($providersParams) as $index => $key) {
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/' . $key . '/' . $providers[$index]['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], $providersParams[$key]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($providersParams[$key]['name'], $response['body']['name']);
$providers[$index] = $response['body'];
}
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/mailgun/' . $providers[1]['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], [
'name' => 'Mailgun2',
'apiKey' => 'my-apikey',
'domain' => 'my-domain',
'isEuRegion' => true,
'enabled' => false,
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('Mailgun2', $response['body']['name']);
$this->assertEquals(false, $response['body']['enabled']);
$providers[1] = $response['body'];
return $providers;
}
/**
* @depends testUpdateProviders
*/
public function testListProviders(array $providers)
{
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(\count($providers), \count($response['body']['providers']));
}
/**
* @depends testUpdateProviders
*/
public function testGetProvider(array $providers)
{
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $providers[0]['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($providers[0]['name'], $response['body']['name']);
}
/**
* @depends testUpdateProviders
*/
public function testDeleteProvider(array $providers)
{
foreach ($providers as $provider) {
$response = $this->client->call(Client::METHOD_DELETE, '/messaging/providers/' . $provider['$id'], [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(204, $response['headers']['status-code']);
}
}
}

View file

@ -0,0 +1,14 @@
<?php
namespace Tests\E2E\Services\Messaging;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
class MessagingCustomServerTest extends Scope
{
use MessagingBase;
use ProjectCustom;
use SideServer;
}

View file

@ -1223,6 +1223,94 @@ trait UsersBase
$this->assertEquals($response['headers']['status-code'], 400);
}
/**
* @depends testGetUser
*/
public function testCreateUserTarget(array $data): array
{
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'providerId' => 'unique()',
'name' => 'Sengrid1',
'apiKey' => 'my-apikey'
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/users/' . $data['userId'] . '/targets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'targetId' => ID::unique(),
'providerId' => $provider['body']['$id'],
'identifier' => 'my-token',
]);
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertEquals($provider['body']['$id'], $response['body']['providerId']);
$this->assertEquals('my-token', $response['body']['identifier']);
return $response['body'];
}
/**
* @depends testCreateUserTarget
*/
public function testUpdateUserTarget(array $data): array
{
$response = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/targets/' . $data['$id'] . '/identifier', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'identifier' => 'my-updated-token',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('my-updated-token', $response['body']['identifier']);
return $response['body'];
}
/**
* @depends testUpdateUserTarget
*/
public function testListUserTarget(array $data)
{
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(1, \count($response['body']['targets']));
}
/**
* @depends testUpdateUserTarget
*/
public function testGetUserTarget(array $data)
{
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals($data['$id'], $response['body']['$id']);
}
/**
* @depends testUpdateUserTarget
*/
public function testDeleteUserTarget(array $data)
{
$response = $this->client->call(Client::METHOD_DELETE, '/users/' . $data['userId'] . '/targets/' . $data['$id'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/targets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(0, $response['body']['total']);
}
/**
* @depends testGetUser
*/