1
0
Fork 0
mirror of synced 2024-06-26 10:10:57 +12:00

Merge remote-tracking branch 'origin/1.5.x' into feat-improve-logging

This commit is contained in:
Matej Bačo 2024-01-02 11:08:47 +00:00
commit 0ba768125d
103 changed files with 10200 additions and 815 deletions

7
.env
View file

@ -87,7 +87,7 @@ _APP_LOGGING_PROVIDER=
_APP_LOGGING_CONFIG=
_APP_GRAPHQL_MAX_BATCH_SIZE=10
_APP_GRAPHQL_MAX_COMPLEXITY=250
_APP_GRAPHQL_MAX_DEPTH=3
_APP_GRAPHQL_MAX_DEPTH=4
_APP_DOCKER_HUB_USERNAME=
_APP_DOCKER_HUB_PASSWORD=
_APP_VCS_GITHUB_APP_NAME=
@ -98,4 +98,7 @@ _APP_VCS_GITHUB_CLIENT_SECRET=
_APP_VCS_GITHUB_WEBHOOK_SECRET=
_APP_MIGRATIONS_FIREBASE_CLIENT_ID=
_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=
_APP_ASSISTANT_OPENAI_API_KEY=
_APP_ASSISTANT_OPENAI_API_KEY=
_APP_MESSAGE_SMS_TEST_DSN=
_APP_MESSAGE_EMAIL_TEST_DSN=
_APP_MESSAGE_PUSH_TEST_DSN=

View file

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

1
.gitignore vendored
View file

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

2
.gitmodules vendored
View file

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

View file

@ -484,7 +484,7 @@
## Features
- Added Phone Authentication by @TorstenDittmann in https://github.com/appwrite/appwrite/pull/3357
- Added Twilio Support
- Added TextMagic Support
- Added Textmagic Support
- Added Telesign Support
- Added Endpoint to create Phone Session (`POST:/v1/account/sessions/phone`)
- Added Endpoint to confirm Phone Session (`PUT:/v1/account/sessions/phone`)

View file

@ -94,7 +94,8 @@ RUN chmod +x /usr/local/bin/doctor && \
chmod +x /usr/local/bin/worker-mails && \
chmod +x /usr/local/bin/worker-messaging && \
chmod +x /usr/local/bin/worker-webhooks && \
chmod +x /usr/local/bin/worker-migrations
chmod +x /usr/local/bin/worker-migrations && \
chmod +x /usr/local/bin/worker-hamster
# Cloud Executabless
RUN chmod +x /usr/local/bin/hamster && \

View file

@ -6,6 +6,7 @@ require_once __DIR__ . '/controllers/general.php';
use Appwrite\Event\Delete;
use Appwrite\Event\Certificate;
use Appwrite\Event\Func;
use Appwrite\Event\Hamster;
use Appwrite\Platform\Appwrite;
use Utopia\CLI\CLI;
use Utopia\Database\Validator\Authorization;
@ -154,6 +155,9 @@ CLI::setResource('queue', function (Group $pools) {
CLI::setResource('queueForFunctions', function (Connection $queue) {
return new Func($queue);
}, ['queue']);
CLI::setResource('queueForHamster', function (Connection $queue) {
return new Hamster($queue);
}, ['queue']);
CLI::setResource('queueForDeletes', function (Connection $queue) {
return new Delete($queue);
}, ['queue']);

View file

@ -5,7 +5,7 @@ use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
$providers = Config::getParam('providers', []);
$providers = Config::getParam('oAuthProviders', []);
$auth = Config::getParam('auth', []);
/**
@ -221,6 +221,17 @@ $commonCollections = [
'array' => false,
'filters' => ['subQueryMemberships'],
],
[
'$id' => ID::custom('targets'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['subQueryTargets'],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
@ -1366,7 +1377,608 @@ $commonCollections = [
],
],
],
];
'providers' => [
'$collection' => ID::custom(DATABASE::METADATA),
'$id' => ID::custom('providers'),
'name' => 'Providers',
'attributes' => [
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('provider'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('type'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('enabled'),
'type' => Database::VAR_BOOLEAN,
'signed' => true,
'size' => 0,
'format' => '',
'filters' => [],
'required' => true,
'default' => true,
'array' => false,
],
[
'$id' => ID::custom('credentials'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['json', 'encrypt'],
],
[
'$id' => ID::custom('options'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => [],
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => ['providerSearch'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_provider'),
'type' => Database::INDEX_KEY,
'attributes' => ['provider'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['name'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['type'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_enabled_type'),
'type' => Database::INDEX_KEY,
'attributes' => ['enabled','type'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
]
],
],
'messages' => [
'$collection' => ID::custom(DATABASE::METADATA),
'$id' => ID::custom('messages'),
'name' => 'Messages',
'attributes' => [
[
'$id' => ID::custom('providerType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('description'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 256,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('status'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => 'processing',
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('data'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => ['json'],
],
[
'$id' => ID::custom('topics'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 21845,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('users'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 21845,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('targets'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 21845,
'signed' => true,
'required' => false,
'default' => [],
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('scheduledAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('deliveredAt'),
'type' => Database::VAR_DATETIME,
'format' => '',
'size' => 0,
'signed' => false,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['datetime'],
],
[
'$id' => ID::custom('deliveryErrors'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 65535,
'signed' => true,
'required' => false,
'default' => null,
'array' => true,
'filters' => [],
],
[
'$id' => ID::custom('deliveredTotal'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => ['messageSearch'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [],
],
],
],
'topics' => [
'$collection' => ID::custom(DATABASE::METADATA),
'$id' => ID::custom('topics'),
'name' => 'Topics',
'attributes' => [
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('description'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 2048,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('total'),
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => true,
'required' => false,
'default' => 0,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('targets'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => ['subQueryTopicTargets'],
],
[
'$id' => ID::custom('search'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,
'signed' => true,
'required' => false,
'default' => '',
'array' => false,
'filters' => ['topicSearch'],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_name'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['name'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_search'),
'type' => Database::INDEX_FULLTEXT,
'attributes' => ['search'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
]
],
],
'subscribers' => [
'$collection' => ID::custom(DATABASE::METADATA),
'$id' => ID::custom('subscribers'),
'name' => 'Subscribers',
'attributes' => [
[
'$id' => ID::custom('targetId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('targetInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('topicId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('topicInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('providerType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 128,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_targetId'),
'type' => Database::INDEX_KEY,
'attributes' => ['targetId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_targetInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['targetInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_userId'),
'type' => Database::INDEX_KEY,
'attributes' => ['userId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_userInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['userInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_topicId'),
'type' => Database::INDEX_KEY,
'attributes' => ['topicId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_topicInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['topicInternalId'],
'lengths' => [],
'orders' => [],
]
],
],
'targets' => [
'$collection' => ID::custom(DATABASE::METADATA),
'$id' => ID::custom('targets'),
'name' => 'Targets',
'attributes' => [
[
'$id' => ID::custom('userId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('userInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('providerType'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('providerId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('providerInternalId'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('identifier'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => ID::custom('name'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => Database::LENGTH_KEY,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => ID::custom('_key_userId'),
'type' => Database::INDEX_KEY,
'attributes' => ['userId'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_userInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['userInternalId'],
'lengths' => [],
'orders' => [Database::ORDER_ASC],
],
[
'$id' => ID::custom('_key_providerId'),
'type' => Database::INDEX_KEY,
'attributes' => ['providerId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_providerInternalId'),
'type' => Database::INDEX_KEY,
'attributes' => ['providerInternalId'],
'lengths' => [],
'orders' => [],
],
[
'$id' => ID::custom('_key_identifier'),
'type' => Database::INDEX_UNIQUE,
'attributes' => ['identifier'],
'lengths' => [],
'orders' => [],
],
],
],
];
$projectCollections = array_merge([
'databases' => [
@ -3412,7 +4024,7 @@ $consoleCollections = array_merge([
'filters' => ['json'],
],
[
'$id' => ID::custom('authProviders'),
'$id' => ID::custom('oAuthProviders'),
'type' => Database::VAR_STRING,
'format' => '',
'size' => 16384,

View file

@ -103,6 +103,16 @@ return [
'description' => 'This method was not fully implemented yet. If you believe this is a mistake, please upgrade your Appwrite server version.',
'code' => 405,
],
Exception::GENERAL_INVALID_EMAIL => [
'name' => Exception::GENERAL_INVALID_EMAIL,
'description' => 'Value must be a valid email address.',
'code' => 400,
],
Exception::GENERAL_INVALID_PHONE => [
'name' => Exception::GENERAL_INVALID_PHONE,
'description' => 'Value must be a valid phone number. Format this number with a leading \'+\' and a country code, e.g., +16175551212.',
'code' => 400,
],
/** User Errors */
Exception::USER_COUNT_EXCEEDED => [
@ -251,6 +261,16 @@ return [
'description' => 'User phone is already verified',
'code' => 409
],
Exception::USER_TARGET_NOT_FOUND => [
'name' => Exception::USER_TARGET_NOT_FOUND,
'description' => 'The target could not be found.',
'code' => 404,
],
Exception::USER_TARGET_ALREADY_EXISTS => [
'name' => Exception::USER_TARGET_ALREADY_EXISTS,
'description' => 'A target with the same ID already exists.',
'code' => 409,
],
/** Teams */
Exception::TEAM_NOT_FOUND => [
@ -773,4 +793,83 @@ return [
'code' => 503,
'publish' => false
],
/** Providers */
Exception::PROVIDER_NOT_FOUND => [
'name' => Exception::PROVIDER_NOT_FOUND,
'description' => 'Provider with the requested ID could not be found.',
'code' => 404,
],
Exception::PROVIDER_ALREADY_EXISTS => [
'name' => Exception::PROVIDER_ALREADY_EXISTS,
'description' => 'Provider with the requested ID already exists.',
'code' => 409,
],
Exception::PROVIDER_INCORRECT_TYPE => [
'name' => Exception::PROVIDER_INCORRECT_TYPE,
'description' => 'Provider with the requested ID is of incorrect type: ',
'code' => 400,
],
/** Topics */
Exception::TOPIC_NOT_FOUND => [
'name' => Exception::TOPIC_NOT_FOUND,
'description' => 'Topic with the request ID could not be found.',
'code' => 404,
],
Exception::TOPIC_ALREADY_EXISTS => [
'name' => Exception::TOPIC_ALREADY_EXISTS,
'description' => 'Topic with the request ID already exists.',
'code' => 409,
],
/** Subscribers */
Exception::SUBSCRIBER_NOT_FOUND => [
'name' => Exception::SUBSCRIBER_NOT_FOUND,
'description' => 'Subscriber with the request ID could not be found.',
'code' => 404,
],
Exception::SUBSCRIBER_ALREADY_EXISTS => [
'name' => Exception::SUBSCRIBER_ALREADY_EXISTS,
'description' => 'Subscriber with the request ID already exists.',
'code' => 409,
],
/** Messages */
Exception::MESSAGE_NOT_FOUND => [
'name' => Exception::MESSAGE_NOT_FOUND,
'description' => 'Message with the requested ID could not be found.',
'code' => 404,
],
Exception::MESSAGE_MISSING_TARGET => [
'name' => Exception::MESSAGE_MISSING_TARGET,
'description' => 'Message with the requested ID is missing a target (Topics or Users or Targets).',
'code' => 400,
],
Exception::MESSAGE_ALREADY_SENT => [
'name' => Exception::MESSAGE_ALREADY_SENT,
'description' => 'Message with the requested ID has already been sent.',
'code' => 400,
],
Exception::MESSAGE_ALREADY_SCHEDULED => [
'name' => Exception::MESSAGE_ALREADY_SCHEDULED,
'description' => 'Message with the requested ID has already been scheduled for delivery.',
'code' => 400,
],
Exception::MESSAGE_TARGET_NOT_EMAIL => [
'name' => Exception::MESSAGE_TARGET_NOT_EMAIL,
'description' => 'Message with the target ID is not an email target:',
'code' => 400,
],
Exception::MESSAGE_TARGET_NOT_SMS => [
'name' => Exception::MESSAGE_TARGET_NOT_SMS,
'description' => 'Message with the target ID is not an SMS target:',
'code' => 400,
],
Exception::MESSAGE_TARGET_NOT_PUSH => [
'name' => Exception::MESSAGE_TARGET_NOT_PUSH,
'description' => 'Message with the target ID is not a push target:',
'code' => 400,
],
];

View file

@ -44,6 +44,20 @@ return [
'$description' => 'This event triggers when a verification token for a user is validated.'
],
],
'targets' => [
'$model' => Response::MODEL_TARGET,
'$resource' => true,
'$description' => 'This event triggers on any user\'s target event.',
'create' => [
'$description' => 'This event triggers when a user\'s target is created.',
],
'update' => [
'$description' => 'This event triggers when a user\'s target is updated.',
],
'delete' => [
'$description' => 'This event triggers when a user\'s target is deleted.',
],
],
'create' => [
'$description' => 'This event triggers when a user is created.'
],
@ -237,6 +251,56 @@ return [
'$description' => 'This event triggers when a function is updated.',
]
],
'messages' => [
'$model' => Response::MODEL_MESSAGE,
'$resource' => true,
'$description' => 'This event triggers on any messaging event.',
'create' => [
'$description' => 'This event triggers when a message is created.',
],
'update' => [
'$description' => 'This event triggers when a message is updated.',
],
],
'topics' => [
'$model' => Response::MODEL_TOPIC,
'$resource' => true,
'$description' => 'This event triggers on any topic event.',
'create' => [
'$description' => 'This event triggers when a topic is created.',
],
'update' => [
'$description' => 'This event triggers when a topic is updated.',
],
'delete' => [
'$description' => 'This event triggers when a topic is deleted.'
],
'subscribers' => [
'$model' => Response::MODEL_SUBSCRIBER,
'$resource' => true,
'$description' => 'This event triggers on any subscriber event.',
'create' => [
'$description' => 'This event triggers when a subscriber is created.',
],
'delete' => [
'$description' => 'This event triggers when a subscriber is deleted.'
],
],
],
'providers' => [
'$model' => Response::MODEL_PROVIDER,
'$resource' => true,
'$description' => 'This event triggers on any provider event.',
'create' => [
'$description' => 'This event triggers when a provider is created.',
],
'update' => [
'$description' => 'This event triggers when a provider is updated.',
],
'delete' => [
'$description' => 'This event triggers when a provider is deleted.'
],
],
'rules' => [
'$model' => Response::MODEL_PROXY_RULE,
'$resource' => true,

View file

@ -15,7 +15,7 @@ return [
[
'key' => 'web',
'name' => 'Web',
'version' => '13.0.0',
'version' => '13.0.1',
'url' => 'https://github.com/appwrite/sdk-for-web',
'package' => 'https://www.npmjs.com/package/appwrite',
'enabled' => true,
@ -63,7 +63,7 @@ return [
[
'key' => 'flutter',
'name' => 'Flutter',
'version' => '11.0.0',
'version' => '11.0.1',
'url' => 'https://github.com/appwrite/sdk-for-flutter',
'package' => 'https://pub.dev/packages/appwrite',
'enabled' => true,
@ -81,7 +81,7 @@ return [
[
'key' => 'apple',
'name' => 'Apple',
'version' => '4.0.1',
'version' => '4.0.2',
'url' => 'https://github.com/appwrite/sdk-for-apple',
'package' => 'https://github.com/appwrite/sdk-for-apple',
'enabled' => true,
@ -116,7 +116,7 @@ return [
[
'key' => 'android',
'name' => 'Android',
'version' => '4.0.0',
'version' => '4.0.1',
'url' => 'https://github.com/appwrite/sdk-for-android',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-android',
'enabled' => true,
@ -203,7 +203,7 @@ return [
[
'key' => 'cli',
'name' => 'Command Line',
'version' => '4.1.0',
'version' => '4.2.0',
'url' => 'https://github.com/appwrite/sdk-for-cli',
'package' => 'https://www.npmjs.com/package/appwrite-cli',
'enabled' => true,
@ -231,7 +231,7 @@ return [
[
'key' => 'nodejs',
'name' => 'Node.js',
'version' => '11.0.0',
'version' => '11.1.0',
'url' => 'https://github.com/appwrite/sdk-for-node',
'package' => 'https://www.npmjs.com/package/node-appwrite',
'enabled' => true,
@ -249,7 +249,7 @@ return [
[
'key' => 'deno',
'name' => 'Deno',
'version' => '9.0.0',
'version' => '9.1.0',
'url' => 'https://github.com/appwrite/sdk-for-deno',
'package' => 'https://deno.land/x/appwrite',
'enabled' => true,
@ -267,7 +267,7 @@ return [
[
'key' => 'php',
'name' => 'PHP',
'version' => '10.0.0',
'version' => '10.1.0',
'url' => 'https://github.com/appwrite/sdk-for-php',
'package' => 'https://packagist.org/packages/appwrite/appwrite',
'enabled' => true,
@ -285,7 +285,7 @@ return [
[
'key' => 'python',
'name' => 'Python',
'version' => '4.0.0',
'version' => '4.1.0',
'url' => 'https://github.com/appwrite/sdk-for-python',
'package' => 'https://pypi.org/project/appwrite/',
'enabled' => true,
@ -303,7 +303,7 @@ return [
[
'key' => 'ruby',
'name' => 'Ruby',
'version' => '10.0.0',
'version' => '10.1.0',
'url' => 'https://github.com/appwrite/sdk-for-ruby',
'package' => 'https://rubygems.org/gems/appwrite',
'enabled' => true,
@ -321,7 +321,7 @@ return [
[
'key' => 'go',
'name' => 'Go',
'version' => '3.0.0',
'version' => '3.1.0',
'url' => 'https://github.com/appwrite/sdk-for-go',
'package' => '',
'enabled' => false,
@ -339,7 +339,7 @@ return [
[
'key' => 'java',
'name' => 'Java',
'version' => '3.0.0',
'version' => '3.1.0',
'url' => 'https://github.com/appwrite/sdk-for-java',
'package' => '',
'enabled' => false,
@ -357,7 +357,7 @@ return [
[
'key' => 'dotnet',
'name' => '.NET',
'version' => '0.6.0',
'version' => '0.7.0',
'url' => 'https://github.com/appwrite/sdk-for-dotnet',
'package' => 'https://www.nuget.org/packages/Appwrite',
'enabled' => true,
@ -375,7 +375,7 @@ return [
[
'key' => 'dart',
'name' => 'Dart',
'version' => '10.0.0',
'version' => '10.1.0',
'url' => 'https://github.com/appwrite/sdk-for-dart',
'package' => 'https://pub.dev/packages/dart_appwrite',
'enabled' => true,
@ -393,7 +393,7 @@ return [
[
'key' => 'kotlin',
'name' => 'Kotlin',
'version' => '4.0.0',
'version' => '4.1.0',
'url' => 'https://github.com/appwrite/sdk-for-kotlin',
'package' => 'https://search.maven.org/artifact/io.appwrite/sdk-for-kotlin',
'enabled' => true,
@ -415,7 +415,7 @@ return [
[
'key' => 'swift',
'name' => 'Swift',
'version' => '4.0.1',
'version' => '4.1.0',
'url' => 'https://github.com/appwrite/sdk-for-swift',
'package' => 'https://github.com/appwrite/sdk-for-swift',
'enabled' => true,

View file

@ -21,6 +21,10 @@ $member = [
'avatars.read',
'execution.read',
'execution.write',
'targets.read',
'targets.write',
'subscribers.write',
'subscribers.read',
'assistant.read',
];
@ -60,6 +64,16 @@ $admins = [
'migrations.write',
'vcs.read',
'vcs.write',
'targets.read',
'targets.write',
'providers.write',
'providers.read',
'messages.write',
'messages.read',
'topics.write',
'topics.read',
'subscribers.write',
'subscribers.read'
];
return [

View file

@ -76,6 +76,36 @@ return [ // List of publicly visible scopes
'health.read' => [
'description' => 'Access to read your project\'s health status',
],
'providers.read' => [
'description' => 'Access to read your project\'s providers',
],
'providers.write' => [
'description' => 'Access to create, update, and delete your project\'s providers',
],
'messages.read' => [
'description' => 'Access to read your project\'s messages',
],
'messages.write' => [
'description' => 'Access to create, update, and delete your project\'s messages',
],
'topics.read' => [
'description' => 'Access to read your project\'s topics',
],
'topics.write' => [
'description' => 'Access to create, update, and delete your project\'s topics',
],
'subscribers.read' => [
'description' => 'Access to read your project\'s subscribers',
],
'subscribers.write' => [
'description' => 'Access to create, update, and delete your project\'s subscribers',
],
'targets.read' => [
'description' => 'Access to read your project\'s targets',
],
'targets.write' => [
'description' => 'Access to create, update, and delete your project\'s targets',
],
'rules.read' => [
'description' => 'Access to read your project\'s proxy rules',
],

View file

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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 212b7429926d097a31ed71d2410e39c600c56f3b
Subproject commit 94e4c1a73024b0e974fbe6077674281f6e973c9d

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';
@ -148,7 +148,22 @@ App::post('/v1/account')
'accessedAt' => DateTime::now(),
]);
$user->removeAttribute('$internalId');
Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
$user = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
try {
$target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'providerType' => MESSAGE_TYPE_EMAIL,
'identifier' => $email,
])));
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
} catch (Duplicate) {
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]);
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@ -299,7 +314,7 @@ App::get('/v1/account/sessions/oauth2/:provider')
->label('sdk.methodType', 'webAuth')
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('providers'), fn($node) => (!$node['mock'])))) . '.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn($node) => (!$node['mock'])))) . '.')
->param('success', '', fn($clients) => new Host($clients), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
->param('failure', '', fn($clients) => new Host($clients), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['clients'])
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
@ -311,14 +326,14 @@ App::get('/v1/account/sessions/oauth2/:provider')
$protocol = $request->getProtocol();
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$providerEnabled = $project->getAttribute('authProviders', [])[$provider . 'Enabled'] ?? false;
$providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
if (!$providerEnabled) {
throw new Exception(Exception::PROJECT_PROVIDER_DISABLED, 'This provider is disabled. Please enable the provider from your ' . APP_NAME . ' console to continue.');
}
$appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
if (!empty($appSecret) && isset($appSecret['version'])) {
$key = App::getEnv('_APP_OPENSSL_KEY_V' . $appSecret['version']);
@ -358,7 +373,7 @@ App::get('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('scope', 'public')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->param('state', '', new Text(2048), 'Login state params.', true)
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
@ -391,7 +406,7 @@ App::post('/v1/account/sessions/oauth2/callback/:provider/:projectId')
->label('origin', '*')
->label('docs', false)
->param('projectId', '', new Text(1024), 'Project ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->param('state', '', new Text(2048), 'Login state params.', true)
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
@ -430,7 +445,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->label('docs', false)
->label('usage.metric', 'sessions.{scope}.requests.create')
->label('usage.params', ['provider:{request.provider}'])
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'OAuth2 provider.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 provider.')
->param('code', '', new Text(2048, 0), 'OAuth2 code. This is a temporary code that the will be later exchanged for an access token.', true)
->param('state', '', new Text(2048), 'OAuth2 state params.', true)
->param('error', '', new Text(2048, 0), 'Error code returned from the OAuth2 provider.', true)
@ -448,9 +463,9 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$callback = $protocol . '://' . $request->getHostname() . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => ''];
$validateURL = new URL();
$appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
$providerEnabled = $project->getAttribute('authProviders', [])[$provider . 'Enabled'] ?? false;
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
@ -458,7 +473,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$providers = Config::getParam('providers');
$providers = Config::getParam('oAuthProviders');
$providerName = $providers[$provider]['name'] ?? '';
/** @var Appwrite\Auth\OAuth2 $oauth2 */
@ -656,7 +671,18 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
'accessedAt' => DateTime::now(),
]);
$user->removeAttribute('$internalId');
Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
$userDoc = Authorization::skip(fn() => $dbForProject->createDocument('users', $user));
$dbForProject->createDocument('targets', new Document([
'$permissions' => [
Permission::read(Role::any()),
Permission::update(Role::user($user->getId())),
Permission::delete(Role::user($user->getId())),
],
'userId' => $userDoc->getId(),
'userInternalId' => $userDoc->getInternalId(),
'providerType' => MESSAGE_TYPE_EMAIL,
'identifier' => $email,
]));
} catch (Duplicate) {
$failureRedirect(Exception::USER_ALREADY_EXISTS);
}
@ -1235,8 +1261,7 @@ App::post('/v1/account/sessions/phone')
->inject('queueForEvents')
->inject('queueForMessaging')
->inject('locale')
->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, EventPhone $queueForMessaging, Locale $locale) {
->action(function (string $userId, string $phone, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
@ -1286,6 +1311,21 @@ App::post('/v1/account/sessions/phone')
$user->removeAttribute('$internalId');
Authorization::skip(fn () => $dbForProject->createDocument('users', $user));
try {
$target = Authorization::skip(fn() => $dbForProject->createDocument('targets', new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'providerType' => MESSAGE_TYPE_SMS,
'identifier' => $phone,
])));
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
} catch (Duplicate) {
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
]);
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
}
$dbForProject->deleteCachedDocument('users', $user->getId());
}
$secret = Auth::codeGenerator();
@ -1323,9 +1363,19 @@ App::post('/v1/account/sessions/phone')
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
$messageDoc = new Document([
'$id' => $token->getId(),
'data' => [
'content' => $message,
],
]);
$queueForMessaging
->setRecipient($phone)
->setMessage($message)
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType(MESSAGE_TYPE_SMS)
->setProject($project)
->trigger();
$queueForEvents->setPayload(
@ -1644,6 +1694,81 @@ App::post('/v1/account/jwt')
])]), Response::MODEL_JWT);
});
App::post('/v1/account/targets/push')
->desc('Create Account\'s push target')
->groups(['api', 'account'])
->label('error', __DIR__ . '/../../views/general/error.phtml')
->label('audits.event', 'target.create')
->label('audits.resource', 'target/response.$id')
->label('event', 'users.[userId].targets.[targetId].create')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION])
->label('sdk.namespace', 'account')
->label('sdk.method', 'createPushTarget')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET)
->label('docs', false)
->param('targetId', '', new CustomId(), 'Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.')
->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
->inject('queueForEvents')
->inject('user')
->inject('request')
->inject('response')
->inject('dbForProject')
->action(function (string $targetId, string $providerId, string $identifier, Event $queueForEvents, Document $user, Request $request, Response $response, Database $dbForProject) {
$targetId = $targetId == 'unique()' ? ID::unique() : $targetId;
$provider = Authorization::skip(fn () => $dbForProject->getDocument('providers', $providerId));
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
}
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = Authorization::skip(fn () => $dbForProject->getDocument('targets', $targetId));
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
$detector = new Detector($request->getUserAgent());
$detector->skipBotDetection(); // OPTIONAL: If called, bot detection will completely be skipped (bots will be detected as regular devices then)
$device = $detector->getDevice();
try {
$target = $dbForProject->createDocument('targets', new Document([
'$id' => $targetId,
'$permissions' => [
Permission::read(Role::user($user->getId())),
Permission::update(Role::user($user->getId())),
],
'providerId' => $providerId ?? null,
'providerInternalId' => $provider->getInternalId() ?? null,
'providerType' => MESSAGE_TYPE_PUSH,
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'identifier' => $identifier,
'name' => "{$device['deviceBrand']} {$device['deviceModel']}"
]));
} catch (Duplicate) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
$dbForProject->deleteCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('targetId', $target->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($target, Response::MODEL_TARGET);
});
App::get('/v1/account')
->desc('Get account')
->groups(['api', 'account'])
@ -1969,6 +2094,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
@ -1993,8 +2119,25 @@ App::patch('/v1/account/email')
->setAttribute('passwordUpdate', DateTime::now());
}
$target = Authorization::skip(fn () => $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]));
if ($target instanceof Document && !$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
try {
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email)));
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@ -2039,6 +2182,16 @@ App::patch('/v1/account/phone')
throw new Exception(Exception::USER_INVALID_CREDENTIALS);
}
$target = Authorization::skip(fn () => $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
]));
if ($target instanceof Document && !$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
$oldPhone = $user->getAttribute('phone');
$user
->setAttribute('phone', $phone)
->setAttribute('phoneVerification', false) // After this user needs to confirm phone number again
@ -2054,6 +2207,15 @@ App::patch('/v1/account/phone')
try {
$user = $dbForProject->withRequestTimestamp($requestTimestamp, fn () => $dbForProject->updateDocument('users', $user->getId(), $user));
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
Authorization::skip(fn () => $dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $phone)));
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS);
}
@ -2264,8 +2426,8 @@ App::patch('/v1/account/sessions/:sessionId')
$provider = $session->getAttribute('provider');
$refreshToken = $session->getAttribute('providerRefreshToken');
$appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);
@ -2877,10 +3039,9 @@ App::post('/v1/account/verification/phone')
->inject('queueForMessaging')
->inject('project')
->inject('locale')
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, EventPhone $queueForMessaging, Document $project, Locale $locale) {
->action(function (Request $request, Response $response, Document $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED);
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
if (empty($user->getAttribute('phone'))) {
@ -2929,11 +3090,19 @@ App::post('/v1/account/verification/phone')
$message = $message->setParam('{{token}}', $secret);
$message = $message->render();
$messageDoc = new Document([
'$id' => $verification->getId(),
'data' => [
'content' => $message,
],
]);
$queueForMessaging
->setRecipient($user->getAttribute('phone'))
->setMessage($message)
->trigger()
;
->setMessage($messageDoc)
->setRecipients([$user->getAttribute('phone')])
->setProviderType(MESSAGE_TYPE_SMS)
->setProject($project)
->trigger();
$queueForEvents
->setParam('userId', $user->getId())
@ -3010,3 +3179,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

@ -84,8 +84,8 @@ $getUserGitHub = function (string $userId, Document $project, Database $dbForPro
$accessTokenExpiry = $gitHubSession->getAttribute('providerAccessTokenExpiry');
$refreshToken = $gitHubSession->getAttribute('providerRefreshToken');
$appId = $project->getAttribute('authProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('authProviders', [])[$provider . 'Secret'] ?? '{}';
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$className = 'Appwrite\\Auth\\OAuth2\\' . \ucfirst($provider);

View file

@ -1561,7 +1561,7 @@ App::post('/v1/databases/:databaseId/collections/:collectionId/attributes/dateti
->param('collectionId', '', new UID(), 'Collection ID. You can create a new collection using the Database service [server integration](https://appwrite.io/docs/server/databases#databasesCreateCollection).')
->param('key', '', new Key(), 'Attribute Key.')
->param('required', null, new Boolean(), 'Is attribute required?')
->param('default', null, new DatetimeValidator(), 'Default value for the attribute in ISO 8601 format. Cannot be set when attribute is required.', true)
->param('default', null, new DatetimeValidator(), 'Default value for the attribute in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Cannot be set when attribute is required.', true)
->param('array', false, new Boolean(), 'Is attribute an array?', true)
->inject('response')
->inject('dbForProject')

File diff suppressed because it is too large Load diff

View file

@ -159,7 +159,7 @@ App::post('/v1/projects')
'legalTaxId' => ID::custom($legalTaxId),
'services' => new stdClass(),
'platforms' => null,
'authProviders' => [],
'oAuthProviders' => [],
'webhooks' => null,
'keys' => null,
'auths' => $auths,
@ -598,7 +598,7 @@ App::patch('/v1/projects/:projectId/oauth2')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_PROJECT)
->param('projectId', '', new UID(), 'Project unique ID.')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('providers')), true), 'Provider Name')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'Provider Name')
->param('appId', null, new Text(256), 'Provider app ID. Max length: 256 chars.', true)
->param('secret', null, new text(512), 'Provider secret key. Max length: 512 chars.', true)
->param('enabled', null, new Boolean(), 'Provider status. Set to \'false\' to disable new session creation.', true)
@ -612,7 +612,7 @@ App::patch('/v1/projects/:projectId/oauth2')
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$providers = $project->getAttribute('authProviders', []);
$providers = $project->getAttribute('oAuthProviders', []);
if ($appId !== null) {
$providers[$provider . 'Appid'] = $appId;
@ -626,7 +626,7 @@ App::patch('/v1/projects/:projectId/oauth2')
$providers[$provider . 'Enabled'] = $enabled;
}
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('authProviders', $providers));
$project = $dbForConsole->updateDocument('projects', $project->getId(), $project->setAttribute('oAuthProviders', $providers));
$response->dynamic($project, Response::MODEL_PROJECT);
});
@ -1151,7 +1151,7 @@ App::post('/v1/projects/:projectId/keys')
->param('projectId', '', new UID(), 'Project unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in ISO 8601 format. Use null for unlimited expiration.', true)
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) {
@ -1268,7 +1268,7 @@ App::put('/v1/projects/:projectId/keys/:keyId')
->param('keyId', '', new UID(), 'Key unique ID.')
->param('name', null, new Text(128), 'Key name. Max length: 128 chars.')
->param('scopes', null, new ArrayList(new WhiteList(array_keys(Config::getParam('scopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'Key scopes list. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' events are allowed.')
->param('expire', null, new DatetimeValidator(), 'Expiration time in ISO 8601 format. Use null for unlimited expiration.', true)
->param('expire', null, new DatetimeValidator(), 'Expiration time in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. Use null for unlimited expiration.', true)
->inject('response')
->inject('dbForConsole')
->action(function (string $projectId, string $keyId, string $name, array $scopes, ?string $expire, Response $response, Database $dbForConsole) {

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;
@ -388,7 +388,7 @@ App::post('/v1/teams/:teamId/memberships')
->inject('queueForMails')
->inject('queueForMessaging')
->inject('queueForEvents')
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, EventPhone $queueForMessaging, Event $queueForEvents) {
->action(function (string $teamId, string $email, string $userId, string $phone, array $roles, string $url, string $name, Response $response, Document $project, Document $user, Database $dbForProject, Locale $locale, Mail $queueForMails, Messaging $queueForMessaging, Event $queueForEvents) {
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
@ -628,6 +628,10 @@ App::post('/v1/teams/:teamId/memberships')
->trigger()
;
} elseif (!empty($phone)) {
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
}
$message = Template::fromFile(__DIR__ . '/../../config/locale/templates/sms-base.tpl');
$customTemplate = $project->getAttribute('templates', [])['sms.invitation-' . $locale->default] ?? [];
@ -638,9 +642,18 @@ App::post('/v1/teams/:teamId/memberships')
$message = $message->setParam('{{token}}', $url);
$message = $message->render();
$messageDoc = new Document([
'$id' => ID::unique(),
'data' => [
'content' => $message,
],
]);
$queueForMessaging
->setRecipient($phone)
->setMessage($message)
->setMessage($messageDoc)
->setRecipients([$phone])
->setProviderType('SMS')
->setProject($project)
->trigger();
}
}

View file

@ -9,6 +9,7 @@ use Appwrite\Event\Event;
use Appwrite\Network\Validator\Email;
use Appwrite\Utopia\Database\Validator\CustomId;
use Appwrite\Utopia\Database\Validator\Queries\Identities;
use Appwrite\Utopia\Database\Validator\Queries\Targets;
use Utopia\Database\Validator\Queries;
use Appwrite\Utopia\Database\Validator\Queries\Users;
use Utopia\Database\Validator\Query\Limit;
@ -98,6 +99,42 @@ function createUser(string $hash, mixed $hashOptions, string $userId, ?string $e
'memberships' => null,
'search' => implode(' ', [$userId, $email, $phone, $name]),
]));
if ($email) {
try {
$target = $dbForProject->createDocument('targets', new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'providerType' => 'email',
'identifier' => $email,
]));
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
} catch (Duplicate) {
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]);
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
}
}
if ($phone) {
try {
$target = $dbForProject->createDocument('targets', new Document([
'userId' => $user->getId(),
'userInternalId' => $user->getInternalId(),
'providerType' => 'sms',
'identifier' => $phone,
]));
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $target]);
} catch (Duplicate) {
$existingTarget = $dbForProject->findOne('targets', [
Query::equal('identifier', [$phone]),
]);
$user->setAttribute('targets', [...$user->getAttribute('targets', []), $existingTarget]);
}
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_ALREADY_EXISTS);
}
@ -379,6 +416,98 @@ App::post('/v1/users/scrypt-modified')
->dynamic($user, Response::MODEL_USER);
});
App::post('/v1/users/:userId/targets')
->desc('Create User Target')
->groups(['api', 'users'])
->label('audits.event', 'target.create')
->label('audits.resource', 'target/response.$id')
->label('event', 'users.[userId].targets.[targetId].create')
->label('scope', 'targets.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'createTarget')
->label('sdk.description', '/docs/references/users/create-target.md')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET)
->param('targetId', '', new CustomId(), 'Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.')
->param('userId', '', new UID(), 'User ID.')
->param('providerType', '', new WhiteList([MESSAGE_TYPE_EMAIL, MESSAGE_TYPE_SMS, MESSAGE_TYPE_PUSH]), 'The target provider type. Can be one of the following: `email`, `sms` or `push`.')
->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)')
->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true)
->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true)
->inject('queueForEvents')
->inject('response')
->inject('dbForProject')
->action(function (string $targetId, string $userId, string $providerType, string $identifier, string $providerId, string $name, Event $queueForEvents, Response $response, Database $dbForProject) {
$targetId = $targetId == 'unique()' ? ID::unique() : $targetId;
$provider = new Document();
if ($providerType === MESSAGE_TYPE_PUSH) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
}
}
switch ($providerType) {
case 'email':
$validator = new Email();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
break;
case MESSAGE_TYPE_SMS:
$validator = new Phone();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_PHONE);
}
break;
case MESSAGE_TYPE_PUSH:
break;
default:
throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
}
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $dbForProject->getDocument('targets', $targetId);
if (!$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
try {
$target = $dbForProject->createDocument('targets', new Document([
'$id' => $targetId,
'providerId' => $providerId ?? null,
'providerInternalId' => $provider->getInternalId() ?? null,
'providerType' => $providerType,
'userId' => $userId,
'userInternalId' => $user->getInternalId(),
'identifier' => $identifier,
'name' => ($name !== '') ? $name : null,
]));
} catch (Duplicate) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
$dbForProject->deleteCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('targetId', $target->getId());
$response
->setStatusCode(Response::STATUS_CODE_CREATED)
->dynamic($target, Response::MODEL_TARGET);
});
App::get('/v1/users')
->desc('List users')
->groups(['api', 'users'])
@ -482,6 +611,38 @@ App::get('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::get('/v1/users/:userId/targets/:targetId')
->desc('Get User Target')
->groups(['api', 'users'])
->label('scope', 'targets.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'getTarget')
->label('sdk.description', '/docs/references/users/get-user-target.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET)
->param('userId', '', new UID(), 'User ID.')
->param('targetId', '', new UID(), 'Target ID.')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $user->find('$id', $targetId, 'targets');
if (empty($target)) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
$response->dynamic($target, Response::MODEL_TARGET);
});
App::get('/v1/users/:userId/sessions')
->desc('List user sessions')
->groups(['api', 'users'])
@ -646,6 +807,53 @@ App::get('/v1/users/:userId/logs')
]), Response::MODEL_LOG_LIST);
});
App::get('/v1/users/:userId/targets')
->desc('List User Targets')
->groups(['api', 'users'])
->label('scope', 'targets.read')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'listTargets')
->label('sdk.description', '/docs/references/users/list-user-targets.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET_LIST)
->param('userId', '', new UID(), 'User ID.')
->param('queries', [], new Targets(), 'Array of query strings generated using the Query class provided by the SDK. [Learn more about queries](https://appwrite.io/docs/queries). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' queries are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long. You may filter on the following attributes: ' . implode(', ', Users::ALLOWED_ATTRIBUTES), true)
->inject('response')
->inject('dbForProject')
->action(function (string $userId, array $queries, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$queries = Query::parseQueries($queries);
$queries[] = Query::equal('userId', [$userId]);
// Get cursor document if there was a cursor query
$cursor = Query::getByType($queries, [Query::TYPE_CURSORAFTER, Query::TYPE_CURSORBEFORE]);
$cursor = reset($cursor);
if ($cursor) {
$targetId = $cursor->getValue();
$cursorDocument = $dbForProject->getDocument('targets', $targetId);
if ($cursorDocument->isEmpty()) {
throw new Exception(Exception::GENERAL_CURSOR_NOT_FOUND, "Target '{$targetId}' for the 'cursor' value not found.");
}
$cursor->setValue($cursorDocument);
}
$response->dynamic(new Document([
'targets' => $dbForProject->find('targets', $queries),
'total' => $dbForProject->count('targets', $queries, APP_LIMIT_COUNT),
]), Response::MODEL_TARGET_LIST);
});
App::get('/v1/users/identities')
->desc('List Identities')
->groups(['api', 'users'])
@ -949,6 +1157,16 @@ App::patch('/v1/users/:userId/email')
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
$target = $dbForProject->findOne('targets', [
Query::equal('identifier', [$email]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
$oldEmail = $user->getAttribute('email');
$user
->setAttribute('email', $email)
->setAttribute('emailVerification', false)
@ -957,6 +1175,15 @@ App::patch('/v1/users/:userId/email')
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldEmail, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $email));
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_EMAIL_ALREADY_EXISTS);
}
@ -994,13 +1221,32 @@ App::patch('/v1/users/:userId/phone')
throw new Exception(Exception::USER_NOT_FOUND);
}
$oldPhone = $user->getAttribute('phone');
$user
->setAttribute('phone', $number)
->setAttribute('phoneVerification', false)
;
$target = $dbForProject->findOne('targets', [
Query::equal('identifier', [$number]),
]);
if ($target instanceof Document && !$target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_ALREADY_EXISTS);
}
try {
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
/**
* @var Document $oldTarget
*/
$oldTarget = $user->find('identifier', $oldPhone, 'targets');
if ($oldTarget instanceof Document && !$oldTarget->isEmpty()) {
$dbForProject->updateDocument('targets', $oldTarget->getId(), $oldTarget->setAttribute('identifier', $number));
}
$dbForProject->deleteCachedDocument('users', $user->getId());
} catch (Duplicate $th) {
throw new Exception(Exception::USER_PHONE_ALREADY_EXISTS);
}
@ -1080,6 +1326,100 @@ App::patch('/v1/users/:userId/prefs')
$response->dynamic(new Document($prefs), Response::MODEL_PREFERENCES);
});
App::patch('/v1/users/:userId/targets/:targetId')
->desc('Update User target')
->groups(['api', 'users'])
->label('audits.event', 'target.update')
->label('audits.resource', 'target/{response.$id}')
->label('event', 'users.[userId].targets.[targetId].update')
->label('scope', 'targets.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'updateTarget')
->label('sdk.description', '/docs/references/users/update-target.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_TARGET)
->param('userId', '', new UID(), 'User ID.')
->param('targetId', '', new UID(), 'Target ID.')
->param('identifier', '', new Text(Database::LENGTH_KEY), 'The target identifier (token, email, phone etc.)', true)
->param('providerId', '', new UID(), 'Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used.', true)
->param('name', '', new Text(128), 'Target name. Max length: 128 chars. For example: My Awesome App Galaxy S23.', true)
->inject('queueForEvents')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, string $identifier, string $providerId, string $name, Event $queueForEvents, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $dbForProject->getDocument('targets', $targetId);
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
if ($user->getId() !== $target->getAttribute('userId')) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
if ($identifier) {
$providerType = $target->getAttribute('providerType');
switch ($providerType) {
case 'email':
$validator = new Email();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_EMAIL);
}
break;
case MESSAGE_TYPE_SMS:
$validator = new Phone();
if (!$validator->isValid($identifier)) {
throw new Exception(Exception::GENERAL_INVALID_PHONE);
}
break;
case MESSAGE_TYPE_PUSH:
break;
default:
throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
}
$target->setAttribute('identifier', $identifier);
}
if ($providerId) {
$provider = $dbForProject->getDocument('providers', $providerId);
if ($provider->isEmpty()) {
throw new Exception(Exception::PROVIDER_NOT_FOUND);
}
if ($provider->getAttribute('type') !== $target->getAttribute('providerType')) {
throw new Exception(Exception::PROVIDER_INCORRECT_TYPE);
}
$target->setAttribute('providerId', $provider->getId());
$target->setAttribute('providerInternalId', $provider->getInternalId());
}
if ($name) {
$target->setAttribute('name', $name);
}
$target = $dbForProject->updateDocument('targets', $target->getId(), $target);
$dbForProject->deleteCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('targetId', $target->getId());
$response
->dynamic($target, Response::MODEL_TARGET);
});
App::delete('/v1/users/:userId/sessions/:sessionId')
->desc('Delete user session')
->groups(['api', 'users'])
@ -1210,6 +1550,53 @@ App::delete('/v1/users/:userId')
$response->noContent();
});
App::delete('/v1/users/:userId/targets/:targetId')
->desc('Delete user target')
->groups(['api', 'users'])
->label('audits.event', 'target.delete')
->label('audits.resource', 'target/{request.$targetId}')
->label('event', 'users.[userId].targets.[targetId].delete')
->label('scope', 'targets.write')
->label('sdk.auth', [APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_ADMIN])
->label('sdk.namespace', 'users')
->label('sdk.method', 'deleteTarget')
->label('sdk.description', '/docs/references/users/delete-target.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_NONE)
->param('userId', '', new UID(), 'User ID.')
->param('targetId', '', new UID(), 'Target ID.')
->inject('queueForEvents')
->inject('response')
->inject('dbForProject')
->action(function (string $userId, string $targetId, Event $queueForEvents, Response $response, Database $dbForProject) {
$user = $dbForProject->getDocument('users', $userId);
if ($user->isEmpty()) {
throw new Exception(Exception::USER_NOT_FOUND);
}
$target = $dbForProject->getDocument('targets', $targetId);
if ($target->isEmpty()) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
if ($user->getId() !== $target->getAttribute('userId')) {
throw new Exception(Exception::USER_TARGET_NOT_FOUND);
}
$dbForProject->deleteDocument('targets', $target->getId());
$dbForProject->deleteCachedDocument('users', $user->getId());
$queueForEvents
->setParam('userId', $user->getId())
->setParam('targetId', $target->getId());
$response->noContent();
});
App::delete('/v1/users/identities/:identityId')
->desc('Delete Identity')
->groups(['api', 'users'])
@ -1251,7 +1638,7 @@ App::get('/v1/users/usage')
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_USAGE_USERS)
->param('range', '30d', new WhiteList(['24h', '7d', '30d', '90d'], true), 'Date range.', true)
->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn ($value) => "oauth-" . $value, \array_keys(Config::getParam('providers', [])))), true), 'Provider Name.', true)
->param('provider', '', new WhiteList(\array_merge(['email', 'anonymous'], \array_map(fn ($value) => "oauth-" . $value, \array_keys(Config::getParam('oAuthProviders', [])))), true), 'Provider Name.', true)
->inject('response')
->inject('dbForProject')
->inject('register')

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;
@ -47,13 +47,7 @@ use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\Database\Validator\Structure;
use Utopia\Locale\Locale;
use Utopia\DSN\DSN;
use Utopia\Messaging\Adapters\SMS\Mock;
use Appwrite\GraphQL\Promises\Adapter\Swoole;
use Utopia\Messaging\Adapters\SMS\Msg91;
use Utopia\Messaging\Adapters\SMS\Telesign;
use Utopia\Messaging\Adapters\SMS\TextMagic;
use Utopia\Messaging\Adapters\SMS\Twilio;
use Utopia\Messaging\Adapters\SMS\Vonage;
use Utopia\Registry\Registry;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Backblaze;
@ -83,6 +77,7 @@ use Utopia\Validator\Range;
use Utopia\Validator\IP;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
use Utopia\CLI\Console;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
@ -103,13 +98,14 @@ const APP_LIMIT_COMPRESSION = 20000000; //20MB
const APP_LIMIT_ARRAY_PARAMS_SIZE = 100; // Default maximum of how many elements can there be in API parameter that expects array value
const APP_LIMIT_ARRAY_ELEMENT_SIZE = 4096; // Default maximum length of element in array parameter represented by maximum URL length.
const APP_LIMIT_SUBQUERY = 1000;
const APP_LIMIT_SUBSCRIBERS_SUBQUERY = 1000000;
const APP_LIMIT_WRITE_RATE_DEFAULT = 60; // Default maximum write rate per rate period
const APP_LIMIT_WRITE_RATE_PERIOD_DEFAULT = 60; // Default maximum write rate period in seconds
const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return in list API calls
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 = 328;
const APP_CACHE_BUSTER = 329;
const APP_VERSION_STABLE = '1.4.13';
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
@ -173,6 +169,7 @@ const DELETE_TYPE_SESSIONS = 'sessions';
const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp';
const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource';
const DELETE_TYPE_SCHEDULES = 'schedules';
const DELETE_TYPE_TOPIC = 'topic';
// Compression type
const COMPRESSION_TYPE_NONE = 'none';
const COMPRESSION_TYPE_GZIP = 'gzip';
@ -193,6 +190,10 @@ const MAX_OUTPUT_CHUNK_SIZE = 2 * 1024 * 1024; // 2MB
// Function headers
const FUNCTION_ALLOWLIST_HEADERS_REQUEST = ['content-type', 'agent', 'content-length', 'host'];
const FUNCTION_ALLOWLIST_HEADERS_RESPONSE = ['content-type', 'content-length'];
// Message types
const MESSAGE_TYPE_EMAIL = 'email';
const MESSAGE_TYPE_SMS = 'sms';
const MESSAGE_TYPE_PUSH = 'push';
// Usage metrics
const METRIC_TEAMS = 'teams';
const METRIC_USERS = 'users';
@ -237,7 +238,7 @@ App::setMode(App::getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION));
Config::load('events', __DIR__ . '/config/events.php');
Config::load('auth', __DIR__ . '/config/auth.php');
Config::load('errors', __DIR__ . '/config/errors.php');
Config::load('providers', __DIR__ . '/config/providers.php');
Config::load('oAuthProviders', __DIR__ . '/config/oAuthProviders.php');
Config::load('platforms', __DIR__ . '/config/platforms.php');
Config::load('collections', __DIR__ . '/config/collections.php');
Config::load('runtimes', __DIR__ . '/config/runtimes.php');
@ -526,6 +527,107 @@ Database::addFilter(
}
);
Database::addFilter(
'subQueryTargets',
function (mixed $value) {
return null;
},
function (mixed $value, Document $document, Database $database) {
return Authorization::skip(fn() => $database
->find('targets', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBQUERY)
]));
}
);
Database::addFilter(
'subQueryTopicTargets',
function (mixed $value) {
return null;
},
function (mixed $value, Document $document, Database $database) {
$targetIds = Authorization::skip(fn () => \array_map(
fn ($document) => $document->getAttribute('targetId'),
$database
->find('subscribers', [
Query::equal('topicInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_SUBSCRIBERS_SUBQUERY)
])
));
if (\count($targetIds) > 0) {
return $database->find('targets', [Query::equal('$id', $targetIds)]);
}
return [];
}
);
Database::addFilter(
'providerSearch',
function (mixed $value, Document $provider) {
$searchValues = [
$provider->getId(),
$provider->getAttribute('name', ''),
$provider->getAttribute('provider', ''),
$provider->getAttribute('type', '')
];
$search = \implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);
Database::addFilter(
'topicSearch',
function (mixed $value, Document $topic) {
$searchValues = [
$topic->getId(),
$topic->getAttribute('name', ''),
$topic->getAttribute('description', ''),
];
$search = \implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);
Database::addFilter(
'messageSearch',
function (mixed $value, Document $message) {
$searchValues = [
$message->getId(),
$message->getAttribute('description', ''),
$message->getAttribute('status', ''),
];
$data = \json_decode($message->getAttribute('data', []), true);
$providerType = $message->getAttribute('providerType', '');
if ($providerType === MESSAGE_TYPE_EMAIL) {
$searchValues = \array_merge($searchValues, [$data['subject'], MESSAGE_TYPE_EMAIL]);
} elseif ($providerType === MESSAGE_TYPE_SMS) {
$searchValues = \array_merge($searchValues, [$data['content'], MESSAGE_TYPE_SMS]);
} else {
$searchValues = \array_merge($searchValues, [$data['title'], MESSAGE_TYPE_PUSH]);
}
$search = \implode(' ', \array_filter($searchValues));
return $search;
},
function (mixed $value) {
return $value;
}
);
/**
* DB Formats
*/
@ -906,7 +1008,7 @@ App::setResource('queue', function (Group $pools) {
return $pools->get('queue')->pop()->getResource();
}, ['pools']);
App::setResource('queueForMessaging', function (Connection $queue) {
return new Phone($queue);
return new Messaging($queue);
}, ['queue']);
App::setResource('queueForMails', function (Connection $queue) {
return new Mail($queue);
@ -1122,7 +1224,7 @@ App::setResource('console', function () {
],
'authWhitelistEmails' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_EMAILS', null)) : [],
'authWhitelistIPs' => (!empty(App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null))) ? \explode(',', App::getEnv('_APP_CONSOLE_WHITELIST_IPS', null)) : [],
'authProviders' => [
'oAuthProviders' => [
'githubEnabled' => true,
'githubSecret' => App::getEnv('_APP_CONSOLE_GITHUB_SECRET', ''),
'githubAppid' => App::getEnv('_APP_CONSOLE_GITHUB_APP_ID', '')
@ -1346,21 +1448,6 @@ App::setResource('passwordsDictionary', function ($register) {
return $register->get('passwordsDictionary');
}, ['register']);
App::setResource('sms', function () {
$dsn = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$user = $dsn->getUser();
$secret = $dsn->getPassword();
return match ($dsn->getHost()) {
'mock' => new Mock($user, $secret), // used for tests
'twilio' => new Twilio($user, $secret),
'text-magic' => new TextMagic($user, $secret),
'telesign' => new Telesign($user, $secret),
'msg91' => new Msg91($user, $secret),
'vonage' => new Vonage($user, $secret),
default => null
};
});
App::setResource('servers', function () {
$platforms = Config::getParam('platforms');

View file

@ -521,14 +521,20 @@ services:
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_SMS_FROM
- _APP_SMS_PROVIDER
appwrite-worker-migrations:
image: <?php echo $organization; ?>/<?php echo $image; ?>:<?php echo $version."\n"; ?>

View file

@ -9,10 +9,10 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Func;
use Appwrite\Event\Hamster;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Phone;
use Appwrite\Platform\Appwrite;
use Appwrite\Usage\Stats;
use Swoole\Runtime;
@ -129,7 +129,7 @@ Server::setResource('queueForDatabase', function (Connection $queue) {
return new EventDatabase($queue);
}, ['queue']);
Server::setResource('queueForMessaging', function (Connection $queue) {
return new Phone($queue);
return new Messaging($queue);
}, ['queue']);
Server::setResource('queueForMails', function (Connection $queue) {
return new Mail($queue);
@ -155,6 +155,9 @@ Server::setResource('queueForCertificates', function (Connection $queue) {
Server::setResource('queueForMigrations', function (Connection $queue) {
return new Migration($queue);
}, ['queue']);
Server::setResource('queueForHamster', function (Connection $queue) {
return new Hamster($queue);
}, ['queue']);
Server::setResource('logger', function (Registry $register) {
return $register->get('logger');
}, ['register']);

3
bin/worker-hamster Normal file
View file

@ -0,0 +1,3 @@
#!/bin/sh
php /usr/src/code/app/worker.php hamster $@

View file

@ -52,7 +52,7 @@
"utopia-php/database": "0.45.*",
"utopia-php/domains": "0.3.*",
"utopia-php/dsn": "0.1.*",
"utopia-php/framework": "0.31.0",
"utopia-php/framework": "0.31.1",
"utopia-php/image": "0.5.*",
"utopia-php/locale": "0.4.*",
"utopia-php/logger": "0.3.*",
@ -86,7 +86,7 @@
],
"require-dev": {
"ext-fileinfo": "*",
"appwrite/sdk-generator": "0.35.*",
"appwrite/sdk-generator": "0.36.*",
"phpunit/phpunit": "9.5.20",
"squizlabs/php_codesniffer": "^3.7",
"swoole/ide-helper": "5.0.2",

207
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": "69bc2e21a65b78344393706b39d789b4",
"content-hash": "7041499af2e7b23795d8ef82c9d7a072",
"packages": [
{
"name": "adhocore/jwt",
@ -156,11 +156,11 @@
},
{
"name": "appwrite/php-runtimes",
"version": "0.13.1",
"version": "0.13.2",
"source": {
"type": "git",
"url": "https://github.com/appwrite/runtimes.git",
"reference": "b584d19cdcd82737d0ee5c34d23de791f5ed3610"
"reference": "214a37c2c66e0f2bc9c30fdfde66955d9fd084a1"
},
"require": {
"php": ">=8.0",
@ -195,7 +195,7 @@
"php",
"runtimes"
],
"time": "2023-10-16T15:39:53+00:00"
"time": "2023-11-22T15:36:00+00:00"
},
{
"name": "chillerlan/php-qrcode",
@ -402,16 +402,16 @@
},
{
"name": "guzzlehttp/guzzle",
"version": "7.8.0",
"version": "7.8.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9"
"reference": "41042bc7ab002487b876a0683fc8dce04ddce104"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/1110f66a6530a40fe7aea0378fe608ee2b2248f9",
"reference": "1110f66a6530a40fe7aea0378fe608ee2b2248f9",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104",
"reference": "41042bc7ab002487b876a0683fc8dce04ddce104",
"shasum": ""
},
"require": {
@ -426,11 +426,11 @@
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*",
"php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999",
"php-http/message-factory": "^1.1",
"phpunit/phpunit": "^8.5.29 || ^9.5.23",
"phpunit/phpunit": "^8.5.36 || ^9.6.15",
"psr/log": "^1.1 || ^2.0 || ^3.0"
},
"suggest": {
@ -508,7 +508,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.8.0"
"source": "https://github.com/guzzle/guzzle/tree/7.8.1"
},
"funding": [
{
@ -524,28 +524,28 @@
"type": "tidelift"
}
],
"time": "2023-08-27T10:20:53+00:00"
"time": "2023-12-03T20:35:24+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "2.0.1",
"version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "111166291a0f8130081195ac4556a5587d7f1b5d"
"reference": "bbff78d96034045e58e13dedd6ad91b5d1253223"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/111166291a0f8130081195ac4556a5587d7f1b5d",
"reference": "111166291a0f8130081195ac4556a5587d7f1b5d",
"url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223",
"reference": "bbff78d96034045e58e13dedd6ad91b5d1253223",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"phpunit/phpunit": "^8.5.29 || ^9.5.23"
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.36 || ^9.6.15"
},
"type": "library",
"extra": {
@ -591,7 +591,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/2.0.1"
"source": "https://github.com/guzzle/promises/tree/2.0.2"
},
"funding": [
{
@ -607,20 +607,20 @@
"type": "tidelift"
}
],
"time": "2023-08-03T15:11:55+00:00"
"time": "2023-12-03T20:19:20+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "2.6.1",
"version": "2.6.2",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
"reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727"
"reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/be45764272e8873c72dbe3d2edcfdfcc3bc9f727",
"reference": "be45764272e8873c72dbe3d2edcfdfcc3bc9f727",
"url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221",
"reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221",
"shasum": ""
},
"require": {
@ -634,9 +634,9 @@
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.1",
"bamarni/composer-bin-plugin": "^1.8.2",
"http-interop/http-factory-tests": "^0.9",
"phpunit/phpunit": "^8.5.29 || ^9.5.23"
"phpunit/phpunit": "^8.5.36 || ^9.6.15"
},
"suggest": {
"laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
@ -707,7 +707,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
"source": "https://github.com/guzzle/psr7/tree/2.6.1"
"source": "https://github.com/guzzle/psr7/tree/2.6.2"
},
"funding": [
{
@ -723,7 +723,7 @@
"type": "tidelift"
}
],
"time": "2023-08-27T10:13:57+00:00"
"time": "2023-12-03T20:05:35+00:00"
},
{
"name": "influxdb/influxdb-php",
@ -1465,7 +1465,7 @@
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.3.0",
"version": "v3.4.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
@ -1512,7 +1512,7 @@
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.3.0"
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0"
},
"funding": [
{
@ -2069,16 +2069,16 @@
},
{
"name": "utopia-php/framework",
"version": "0.31.0",
"version": "0.31.1",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/framework.git",
"reference": "207f77378965fca9a9bc3783ea379d3549f86bc0"
"reference": "e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/framework/zipball/207f77378965fca9a9bc3783ea379d3549f86bc0",
"reference": "207f77378965fca9a9bc3783ea379d3549f86bc0",
"url": "https://api.github.com/repos/utopia-php/framework/zipball/e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68",
"reference": "e50d2d16f4bc31319043f3f6d3dbea36c6fd6b68",
"shasum": ""
},
"require": {
@ -2108,9 +2108,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/framework/issues",
"source": "https://github.com/utopia-php/framework/tree/0.31.0"
"source": "https://github.com/utopia-php/framework/tree/0.31.1"
},
"time": "2023-08-30T16:10:04+00:00"
"time": "2023-12-08T18:47:29+00:00"
},
{
"name": "utopia-php/image",
@ -2217,16 +2217,16 @@
},
{
"name": "utopia-php/logger",
"version": "0.3.1",
"version": "0.3.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/logger.git",
"reference": "de623f1ec1c672c795d113dd25c5bf212f7ef4fc"
"reference": "ba763c10688fe2ed715ad2bed3f13d18dfec6253"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/logger/zipball/de623f1ec1c672c795d113dd25c5bf212f7ef4fc",
"reference": "de623f1ec1c672c795d113dd25c5bf212f7ef4fc",
"url": "https://api.github.com/repos/utopia-php/logger/zipball/ba763c10688fe2ed715ad2bed3f13d18dfec6253",
"reference": "ba763c10688fe2ed715ad2bed3f13d18dfec6253",
"shasum": ""
},
"require": {
@ -2264,9 +2264,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/logger/issues",
"source": "https://github.com/utopia-php/logger/tree/0.3.1"
"source": "https://github.com/utopia-php/logger/tree/0.3.2"
},
"time": "2023-02-10T15:52:50+00:00"
"time": "2023-11-22T14:45:43+00:00"
},
{
"name": "utopia-php/messaging",
@ -3136,16 +3136,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "0.35.2",
"version": "0.36.0",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "2dfe0430a64ffd2a07078d83b20144b871acac3b"
"reference": "3a10f1f895ed71120442ff71eb6adec3fd6b4e8a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/2dfe0430a64ffd2a07078d83b20144b871acac3b",
"reference": "2dfe0430a64ffd2a07078d83b20144b871acac3b",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/3a10f1f895ed71120442ff71eb6adec3fd6b4e8a",
"reference": "3a10f1f895ed71120442ff71eb6adec3fd6b4e8a",
"shasum": ""
},
"require": {
@ -3181,9 +3181,9 @@
"description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms",
"support": {
"issues": "https://github.com/appwrite/sdk-generator/issues",
"source": "https://github.com/appwrite/sdk-generator/tree/0.35.2"
"source": "https://github.com/appwrite/sdk-generator/tree/0.36.0"
},
"time": "2023-09-14T14:59:50+00:00"
"time": "2023-11-20T10:03:06+00:00"
},
{
"name": "doctrine/deprecations",
@ -3822,29 +3822,29 @@
},
{
"name": "phpspec/prophecy",
"version": "v1.17.0",
"version": "v1.18.0",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
"reference": "15873c65b207b07765dbc3c95d20fdf4a320cbe2"
"reference": "d4f454f7e1193933f04e6500de3e79191648ed0c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/15873c65b207b07765dbc3c95d20fdf4a320cbe2",
"reference": "15873c65b207b07765dbc3c95d20fdf4a320cbe2",
"url": "https://api.github.com/repos/phpspec/prophecy/zipball/d4f454f7e1193933f04e6500de3e79191648ed0c",
"reference": "d4f454f7e1193933f04e6500de3e79191648ed0c",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.2 || ^2.0",
"php": "^7.2 || 8.0.* || 8.1.* || 8.2.*",
"php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.*",
"phpdocumentor/reflection-docblock": "^5.2",
"sebastian/comparator": "^3.0 || ^4.0",
"sebastian/recursion-context": "^3.0 || ^4.0"
"sebastian/comparator": "^3.0 || ^4.0 || ^5.0",
"sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0"
},
"require-dev": {
"phpspec/phpspec": "^6.0 || ^7.0",
"phpstan/phpstan": "^1.9",
"phpunit/phpunit": "^8.0 || ^9.0"
"phpunit/phpunit": "^8.0 || ^9.0 || ^10.0"
},
"type": "library",
"extra": {
@ -3877,6 +3877,7 @@
"keywords": [
"Double",
"Dummy",
"dev",
"fake",
"mock",
"spy",
@ -3884,22 +3885,22 @@
],
"support": {
"issues": "https://github.com/phpspec/prophecy/issues",
"source": "https://github.com/phpspec/prophecy/tree/v1.17.0"
"source": "https://github.com/phpspec/prophecy/tree/v1.18.0"
},
"time": "2023-02-02T15:41:36+00:00"
"time": "2023-12-07T16:22:33+00:00"
},
{
"name": "phpstan/phpdoc-parser",
"version": "1.24.2",
"version": "1.24.4",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
"reference": "bcad8d995980440892759db0c32acae7c8e79442"
"reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bcad8d995980440892759db0c32acae7c8e79442",
"reference": "bcad8d995980440892759db0c32acae7c8e79442",
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/6bd0c26f3786cd9b7c359675cb789e35a8e07496",
"reference": "6bd0c26f3786cd9b7c359675cb789e35a8e07496",
"shasum": ""
},
"require": {
@ -3931,9 +3932,9 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"support": {
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.2"
"source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.4"
},
"time": "2023-09-26T12:28:12+00:00"
"time": "2023-11-26T18:29:22+00:00"
},
{
"name": "phpunit/php-code-coverage",
@ -5373,16 +5374,16 @@
},
{
"name": "squizlabs/php_codesniffer",
"version": "3.7.2",
"version": "3.8.0",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879"
"url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git",
"reference": "5805f7a4e4958dbb5e944ef1e6edae0a303765e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879",
"reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879",
"url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5805f7a4e4958dbb5e944ef1e6edae0a303765e7",
"reference": "5805f7a4e4958dbb5e944ef1e6edae0a303765e7",
"shasum": ""
},
"require": {
@ -5392,7 +5393,7 @@
"php": ">=5.4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
"phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0"
},
"bin": [
"bin/phpcs",
@ -5411,22 +5412,45 @@
"authors": [
{
"name": "Greg Sherwood",
"role": "lead"
"role": "Former lead"
},
{
"name": "Juliette Reinders Folmer",
"role": "Current lead"
},
{
"name": "Contributors",
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors"
}
],
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
"homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards",
"static analysis"
],
"support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
"issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues",
"security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy",
"source": "https://github.com/PHPCSStandards/PHP_CodeSniffer",
"wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki"
},
"time": "2023-02-22T23:07:41+00:00"
"funding": [
{
"url": "https://github.com/PHPCSStandards",
"type": "github"
},
{
"url": "https://github.com/jrfnl",
"type": "github"
},
{
"url": "https://opencollective.com/php_codesniffer",
"type": "open_collective"
}
],
"time": "2023-12-08T12:32:31+00:00"
},
{
"name": "swoole/ide-helper",
@ -5676,16 +5700,16 @@
},
{
"name": "theseer/tokenizer",
"version": "1.2.1",
"version": "1.2.2",
"source": {
"type": "git",
"url": "https://github.com/theseer/tokenizer.git",
"reference": "34a41e998c2183e22995f158c581e7b5e755ab9e"
"reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/34a41e998c2183e22995f158c581e7b5e755ab9e",
"reference": "34a41e998c2183e22995f158c581e7b5e755ab9e",
"url": "https://api.github.com/repos/theseer/tokenizer/zipball/b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
"reference": "b2ad5003ca10d4ee50a12da31de12a5774ba6b96",
"shasum": ""
},
"require": {
@ -5714,7 +5738,7 @@
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
"source": "https://github.com/theseer/tokenizer/tree/1.2.1"
"source": "https://github.com/theseer/tokenizer/tree/1.2.2"
},
"funding": [
{
@ -5722,30 +5746,31 @@
"type": "github"
}
],
"time": "2021-07-28T10:34:58+00:00"
"time": "2023-11-20T00:12:19+00:00"
},
{
"name": "twig/twig",
"version": "v3.7.1",
"version": "v3.8.0",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554"
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/a0ce373a0ca3bf6c64b9e3e2124aca502ba39554",
"reference": "a0ce373a0ca3bf6c64b9e3e2124aca502ba39554",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
"reference": "9d15f0ac07f44dc4217883ec6ae02fd555c6f71d",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
"symfony/polyfill-mbstring": "^1.3",
"symfony/polyfill-php80": "^1.22"
},
"require-dev": {
"psr/container": "^1.0|^2.0",
"symfony/phpunit-bridge": "^5.4.9|^6.3"
"symfony/phpunit-bridge": "^5.4.9|^6.3|^7.0"
},
"type": "library",
"autoload": {
@ -5781,7 +5806,7 @@
],
"support": {
"issues": "https://github.com/twigphp/Twig/issues",
"source": "https://github.com/twigphp/Twig/tree/v3.7.1"
"source": "https://github.com/twigphp/Twig/tree/v3.8.0"
},
"funding": [
{
@ -5793,7 +5818,7 @@
"type": "tidelift"
}
],
"time": "2023-08-28T11:09:02+00:00"
"time": "2023-11-21T18:54:41+00:00"
}
],
"aliases": [],
@ -5822,5 +5847,5 @@
"platform-overrides": {
"php": "8.0"
},
"plugin-api-version": "2.6.0"
"plugin-api-version": "2.3.0"
}

View file

@ -188,7 +188,9 @@ services:
- _APP_MIGRATIONS_FIREBASE_CLIENT_ID
- _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET
- _APP_ASSISTANT_OPENAI_API_KEY
- _APP_MESSAGE_SMS_TEST_DSN
- _APP_MESSAGE_EMAIL_TEST_DSN
- _APP_MESSAGE_PUSH_TEST_DSN
appwrite-realtime:
entrypoint: realtime
<<: *x-logging
@ -568,14 +570,20 @@ services:
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_SMS_PROVIDER
- _APP_SMS_FROM
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_LOGGING_PROVIDER
- _APP_LOGGING_CONFIG
- _APP_SMS_FROM
- _APP_SMS_PROVIDER
appwrite-worker-migrations:
entrypoint: worker-migrations
@ -717,6 +725,63 @@ services:
environment:
- _APP_ASSISTANT_OPENAI_API_KEY
appwrite-worker-hamster:
entrypoint: worker-hamster
<<: *x-logging
container_name: appwrite-worker-hamster
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_MIXPANEL_TOKEN
appwrite-hamster-scheduler:
entrypoint: hamster
<<: *x-logging
container_name: appwrite-hamster-scheduler
image: appwrite-dev
networks:
- appwrite
volumes:
- ./app:/usr/src/code/app
- ./src:/usr/src/code/src
depends_on:
- redis
- mariadb
environment:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
- _APP_REDIS_PORT
- _APP_REDIS_USER
- _APP_REDIS_PASS
- _APP_DB_HOST
- _APP_DB_PORT
- _APP_DB_SCHEMA
- _APP_DB_USER
- _APP_DB_PASS
- _APP_HAMSTER_TIME
- _APP_HAMSTER_INTERVAL
openruntimes-executor:
container_name: openruntimes-executor
hostname: appwrite-executor

View file

@ -1 +1,2 @@
appwrite health getQueueBuilds
appwrite health getQueueBuilds \

View file

@ -1 +1,2 @@
appwrite health getQueueCertificates
appwrite health getQueueCertificates \

View file

@ -1,2 +1,3 @@
appwrite health getQueueDatabases \

View file

@ -1 +1,2 @@
appwrite health getQueueDeletes
appwrite health getQueueDeletes \

View file

@ -1 +1,2 @@
appwrite health getQueueFunctions
appwrite health getQueueFunctions \

View file

@ -1 +1,2 @@
appwrite health getQueueLogs
appwrite health getQueueLogs \

View file

@ -1 +1,2 @@
appwrite health getQueueMails
appwrite health getQueueMails \

View file

@ -1 +1,2 @@
appwrite health getQueueMessaging
appwrite health getQueueMessaging \

View file

@ -1 +1,2 @@
appwrite health getQueueMigrations
appwrite health getQueueMigrations \

View file

@ -1 +1,2 @@
appwrite health getQueueWebhooks
appwrite health getQueueWebhooks \

View file

@ -1,3 +1,8 @@
## 10.1.0
* Add new queue health endpoints
* Fix between queries
## 10.0.0
* Parameter `url` is now optional in the `createMembership` endpoint

View file

@ -1,3 +1,7 @@
## 11.0.1
* Fix between queries
## 11.0.0
* Parameter `url` is now optional in the `createMembership` endpoint

View file

@ -37,7 +37,7 @@ Finally, you will need to create a `feat-XXX-YYY-oauth` branch based on the `mas
The first step in adding a new OAuth2 provider is to add it to the list of providers located at:
```
app/config/providers.php
app/config/oAuthProviders.php
```
Make sure to fill in all data needed and that your provider array key name:
@ -45,7 +45,7 @@ Make sure to fill in all data needed and that your provider array key name:
- is in [`camelCase`](https://en.wikipedia.org/wiki/Camel_case) format for sentence, but lowercase for names. `github` must be all lowercased, but `paypalSandbox` should have uppercase S
- has no spaces or special characters
> Please make sure to keep the list of providers in `providers.php` in the alphabetical order A-Z.
> Please make sure to keep the list of providers in `oAuthProviders.php` in the alphabetical order A-Z.
### 2.2 Add Provider Logo
@ -199,7 +199,7 @@ If you need any help with the contribution, feel free to head over to [our Disco
If your OAuth provider requires special configuration apart from `clientId` and `clientSecret` you can create a custom form. Currently this is being realized through putting all custom fields as JSON into the `clientSecret` field to keep the project API stable. You can implement your custom form following these steps:
1. Add your custom form in `app/views/console/users/oauth/[PROVIDER].phtml`. Below is a template you can use. Add the filename to `app/config/providers.php`.
1. Add your custom form in `app/views/console/users/oauth/[PROVIDER].phtml`. Below is a template you can use. Add the filename to `app/config/oAuthProviders.php`.
```php
<?php

View file

@ -32,6 +32,7 @@
<directory>./tests/e2e/Services/Projects</directory>
<directory>./tests/e2e/Services/Storage</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

@ -42,6 +42,9 @@ class Event
public const MIGRATIONS_QUEUE_NAME = 'v1-migrations';
public const MIGRATIONS_CLASS_NAME = 'MigrationsV1';
public const HAMSTER_QUEUE_NAME = 'v1-hamster';
public const HAMSTER_CLASS_NAME = 'HamsterV1';
protected string $queue = '';
protected string $class = '';
protected string $event = '';

View file

@ -0,0 +1,157 @@
<?php
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
class Hamster extends Event
{
protected string $type = '';
protected ?Document $project = null;
protected ?Document $organization = null;
protected ?Document $user = null;
public const TYPE_PROJECT = 'project';
public const TYPE_ORGANISATION = 'organisation';
public const TYPE_USER = 'user';
public function __construct(protected Connection $connection)
{
parent::__construct($connection);
$this
->setQueue(Event::HAMSTER_QUEUE_NAME)
->setClass(Event::HAMSTER_CLASS_NAME);
}
/**
* Sets the type for the hamster event.
*
* @param string $type
* @return self
*/
public function setType(string $type): self
{
$this->type = $type;
return $this;
}
/**
* Returns the set type for the hamster event.
*
* @return string
*/
public function getType(): string
{
return $this->type;
}
/**
* Sets the project for the hamster event.
*
* @param Document $project
*/
public function setProject(Document $project): self
{
$this->project = $project;
return $this;
}
/**
* Returns the set project for the hamster event.
*
* @return Document
*/
public function getProject(): Document
{
return $this->project;
}
/**
* Sets the organization for the hamster event.
*
* @param Document $organization
*/
public function setOrganization(Document $organization): self
{
$this->organization = $organization;
return $this;
}
/**
* Returns the set organization for the hamster event.
*
* @return string
*/
public function getOrganization(): Document
{
return $this->organization;
}
/**
* Sets the user for the hamster event.
*
* @param Document $user
*/
public function setUser(Document $user): self
{
$this->user = $user;
return $this;
}
/**
* Returns the set user for the hamster event.
*
* @return Document
*/
public function getUser(): Document
{
return $this->user;
}
/**
* Executes the function event and sends it to the functions worker.
*
* @return string|bool
* @throws \InvalidArgumentException
*/
public function trigger(): string|bool
{
if ($this->paused) {
return false;
}
$client = new Client($this->queue, $this->connection);
$events = $this->getEvent() ? Event::generateEvents($this->getEvent(), $this->getParams()) : null;
return $client->enqueue([
'type' => $this->type,
'project' => $this->project,
'organization' => $this->organization,
'user' => $this->user,
'events' => $events,
]);
}
/**
* Generate a function event from a base event
*
* @param Event $event
*
* @return self
*
*/
public function from(Event $event): self
{
$this->event = $event->getEvent();
$this->params = $event->getParams();
return $this;
}
}

View file

@ -0,0 +1,173 @@
<?php
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\Queue\Connection;
use Utopia\Queue\Client;
class Messaging extends Event
{
protected ?string $messageId = null;
protected ?Document $message = null;
protected ?array $recipients = null;
protected ?string $scheduledAt = null;
protected ?string $providerType = null;
public function __construct(protected Connection $connection)
{
parent::__construct($connection);
$this
->setQueue(Event::MESSAGING_QUEUE_NAME)
->setClass(Event::MESSAGING_CLASS_NAME);
}
/**
* Sets recipient for the messaging event.
*
* @param string[] $recipients
* @return self
*/
public function setRecipients(array $recipients): self
{
$this->recipients = $recipients;
return $this;
}
/**
* Returns set recipient for messaging event.
*
* @return string[]
*/
public function getRecipient(): array
{
return $this->recipients;
}
/**
* Sets message document for the messaging event.
*
* @param Document $message
* @return self
*/
public function setMessage(Document $message): self
{
$this->message = $message;
return $this;
}
/**
* Returns message document for the messaging event.
*
* @return string
*/
public function getMessage(): Document
{
return $this->message;
}
/**
* Sets message ID for the messaging event.
*
* @param string $message
* @return self
*/
public function setMessageId(string $messageId): self
{
$this->messageId = $messageId;
return $this;
}
/**
* Returns set message ID for the messaging event.
*
* @return string
*/
public function getMessageId(): string
{
return $this->messageId;
}
/**
* Sets provider type for the messaging event.
*
* @param string $providerType
* @return self
*/
public function setProviderType(string $providerType): self
{
$this->providerType = $providerType;
return $this;
}
/**
* Returns set provider type for the messaging event.
*
* @return string
*/
public function getProviderType(): string
{
return $this->providerType;
}
/**
* Sets Scheduled delivery time for the messaging event.
*
* @param string $scheduledAt
* @return self
*/
public function setScheduledAt(string $scheduledAt): self
{
$this->scheduledAt = $scheduledAt;
return $this;
}
/**
* Returns set Delivery Time for the messaging event.
*
* @return string
*/
public function getScheduledAt(): string
{
return $this->scheduledAt;
}
/**
* Set project for this event.
*
* @param Document $project
* @return self
*/
public function setProject(Document $project): self
{
$this->project = $project;
return $this;
}
/**
* Executes the event and sends it to the messaging worker.
* @return string|bool
* @throws \InvalidArgumentException
*/
public function trigger(): string | bool
{
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
'project' => $this->project,
'user' => $this->user,
'messageId' => $this->messageId,
'message' => $this->message,
'recipients' => $this->recipients,
'providerType' => $this->providerType,
]);
}
}

View file

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

View file

@ -55,6 +55,8 @@ class Exception extends \Exception
public const GENERAL_CODES_DISABLED = 'general_codes_disabled';
public const GENERAL_USAGE_DISABLED = 'general_usage_disabled';
public const GENERAL_NOT_IMPLEMENTED = 'general_not_implemented';
public const GENERAL_INVALID_EMAIL = 'general_invalid_email';
public const GENERAL_INVALID_PHONE = 'general_invalid_phone';
/** Users */
public const USER_COUNT_EXCEEDED = 'user_count_exceeded';
@ -86,6 +88,8 @@ class Exception extends \Exception
public const USER_OAUTH2_PROVIDER_ERROR = 'user_oauth2_provider_error';
public const USER_EMAIL_ALREADY_VERIFIED = 'user_email_alread_verified';
public const USER_PHONE_ALREADY_VERIFIED = 'user_phone_already_verified';
public const USER_TARGET_NOT_FOUND = 'user_target_not_found';
public const USER_TARGET_ALREADY_EXISTS = 'user_target_already_exists';
/** Teams */
public const TEAM_NOT_FOUND = 'team_not_found';
@ -236,6 +240,30 @@ class Exception extends \Exception
/** Health */
public const QUEUE_SIZE_EXCEEDED = 'queue_size_exceeded';
/** Provider */
public const PROVIDER_NOT_FOUND = 'provider_not_found';
public const PROVIDER_ALREADY_EXISTS = 'provider_already_exists';
public const PROVIDER_INCORRECT_TYPE = 'provider_incorrect_type';
public const PROVIDER_INTERNAL_UPDATE_DISABLED = 'provider_internal_update_disabled';
/** Topic */
public const TOPIC_NOT_FOUND = 'topic_not_found';
public const TOPIC_ALREADY_EXISTS = 'topic_already_exists';
/** Subscriber */
public const SUBSCRIBER_NOT_FOUND = 'subscriber_not_found';
public const SUBSCRIBER_ALREADY_EXISTS = 'subscriber_already_exists';
/** Message */
public const MESSAGE_NOT_FOUND = 'message_not_found';
public const MESSAGE_MISSING_TARGET = 'message_missing_target';
public const MESSAGE_ALREADY_SENT = 'message_already_sent';
public const MESSAGE_ALREADY_SCHEDULED = 'message_already_scheduled';
public const MESSAGE_TARGET_NOT_EMAIL = 'message_target_not_email';
public const MESSAGE_TARGET_NOT_SMS = 'message_target_not_sms';
public const MESSAGE_TARGET_NOT_PUSH = 'message_target_not_push';
protected string $type = '';
protected array $errors = [];
protected bool $publish = true;

View file

@ -34,7 +34,7 @@ class V15 extends Migration
['email', 'anonymous'],
\array_map(
fn ($value) => "oauth-" . $value,
\array_keys(Config::getParam('providers', []))
\array_keys(Config::getParam('oAuthProviders', []))
)
);

View file

@ -124,23 +124,23 @@ class V16 extends Migration
/**
* Enable OAuth providers with data
*/
$authProviders = $document->getAttribute('authProviders', []);
$oAuthProviders = $document->getAttribute('oAuthProviders', []);
foreach (Config::getParam('providers') as $provider => $value) {
foreach (Config::getParam('oAuthProviders') as $provider => $value) {
if (!$value['enabled']) {
continue;
}
if (($authProviders[$provider . 'Appid'] ?? false) && ($authProviders[$provider . 'Secret'] ?? false)) {
if (array_key_exists($provider . 'Enabled', $authProviders)) {
if (($oAuthProviders[$provider . 'Appid'] ?? false) && ($oAuthProviders[$provider . 'Secret'] ?? false)) {
if (array_key_exists($provider . 'Enabled', $oAuthProviders)) {
continue;
}
$authProviders[$provider . 'Enabled'] = true;
$oAuthProviders[$provider . 'Enabled'] = true;
}
}
$document->setAttribute('authProviders', $authProviders);
$document->setAttribute('oAuthProviders', $oAuthProviders);
break;
}

View file

@ -12,6 +12,7 @@ use Appwrite\Platform\Workers\Databases;
use Appwrite\Platform\Workers\Functions;
use Appwrite\Platform\Workers\Builds;
use Appwrite\Platform\Workers\Deletes;
use Appwrite\Platform\Workers\Hamster;
use Appwrite\Platform\Workers\Migrations;
class Workers extends Service
@ -30,6 +31,7 @@ class Workers extends Service
->addAction(Builds::getName(), new Builds())
->addAction(Deletes::getName(), new Deletes())
->addAction(Migrations::getName(), new Migrations())
->addAction(Hamster::getName(), new Hamster())
;
}

View file

@ -2,45 +2,17 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Network\Validator\Origin;
use Appwrite\Event\Hamster as EventHamster;
use Exception;
use Utopia\App;
use Utopia\Platform\Action;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Analytics\Adapter\Mixpanel;
use Utopia\Analytics\Event;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Pools\Group;
class Hamster extends Action
{
private array $metrics = [
'usage_files' => 'files.$all.count.total',
'usage_buckets' => 'buckets.$all.count.total',
'usage_databases' => 'databases.$all.count.total',
'usage_documents' => 'documents.$all.count.total',
'usage_collections' => 'collections.$all.count.total',
'usage_storage' => 'project.$all.storage.size',
'usage_requests' => 'project.$all.network.requests',
'usage_bandwidth' => 'project.$all.network.bandwidth',
'usage_users' => 'users.$all.count.total',
'usage_sessions' => 'sessions.email.requests.create',
'usage_executions' => 'executions.$all.compute.total',
];
protected string $directory = '/usr/local';
protected string $path;
protected string $date;
protected Mixpanel $mixpanel;
public static function getName(): string
{
return 'hamster';
@ -48,266 +20,31 @@ class Hamster extends Action
public function __construct()
{
$this->mixpanel = new Mixpanel(App::getEnv('_APP_MIXPANEL_TOKEN', ''));
$this
->desc('Get stats for projects')
->inject('pools')
->inject('cache')
->inject('queueForHamster')
->inject('dbForConsole')
->callback(function (Group $pools, Cache $cache, Database $dbForConsole) {
$this->action($pools, $cache, $dbForConsole);
->callback(function (EventHamster $queueForHamster, Database $dbForConsole) {
$this->action($queueForHamster, $dbForConsole);
});
}
private function getStatsPerProject(Group $pools, Cache $cache, Database $dbForConsole)
public function action(EventHamster $queueForHamster, Database $dbForConsole): void
{
$this->calculateByGroup('projects', $dbForConsole, function (Database $dbForConsole, Document $project) use ($pools, $cache) {
/**
* Skip user projects with id 'console'
*/
if ($project->getId() === 'console') {
Console::info("Skipping project console");
return;
}
Console::log("Getting stats for {$project->getId()}");
try {
$db = $project->getAttribute('database');
$adapter = $pools
->get($db)
->pop()
->getResource();
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDefaultDatabase('appwrite');
$dbForProject->setNamespace('_' . $project->getInternalId());
$statsPerProject = [];
$statsPerProject['time'] = microtime(true);
/** Get Project ID */
$statsPerProject['project_id'] = $project->getId();
/** Get project created time */
$statsPerProject['project_created'] = $project->getAttribute('$createdAt');
/** Get Project Name */
$statsPerProject['project_name'] = $project->getAttribute('name');
/** Total Project Variables */
$statsPerProject['custom_variables'] = $dbForProject->count('variables', [], APP_LIMIT_COUNT);
/** Total Migrations */
$statsPerProject['custom_migrations'] = $dbForProject->count('migrations', [], APP_LIMIT_COUNT);
/** Get Custom SMTP */
$smtp = $project->getAttribute('smtp', null);
if ($smtp) {
$statsPerProject['custom_smtp_status'] = $smtp['enabled'] === true ? 'enabled' : 'disabled';
/** Get Custom Templates Count */
$templates = array_keys($project->getAttribute('templates', []));
$statsPerProject['custom_email_templates'] = array_filter($templates, function ($template) {
return str_contains($template, 'email');
});
$statsPerProject['custom_sms_templates'] = array_filter($templates, function ($template) {
return str_contains($template, 'sms');
});
}
/** Get total relationship attributes */
$statsPerProject['custom_relationship_attributes'] = $dbForProject->count('attributes', [
Query::equal('type', ['relationship'])
], APP_LIMIT_COUNT);
/** Get Total Functions */
$statsPerProject['custom_functions'] = $dbForProject->count('functions', [], APP_LIMIT_COUNT);
foreach (\array_keys(Config::getParam('runtimes')) as $runtime) {
$statsPerProject['custom_functions_' . $runtime] = $dbForProject->count('functions', [
Query::equal('runtime', [$runtime]),
], APP_LIMIT_COUNT);
}
/** Get Total Deployments */
$statsPerProject['custom_deployments'] = $dbForProject->count('deployments', [], APP_LIMIT_COUNT);
$statsPerProject['custom_deployments_manual'] = $dbForProject->count('deployments', [
Query::equal('type', ['manual'])
], APP_LIMIT_COUNT);
$statsPerProject['custom_deployments_git'] = $dbForProject->count('deployments', [
Query::equal('type', ['vcs'])
], APP_LIMIT_COUNT);
/** Get VCS repos connected */
$statsPerProject['custom_vcs_repositories'] = $dbForConsole->count('repositories', [
Query::equal('projectInternalId', [$project->getInternalId()])
], APP_LIMIT_COUNT);
/** Get Total Teams */
$statsPerProject['custom_teams'] = $dbForProject->count('teams', [], APP_LIMIT_COUNT);
/** Get Total Migrations */
$statsPerProject['custom_migrations'] = $dbForProject->count('migrations', [], APP_LIMIT_COUNT);
/** Get Total Members */
$teamInternalId = $project->getAttribute('teamInternalId', null);
if ($teamInternalId) {
$statsPerProject['custom_organization_members'] = $dbForConsole->count('memberships', [
Query::equal('teamInternalId', [$teamInternalId])
], APP_LIMIT_COUNT);
} else {
$statsPerProject['custom_organization_members'] = 0;
}
/** Get Email and Name of the project owner */
if ($teamInternalId) {
$membership = $dbForConsole->findOne('memberships', [
Query::equal('teamInternalId', [$teamInternalId]),
]);
if (!$membership || $membership->isEmpty()) {
throw new Exception('Membership not found. Skipping project : ' . $project->getId());
}
$userId = $membership->getAttribute('userId', null);
if ($userId) {
$user = $dbForConsole->getDocument('users', $userId);
$statsPerProject['email'] = $user->getAttribute('email', null);
$statsPerProject['name'] = $user->getAttribute('name', null);
}
}
/** Get Domains */
$statsPerProject['custom_domains'] = $dbForConsole->count('rules', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
/** Get Platforms */
$platforms = $dbForConsole->find('platforms', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
$statsPerProject['custom_platforms_web'] = sizeof(array_filter($platforms, function ($platform) {
return $platform['type'] === 'web';
}));
$statsPerProject['custom_platforms_android'] = sizeof(array_filter($platforms, function ($platform) {
return $platform['type'] === 'android';
}));
$statsPerProject['custom_platforms_apple'] = sizeof(array_filter($platforms, function ($platform) {
return str_contains($platform['type'], 'apple');
}));
$statsPerProject['custom_platforms_flutter'] = sizeof(array_filter($platforms, function ($platform) {
return str_contains($platform['type'], 'flutter');
}));
$flutterPlatforms = [Origin::CLIENT_TYPE_FLUTTER_ANDROID, Origin::CLIENT_TYPE_FLUTTER_IOS, Origin::CLIENT_TYPE_FLUTTER_MACOS, Origin::CLIENT_TYPE_FLUTTER_WINDOWS, Origin::CLIENT_TYPE_FLUTTER_LINUX];
foreach ($flutterPlatforms as $flutterPlatform) {
$statsPerProject['custom_platforms_' . $flutterPlatform] = sizeof(array_filter($platforms, function ($platform) use ($flutterPlatform) {
return $platform['type'] === $flutterPlatform;
}));
}
$statsPerProject['custom_platforms_api_keys'] = $dbForConsole->count('keys', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
/** Get Usage $statsPerProject */
$periods = [
'infinity' => [
'period' => '1d',
'limit' => 90,
],
'24h' => [
'period' => '1h',
'limit' => 24,
],
];
Authorization::skip(function () use ($dbForProject, $periods, &$statsPerProject) {
foreach ($this->metrics as $key => $metric) {
foreach ($periods as $periodKey => $periodValue) {
$limit = $periodValue['limit'];
$period = $periodValue['period'];
$requestDocs = $dbForProject->find('stats', [
Query::equal('period', [$period]),
Query::equal('metric', [$metric]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$statsPerProject[$key . '_' . $periodKey] = [];
foreach ($requestDocs as $requestDoc) {
$statsPerProject[$key . '_' . $periodKey][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
$statsPerProject[$key . '_' . $periodKey] = array_reverse($statsPerProject[$key . '_' . $periodKey]);
// Calculate aggregate of each metric
$statsPerProject[$key . '_' . $periodKey] = array_sum(array_column($statsPerProject[$key . '_' . $periodKey], 'value'));
}
}
});
if (isset($statsPerProject['email'])) {
/** Send data to mixpanel */
$res = $this->mixpanel->createProfile($statsPerProject['email'], '', [
'name' => $statsPerProject['name'],
'email' => $statsPerProject['email']
]);
if (!$res) {
Console::error('Failed to create user profile for project: ' . $project->getId());
}
}
$event = new Event();
$event
->setName('Project Daily Usage')
->setProps($statsPerProject);
$res = $this->mixpanel->createEvent($event);
if (!$res) {
Console::error('Failed to create event for project: ' . $project->getId());
}
} catch (Exception $e) {
Console::error('Failed to send stats for project: ' . $project->getId());
Console::error($e->getMessage());
} finally {
$pools
->get($db)
->reclaim();
}
});
}
public function action(Group $pools, Cache $cache, Database $dbForConsole): void
{
Console::title('Cloud Hamster V1');
Console::success(APP_NAME . ' cloud hamster process has started');
$sleep = (int) App::getEnv('_APP_HAMSTER_INTERVAL', '30'); // 30 seconds (by default)
$jobInitTime = App::getEnv('_APP_HAMSTER_TIME', '22:00'); // (hour:minutes)
$now = new \DateTime();
$now->setTimezone(new \DateTimeZone(date_default_timezone_get()));
$next = new \DateTime($now->format("Y-m-d $jobInitTime"));
$next->setTimezone(new \DateTimeZone(date_default_timezone_get()));
$delay = $next->getTimestamp() - $now->getTimestamp();
$delay = $next->getTimestamp() - $now->getTimestamp();
/**
* If time passed for the target day.
*/
@ -318,29 +55,22 @@ class Hamster extends Action
Console::log('[' . $now->format("Y-m-d H:i:s.v") . '] Delaying for ' . $delay . ' setting loop to [' . $next->format("Y-m-d H:i:s.v") . ']');
Console::loop(function () use ($pools, $cache, $dbForConsole, $sleep) {
Console::loop(function () use ($queueForHamster, $dbForConsole, $sleep) {
$now = date('d-m-Y H:i:s', time());
Console::info("[{$now}] Getting Cloud Usage Stats every {$sleep} seconds");
Console::info("[{$now}] Queuing Cloud Usage Stats every {$sleep} seconds");
$loopStart = microtime(true);
/* Initialise new Utopia app */
$app = new App('UTC');
Console::info('Queuing stats for all projects');
$this->getStatsPerProject($queueForHamster, $dbForConsole, $loopStart);
Console::success('Completed queuing stats for all projects');
Console::info('Getting stats for all projects');
$this->getStatsPerProject($pools, $cache, $dbForConsole);
Console::success('Completed getting stats for all projects');
Console::info('Queuing stats for all organizations');
$this->getStatsPerOrganization($queueForHamster, $dbForConsole, $loopStart);
Console::success('Completed queuing stats for all organizations');
Console::info('Getting stats for all organizations');
$this->getStatsPerOrganization($dbForConsole);
Console::success('Completed getting stats for all organizations');
Console::info('Getting stats for all users');
$this->getStatsPerUser($dbForConsole);
Console::success('Completed getting stats for all users');
$pools
->get('console')
->reclaim();
Console::info('Queuing stats for all users');
$this->getStatsPerUser($queueForHamster, $dbForConsole, $loopStart);
Console::success('Completed queuing stats for all users');
$loopTook = microtime(true) - $loopStart;
$now = date('d-m-Y H:i:s', time());
@ -348,7 +78,7 @@ class Hamster extends Action
}, $sleep, $delay);
}
protected function calculateByGroup(string $collection, Database $dbForConsole, callable $callback)
protected function calculateByGroup(string $collection, Database $database, callable $callback)
{
$count = 0;
$chunk = 0;
@ -361,7 +91,7 @@ class Hamster extends Action
while ($sum === $limit) {
$chunk++;
$results = $dbForConsole->find($collection, \array_merge([
$results = $database->find($collection, \array_merge([
Query::limit($limit),
Query::offset($count)
]));
@ -371,7 +101,7 @@ class Hamster extends Action
Console::log('Processing chunk #' . $chunk . '. Found ' . $sum . ' documents');
foreach ($results as $document) {
call_user_func($callback, $dbForConsole, $document);
call_user_func($callback, $database, $document);
$count++;
}
}
@ -381,96 +111,45 @@ class Hamster extends Action
Console::log("Processed {$count} document by group in " . ($executionEnd - $executionStart) . " seconds");
}
protected function getStatsPerOrganization(Database $dbForConsole)
protected function getStatsPerOrganization(EventHamster $hamster, Database $dbForConsole, float $loopStart)
{
$this->calculateByGroup('teams', $dbForConsole, function (Database $dbForConsole, Document $document) {
$this->calculateByGroup('teams', $dbForConsole, function (Database $dbForConsole, Document $organization) use ($hamster, $loopStart) {
try {
$statsPerOrganization = [];
/** Organization name */
$statsPerOrganization['name'] = $document->getAttribute('name');
/** Get Email and of the organization owner */
$membership = $dbForConsole->findOne('memberships', [
Query::equal('teamInternalId', [$document->getInternalId()]),
]);
if (!$membership || $membership->isEmpty()) {
throw new Exception('Membership not found. Skipping organization : ' . $document->getId());
}
$userId = $membership->getAttribute('userId', null);
if ($userId) {
$user = $dbForConsole->getDocument('users', $userId);
$statsPerOrganization['email'] = $user->getAttribute('email', null);
}
/** Organization Creation Date */
$statsPerOrganization['created'] = $document->getAttribute('$createdAt');
/** Number of team members */
$statsPerOrganization['members'] = $document->getAttribute('total');
/** Number of projects in this organization */
$statsPerOrganization['projects'] = $dbForConsole->count('projects', [
Query::equal('teamId', [$document->getId()]),
Query::limit(APP_LIMIT_COUNT)
]);
if (!isset($statsPerOrganization['email'])) {
throw new Exception('Email not found. Skipping organization : ' . $document->getId());
}
$event = new Event();
$event
->setName('Organization Daily Usage')
->setProps($statsPerOrganization);
$res = $this->mixpanel->createEvent($event);
if (!$res) {
throw new Exception('Failed to create event for organization : ' . $document->getId());
}
$organization->setAttribute('$time', $loopStart);
$hamster
->setType(EventHamster::TYPE_ORGANISATION)
->setOrganization($organization)
->trigger();
} catch (Exception $e) {
Console::error($e->getMessage());
}
});
}
protected function getStatsPerUser(Database $dbForConsole)
private function getStatsPerProject(EventHamster $hamster, Database $dbForConsole, float $loopStart)
{
$this->calculateByGroup('users', $dbForConsole, function (Database $dbForConsole, Document $document) {
$this->calculateByGroup('projects', $dbForConsole, function (Database $dbForConsole, Document $project) use ($hamster, $loopStart) {
try {
$statsPerUser = [];
$project->setAttribute('$time', $loopStart);
$hamster
->setType(EventHamster::TYPE_PROJECT)
->setProject($project)
->trigger();
} catch (Exception $e) {
Console::error($e->getMessage());
}
});
}
/** Organization name */
$statsPerUser['name'] = $document->getAttribute('name');
/** Organization ID (needs to be stored as an email since mixpanel uses the email attribute as a distinctID) */
$statsPerUser['email'] = $document->getAttribute('email');
/** Organization Creation Date */
$statsPerUser['created'] = $document->getAttribute('$createdAt');
/** Number of teams this user is a part of */
$statsPerUser['memberships'] = $dbForConsole->count('memberships', [
Query::equal('userInternalId', [$document->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
if (!isset($statsPerUser['email'])) {
throw new Exception('User has no email: ' . $document->getId());
}
/** Send data to mixpanel */
$event = new Event();
$event
->setName('User Daily Usage')
->setProps($statsPerUser);
$res = $this->mixpanel->createEvent($event);
if (!$res) {
throw new Exception('Failed to create user profile for user: ' . $document->getId());
}
protected function getStatsPerUser(EventHamster $hamster, Database $dbForConsole, float $loopStart)
{
$this->calculateByGroup('users', $dbForConsole, function (Database $dbForConsole, Document $user) use ($hamster, $loopStart) {
try {
$user->setAttribute('$time', $loopStart);
$hamster
->setType(EventHamster::TYPE_USER)
->setUser($user)
->trigger();
} catch (Exception $e) {
Console::error($e->getMessage());
}

View file

@ -235,6 +235,11 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
'X-Appwrite-Response-Format' => '1.4.0',
]);
// Make sure we have a clean slate.
// Otherwise, all files in this dir will be pushed,
// regardless of whether they were just generated or not.
\exec('rm -rf ' . $result);
try {
$sdk->generate($result);
} catch (Exception $exception) {

View file

@ -155,6 +155,9 @@ class Deletes extends Action
case DELETE_TYPE_SCHEDULES:
$this->deleteSchedules($dbForConsole, $getProjectDB, $datetime);
break;
case DELETE_TYPE_TOPIC:
$this->deleteTopic($project, $getProjectDB, $document);
break;
default:
Console::error('No delete operation for type: ' . $type);
break;
@ -199,6 +202,25 @@ class Deletes extends Action
);
}
/**
* @param Document $project
* @param callable $getProjectDB
* @param Document $topic
* @throws Exception
*/
protected function deleteTopic(Document $project, callable $getProjectDB, Document $topic)
{
if ($topic->isEmpty()) {
Console::error('Failed to delete subscribers. Topic not found');
return;
}
$dbForProject = $getProjectDB($project);
$this->deleteByGroup('subscribers', [
Query::equal('topicInternalId', [$topic->getInternalId()])
], $dbForProject);
}
/**
* @param Document $project
* @param callable $getProjectDB
@ -539,6 +561,11 @@ class Deletes extends Action
$this->deleteByGroup('identities', [
Query::equal('userInternalId', [$userInternalId])
], $dbForProject);
// Delete targets
$this->deleteByGroup('targets', [
Query::equal('userInternalId', [$userInternalId])
], $dbForProject);
}
/**

View file

@ -0,0 +1,437 @@
<?php
namespace Appwrite\Platform\Workers;
use Appwrite\Event\Hamster as EventHamster;
use Appwrite\Network\Validator\Origin;
use Utopia\Analytics\Adapter\Mixpanel;
use Utopia\Analytics\Event as AnalyticsEvent;
use Utopia\App;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Platform\Action;
use Utopia\Database\Database;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Document;
use Utopia\Queue\Message;
use Utopia\Logger\Log;
use Utopia\Pools\Group;
class Hamster extends Action
{
private array $metrics = [
'usage_files' => 'files.$all.count.total',
'usage_buckets' => 'buckets.$all.count.total',
'usage_databases' => 'databases.$all.count.total',
'usage_documents' => 'documents.$all.count.total',
'usage_collections' => 'collections.$all.count.total',
'usage_storage' => 'project.$all.storage.size',
'usage_requests' => 'project.$all.network.requests',
'usage_bandwidth' => 'project.$all.network.bandwidth',
'usage_users' => 'users.$all.count.total',
'usage_sessions' => 'sessions.email.requests.create',
'usage_executions' => 'executions.$all.compute.total',
];
protected Mixpanel $mixpanel;
public static function getName(): string
{
return 'hamster';
}
/**
* @throws \Exception
*/
public function __construct()
{
$this
->desc('Hamster worker')
->inject('message')
->inject('pools')
->inject('cache')
->inject('dbForConsole')
->callback(fn (Message $message, Group $pools, Cache $cache, Database $dbForConsole) => $this->action($message, $pools, $cache, $dbForConsole));
}
/**
* @param Message $message
* @param Group $pools
* @param Cache $cache
* @param Database $dbForConsole
*
* @return void
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Group $pools, Cache $cache, Database $dbForConsole): void
{
$token = App::getEnv('_APP_MIXPANEL_TOKEN', '');
if (empty($token)) {
throw new \Exception('Missing MixPanel Token');
}
$this->mixpanel = new Mixpanel($token);
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
throw new \Exception('Missing payload');
}
$type = $payload['type'] ?? '';
switch ($type) {
case EventHamster::TYPE_PROJECT:
$this->getStatsForProject(new Document($payload['project']), $pools, $cache, $dbForConsole);
break;
case EventHamster::TYPE_ORGANISATION:
$this->getStatsForOrganization(new Document($payload['organization']), $dbForConsole);
break;
case EventHamster::TYPE_USER:
$this->getStatsPerUser(new Document($payload['user']), $dbForConsole);
break;
}
}
/**
* @param Document $project
* @param Group $pools
* @param Cache $cache
* @param Database $dbForConsole
* @throws \Utopia\Database\Exception
*/
private function getStatsForProject(Document $project, Group $pools, Cache $cache, Database $dbForConsole): void
{
/**
* Skip user projects with id 'console'
*/
if ($project->getId() === 'console') {
Console::info("Skipping project console");
return;
}
Console::log("Getting stats for Project {$project->getId()}");
try {
$db = $project->getAttribute('database');
$adapter = $pools
->get($db)
->pop()
->getResource();
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDefaultDatabase('appwrite');
$dbForProject->setNamespace('_' . $project->getInternalId());
$statsPerProject = [];
$statsPerProject['time'] = $project->getAttribute('$time');
/** Get Project ID */
$statsPerProject['project_id'] = $project->getId();
/** Get project created time */
$statsPerProject['project_created'] = $project->getAttribute('$createdAt');
/** Get Project Name */
$statsPerProject['project_name'] = $project->getAttribute('name');
/** Total Project Variables */
$statsPerProject['custom_variables'] = $dbForProject->count('variables', [], APP_LIMIT_COUNT);
/** Total Migrations */
$statsPerProject['custom_migrations'] = $dbForProject->count('migrations', [], APP_LIMIT_COUNT);
/** Get Custom SMTP */
$smtp = $project->getAttribute('smtp', null);
if ($smtp) {
$statsPerProject['custom_smtp_status'] = $smtp['enabled'] === true ? 'enabled' : 'disabled';
/** Get Custom Templates Count */
$templates = array_keys($project->getAttribute('templates', []));
$statsPerProject['custom_email_templates'] = array_filter($templates, function ($template) {
return str_contains($template, 'email');
});
$statsPerProject['custom_sms_templates'] = array_filter($templates, function ($template) {
return str_contains($template, 'sms');
});
}
/** Get total relationship attributes */
$statsPerProject['custom_relationship_attributes'] = $dbForProject->count('attributes', [
Query::equal('type', ['relationship'])
], APP_LIMIT_COUNT);
/** Get Total Functions */
$statsPerProject['custom_functions'] = $dbForProject->count('functions', [], APP_LIMIT_COUNT);
foreach (\array_keys(Config::getParam('runtimes')) as $runtime) {
$statsPerProject['custom_functions_' . $runtime] = $dbForProject->count('functions', [
Query::equal('runtime', [$runtime]),
], APP_LIMIT_COUNT);
}
/** Get Total Deployments */
$statsPerProject['custom_deployments'] = $dbForProject->count('deployments', [], APP_LIMIT_COUNT);
$statsPerProject['custom_deployments_manual'] = $dbForProject->count('deployments', [
Query::equal('type', ['manual'])
], APP_LIMIT_COUNT);
$statsPerProject['custom_deployments_git'] = $dbForProject->count('deployments', [
Query::equal('type', ['vcs'])
], APP_LIMIT_COUNT);
/** Get VCS repos connected */
$statsPerProject['custom_vcs_repositories'] = $dbForConsole->count('repositories', [
Query::equal('projectInternalId', [$project->getInternalId()])
], APP_LIMIT_COUNT);
/** Get Total Teams */
$statsPerProject['custom_teams'] = $dbForProject->count('teams', [], APP_LIMIT_COUNT);
/** Get Total Members */
$teamInternalId = $project->getAttribute('teamInternalId', null);
if ($teamInternalId) {
$statsPerProject['custom_organization_members'] = $dbForConsole->count('memberships', [
Query::equal('teamInternalId', [$teamInternalId])
], APP_LIMIT_COUNT);
} else {
$statsPerProject['custom_organization_members'] = 0;
}
/** Get Email and Name of the project owner */
if ($teamInternalId) {
$membership = $dbForConsole->findOne('memberships', [
Query::equal('teamInternalId', [$teamInternalId]),
]);
if (!$membership || $membership->isEmpty()) {
throw new \Exception('Membership not found. Skipping project : ' . $project->getId());
}
$userId = $membership->getAttribute('userId', null);
if ($userId) {
$user = $dbForConsole->getDocument('users', $userId);
$statsPerProject['email'] = $user->getAttribute('email', null);
$statsPerProject['name'] = $user->getAttribute('name', null);
}
}
/** Get Domains */
$statsPerProject['custom_domains'] = $dbForConsole->count('rules', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
/** Get Platforms */
$platforms = $dbForConsole->find('platforms', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
$statsPerProject['custom_platforms_web'] = sizeof(array_filter($platforms, function ($platform) {
return $platform['type'] === 'web';
}));
$statsPerProject['custom_platforms_android'] = sizeof(array_filter($platforms, function ($platform) {
return $platform['type'] === 'android';
}));
$statsPerProject['custom_platforms_apple'] = sizeof(array_filter($platforms, function ($platform) {
return str_contains($platform['type'], 'apple');
}));
$statsPerProject['custom_platforms_flutter'] = sizeof(array_filter($platforms, function ($platform) {
return str_contains($platform['type'], 'flutter');
}));
$flutterPlatforms = [Origin::CLIENT_TYPE_FLUTTER_ANDROID, Origin::CLIENT_TYPE_FLUTTER_IOS, Origin::CLIENT_TYPE_FLUTTER_MACOS, Origin::CLIENT_TYPE_FLUTTER_WINDOWS, Origin::CLIENT_TYPE_FLUTTER_LINUX];
foreach ($flutterPlatforms as $flutterPlatform) {
$statsPerProject['custom_platforms_' . $flutterPlatform] = sizeof(array_filter($platforms, function ($platform) use ($flutterPlatform) {
return $platform['type'] === $flutterPlatform;
}));
}
$statsPerProject['custom_platforms_api_keys'] = $dbForConsole->count('keys', [
Query::equal('projectInternalId', [$project->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
/** Get Usage $statsPerProject */
$periods = [
'infinity' => [
'period' => '1d',
'limit' => 90,
],
'24h' => [
'period' => '1h',
'limit' => 24,
],
];
Authorization::skip(function () use ($dbForProject, $periods, &$statsPerProject) {
foreach ($this->metrics as $key => $metric) {
foreach ($periods as $periodKey => $periodValue) {
$limit = $periodValue['limit'];
$period = $periodValue['period'];
$requestDocs = $dbForProject->find('stats', [
Query::equal('period', [$period]),
Query::equal('metric', [$metric]),
Query::limit($limit),
Query::orderDesc('time'),
]);
$statsPerProject[$key . '_' . $periodKey] = [];
foreach ($requestDocs as $requestDoc) {
$statsPerProject[$key . '_' . $periodKey][] = [
'value' => $requestDoc->getAttribute('value'),
'date' => $requestDoc->getAttribute('time'),
];
}
$statsPerProject[$key . '_' . $periodKey] = array_reverse($statsPerProject[$key . '_' . $periodKey]);
// Calculate aggregate of each metric
$statsPerProject[$key . '_' . $periodKey] = array_sum(array_column($statsPerProject[$key . '_' . $periodKey], 'value'));
}
}
});
if (isset($statsPerProject['email'])) {
/** Send data to mixpanel */
$res = $this->mixpanel->createProfile($statsPerProject['email'], '', [
'name' => $statsPerProject['name'],
'email' => $statsPerProject['email']
]);
if (!$res) {
Console::error('Failed to create user profile for project: ' . $project->getId());
}
}
$event = new AnalyticsEvent();
$event
->setName('Project Daily Usage')
->setProps($statsPerProject);
$res = $this->mixpanel->createEvent($event);
if (!$res) {
Console::error('Failed to create event for project: ' . $project->getId());
}
} catch (\Exception $e) {
Console::error('Failed to send stats for project: ' . $project->getId());
Console::error($e->getMessage());
} finally {
$pools
->get($db)
->reclaim();
}
}
/**
* @param Document $organization
* @param Database $dbForConsole
* @throws \Utopia\Database\Exception
*/
private function getStatsForOrganization(Document $organization, Database $dbForConsole): void
{
Console::log("Getting stats for Organization {$organization->getId()}");
try {
$statsPerOrganization = [];
$statsPerOrganization['time'] = $organization->getAttribute('$time');
/** Organization name */
$statsPerOrganization['name'] = $organization->getAttribute('name');
/** Get Email and of the organization owner */
$membership = $dbForConsole->findOne('memberships', [
Query::equal('teamInternalId', [$organization->getInternalId()]),
]);
if (!$membership || $membership->isEmpty()) {
throw new \Exception('Membership not found. Skipping organization : ' . $organization->getId());
}
$userId = $membership->getAttribute('userId', null);
if ($userId) {
$user = $dbForConsole->getDocument('users', $userId);
$statsPerOrganization['email'] = $user->getAttribute('email', null);
}
/** Organization Creation Date */
$statsPerOrganization['created'] = $organization->getAttribute('$createdAt');
/** Number of team members */
$statsPerOrganization['members'] = $organization->getAttribute('total');
/** Number of projects in this organization */
$statsPerOrganization['projects'] = $dbForConsole->count('projects', [
Query::equal('teamId', [$organization->getId()]),
Query::limit(APP_LIMIT_COUNT)
]);
if (!isset($statsPerOrganization['email'])) {
throw new \Exception('Email not found. Skipping organization : ' . $organization->getId());
}
$event = new AnalyticsEvent();
$event
->setName('Organization Daily Usage')
->setProps($statsPerOrganization);
$res = $this->mixpanel->createEvent($event);
if (!$res) {
throw new \Exception('Failed to create event for organization : ' . $organization->getId());
}
} catch (\Exception $e) {
Console::error($e->getMessage());
}
}
protected function getStatsPerUser(Document $user, Database $dbForConsole)
{
Console::log("Getting stats for User {$user->getId()}");
try {
$statsPerUser = [];
$statsPerUser['time'] = $user->getAttribute('$time');
/** Organization name */
$statsPerUser['name'] = $user->getAttribute('name');
/** Organization ID (needs to be stored as an email since mixpanel uses the email attribute as a distinctID) */
$statsPerUser['email'] = $user->getAttribute('email');
/** Organization Creation Date */
$statsPerUser['created'] = $user->getAttribute('$createdAt');
/** Number of teams this user is a part of */
$statsPerUser['memberships'] = $dbForConsole->count('memberships', [
Query::equal('userInternalId', [$user->getInternalId()]),
Query::limit(APP_LIMIT_COUNT)
]);
if (!isset($statsPerUser['email'])) {
throw new \Exception('User has no email: ' . $user->getId());
}
/** Send data to mixpanel */
$event = new AnalyticsEvent();
$event
->setName('User Daily Usage')
->setProps($statsPerUser);
$res = $this->mixpanel->createEvent($event);
if (!$res) {
throw new \Exception('Failed to create user profile for user: ' . $user->getId());
}
} catch (\Exception $e) {
Console::error($e->getMessage());
}
}
}

View file

@ -2,31 +2,42 @@
namespace Appwrite\Platform\Workers;
use Exception;
use Appwrite\Extend\Exception;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Helpers\ID;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Messaging\Messages\Sms;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Messaging\Adapters\SMS as SMSAdapter;
use Utopia\Messaging\Adapters\SMS\Mock;
use Utopia\Messaging\Adapters\SMS\Msg91;
use Utopia\Messaging\Adapters\SMS\Telesign;
use Utopia\Messaging\Adapters\SMS\TextMagic;
use Utopia\Messaging\Adapters\SMS\Textmagic;
use Utopia\Messaging\Adapters\SMS\Twilio;
use Utopia\Messaging\Adapters\SMS\Vonage;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Messaging\Adapters\Push as PushAdapter;
use Utopia\Messaging\Adapters\Push\APNS;
use Utopia\Messaging\Adapters\Push\FCM;
use Utopia\Messaging\Adapters\Email as EmailAdapter;
use Utopia\Messaging\Adapters\Email\Mailgun;
use Utopia\Messaging\Adapters\Email\SendGrid;
use Utopia\Messaging\Messages\Email;
use Utopia\Messaging\Messages\Push;
use Utopia\Messaging\Messages\SMS;
use function Swoole\Coroutine\batch;
class Messaging extends Action
{
private ?DSN $dsn = null;
private string $user = '';
private string $secret = '';
private string $provider = '';
public static function getName(): string
{
return 'messaging';
return "messaging";
}
/**
@ -34,79 +45,342 @@ class Messaging extends Action
*/
public function __construct()
{
$this->provider = App::getEnv('_APP_SMS_PROVIDER', '');
if (!empty($this->provider)) {
$this->dsn = new DSN($this->provider);
$this->user = $this->dsn->getUser();
$this->secret = $this->dsn->getPassword();
}
$this
->desc('Messaging worker')
->inject('message')
->inject('log')
->callback(fn(Message $message, Log $log) => $this->action($message, $log));
->inject('dbForProject')
->callback(fn(Message $message, Log $log, Database $dbForProject) => $this->action($message, $log, $dbForProject));
}
/**
* @param Message $message
* @param Log $log
* @param Database $dbForProject
* @return void
* @throws Exception
*/
public function action(Message $message, Log $log): void
public function action(Message $message, Log $log, Database $dbForProject): void
{
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
Console::error('Payload arg not found');
return;
throw new \Exception('Skipped SMS processing. Payload is empty.');
}
if (empty($payload['recipient'])) {
Console::error('Recipient arg not found');
return;
if (!\is_null($payload['message']) && !\is_null($payload['recipients'])) {
if ($payload['providerType'] === MESSAGE_TYPE_SMS) {
$this->processInternalSMSMessage($log, new Document($payload['message']), $payload['recipients']);
}
} else {
$message = $dbForProject->getDocument('messages', $payload['messageId']);
$this->processMessage($dbForProject, $message);
}
}
private function processMessage(Database $dbForProject, Document $message): void
{
$topicsId = $message->getAttribute('topics', []);
$targetsId = $message->getAttribute('targets', []);
$usersId = $message->getAttribute('users', []);
/**
* @var Document[] $recipients
*/
$recipients = [];
if (\count($topicsId) > 0) {
$topics = $dbForProject->find('topics', [Query::equal('$id', $topicsId)]);
foreach ($topics as $topic) {
$targets = \array_filter($topic->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType'));
$recipients = \array_merge($recipients, $targets);
}
}
if (empty($payload['message'])) {
Console::error('Message arg not found');
return;
if (\count($usersId) > 0) {
$users = $dbForProject->find('users', [Query::equal('$id', $usersId)]);
foreach ($users as $user) {
$targets = \array_filter($user->getAttribute('targets'), fn (Document $target) => $target->getAttribute('providerType') === $message->getAttribute('providerType'));
$recipients = \array_merge($recipients, $targets);
}
}
$log->addTag('type', $this->dsn->getHost());
$sms = match ($this->dsn->getHost()) {
'mock' => new Mock($this->user, $this->secret), // used for tests
'twilio' => new Twilio($this->user, $this->secret),
'text-magic' => new TextMagic($this->user, $this->secret),
'telesign' => new Telesign($this->user, $this->secret),
'msg91' => new Msg91($this->user, $this->secret),
'vonage' => new Vonage($this->user, $this->secret),
default => null
};
if (empty(App::getEnv('_APP_SMS_PROVIDER'))) {
Console::error('Skipped sms processing. No Phone provider has been set.');
return;
if (\count($targetsId) > 0) {
$targets = $dbForProject->find('targets', [Query::equal('$id', $targetsId)]);
$recipients = \array_merge($recipients, $targets);
}
$primaryProvider = $dbForProject->findOne('providers', [
Query::equal('enabled', [true]),
Query::equal('type', [$recipients[0]->getAttribute('providerType')]),
]);
/**
* @var array<string, array<string>> $identifiersByProviderId
*/
$identifiersByProviderId = [];
/**
* @var Document[] $providers
*/
$providers = [];
foreach ($recipients as $recipient) {
$providerId = $recipient->getAttribute('providerId');
if (!$providerId && $primaryProvider instanceof Document && !$primaryProvider->isEmpty()) {
$providerId = $primaryProvider->getId();
}
if ($providerId) {
if (!isset($identifiersByProviderId[$providerId])) {
$identifiersByProviderId[$providerId] = [];
}
$identifiersByProviderId[$providerId][] = $recipient->getAttribute('identifier');
}
}
/**
* @var array[] $results
*/
$results = batch(\array_map(function ($providerId) use ($identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) {
return function () use ($providerId, $identifiersByProviderId, $providers, $primaryProvider, $message, $dbForProject) {
$provider = new Document();
if ($primaryProvider->getId() === $providerId) {
$provider = $primaryProvider;
} else {
$provider = $dbForProject->getDocument('providers', $providerId, [Query::equal('enabled', [true])]);
if ($provider->isEmpty()) {
$provider = $primaryProvider;
}
}
$providers[] = $provider;
$identifiers = $identifiersByProviderId[$providerId];
$adapter = match ($provider->getAttribute('type')) {
MESSAGE_TYPE_SMS => $this->sms($provider),
MESSAGE_TYPE_PUSH => $this->push($provider),
MESSAGE_TYPE_EMAIL => $this->email($provider),
default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
};
$maxBatchSize = $adapter->getMaxMessagesPerRequest();
$batches = \array_chunk($identifiers, $maxBatchSize);
$batchIndex = 0;
$results = batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) {
return function () use ($batch, $message, $provider, $adapter, $batchIndex) {
$deliveredTotal = 0;
$deliveryErrors = [];
$messageData = clone $message;
$messageData->setAttribute('to', $batch);
$data = match ($provider->getAttribute('type')) {
MESSAGE_TYPE_SMS => $this->buildSMSMessage($messageData, $provider),
MESSAGE_TYPE_PUSH => $this->buildPushMessage($messageData),
MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($messageData, $provider),
default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
};
try {
$adapter->send($data);
$deliveredTotal += \count($batch);
} catch (\Exception $e) {
$deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage();
} finally {
$batchIndex++;
return [
'deliveredTotal' => $deliveredTotal,
'deliveryErrors' => $deliveryErrors,
];
}
};
}, $batches));
return $results;
};
}, \array_keys($identifiersByProviderId)));
$results = array_merge(...$results);
$deliveredTotal = 0;
$deliveryErrors = [];
foreach ($results as $result) {
$deliveredTotal += $result['deliveredTotal'];
$deliveryErrors = \array_merge($deliveryErrors, $result['deliveryErrors']);
}
$message->setAttribute('deliveryErrors', $deliveryErrors);
if (\count($message->getAttribute('deliveryErrors')) > 0) {
$message->setAttribute('status', 'failed');
} else {
$message->setAttribute('status', 'sent');
}
$message->removeAttribute('to');
foreach ($providers as $provider) {
$message->setAttribute('search', "{$message->getAttribute('search')} {$provider->getAttribute('name')} {$provider->getAttribute('provider')} {$provider->getAttribute('type')}");
}
$message->setAttribute('deliveredTotal', $deliveredTotal);
$message->setAttribute('deliveredAt', DateTime::now());
$dbForProject->updateDocument('messages', $message->getId(), $message);
}
private function processInternalSMSMessage(Log $log, Document $message, array $recipients): void
{
if (empty(App::getEnv('_APP_SMS_PROVIDER')) || empty(App::getEnv('_APP_SMS_FROM'))) {
throw new \Exception('Skipped SMS processing. No Phone configuration has been set.');
}
$smsDSN = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$host = $smsDSN->getHost();
$password = $smsDSN->getPassword();
$user = $smsDSN->getUser();
$log->addTag('type', $host);
$from = App::getEnv('_APP_SMS_FROM');
if (empty($from)) {
Console::error('Skipped sms processing. No phone number has been set.');
return;
}
$provider = new Document([
'$id' => ID::unique(),
'provider' => $host,
'type' => MESSAGE_TYPE_SMS,
'name' => 'Internal SMS',
'enabled' => true,
'credentials' => match ($host) {
'twilio' => [
'accountSid' => $user,
'authToken' => $password
],
'textmagic' => [
'username' => $user,
'apiKey' => $password
],
'telesign' => [
'username' => $user,
'password' => $password
],
'msg91' => [
'senderId' => $user,
'authKey' => $password
],
'vonage' => [
'apiKey' => $user,
'apiSecret' => $password
],
default => null
},
'options' => [
'from' => $from
]
]);
$message = new SMS(
to: [$payload['recipient']],
content: $payload['message'],
from: $from,
);
$adapter = $this->sms($provider);
try {
$sms->send($message);
} catch (\Exception $error) {
throw new Exception('Error sending message: ' . $error->getMessage(), 500);
}
$maxBatchSize = $adapter->getMaxMessagesPerRequest();
$batches = \array_chunk($recipients, $maxBatchSize);
$batchIndex = 0;
batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) {
return function () use ($batch, $message, $provider, $adapter, $batchIndex) {
$message->setAttribute('to', $batch);
$data = $this->buildSMSMessage($message, $provider);
try {
$adapter->send($data);
} catch (\Exception $e) {
Console::error('Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage()); // TODO: Find a way to log into Sentry
}
};
}, $batches));
}
public function shutdown(): void
{
}
private function sms(Document $provider): ?SMSAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'mock' => new Mock('username', 'password'),
'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']),
'textmagic' => new Textmagic($credentials['username'], $credentials['apiKey']),
'telesign' => new Telesign($credentials['username'], $credentials['password']),
'msg91' => new Msg91($credentials['senderId'], $credentials['authKey']),
'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']),
default => null
};
}
private function push(Document $provider): ?PushAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'apns' => new APNS(
$credentials['authKey'],
$credentials['authKeyId'],
$credentials['teamId'],
$credentials['bundleId'],
$credentials['endpoint']
),
'fcm' => new FCM($credentials['serverKey']),
default => null
};
}
private function email(Document $provider): ?EmailAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'mailgun' => new Mailgun($credentials['apiKey'], $credentials['domain'], $credentials['isEuRegion']),
'sendgrid' => new SendGrid($credentials['apiKey']),
default => null
};
}
private function buildEmailMessage(Document $message, Document $provider): Email
{
$from = $provider['options']['from'];
$to = $message['to'];
$subject = $message['data']['subject'];
$content = $message['data']['content'];
$html = $message['data']['html'];
return new Email($to, $subject, $content, $from, null, $html);
}
private function buildSMSMessage(Document $message, Document $provider): SMS
{
$to = $message['to'];
$content = $message['data']['content'];
$from = $provider['options']['from'];
return new SMS($to, $content, $from);
}
private function buildPushMessage(Document $message): Push
{
$to = $message['to'];
$title = $message['data']['title'];
$body = $message['data']['body'];
$data = $message['data']['data'];
$action = $message['data']['action'];
$sound = $message['data']['sound'];
$icon = $message['data']['icon'];
$color = $message['data']['color'];
$tag = $message['data']['tag'];
$badge = $message['data']['badge'];
return new Push($to, $title, $body, $data, $action, $sound, $icon, $color, $tag, $badge);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Messages extends Base
{
public const ALLOWED_ATTRIBUTES = [
'topics',
'users',
'targets',
'providerId',
'deliveredAt',
'deliveredTo',
'deliveryErrors',
'status',
'description',
'data'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('messages', self::ALLOWED_ATTRIBUTES);
}
}

View file

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

View file

@ -0,0 +1,22 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Subscribers extends Base
{
public const ALLOWED_ATTRIBUTES = [
'targetId',
'topicId',
'userId',
'providerType'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('subscribers', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Targets extends Base
{
public const ALLOWED_ATTRIBUTES = [
'userId',
'providerId',
'identifier',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('targets', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Topics extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'description',
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('topics', self::ALLOWED_ATTRIBUTES);
}
}

View file

@ -31,6 +31,7 @@ use Appwrite\Utopia\Response\Model\AttributeIP;
use Appwrite\Utopia\Response\Model\AttributeURL;
use Appwrite\Utopia\Response\Model\AttributeDatetime;
use Appwrite\Utopia\Response\Model\AttributeRelationship;
use Appwrite\Utopia\Response\Model\AuthProvider;
use Appwrite\Utopia\Response\Model\BaseList;
use Appwrite\Utopia\Response\Model\Branch;
use Appwrite\Utopia\Response\Model\Collection;
@ -80,8 +81,12 @@ use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\LocaleCode;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\Message;
use Appwrite\Utopia\Response\Model\Subscriber;
use Appwrite\Utopia\Response\Model\Topic;
use Appwrite\Utopia\Response\Model\ProviderRepository;
use Appwrite\Utopia\Response\Model\Runtime;
use Appwrite\Utopia\Response\Model\Target;
use Appwrite\Utopia\Response\Model\TemplateSMS;
use Appwrite\Utopia\Response\Model\UsageBuckets;
use Appwrite\Utopia\Response\Model\UsageCollection;
@ -191,6 +196,18 @@ class Response extends SwooleResponse
public const MODEL_PHONE = 'phone';
public const MODEL_PHONE_LIST = 'phoneList';
// Messaging
public const MODEL_PROVIDER = 'provider';
public const MODEL_PROVIDER_LIST = 'providerList';
public const MODEL_MESSAGE = 'message';
public const MODEL_MESSAGE_LIST = 'messageList';
public const MODEL_TOPIC = 'topic';
public const MODEL_TOPIC_LIST = 'topicList';
public const MODEL_SUBSCRIBER = 'subscriber';
public const MODEL_SUBSCRIBER_LIST = 'subscriberList';
public const MODEL_TARGET = 'target';
public const MODEL_TARGET_LIST = 'targetList';
// Teams
public const MODEL_TEAM = 'team';
public const MODEL_TEAM_LIST = 'teamList';
@ -238,8 +255,8 @@ class Response extends SwooleResponse
public const MODEL_WEBHOOK_LIST = 'webhookList';
public const MODEL_KEY = 'key';
public const MODEL_KEY_LIST = 'keyList';
public const MODEL_PROVIDER = 'provider';
public const MODEL_PROVIDER_LIST = 'providerList';
public const MODEL_AUTH_PROVIDER = 'authProvider';
public const MODEL_AUTH_PROVIDER_LIST = 'authProviderList';
public const MODEL_PLATFORM = 'platform';
public const MODEL_PLATFORM_LIST = 'platformList';
public const MODEL_VARIABLE = 'variable';
@ -316,7 +333,7 @@ class Response extends SwooleResponse
->setModel(new BaseList('Projects List', self::MODEL_PROJECT_LIST, 'projects', self::MODEL_PROJECT, true, false))
->setModel(new BaseList('Webhooks List', self::MODEL_WEBHOOK_LIST, 'webhooks', self::MODEL_WEBHOOK, true, false))
->setModel(new BaseList('API Keys List', self::MODEL_KEY_LIST, 'keys', self::MODEL_KEY, true, false))
->setModel(new BaseList('Providers List', self::MODEL_PROVIDER_LIST, 'platforms', self::MODEL_PROVIDER, true, false))
->setModel(new BaseList('Auth Providers List', self::MODEL_AUTH_PROVIDER_LIST, 'platforms', self::MODEL_AUTH_PROVIDER, true, false))
->setModel(new BaseList('Platforms List', self::MODEL_PLATFORM_LIST, 'platforms', self::MODEL_PLATFORM, true, false))
->setModel(new BaseList('Countries List', self::MODEL_COUNTRY_LIST, 'countries', self::MODEL_COUNTRY))
->setModel(new BaseList('Continents List', self::MODEL_CONTINENT_LIST, 'continents', self::MODEL_CONTINENT))
@ -328,6 +345,11 @@ class Response extends SwooleResponse
->setModel(new BaseList('Status List', self::MODEL_HEALTH_STATUS_LIST, 'statuses', self::MODEL_HEALTH_STATUS))
->setModel(new BaseList('Rule List', self::MODEL_PROXY_RULE_LIST, 'rules', self::MODEL_PROXY_RULE))
->setModel(new BaseList('Locale codes list', self::MODEL_LOCALE_CODE_LIST, 'localeCodes', self::MODEL_LOCALE_CODE))
->setModel(new BaseList('Provider list', self::MODEL_PROVIDER_LIST, 'providers', self::MODEL_PROVIDER))
->setModel(new BaseList('Message list', self::MODEL_MESSAGE_LIST, 'messages', self::MODEL_MESSAGE))
->setModel(new BaseList('Topic list', self::MODEL_TOPIC_LIST, 'topics', self::MODEL_TOPIC))
->setModel(new BaseList('Subscriber list', self::MODEL_SUBSCRIBER_LIST, 'subscribers', self::MODEL_SUBSCRIBER))
->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET))
->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION))
->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT))
// Entities
@ -380,7 +402,7 @@ class Response extends SwooleResponse
->setModel(new Project())
->setModel(new Webhook())
->setModel(new Key())
->setModel(new Provider())
->setModel(new AuthProvider())
->setModel(new Platform())
->setModel(new Variable())
->setModel(new Country())
@ -408,6 +430,11 @@ class Response extends SwooleResponse
->setModel(new TemplateSMS())
->setModel(new TemplateEmail())
->setModel(new ConsoleVariables())
->setModel(new Provider())
->setModel(new Message())
->setModel(new Topic())
->setModel(new Subscriber())
->setModel(new Target())
->setModel(new Migration())
->setModel(new MigrationReport())
->setModel(new MigrationFirebaseProject())

View file

@ -60,8 +60,8 @@ class V13 extends Filter
protected function parseProject($content)
{
$content['providers'] = $content['authProviders'];
unset($content['authProviders']);
$content['providers'] = $content['oAuthProviders'];
unset($content['oAuthProviders']);
return $content;
}

View file

@ -88,9 +88,9 @@ class V16 extends Filter
protected function parseProject(array $content)
{
foreach ($content['providers'] ?? [] as $i => $provider) {
$content['providers'][$i]['name'] = \ucfirst($provider['key']);
unset($content['providers'][$i]['key']);
foreach ($content['oAuthProviders'] ?? [] as $i => $provider) {
$content['oAuthProviders'][$i]['name'] = \ucfirst($provider['key']);
unset($content['oAuthProviders'][$i]['key']);
}
$content['domains'] = [];

View file

@ -0,0 +1,69 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class AuthProvider extends Model
{
/**
* @var bool
*/
protected bool $public = false;
public function __construct()
{
$this
->addRule('key', [
'type' => self::TYPE_STRING,
'description' => 'Auth Provider.',
'default' => '',
'example' => 'github',
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Auth Provider name.',
'default' => '',
'example' => 'GitHub',
])
->addRule('appId', [
'type' => self::TYPE_STRING,
'description' => 'OAuth 2.0 application ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('secret', [
'type' => self::TYPE_STRING,
'description' => 'OAuth 2.0 application secret. Might be JSON string if provider requires extra configuration.',
'default' => '',
'example' => 'Bpw_g9c2TGXxfgLshDbSaL8tsCcqgczQ',
])
->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Auth Provider is active and can be used to create session.',
'example' => '',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'AuthProvider';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_AUTH_PROVIDER;
}
}

View file

@ -0,0 +1,131 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Database\DateTime;
use Utopia\Database\Document as DatabaseDocument;
class Message extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Message ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Message creation time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Message update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('providerType', [
'type' => self::TYPE_STRING,
'description' => 'Message provider type.',
'default' => '',
'example' => MESSAGE_TYPE_EMAIL,
])
->addRule('topics', [
'type' => self::TYPE_STRING,
'description' => 'Topic IDs set as recipients.',
'default' => '',
'array' => true,
'example' => ['5e5ea5c16897e'],
])
->addRule('users', [
'type' => self::TYPE_STRING,
'description' => 'User IDs set as recipients.',
'default' => '',
'array' => true,
'example' => ['5e5ea5c16897e'],
])
->addRule('targets', [
'type' => self::TYPE_STRING,
'description' => 'Target IDs set as recipients.',
'default' => '',
'array' => true,
'example' => ['5e5ea5c16897e'],
])
->addRule('scheduledAt', [
'type' => self::TYPE_DATETIME,
'description' => 'The scheduled time for message.',
'required' => false,
'default' => DateTime::now(),
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('deliveredAt', [
'type' => self::TYPE_DATETIME,
'description' => 'The time when the message was delivered.',
'required' => false,
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('deliveryErrors', [
'type' => self::TYPE_STRING,
'description' => 'Delivery errors if any.',
'required' => false,
'default' => '',
'array' => true,
'example' => ['Failed to send message to target 5e5ea5c16897e: Credentials not valid.'],
])
->addRule('deliveredTotal', [
'type' => self::TYPE_INTEGER,
'description' => 'Number of recipients the message was delivered to.',
'default' => 0,
'example' => 1,
])
->addRule('data', [
'type' => self::TYPE_JSON,
'description' => 'Data of the message.',
'default' => [],
'example' => [
'subject' => 'Welcome to Appwrite',
'content' => 'Hi there, welcome to Appwrite family.',
],
])
->addRule('status', [
'type' => self::TYPE_STRING,
'description' => 'Status of delivery.',
'default' => 'processing',
'example' => 'Message status can be one of the following: processing, sent, cancelled, failed.',
])
->addRule('description', [
'type' => self::TYPE_STRING,
'description' => 'Message description.',
'required' => false,
'default' => '',
'example' => 'Welcome Email.',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Message';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_MESSAGE;
}
}

View file

@ -138,9 +138,9 @@ class Project extends Model
'default' => false,
'example' => true,
])
->addRule('providers', [
'type' => Response::MODEL_PROVIDER,
'description' => 'List of Providers.',
->addRule('oAuthProviders', [
'type' => Response::MODEL_AUTH_PROVIDER,
'description' => 'List of Auth Providers.',
'default' => [],
'example' => [new \stdClass()],
'array' => true,
@ -328,9 +328,9 @@ class Project extends Model
$document->setAttribute('auth' . ucfirst($key), $value);
}
// Providers
$providers = Config::getParam('providers', []);
$providerValues = $document->getAttribute('authProviders', []);
// OAuth Providers
$providers = Config::getParam('oAuthProviders', []);
$providerValues = $document->getAttribute('oAuthProviders', []);
$projectProviders = [];
foreach ($providers as $key => $provider) {
@ -348,7 +348,7 @@ class Project extends Model
]);
}
$document->setAttribute("providers", $projectProviders);
$document->setAttribute('oAuthProviders', $projectProviders);
return $document;
}

View file

@ -7,44 +7,68 @@ use Appwrite\Utopia\Response\Model;
class Provider extends Model
{
/**
* @var bool
*/
protected bool $public = false;
public function __construct()
{
$this
->addRule('key', [
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Provider.',
'description' => 'Provider ID.',
'default' => '',
'example' => 'github',
'example' => '5e5ea5c16897e',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Provider creation time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Provider update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Provider name.',
'description' => 'The name for the provider instance.',
'default' => '',
'example' => 'GitHub',
'example' => 'Mailgun',
])
->addRule('appId', [
->addRule('provider', [
'type' => self::TYPE_STRING,
'description' => 'OAuth 2.0 application ID.',
'description' => 'The name of the provider service.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('secret', [
'type' => self::TYPE_STRING,
'description' => 'OAuth 2.0 application secret. Might be JSON string if provider requires extra configuration.',
'default' => '',
'example' => 'Bpw_g9c2TGXxfgLshDbSaL8tsCcqgczQ',
'example' => 'mailgun',
])
->addRule('enabled', [
'type' => self::TYPE_BOOLEAN,
'description' => 'Provider is active and can be used to create session.',
'example' => '',
'description' => 'Is provider enabled?',
'default' => true,
'example' => true,
])
;
->addRule('type', [
'type' => self::TYPE_STRING,
'description' => 'Type of provider.',
'default' => '',
'example' => MESSAGE_TYPE_SMS,
])
->addRule('credentials', [
'type' => self::TYPE_JSON,
'description' => 'Provider credentials.',
'default' => [],
'example' => [
'key' => '123456789'
],
])
->addRule('options', [
'type' => self::TYPE_JSON,
'description' => 'Provider options.',
'default' => [],
'required' => false,
'example' => [
'from' => 'sender-email@mydomain'
],
]);
}
/**

View file

@ -0,0 +1,97 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Subscriber extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Subscriber ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Subscriber creation time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Subscriber update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('targetId', [
'type' => self::TYPE_STRING,
'description' => 'Target ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('target', [
'type' => Response::MODEL_TARGET,
'description' => 'Target.',
'default' => [],
'example' => [
'$id' => '259125845563242502',
'$createdAt' => self::TYPE_DATETIME_EXAMPLE,
'$updatedAt' => self::TYPE_DATETIME_EXAMPLE,
'providerType' => 'email',
'providerId' => '259125845563242502',
'name' => 'ageon-app-email',
'identifier' => 'random-mail@email.org',
'userId' => '5e5ea5c16897e',
],
])
->addRule('userId', [
'type' => self::TYPE_STRING,
'description' => 'Topic ID.',
'default' => '',
'example' => '5e5ea5c16897e',
])
->addRule('userName', [
'type' => self::TYPE_STRING,
'description' => 'User Name.',
'default' => '',
'example' => 'Aegon Targaryen',
])
->addRule('topicId', [
'type' => self::TYPE_STRING,
'description' => 'Topic ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('providerType', [
'type' => self::TYPE_STRING,
'description' => 'The target provider type. Can be one of the following: `email`, `sms` or `push`.',
'default' => '',
'example' => MESSAGE_TYPE_EMAIL,
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Subscriber';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_SUBSCRIBER;
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Target extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Target ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Target creation time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Target update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Target Name.',
'default' => '',
'example' => 'Aegon apple token',
])
->addRule('userId', [
'type' => self::TYPE_STRING,
'description' => 'User ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('providerId', [
'type' => self::TYPE_STRING,
'description' => 'Provider ID.',
'required' => false,
'default' => '',
'example' => '259125845563242502',
])
->addRule('providerType', [
'type' => self::TYPE_STRING,
'description' => 'The target provider type. Can be one of the following: `email`, `sms` or `push`.',
'default' => '',
'example' => MESSAGE_TYPE_EMAIL,
])
->addRule('identifier', [
'type' => self::TYPE_STRING,
'description' => 'The target identifier.',
'default' => '',
'example' => 'token',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Target';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TARGET;
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class Topic extends Model
{
public function __construct()
{
$this
->addRule('$id', [
'type' => self::TYPE_STRING,
'description' => 'Topic ID.',
'default' => '',
'example' => '259125845563242502',
])
->addRule('$createdAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Topic creation time in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('$updatedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Topic update date in ISO 8601 format.',
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'The name of the topic.',
'default' => '',
'example' => 'events',
])
->addRule('total', [
'type' => self::TYPE_INTEGER,
'description' => 'Total count of subscribers subscribed to topic.',
'default' => 0,
'example' => 100,
])
->addRule('description', [
'type' => self::TYPE_STRING,
'description' => 'Description of the topic.',
'default' => '',
'required' => false,
'example' => 'All events related messages will be sent to this topic.',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Topic';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_TOPIC;
}
}

View file

@ -120,6 +120,13 @@ class User extends Model
'default' => new \stdClass(),
'example' => ['theme' => 'pink', 'timezone' => 'UTC'],
])
->addRule('targets', [
'type' => Response::MODEL_TARGET,
'description' => 'A user-owned message receiver. A single user may have multiple e.g. emails, phones, and a browser. Each target is registered with a single provider.',
'default' => [],
'array' => true,
'example' => [],
])
->addRule('accessedAt', [
'type' => self::TYPE_DATETIME,
'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_USER_ACCCESS / 60 / 60 . ' hours.',

View file

@ -168,6 +168,7 @@ class Client
$headers = array_merge($this->headers, $headers);
$ch = curl_init($this->endpoint . $path . (($method == self::METHOD_GET && !empty($params)) ? '?' . http_build_query($params) : ''));
$responseHeaders = [];
$cookies = [];
$query = match ($headers['content-type']) {
'application/json' => json_encode($params),
@ -189,7 +190,7 @@ class Client
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders) {
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function ($curl, $header) use (&$responseHeaders, &$cookies) {
$len = strlen($header);
$header = explode(':', $header, 2);
@ -197,6 +198,12 @@ class Client
return $len;
}
if (strtolower(trim($header[0])) == 'set-cookie') {
$parsed = $this->parseCookie((string)trim($header[1]));
$name = array_key_first($parsed);
$cookies[$name] = $parsed[$name];
}
$responseHeaders[strtolower(trim($header[0]))] = trim($header[1]);
return $len;
@ -241,6 +248,7 @@ class Client
return [
'headers' => $responseHeaders,
'cookies' => $cookies,
'body' => $responseBody
];
}

View file

@ -82,7 +82,17 @@ trait ProjectCustom
'avatars.read',
'health.read',
'rules.read',
'rules.write'
'rules.write',
'targets.read',
'targets.write',
'providers.read',
'providers.write',
'messages.read',
'messages.write',
'topics.write',
'topics.read',
'subscribers.write',
'subscribers.read',
],
]);

View file

@ -98,7 +98,7 @@ abstract class Scope extends TestCase
'password' => $password,
]);
$session = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_console'];
$session = $session['cookies']['a_session_console'];
self::$root = [
'$id' => ID::custom($root['body']['$id']),
@ -150,7 +150,7 @@ abstract class Scope extends TestCase
'password' => $password,
]);
$token = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$token = $session['cookies']['a_session_' . $this->getProject()['$id']];
self::$user[$this->getProject()['$id']] = [
'$id' => ID::custom($user['body']['$id']),

View file

@ -126,7 +126,7 @@ trait AccountBase
$this->assertNotFalse(\DateTime::createFromFormat('Y-m-d\TH:i:s.uP', $response['body']['expire']));
$sessionId = $response['body']['$id'];
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
// apiKey is only available in custom client test
$apiKey = $this->getProject()['apiKey'];
@ -993,7 +993,7 @@ trait AccountBase
]);
$sessionNewId = $response['body']['$id'];
$sessionNew = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$sessionNew = $response['cookies']['a_session_' . $this->getProject()['$id']];
$this->assertEquals($response['headers']['status-code'], 201);
@ -1059,7 +1059,7 @@ trait AccountBase
'password' => $password,
]);
$sessionNew = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$sessionNew = $response['cookies']['a_session_' . $this->getProject()['$id']];
$this->assertEquals($response['headers']['status-code'], 201);
@ -1141,7 +1141,7 @@ trait AccountBase
'password' => $password,
]);
$data['session'] = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$data['session'] = $response['cookies']['a_session_' . $this->getProject()['$id']];
return $data;
}
@ -1417,7 +1417,7 @@ trait AccountBase
$this->assertNotEmpty($response['body']['userId']);
$sessionId = $response['body']['$id'];
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',

View file

@ -2,16 +2,16 @@
namespace Tests\E2E\Services\Account;
use Appwrite\Extend\Exception;
use Appwrite\SMS\Adapter\Mock;
use Appwrite\Tests\Retry;
use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideClient;
use Utopia\App;
use Utopia\Database\DateTime;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\Datetime as DatetimeValidator;
use Utopia\DSN\DSN;
use function sleep;
@ -126,7 +126,7 @@ class AccountCustomClientTest extends Scope
$this->assertEquals($response['headers']['status-code'], 201);
$sessionId = $response['body']['$id'];
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',
@ -206,7 +206,7 @@ class AccountCustomClientTest extends Scope
$this->assertEquals($response['headers']['status-code'], 201);
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',
@ -288,7 +288,7 @@ class AccountCustomClientTest extends Scope
$this->assertEquals($response['headers']['status-code'], 201);
$sessionId = $response['body']['$id'];
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',
@ -368,7 +368,7 @@ class AccountCustomClientTest extends Scope
$this->assertNotEmpty($response['body']);
$this->assertNotEmpty($response['body']['$id']);
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
\usleep(1000 * 30); // wait for 30ms to let the shutdown update accessedAt
@ -571,7 +571,7 @@ class AccountCustomClientTest extends Scope
'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure',
]);
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('success', $response['body']['result']);
@ -764,6 +764,7 @@ class AccountCustomClientTest extends Scope
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['expire']));
$userId = $response['body']['userId'];
$messageId = $response['body']['$id'];
/**
* Test for FAILURE
@ -849,7 +850,7 @@ class AccountCustomClientTest extends Scope
$this->assertNotEmpty($response['body']['$id']);
$this->assertNotEmpty($response['body']['userId']);
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',

View file

@ -2791,7 +2791,7 @@ trait DatabasesBase
'email' => $email,
'password' => $password,
]);
$session2 = $this->client->parseCookie((string)$session2['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session2 = $session2['cookies']['a_session_' . $this->getProject()['$id']];
$document3GetWithDocumentRead = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $document3['body']['$id'], [
'origin' => 'http://localhost',
@ -2979,7 +2979,7 @@ trait DatabasesBase
'email' => $email,
'password' => $password,
]);
$session2 = $this->client->parseCookie((string)$session2['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session2 = $session2['cookies']['a_session_' . $this->getProject()['$id']];
$document3GetWithDocumentRead = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $document3['body']['$id'], [
'origin' => 'http://localhost',

View file

@ -32,7 +32,7 @@ trait DatabasesPermissionsScope
'password' => $password,
]);
$session = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session = $session['cookies']['a_session_' . $this->getProject()['$id']];
$user = [
'$id' => $user['body']['$id'],

View file

@ -6,7 +6,9 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\App;
use Utopia\Database\Helpers\ID;
use Utopia\DSN\DSN;
class AccountTest extends Scope
{
@ -63,7 +65,7 @@ class AccountTest extends Scope
$this->assertIsArray($session['body']['data']);
$this->assertIsArray($session['body']['data']['accountCreateEmailSession']);
$cookie = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$cookie = $session['cookies']['a_session_' . $this->getProject()['$id']];
$this->assertNotEmpty($cookie);
}
@ -123,6 +125,7 @@ class AccountTest extends Scope
public function testCreatePhoneVerification(): array
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_PHONE_VERIFICATION);
$graphQLPayload = [
'query' => $query,

View file

@ -73,9 +73,7 @@ class AuthTest extends Scope
'x-appwrite-project' => $projectId,
], $graphQLPayload);
$this->token1 = $this->client->parseCookie(
(string)$session1['headers']['set-cookie']
)['a_session_' . $projectId];
$this->token1 = $session1['cookies']['a_session_' . $projectId];
// Create session 2
$graphQLPayload['variables']['email'] = $email2;
@ -85,9 +83,7 @@ class AuthTest extends Scope
'x-appwrite-project' => $projectId,
], $graphQLPayload);
$this->token2 = $this->client->parseCookie(
(string)$session2['headers']['set-cookie']
)['a_session_' . $projectId];
$this->token2 = $session2['cookies']['a_session_' . $projectId];
// Create database
$query = $this->getQuery(self::$CREATE_DATABASE);

View file

@ -109,6 +109,11 @@ trait Base
public static string $DELETE_USER_SESSIONS = 'delete_user_sessions';
public static string $DELETE_USER_SESSION = 'delete_user_session';
public static string $DELETE_USER = 'delete_user';
public static string $CREATE_USER_TARGET = 'create_user_target';
public static string $LIST_USER_TARGETS = 'list_user_targets';
public static string $GET_USER_TARGET = 'get_user_target';
public static string $UPDATE_USER_TARGET = 'update_user_target';
public static string $DELETE_USER_TARGET = 'delete_user_target';
// Teams
public static string $GET_TEAM = 'get_team';
@ -199,6 +204,53 @@ trait Base
public static string $GET_QRCODE = 'get_qrcode';
public static string $GET_USER_INITIALS = 'get_user_initials';
// Providers
public static string $CREATE_MAILGUN_PROVIDER = 'create_mailgun_provider';
public static string $CREATE_SENDGRID_PROVIDER = 'create_sendgrid_provider';
public static string $CREATE_TWILIO_PROVIDER = 'create_twilio_provider';
public static string $CREATE_TELESIGN_PROVIDER = 'create_telesign_provider';
public static string $CREATE_TEXTMAGIC_PROVIDER = 'create_textmagic_provider';
public static string $CREATE_MSG91_PROVIDER = 'create_msg91_provider';
public static string $CREATE_VONAGE_PROVIDER = 'create_vonage_provider';
public static string $CREATE_FCM_PROVIDER = 'create_fcm_provider';
public static string $CREATE_APNS_PROVIDER = 'create_apns_provider';
public static string $LIST_PROVIDERS = 'list_providers';
public static string $GET_PROVIDER = 'get_provider';
public static string $UPDATE_MAILGUN_PROVIDER = 'update_mailgun_provider';
public static string $UPDATE_SENDGRID_PROVIDER = 'update_sendgrid_provider';
public static string $UPDATE_TWILIO_PROVIDER = 'update_twilio_provider';
public static string $UPDATE_TELESIGN_PROVIDER = 'update_telesign_provider';
public static string $UPDATE_TEXTMAGIC_PROVIDER = 'update_textmagic_provider';
public static string $UPDATE_MSG91_PROVIDER = 'update_msg91_provider';
public static string $UPDATE_VONAGE_PROVIDER = 'update_vonage_provider';
public static string $UPDATE_FCM_PROVIDER = 'update_fcm_provider';
public static string $UPDATE_APNS_PROVIDER = 'update_apns_provider';
public static string $DELETE_PROVIDER = 'delete_provider';
// Topics
public static string $CREATE_TOPIC = 'create_topic';
public static string $LIST_TOPICS = 'list_topics';
public static string $GET_TOPIC = 'get_topic';
public static string $UPDATE_TOPIC = 'update_topic';
public static string $DELETE_TOPIC = 'delete_topic';
// Subscriptions
public static string $CREATE_SUBSCRIBER = 'create_subscriber';
public static string $LIST_SUBSCRIBERS = 'list_subscribers';
public static string $GET_SUBSCRIBER = 'get_subscriber';
public static string $DELETE_SUBSCRIBER = 'delete_subscriber';
// Messages
public static string $CREATE_EMAIL = 'create_email';
public static string $CREATE_SMS = 'create_sms';
public static string $CREATE_PUSH_NOTIFICATION = 'create_push_notification';
public static string $LIST_MESSAGES = 'list_messages';
public static string $GET_MESSAGE = 'get_message';
public static string $UPDATE_EMAIL = 'update_email';
public static string $UPDATE_SMS = 'update_sms';
public static string $UPDATE_PUSH_NOTIFICATION = 'update_push_notification';
// Complex queries
public static string $COMPLEX_QUERY = 'complex_query';
@ -881,6 +933,55 @@ trait Base
status
}
}';
case self::$CREATE_USER_TARGET:
return 'mutation createUserTarget($userId: String!, $targetId: String!, $providerType: String!, $identifier: String! $providerId: String){
usersCreateTarget(userId: $userId, targetId: $targetId, providerType: $providerType, identifier: $identifier, providerId: $providerId) {
_id
userId
providerType
providerId
identifier
}
}';
case self::$LIST_USER_TARGETS:
return 'query listUserTargets($userId: String!) {
usersListTargets(userId: $userId) {
total
targets {
_id
userId
providerType
providerId
identifier
}
}
}';
case self::$GET_USER_TARGET:
return 'query getUserTarget($userId: String!, $targetId: String!) {
usersGetTarget(userId: $userId, targetId: $targetId) {
_id
userId
providerType
providerId
identifier
}
}';
case self::$UPDATE_USER_TARGET:
return 'mutation updateUserTarget($userId: String!, $targetId: String!, $providerId: String, $identifier: String){
usersUpdateTarget(userId: $userId, targetId: $targetId, providerId: $providerId, identifier: $identifier) {
_id
userId
providerType
providerId
identifier
}
}';
case self::$DELETE_USER_TARGET:
return 'mutation deleteUserTarget($userId: String!, $targetId: String!){
usersDeleteTarget(userId: $userId, targetId: $targetId) {
status
}
}';
case self::$GET_LOCALE:
return 'query getLocale {
localeGet {
@ -1688,6 +1789,439 @@ trait Base
status
}
}';
case self::$CREATE_MAILGUN_PROVIDER:
return 'mutation createMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $from: String!, $isEuRegion: Boolean!) {
messagingCreateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, from: $from, isEuRegion: $isEuRegion) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_SENDGRID_PROVIDER:
return 'mutation createSendgridProvider($providerId: String!, $name: String!, $from: String!, $apiKey: String!) {
messagingCreateSendgridProvider(providerId: $providerId, name: $name, from: $from, apiKey: $apiKey) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_TWILIO_PROVIDER:
return 'mutation createTwilioProvider($providerId: String!, $name: String!, $from: String!, $accountSid: String!, $authToken: String!) {
messagingCreateTwilioProvider(providerId: $providerId, name: $name, from: $from, accountSid: $accountSid, authToken: $authToken) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_TELESIGN_PROVIDER:
return 'mutation createTelesignProvider($providerId: String!, $name: String!, $from: String!, $username: String!, $password: String!) {
messagingCreateTelesignProvider(providerId: $providerId, name: $name, from: $from, username: $username, password: $password) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_TEXTMAGIC_PROVIDER:
return 'mutation createTextmagicProvider($providerId: String!, $name: String!, $from: String!, $username: String!, $apiKey: String!) {
messagingCreateTextmagicProvider(providerId: $providerId, name: $name, from: $from, username: $username, apiKey: $apiKey) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_MSG91_PROVIDER:
return 'mutation createMsg91Provider($providerId: String!, $name: String!, $from: String!, $senderId: String!, $authKey: String!, $enabled: Boolean) {
messagingCreateMsg91Provider(providerId: $providerId, name: $name, from: $from, senderId: $senderId, authKey: $authKey, enabled: $enabled) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_VONAGE_PROVIDER:
return 'mutation createVonageProvider($providerId: String!, $name: String!, $from: String!, $apiKey: String!, $apiSecret: String!) {
messagingCreateVonageProvider(providerId: $providerId, name: $name, from: $from, apiKey: $apiKey, apiSecret: $apiSecret) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_FCM_PROVIDER:
return 'mutation createFcmProvider($providerId: String!, $name: String!, $serverKey: String!) {
messagingCreateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) {
_id
name
provider
type
enabled
}
}';
case self::$CREATE_APNS_PROVIDER:
return 'mutation createApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) {
messagingCreateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) {
_id
name
provider
type
enabled
}
}';
case self::$LIST_PROVIDERS:
return 'query listProviders {
messagingListProviders {
total
providers {
_id
name
provider
type
enabled
}
}
}';
case self::$GET_PROVIDER:
return 'query getProvider($providerId: String!) {
messagingGetProvider(providerId: $providerId) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_MAILGUN_PROVIDER:
return 'mutation updateMailgunProvider($providerId: String!, $name: String!, $domain: String!, $apiKey: String!, $isEuRegion: Boolean, $enabled: Boolean) {
messagingUpdateMailgunProvider(providerId: $providerId, name: $name, domain: $domain, apiKey: $apiKey, isEuRegion: $isEuRegion, enabled: $enabled) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_SENDGRID_PROVIDER:
return 'mutation messagingUpdateSendgridProvider($providerId: String!, $name: String!, $apiKey: String!) {
messagingUpdateSendgridProvider(providerId: $providerId, name: $name, apiKey: $apiKey) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_TWILIO_PROVIDER:
return 'mutation updateTwilioProvider($providerId: String!, $name: String!, $accountSid: String!, $authToken: String!) {
messagingUpdateTwilioProvider(providerId: $providerId, name: $name, accountSid: $accountSid, authToken: $authToken) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_TELESIGN_PROVIDER:
return 'mutation updateTelesignProvider($providerId: String!, $name: String!, $username: String!, $password: String!) {
messagingUpdateTelesignProvider(providerId: $providerId, name: $name, username: $username, password: $password) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_TEXTMAGIC_PROVIDER:
return 'mutation updateTextmagicProvider($providerId: String!, $name: String!, $username: String!, $apiKey: String!) {
messagingUpdateTextmagicProvider(providerId: $providerId, name: $name, username: $username, apiKey: $apiKey) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_MSG91_PROVIDER:
return 'mutation updateMsg91Provider($providerId: String!, $name: String!, $senderId: String!, $authKey: String!) {
messagingUpdateMsg91Provider(providerId: $providerId, name: $name, senderId: $senderId, authKey: $authKey) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_VONAGE_PROVIDER:
return 'mutation updateVonageProvider($providerId: String!, $name: String!, $apiKey: String!, $apiSecret: String!) {
messagingUpdateVonageProvider(providerId: $providerId, name: $name, apiKey: $apiKey, apiSecret: $apiSecret) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_FCM_PROVIDER:
return 'mutation updateFcmProvider($providerId: String!, $name: String!, $serverKey: String!) {
messagingUpdateFcmProvider(providerId: $providerId, name: $name, serverKey: $serverKey) {
_id
name
provider
type
enabled
}
}';
case self::$UPDATE_APNS_PROVIDER:
return 'mutation updateApnsProvider($providerId: String!, $name: String!, $authKey: String!, $authKeyId: String!, $teamId: String!, $bundleId: String!, $endpoint: String!) {
messagingUpdateApnsProvider(providerId: $providerId, name: $name, authKey: $authKey, authKeyId: $authKeyId, teamId: $teamId, bundleId: $bundleId, endpoint: $endpoint) {
_id
name
provider
type
enabled
}
}';
case self::$DELETE_PROVIDER:
return 'mutation deleteProvider($providerId: String!) {
messagingDeleteProvider(providerId: $providerId) {
status
}
}';
case self::$CREATE_TOPIC:
return 'mutation createTopic($topicId: String!, $name: String!, $description: String!) {
messagingCreateTopic(topicId: $topicId, name: $name, description: $description) {
_id
name
description
}
}';
case self::$LIST_TOPICS:
return 'query listTopics {
messagingListTopics {
total
topics {
_id
name
description
}
}
}';
case self::$GET_TOPIC:
return 'query getTopic($topicId: String!) {
messagingGetTopic(topicId: $topicId) {
_id
name
description
}
}';
case self::$UPDATE_TOPIC:
return 'mutation updateTopic($topicId: String!, $name: String!, $description: String!) {
messagingUpdateTopic(topicId: $topicId, name: $name, description: $description) {
_id
name
description
}
}';
case self::$DELETE_TOPIC:
return 'mutation deleteTopic($topicId: String!) {
messagingDeleteTopic(topicId: $topicId) {
status
}
}';
case self::$CREATE_SUBSCRIBER:
return 'mutation createSubscriber($subscriberId: String!, $targetId: String!, $topicId: String!) {
messagingCreateSubscriber(subscriberId: $subscriberId, targetId: $targetId, topicId: $topicId) {
_id
targetId
topicId
userName
target {
_id
userId
name
providerType
identifier
}
}
}';
case self::$LIST_SUBSCRIBERS:
return 'query listSubscribers($topicId: String!) {
messagingListSubscribers(topicId: $topicId) {
total
subscribers {
_id
targetId
topicId
userName
target {
_id
userId
name
providerType
identifier
}
}
}
}';
case self::$GET_SUBSCRIBER:
return 'query getSubscriber($topicId: String!, $subscriberId: String!) {
messagingGetSubscriber(topicId: $topicId, subscriberId: $subscriberId) {
_id
targetId
topicId
userName
target {
_id
userId
name
providerType
identifier
}
}
}';
case self::$DELETE_SUBSCRIBER:
return 'mutation deleteSubscriber($topicId: String!, $subscriberId: String!) {
messagingDeleteSubscriber(topicId: $topicId, subscriberId: $subscriberId) {
status
}
}';
case self::$CREATE_EMAIL:
return 'mutation createEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String!, $content: String!, $status: String, $description: String, $html: Boolean, $scheduledAt: String) {
messagingCreateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$CREATE_SMS:
return 'mutation createSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String!, $status: String, $description: String, $scheduledAt: String) {
messagingCreateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$CREATE_PUSH_NOTIFICATION:
return 'mutation createPushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String!, $body: String!, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $scheduledAt: String) {
messagingCreatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$LIST_MESSAGES:
return 'query listMessages {
messagingListMessages {
total
messages {
_id
providerType
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}
}';
case self::$GET_MESSAGE:
return 'query getMessage($messageId: String!) {
messagingGetMessage(messageId: $messageId) {
_id
providerType
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$UPDATE_EMAIL:
return 'mutation updateEmail($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $subject: String, $content: String, $status: String, $description: String, $html: Boolean, $scheduledAt: String) {
messagingUpdateEmail(messageId: $messageId, topics: $topics, users: $users, targets: $targets, subject: $subject, content: $content, status: $status, description: $description, html: $html, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$UPDATE_SMS:
return 'mutation updateSMS($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $content: String, $status: String, $description: String, $scheduledAt: String) {
messagingUpdateSMS(messageId: $messageId, topics: $topics, users: $users, targets: $targets, content: $content, status: $status, description: $description, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$UPDATE_PUSH_NOTIFICATION:
return 'mutation updatePushNotification($messageId: String!, $topics: [String!], $users: [String!], $targets: [String!], $title: String, $body: String, $data: Json, $action: String, $icon: String, $sound: String, $color: String, $tag: String, $badge: String, $status: String, $description: String, $scheduledAt: String) {
messagingUpdatePushNotification(messageId: $messageId, topics: $topics, users: $users, targets: $targets, title: $title, body: $body, data: $data, action: $action, icon: $icon, sound: $sound, color: $color, tag: $tag, badge: $badge, status: $status, description: $description, scheduledAt: $scheduledAt) {
_id
topics
users
targets
scheduledAt
deliveredAt
deliveryErrors
deliveredTotal
status
description
}
}';
case self::$COMPLEX_QUERY:
return 'mutation complex($databaseId: String!, $databaseName: String!, $collectionId: String!, $collectionName: String!, $documentSecurity: Boolean!, $collectionPermissions: [String!]!) {
databasesCreate(databaseId: $databaseId, name: $databaseName) {
@ -1940,7 +2474,7 @@ trait Base
protected string $stdout = '';
protected string $stderr = '';
protected function packageCode($folder)
protected function packageCode($folder): void
{
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
}

File diff suppressed because it is too large Load diff

View file

@ -45,6 +45,56 @@ class UsersTest extends Scope
return $user;
}
/**
* @depends testCreateUser
*/
public function testCreateUserTarget(array $user)
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_MAILGUN_PROVIDER);
$graphQLPayload = [
'query' => $query,
'variables' => [
'providerId' => ID::unique(),
'name' => 'Mailgun1',
'apiKey' => 'api-key',
'domain' => 'domain',
'from' => 'from@domain.com',
'isEuRegion' => false,
],
];
$provider = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$providerId = $provider['body']['data']['messagingCreateMailgunProvider']['_id'];
$this->assertEquals(200, $provider['headers']['status-code']);
$query = $this->getQuery(self::$CREATE_USER_TARGET);
$graphQLPayload = [
'query' => $query,
'variables' => [
'targetId' => ID::unique(),
'userId' => $user['_id'],
'providerType' => 'email',
'providerId' => $providerId,
'identifier' => 'random-email@mail.org',
]
];
$target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $target['headers']['status-code']);
$this->assertEquals('random-email@mail.org', $target['body']['data']['usersCreateTarget']['identifier']);
return $target['body']['data']['usersCreateTarget'];
}
public function testGetUsers()
{
$projectId = $this->getProject()['$id'];
@ -176,6 +226,54 @@ class UsersTest extends Scope
$this->assertIsArray($user['body']['data']['usersListLogs']);
}
/**
* @depends testCreateUserTarget
*/
public function testListUserTargets(array $target)
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$LIST_USER_TARGETS);
$graphQLPayload = [
'query' => $query,
'variables' => [
'userId' => $target['userId'],
]
];
$targets = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $targets['headers']['status-code']);
$this->assertIsArray($targets['body']['data']['usersListTargets']);
$this->assertCount(2, $targets['body']['data']['usersListTargets']['targets']);
}
/**
* @depends testCreateUserTarget
*/
public function testGetUserTarget(array $target)
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$GET_USER_TARGET);
$graphQLPayload = [
'query' => $query,
'variables' => [
'userId' => $target['userId'],
'targetId' => $target['_id'],
]
];
$target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $target['headers']['status-code']);
$this->assertEquals('random-email@mail.org', $target['body']['data']['usersGetTarget']['identifier']);
}
public function testUpdateUserStatus()
{
$projectId = $this->getProject()['$id'];
@ -360,6 +458,31 @@ class UsersTest extends Scope
$this->assertEquals('{"key":"value"}', $user['body']['data']['usersUpdatePrefs']['data']);
}
/**
* @depends testCreateUserTarget
*/
public function testUpdateUserTarget(array $target)
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$UPDATE_USER_TARGET);
$graphQLPayload = [
'query' => $query,
'variables' => [
'userId' => $target['userId'],
'targetId' => $target['_id'],
'identifier' => 'random-email1@mail.org',
],
];
$target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(200, $target['headers']['status-code']);
$this->assertEquals('random-email1@mail.org', $target['body']['data']['usersUpdateTarget']['identifier']);
}
public function testDeleteUserSessions()
{
$projectId = $this->getProject()['$id'];
@ -407,6 +530,29 @@ class UsersTest extends Scope
$this->getUser();
}
/**
* @depends testCreateUserTarget
*/
public function testDeleteUserTarget(array $target)
{
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$DELETE_USER_TARGET);
$graphQLPayload = [
'query' => $query,
'variables' => [
'userId' => $target['userId'],
'targetId' => $target['_id'],
]
];
$target = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $projectId,
], $this->getHeaders()), $graphQLPayload);
$this->assertEquals(204, $target['headers']['status-code']);
}
public function testDeleteUser()
{
$projectId = $this->getProject()['$id'];

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,413 @@
<?php
namespace Tests\E2E\Services\Messaging;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideConsole;
use Utopia\Database\Helpers\ID;
class MessagingConsoleClientTest extends Scope
{
use MessagingBase;
use ProjectCustom;
use SideConsole;
/**
* @depends testListProviders
*/
public function testGetProviderLogs(array $providers): void
{
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $providers[0]['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$provider = $this->client->call(Client::METHOD_POST, '/messaging/providers/sendgrid/', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'providerId' => ID::unique(),
'name' => 'Sengrid1',
'apiKey' => 'my-apikey',
'from' => 'sender-email@my-domain.com',
]);
$this->assertEquals(201, $provider['headers']['status-code']);
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/providers/sendgrid/' . $provider['body']['$id'], \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'sendgrid' => [
'name' => 'Sendgrid2',
]]);
$this->assertEquals(200, $response['headers']['status-code']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$this->assertCount(2, $logs['body']['logs']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)', 'offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("$id", "asdf")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['orderAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/providers/' . $provider['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['cursorAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
}
/**
* @depends testListTopic
*/
public function testGetTopicLogs(string $topicId): void
{
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topicId . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$topic = $this->client->call(Client::METHOD_POST, '/messaging/topics', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'topicId' => ID::unique(),
'name' => 'my-app',
'description' => 'web app'
]);
$this->assertEquals(201, $topic['headers']['status-code']);
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/topics/' . $topic['body']['$id'], \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'description' => 'updated-description'
]);
$this->assertEquals(200, $response['headers']['status-code']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertCount(2, $logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)', 'offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("$id", "asdf")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['orderAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/topics/' . $topic['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['cursorAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
}
/**
* @depends testSendEmail
*/
public function testGetMessageLogs(array $email): void
{
/**
* Test for SUCCESS
*/
$logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$email = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'messageId' => ID::unique(),
'status' => 'draft',
'topics' => [ID::unique()],
'subject' => 'Khali beats Undertaker',
'content' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
]);
$this->assertEquals(201, $email['headers']['status-code']);
$response = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $email['body']['$id'], \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'subject' => 'Khali beats John Cena!',
]);
$this->assertEquals(200, $response['headers']['status-code']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$this->assertCount(2, $logs['body']['logs']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertIsNumeric($logs['body']['total']);
$logs = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(1)', 'offset(1)'],
]);
$this->assertEquals($logs['headers']['status-code'], 200);
$this->assertIsArray($logs['body']['logs']);
$this->assertLessThanOrEqual(1, count($logs['body']['logs']));
$this->assertIsNumeric($logs['body']['total']);
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['limit(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['offset(-1)']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['equal("$id", "asdf")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['orderAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
$response = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $email['body']['$id'] . '/logs', \array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'queries' => ['cursorAsc("$id")']
]);
$this->assertEquals($response['headers']['status-code'], 400);
}
}

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\SideClient;
class MessagingCustomClientTest extends Scope
{
use MessagingBase;
use ProjectCustom;
use SideClient;
}

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

@ -794,7 +794,7 @@ class ProjectsConsoleClientTest extends Scope
public function testUpdateProjectOAuth($data): array
{
$id = $data['projectId'] ?? '';
$providers = require('app/config/providers.php');
$providers = require('app/config/oAuthProviders.php');
/**
* Test for SUCCESS
@ -825,7 +825,7 @@ class ProjectsConsoleClientTest extends Scope
foreach ($providers as $key => $provider) {
$asserted = false;
foreach ($response['body']['providers'] as $responseProvider) {
foreach ($response['body']['oAuthProviders'] as $responseProvider) {
if ($responseProvider['key'] === $key) {
$this->assertEquals('AppId-' . ucfirst($key), $responseProvider['appId']);
$this->assertEquals('Secret-' . ucfirst($key), $responseProvider['secret']);
@ -867,7 +867,7 @@ class ProjectsConsoleClientTest extends Scope
$i = 0;
foreach ($providers as $key => $provider) {
$asserted = false;
foreach ($response['body']['providers'] as $responseProvider) {
foreach ($response['body']['oAuthProviders'] as $responseProvider) {
if ($responseProvider['key'] === $key) {
// On first provider, test enabled=false
$this->assertEquals($i !== 0, $responseProvider['enabled']);
@ -931,7 +931,7 @@ class ProjectsConsoleClientTest extends Scope
'password' => $originalPassword,
]);
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $id];
$session = $response['cookies']['a_session_' . $id];
/**
* Test for SUCCESS
@ -1313,7 +1313,7 @@ class ProjectsConsoleClientTest extends Scope
'password' => $password,
]);
$this->assertEquals(201, $session['headers']['status-code']);
$session = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_' . $id];
$session = $session['cookies']['a_session_' . $id];
$response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([
'origin' => 'http://localhost',

View file

@ -468,7 +468,7 @@ class RealtimeCustomClientTest extends Scope
'password' => 'new-password',
]);
$sessionNew = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $projectId];
$sessionNew = $response['cookies']['a_session_' . $projectId];
$sessionNewId = $response['body']['$id'];
return array("session" => $sessionNew, "sessionId" => $sessionNewId);

View file

@ -32,7 +32,7 @@ trait StoragePermissionsScope
'password' => $password,
]);
$session = $this->client->parseCookie((string)$session['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session = $session['cookies']['a_session_' . $this->getProject()['$id']];
$user = [

View file

@ -403,7 +403,7 @@ trait TeamsBaseClient
$this->assertCount(2, $response['body']['roles']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($response['body']['joined']));
$this->assertEquals(true, $response['body']['confirm']);
$session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_' . $this->getProject()['$id']];
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$data['session'] = $session;
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([

Some files were not shown because too many files have changed in this diff Show more