Merge remote-tracking branch 'origin/1.5.x' into feat-smtp-test

This commit is contained in:
Matej Bačo
2024-01-15 11:07:32 +00:00
223 changed files with 13709 additions and 2223 deletions
+1 -1
View File
@@ -67,7 +67,7 @@ class Auth
public const TOKEN_EXPIRATION_LOGIN_LONG = 31536000; /* 1 year */
public const TOKEN_EXPIRATION_LOGIN_SHORT = 3600; /* 1 hour */
public const TOKEN_EXPIRATION_RECOVERY = 3600; /* 1 hour */
public const TOKEN_EXPIRATION_CONFIRM = 3600 * 24 * 7; /* 7 days */
public const TOKEN_EXPIRATION_CONFIRM = 3600 * 1; /* 1 hour */
public const TOKEN_EXPIRATION_PHONE = 60 * 15; /* 15 minutes */
/**
+163
View File
@@ -0,0 +1,163 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
// Reference Material
// https://zoho.com/accounts/protocol/oauth.html
class Zoho extends OAuth2
{
/**
* @var string
*/
private string $endpoint = 'https://accounts.zoho.com';
/**
* @var array
*/
protected array $scopes = [
'email',
'profile',
];
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @return string
*/
public function getName(): string
{
return 'zoho';
}
/**
* @return string
*/
public function getLoginURL(): string
{
$url = $this->endpoint . '/oauth/v2/auth?' .
\http_build_query([
'response_type' => 'code',
'client_id' => $this->appID,
'state' => \json_encode($this->state),
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes())
]);
return $url;
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth/v2/token',
["Content-Type: application/x-www-form-urlencoded"],
\http_build_query([
'grant_type' => 'authorization_code',
"client_id" => $this->appID,
"client_secret" => $this->appSecret,
"redirect_uri" => $this->callback,
'code' => $code,
'scope' => \implode(' ', $this->getScopes()),
])
), true);
$this->user = (isset($this->tokens['id_token'])) ? \explode('.', $this->tokens['id_token']) : [0 => '', 1 => ''];
$this->user = (isset($this->user[1])) ? \json_decode(\base64_decode($this->user[1]), true) : [];
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$this->tokens = \json_decode($this->request(
'POST',
$this->endpoint . '/oauth/v2/token',
['Content-Type: application/x-www-form-urlencoded'],
\http_build_query([
'grant_type' => 'refresh_token',
'refresh_token' => $refreshToken,
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
])
), true);
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
$this->user = (isset($this->tokens['id_token'])) ? \explode('.', $this->tokens['id_token']) : [0 => '', 1 => ''];
$this->user = (isset($this->user[1])) ? \json_decode(\base64_decode($this->user[1]), true) : [];
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
return $this->user['sub'] ?? '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
return $this->user['email'] ?? '';
}
/**
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
if ($this->user['email_verified'] ?? false) {
return true;
}
return false;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
return $this->user['name'] ?? '';
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace Appwrite\Auth;
class SecurityPhrase
{
public static function generate(): string
{
$adjectives = ["Abundant", "Adaptable", "Adventurous", "Affectionate", "Agile", "Amiable", "Amazing", "Ambitious", "Amicable", "Amusing", "Astonishing", "Attentive", "Authentic", "Awesome", "Balanced", "Beautiful", "Bold", "Brave", "Bright", "Bubbly", "Calm", "Capable", "Charismatic", "Charming", "Cheerful", "Clever", "Colorful", "Compassionate", "Confident", "Cooperative", "Courageous", "Courteous", "Creative", "Curious", "Dazzling", "Dedicated", "Delightful", "Determined", "Diligent", "Dynamic", "Easygoing", "Effervescent", "Efficient", "Elegant", "Empathetic", "Energetic", "Enthusiastic", "Exuberant", "Faithful", "Fantastic", "Fearless", "Flexible", "Friendly", "Fun-loving", "Generous", "Gentle", "Genuine", "Graceful", "Gracious", "Happy", "Hardworking", "Harmonious", "Helpful", "Honest", "Hopeful", "Humble", "Imaginative", "Impressive", "Incredible", "Inspiring", "Intelligent", "Joyful", "Kind", "Knowledgeable", "Lively", "Lovable", "Lovely", "Loyal", "Majestic", "Magnificent", "Mindful", "Modest", "Passionate", "Patient", "Peaceful", "Perseverant", "Playful", "Polite", "Positive", "Powerful", "Practical", "Precious", "Proactive", "Productive", "Punctual", "Quick-witted", "Radiant", "Reliable", "Resilient", "Resourceful", "Respectful", "Responsible", "Sensitive", "Serene", "Sincere", "Skillful", "Soothing", "Spirited", "Splendid", "Steadfast", "Strong", "Supportive", "Sweet", "Talented", "Thankful", "Thoughtful", "Thriving", "Tranquil", "Trustworthy", "Upbeat", "Versatile", "Vibrant", "Vigilant", "Warmhearted", "Welcoming", "Wholesome", "Witty", "Wonderful", "Zealous"];
$nouns = ["apple", "banana", "cat", "dog", "elephant", "fish", "guitar", "hat", "ice cream", "jacket", "kangaroo", "lemon", "moon", "notebook", "orange", "piano", "quilt", "rabbit", "sun", "tree", "umbrella", "violin", "watermelon", "xylophone", "yogurt", "zebra", "airplane", "ball", "cloud", "diamond", "eagle", "fire", "giraffe", "hammer", "island", "jellyfish", "kiwi", "lamp", "mango", "needle", "ocean", "pear", "quasar", "rose", "star", "turtle", "unicorn", "volcano", "whale", "xylograph", "yarn", "zephyr", "ant", "book", "candle", "door", "envelope", "feather", "globe", "harp", "insect", "jar", "kite", "lighthouse", "magnet", "necklace", "owl", "puzzle", "queen", "rainbow", "sailboat", "telescope", "umbrella", "vase", "wallet", "xylograph", "yacht", "zeppelin", "accordion", "brush", "chocolate", "dolphin", "easel", "fountain", "globe", "hairbrush", "iceberg", "jigsaw", "kettle", "leopard", "marble", "nutmeg", "obstacle", "penguin", "quiver", "raccoon", "sphinx", "trampoline", "utensil", "velvet", "wagon", "xerox", "yodel", "zipper"];
$adjective = $adjectives[array_rand($adjectives)];
$noun = $nouns[array_rand($nouns)];
$phrase = "{$adjective} {$noun}";
return $phrase;
}
}
+5 -1
View File
@@ -20,7 +20,7 @@ class Password extends Validator
*/
public function getDescription(): string
{
return 'Password must be at least 8 characters';
return 'Password must be between 8 and 256 characters long.';
}
/**
@@ -40,6 +40,10 @@ class Password extends Validator
return false;
}
if (\strlen($value) > 256) {
return false;
}
return true;
}
@@ -27,7 +27,7 @@ class PasswordDictionary extends Password
*/
public function getDescription(): string
{
return 'Password must be at least 8 characters and should not be one of the commonly used password.';
return 'Password must be between 8 and 265 characters long, and should not be one of the commonly used password.';
}
/**
+27
View File
@@ -0,0 +1,27 @@
<?php
namespace Appwrite\Enum;
enum MessageStatus: string
{
/**
* Message that is not ready to be sent
*/
case Draft = 'draft';
/**
* Scheduled to be sent for a later time
*/
case Scheduled = 'scheduled';
/**
* Picked up by the worker and starting to send
*/
case Processing = 'processing';
/**
* Sent without errors
*/
case Sent = 'sent';
/**
* Sent with some errors
*/
case Failed = 'failed';
}
+3
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 = '';
+157
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;
}
}
+173
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,
]);
}
}
-87
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())
]);
}
}
+46 -3
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';
@@ -229,12 +233,40 @@ class Exception extends \Exception
public const MIGRATION_PROVIDER_ERROR = 'migration_provider_error';
/** Realtime */
public const REALTIME_MESSAGE_FORMAT_INVALID = 'realtime_message_format_invalid';
public const REALTIME_TOO_MANY_MESSAGES = 'realtime_too_many_messages';
public const REALTIME_POLICY_VIOLATION = 'realtime_policy_violation';
public const REALTIME_MESSAGE_FORMAT_INVALID = 'realtime_message_format_invalid';
public const REALTIME_TOO_MANY_MESSAGES = 'realtime_too_many_messages';
public const REALTIME_POLICY_VIOLATION = 'realtime_policy_violation';
/** 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;
public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int $code = null, \Throwable $previous = null)
{
@@ -244,6 +276,7 @@ class Exception extends \Exception
if (isset($this->errors[$type])) {
$this->code = $this->errors[$type]['code'];
$this->message = $this->errors[$type]['description'];
$this->publish = $this->errors[$type]['publish'] ?? true;
}
$this->message = $message ?? $this->message;
@@ -273,4 +306,14 @@ class Exception extends \Exception
{
$this->type = $type;
}
/**
* Check whether the log is publishable for the exception.
*
* @return bool
*/
public function isPublishable(): bool
{
return $this->publish;
}
}
+12 -4
View File
@@ -76,7 +76,8 @@ abstract class Migration
'1.4.9' => 'V19',
'1.4.10' => 'V19',
'1.4.11' => 'V19',
'1.4.12' => 'V19'
'1.4.12' => 'V19',
'1.4.13' => 'V19'
];
/**
@@ -194,17 +195,24 @@ abstract class Migration
* @return iterable<Document>
* @throws \Exception
*/
public function documentsIterator(string $collectionId): iterable
public function documentsIterator(string $collectionId, $queries = []): iterable
{
$sum = 0;
$nextDocument = null;
$collectionCount = $this->projectDB->count($collectionId);
$queries[] = Query::limit($this->limit);
do {
$queries = [Query::limit($this->limit)];
if ($nextDocument !== null) {
$queries[] = Query::cursorAfter($nextDocument);
$cursorQueryIndex = \array_search('cursorAfter', \array_map(fn (Query $query) => $query->getMethod(), $queries));
if ($cursorQueryIndex !== false) {
$queries[$cursorQueryIndex] = Query::cursorAfter($nextDocument);
} else {
$queries[] = Query::cursorAfter($nextDocument);
}
}
$documents = $this->projectDB->find($collectionId, $queries);
$count = count($documents);
$sum += $count;
+1 -1
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', []))
)
);
+6 -6
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;
}
+22
View File
@@ -10,6 +10,7 @@ use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception;
use Utopia\Database\Query;
class V19 extends Migration
{
@@ -41,6 +42,11 @@ class V19 extends Migration
Console::info('Migrating Buckets');
$this->migrateBuckets();
if ($this->project->getId() !== 'console') {
Console::info('Migrating Enum Attribute Size');
$this->migrateEnumAttributeSize();
}
Console::info('Migrating Documents');
$this->forEachDocument([$this, 'fixDocument']);
@@ -640,6 +646,22 @@ class V19 extends Migration
return $commands;
}
private function migrateEnumAttributeSize(): void
{
foreach (
$this->documentsIterator('attributes', [
Query::equal('format', ['enum']),
Query::lessThan('size', Database::LENGTH_KEY)
]) as $attribute
) {
$attribute->setAttribute('size', Database::LENGTH_KEY);
$this->projectDB->updateDocument('attributes', $attribute->getId(), $attribute);
$databaseInternalId = $attribute->getAttribute('databaseInternalId');
$collectionInternalId = $attribute->getAttribute('collectionInternalId');
$this->projectDB->updateAttribute('database_' . $databaseInternalId . '_collection_' . $collectionInternalId, $attribute->getAttribute('key'), size: 255);
}
}
/**
* Fix run on each document
*
+4
View File
@@ -19,6 +19,8 @@ use Appwrite\Platform\Tasks\VolumeSync;
use Appwrite\Platform\Tasks\CalcTierStats;
use Appwrite\Platform\Tasks\Upgrade;
use Appwrite\Platform\Tasks\DeleteOrphanedProjects;
use Appwrite\Platform\Tasks\DevGenerateTranslations;
use Appwrite\Platform\Tasks\GetMigrationStats;
use Appwrite\Platform\Tasks\PatchRecreateRepositoriesDocuments;
class Tasks extends Service
@@ -44,6 +46,8 @@ class Tasks extends Service
->addAction(CalcTierStats::getName(), new CalcTierStats())
->addAction(DeleteOrphanedProjects::getName(), new DeleteOrphanedProjects())
->addAction(PatchRecreateRepositoriesDocuments::getName(), new PatchRecreateRepositoriesDocuments())
->addAction(GetMigrationStats::getName(), new GetMigrationStats())
->addAction(DevGenerateTranslations::getName(), new DevGenerateTranslations())
;
}
@@ -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())
;
}
+11 -3
View File
@@ -41,6 +41,7 @@ class CalcTierStats extends Action
'Functions',
'Deployments',
'Executions',
'Migrations',
];
protected string $directory = '/usr/local';
@@ -99,8 +100,8 @@ class CalcTierStats extends Action
$projects = [$console];
$count = 0;
$limit = 30;
$sum = 30;
$limit = 100;
$sum = 100;
$offset = 0;
while (!empty($projects)) {
foreach ($projects as $project) {
@@ -200,7 +201,7 @@ class CalcTierStats extends Action
try {
/** Get Domains */
$stats['Domains'] = $dbForConsole->count('domains', [
$stats['Domains'] = $dbForConsole->count('rules', [
Query::equal('projectInternalId', [$project->getInternalId()]),
]);
} catch (\Throwable) {
@@ -290,6 +291,13 @@ class CalcTierStats extends Action
$stats['Executions'] = 0;
}
/** Get Total Migrations */
try {
$stats['Migrations'] = $dbForProject->count('migrations', []);
} catch (\Throwable) {
$stats['Migrations'] = 0;
}
$csv->insertOne(array_values($stats));
} catch (\Throwable $th) {
Console::error('Failed on project ("' . $project->getId() . '") version with error on File: ' . $th->getFile() . ' line no: ' . $th->getline() . ' with message: ' . $th->getMessage());
@@ -4,6 +4,7 @@ namespace Appwrite\Platform\Tasks;
use Utopia\App;
use Utopia\Config\Config;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Platform\Action;
use Utopia\Cache\Cache;
@@ -45,6 +46,17 @@ class DeleteOrphanedProjects extends Action
/** @var array $collections */
$collectionsConfig = Config::getParam('collections', [])['projects'] ?? [];
$collectionsConfig = array_merge([
'audit' => [
'$id' => ID::custom('audit'),
'$collection' => Database::METADATA
],
'abuse' => [
'$id' => ID::custom('abuse'),
'$collection' => Database::METADATA
]
], $collectionsConfig);
/* Initialise new Utopia app */
$app = new App('UTC');
$console = $app->getResource('console');
@@ -54,7 +66,7 @@ class DeleteOrphanedProjects extends Action
$totalProjects = $dbForConsole->count('projects');
Console::success("Found a total of: {$totalProjects} projects");
$orphans = 0;
$orphans = 1;
$cnt = 0;
$count = 0;
$limit = 30;
@@ -80,6 +92,7 @@ class DeleteOrphanedProjects extends Action
$dbForProject = new Database($adapter, $cache);
$dbForProject->setDefaultDatabase('appwrite');
$dbForProject->setNamespace('_' . $project->getInternalId());
$collectionsCreated = 0;
$cnt++;
if ($dbForProject->exists($dbForProject->getDefaultDatabase(), Database::METADATA)) {
@@ -87,10 +100,8 @@ class DeleteOrphanedProjects extends Action
}
$msg = '(' . $cnt . ') found (' . $collectionsCreated . ') collections on project (' . $project->getInternalId() . ') , database (' . $project['database'] . ')';
/**
* +2 = audit+abuse
*/
if ($collectionsCreated >= (count($collectionsConfig) + 2)) {
if ($collectionsCreated >= count($collectionsConfig)) {
Console::log($msg . ' ignoring....');
continue;
}
@@ -107,16 +118,26 @@ class DeleteOrphanedProjects extends Action
Console::info('--Deleting collection (' . $collection->getId() . ') project no (' . $project->getInternalId() . ')');
}
}
if ($commit) {
$dbForConsole->deleteDocument('projects', $project->getId());
$dbForConsole->deleteCachedDocument('projects', $project->getId());
if ($dbForProject->exists($dbForProject->getDefaultDatabase(), Database::METADATA)) {
try {
$dbForProject->deleteCollection(Database::METADATA);
$dbForProject->deleteCachedCollection(Database::METADATA);
} catch (\Throwable $th) {
Console::warning('Metadata collection does not exist');
}
}
}
Console::info('--Deleting project no (' . $project->getInternalId() . ')');
$orphans++;
} catch (\Throwable $th) {
Console::error('Error: ' . $th->getMessage());
Console::error('Error: ' . $th->getMessage() . ' ' . $th->getTraceAsString());
} finally {
$pools
->get($db)
@@ -135,6 +156,6 @@ class DeleteOrphanedProjects extends Action
$count = $count + $sum;
}
Console::log('Iterated through ' . $count - 1 . '/' . $totalProjects . ' projects found ' . $orphans . ' orphans');
Console::log('Iterated through ' . $count - 1 . '/' . $totalProjects . ' projects found ' . $orphans - 1 . ' orphans');
}
}
@@ -0,0 +1,109 @@
<?php
namespace Appwrite\Platform\Tasks;
use Exception;
use Utopia\CLI\Console;
use Utopia\Fetch\Client;
use Utopia\Platform\Action;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class DevGenerateTranslations extends Action
{
private string $apiKey = '';
public static function getName(): string
{
return 'dev-generate-translations';
}
public function __construct()
{
$this
->desc('Generate translations in all languages')
->param('dry-run', 'true', new Boolean(true), 'If action should do a dry run. Dry run does not write into files', true)
->param('api-key', '', new Text(256), 'Open AI API key. Only used during non-dry runs to generate translations.', true)
->callback(fn ($dryRun, $apiKey) => $this->action($dryRun, $apiKey));
}
public function action(bool|string $dryRun, string $apiKey): void
{
$dryRun = \strval($dryRun) === 'true';
Console::info("Started");
if (!$dryRun && empty($apiKey)) {
Console::error("Please specify --api-key='OPEN_AI_API_KEY' or run with --dry-run");
return;
}
$this->apiKey = $apiKey;
$dir = __DIR__ . '/../../../../app/config/locale/translations';
$mainFile = 'en.json';
$mainJson = \json_decode(\file_get_contents($dir . '/' . $mainFile), true);
$mainKeys = \array_keys($mainJson);
$files = array_diff(scandir($dir), array('.', '..', $mainFile));
foreach ($files as $file) {
$fileJson = \json_decode(\file_get_contents($dir . '/' . $file), true);
$fileKeys = \array_keys($fileJson);
// Trick to clear specific key from all translation files:
// $json = \json_decode(\file_get_contents($dir . '/' . $file), true);
// unset($json['emails.magicSession.optionUrl']);
// \file_put_contents($dir . '/' . $file, \json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | 0));
// continue;
foreach ($mainKeys as $key) {
if (!(\in_array($key, $fileKeys))) {
if ($dryRun) {
Console::warning("{$file} missing translation for {$key}");
} else {
$language = \explode('.', $file)[0];
$translation = $this->generateTranslation($language, $mainJson[$key]);
$json = \json_decode(\file_get_contents($dir . '/' . $file), true);
$json[$key] = $translation;
\file_put_contents($dir . '/' . $file, \json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | 0));
Console::success("Generated {$key} for {$language}");
}
}
}
}
Console::info("Done");
}
private function generateTranslation(string $targetLanguage, string $enTranslation): string
{
$response = Client::fetch('https://api.openai.com/v1/chat/completions', [
'content-type' => Client::CONTENT_TYPE_APPLICATION_JSON,
'Authorization' => 'Bearer ' . $this->apiKey
], Client::METHOD_POST, [
'model' => 'gpt-4-1106-preview', // https://platform.openai.com/docs/models/gpt-4-and-gpt-4-turbo
'messages' => [
[
'role' => 'system',
'content' => "Please translate the message user provides from English language to target language. Target language is language of country with country code {$targetLanguage}. Do not translate text inside {{ and }} placeholders. Provide only translated text."
],
[
'role' => 'user',
'content' => $enTranslation
]
]
], [], 60);
$body = \json_decode($response->getBody(), true);
if ($response->getStatusCode() >= 400) {
throw new Exception($response->getBody() . ' with status code ' . $response->getStatusCode() . ' for language ' . $targetLanguage . ' and message ' . $enTranslation);
}
return $body['choices'][0]['message']['content'];
}
}
@@ -0,0 +1,187 @@
<?php
namespace Appwrite\Platform\Tasks;
use Exception;
use League\Csv\CannotInsertRecord;
use Utopia\App;
use Utopia\Platform\Action;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Query;
use League\Csv\Writer;
use PHPMailer\PHPMailer\PHPMailer;
use Utopia\Pools\Group;
use Utopia\Registry\Registry;
class GetMigrationStats extends Action
{
/*
* Csv cols headers
*/
private array $columns = [
'Project ID',
'$id',
'$createdAt',
'status',
'stage',
'source'
];
protected string $directory = '/usr/local';
protected string $path;
protected string $date;
public static function getName(): string
{
return 'get-migration-stats';
}
public function __construct()
{
$this
->desc('Get stats for projects')
->inject('pools')
->inject('cache')
->inject('dbForConsole')
->inject('register')
->callback(function (Group $pools, Cache $cache, Database $dbForConsole, Registry $register) {
$this->action($pools, $cache, $dbForConsole, $register);
});
}
/**
* @throws \Utopia\Exception
* @throws CannotInsertRecord
*/
public function action(Group $pools, Cache $cache, Database $dbForConsole, Registry $register): void
{
//docker compose exec -t appwrite get-migration-stats
Console::title('Migration stats calculation V1');
Console::success(APP_NAME . ' Migration stats calculation has started');
/* Initialise new Utopia app */
$app = new App('UTC');
$console = $app->getResource('console');
/** CSV stuff */
$this->date = date('Y-m-d');
$this->path = "{$this->directory}/migration_stats_{$this->date}.csv";
$csv = Writer::createFromPath($this->path, 'w');
$csv->insertOne($this->columns);
/** Database connections */
$totalProjects = $dbForConsole->count('projects');
Console::success("Found a total of: {$totalProjects} projects");
$projects = [$console];
$count = 0;
$limit = 100;
$sum = 100;
$offset = 0;
while (!empty($projects)) {
foreach ($projects as $project) {
/**
* Skip user projects with id 'console'
*/
if ($project->getId() === 'console') {
continue;
}
Console::info("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());
/** Get Project ID */
$stats['Project ID'] = $project->getId();
/** Get Migration details */
$migrations = $dbForProject->find('migrations', [
Query::limit(500)
]);
$migrations = array_map(function ($migration) use ($project) {
return [
$project->getId(),
$migration->getAttribute('$id'),
$migration->getAttribute('$createdAt'),
$migration->getAttribute('status'),
$migration->getAttribute('stage'),
$migration->getAttribute('source'),
];
}, $migrations);
if (!empty($migrations)) {
$csv->insertAll($migrations);
}
} catch (\Throwable $th) {
Console::error('Failed on project ("' . $project->getId() . '") with error on File: ' . $th->getFile() . ' line no: ' . $th->getline() . ' with message: ' . $th->getMessage());
} finally {
$pools
->get($db)
->reclaim();
}
}
$sum = \count($projects);
$projects = $dbForConsole->find('projects', [
Query::limit($limit),
Query::offset($offset),
]);
$offset = $offset + $limit;
$count = $count + $sum;
}
Console::log('Iterated through ' . $count - 1 . '/' . $totalProjects . ' projects...');
$pools
->get('console')
->reclaim();
/** @var PHPMailer $mail */
$mail = $register->get('smtp');
$mail->clearAddresses();
$mail->clearAllRecipients();
$mail->clearReplyTos();
$mail->clearAttachments();
$mail->clearBCCs();
$mail->clearCCs();
try {
/** Addresses */
$mail->setFrom(App::getEnv('_APP_SYSTEM_EMAIL_ADDRESS', APP_EMAIL_TEAM), 'Appwrite Cloud Hamster');
$recipients = explode(',', App::getEnv('_APP_USERS_STATS_RECIPIENTS', ''));
foreach ($recipients as $recipient) {
$mail->addAddress($recipient);
}
/** Attachments */
$mail->addAttachment($this->path);
/** Content */
$mail->Subject = "Migration Report for {$this->date}";
$mail->Body = "Please find the migration report atttached";
$mail->send();
Console::success('Email has been sent!');
} catch (Exception $e) {
Console::error("Message could not be sent. Mailer Error: {$mail->ErrorInfo}");
}
}
}
+50 -368
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,263 +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 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.
*/
@@ -315,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());
@@ -345,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;
@@ -358,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)
]));
@@ -368,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++;
}
}
@@ -378,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());
}
+11 -7
View File
@@ -8,6 +8,7 @@ use Appwrite\Docker\Env;
use Appwrite\Utopia\View;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Platform\Action;
@@ -24,15 +25,16 @@ class Install extends Action
{
$this
->desc('Install Appwrite')
->param('httpPort', '', new Text(4), 'Server HTTP port', true)
->param('httpsPort', '', new Text(4), 'Server HTTPS port', true)
->param('http-port', '', new Text(4), 'Server HTTP port', true)
->param('https-port', '', new Text(4), 'Server HTTPS port', true)
->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true)
->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true)
->param('interactive', 'Y', new Text(1), 'Run an interactive session', true)
->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive));
->param('no-start', false, new Boolean(true), 'Run an interactive session', true)
->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive, $noStart) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive, $noStart));
}
public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive): void
public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive, bool $noStart): void
{
$config = Config::getParam('variables');
$defaultHTTPPort = '80';
@@ -220,9 +222,11 @@ class Install extends Action
}
}
Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\"");
$exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr);
$exit = 0;
if (!$noStart) {
Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\"");
$exit = Console::execute("$env docker compose --project-directory $this->path up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr);
}
if ($exit !== 0) {
$message = 'Failed to install Appwrite dockers';
+6 -2
View File
@@ -11,6 +11,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\Registry\Registry;
use Utopia\Validator\Text;
class Migrate extends Action
@@ -29,7 +30,8 @@ class Migrate extends Action
->inject('cache')
->inject('dbForConsole')
->inject('getProjectDB')
->callback(fn ($version, $cache, $dbForConsole, $getProjectDB) => $this->action($version, $cache, $dbForConsole, $getProjectDB));
->inject('register')
->callback(fn ($version, $cache, $dbForConsole, $getProjectDB, Registry $register) => $this->action($version, $cache, $dbForConsole, $getProjectDB, $register));
}
private function clearProjectsCache(Cache $cache, Document $project)
@@ -41,7 +43,7 @@ class Migrate extends Action
}
}
public function action(string $version, Cache $cache, Database $dbForConsole, callable $getProjectDB)
public function action(string $version, Cache $cache, Database $dbForConsole, callable $getProjectDB, Registry $register)
{
Authorization::disable();
if (!array_key_exists($version, Migration::$versions)) {
@@ -89,9 +91,11 @@ class Migrate extends Action
try {
// TODO: Iterate through all project DBs
/** @var Database $projectDB */
$projectDB = $getProjectDB($project);
$migration
->setProject($project, $projectDB, $dbForConsole)
->setPDO($register->get('db', true))
->execute();
} catch (\Throwable $th) {
Console::error('Failed to update project ("' . $project->getId() . '") version with error: ' . $th->getMessage());
+5
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) {
+7 -5
View File
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Tasks;
use Utopia\CLI\Console;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
class Upgrade extends Install
@@ -16,15 +17,16 @@ class Upgrade extends Install
{
$this
->desc('Upgrade Appwrite')
->param('httpPort', '', new Text(4), 'Server HTTP port', true)
->param('httpsPort', '', new Text(4), 'Server HTTPS port', true)
->param('http-port', '', new Text(4), 'Server HTTP port', true)
->param('https-port', '', new Text(4), 'Server HTTPS port', true)
->param('organization', 'appwrite', new Text(0), 'Docker Registry organization', true)
->param('image', 'appwrite', new Text(0), 'Main appwrite docker image', true)
->param('interactive', 'Y', new Text(1), 'Run an interactive session', true)
->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive));
->param('no-start', false, new Boolean(true), 'Run an interactive session', true)
->callback(fn ($httpPort, $httpsPort, $organization, $image, $interactive, $noStart) => $this->action($httpPort, $httpsPort, $organization, $image, $interactive, $noStart));
}
public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive): void
public function action(string $httpPort, string $httpsPort, string $organization, string $image, string $interactive, bool $noStart): void
{
// Check for previous installation
$data = @file_get_contents($this->path . '/docker-compose.yml');
@@ -37,6 +39,6 @@ class Upgrade extends Install
Console::log(' └── docker-compose.yml');
Console::exit(1);
}
parent::action($httpPort, $httpsPort, $organization, $image, $interactive);
parent::action($httpPort, $httpsPort, $organization, $image, $interactive, $noStart);
}
}
+20 -12
View File
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Workers;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Usage;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Usage\Stats;
use Appwrite\Utopia\Response\Model\Deployment;
@@ -24,10 +23,10 @@ use Utopia\Database\Exception\Restricted;
use Utopia\Database\Validator\Authorization;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
use Utopia\VCS\Adapter\Git\GitHub;
class Builds extends Action
@@ -52,7 +51,8 @@ class Builds extends Action
->inject('cache')
->inject('dbForProject')
->inject('getFunctionsDevice')
->callback(fn($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Stats $usage, Cache $cache, Database $dbForProject, callable $getFunctionsDevice) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $getFunctionsDevice));
->inject('log')
->callback(fn($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Stats $usage, Cache $cache, Database $dbForProject, callable $getFunctionsDevice, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $getFunctionsDevice, $log));
}
/**
@@ -64,10 +64,11 @@ class Builds extends Action
* @param Cache $cache
* @param Database $dbForProject
* @param callable $getFunctionsDevice
* @param Log $log
* @return void
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Stats $usage, Cache $cache, Database $dbForProject, callable $getFunctionsDevice): void
public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Stats $usage, Cache $cache, Database $dbForProject, callable $getFunctionsDevice, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -81,12 +82,15 @@ class Builds extends Action
$deployment = new Document($payload['deployment'] ?? []);
$template = new Document($payload['template'] ?? []);
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
switch ($type) {
case BUILD_TYPE_DEPLOYMENT:
case BUILD_TYPE_RETRY:
Console::info('Creating build for deployment: ' . $deployment->getId());
$github = new GitHub($cache);
$this->buildDeployment($getFunctionsDevice, $queueForFunctions, $queueForEvents, $usage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template);
$this->buildDeployment($getFunctionsDevice, $queueForFunctions, $queueForEvents, $usage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log);
break;
default:
@@ -106,20 +110,27 @@ class Builds extends Action
* @param Document $function
* @param Document $deployment
* @param Document $template
* @param Log $log
* @return void
* @throws \Utopia\Database\Exception
* @throws Exception
*/
protected function buildDeployment(callable $getFunctionsDevice, Func $queueForFunctions, Event $queueForEvents, Stats $usage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template): void
protected function buildDeployment(callable $getFunctionsDevice, Func $queueForFunctions, Event $queueForEvents, Stats $usage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void
{
$executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST'));
$function = $dbForProject->getDocument('functions', $function->getId());
$functionId = $function->getId();
$log->addTag('functionId', $function->getId());
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception('Function not found', 404);
}
$deployment = $dbForProject->getDocument('deployments', $deployment->getId());
$deploymentId = $deployment->getId();
$log->addTag('deploymentId', $deploymentId);
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->isEmpty()) {
throw new Exception('Deployment not found', 404);
}
@@ -501,10 +512,7 @@ class Builds extends Action
$build->setAttribute('endTime', $endTime);
$build->setAttribute('duration', \intval(\ceil($durationEnd - $durationStart)));
$build->setAttribute('status', 'failed');
$build->setAttribute('logs', $th->getMessage());
Console::error($th->getMessage());
Console::error($th->getFile() . ':' . $th->getLine());
Console::error($th->getTraceAsString());
$build->setAttribute('logs', $th->getMessage() . "\n" . $th->getFile() . ':' . $th->getLine() . "\n" . $th->getTraceAsString());
if ($isVcsEnabled) {
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $function, $deployment->getId(), $dbForProject, $dbForConsole);
@@ -23,6 +23,7 @@ use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Domains\Domain;
use Utopia\Locale\Locale;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
@@ -45,7 +46,8 @@ class Certificates extends Action
->inject('queueForMails')
->inject('queueForEvents')
->inject('queueForFunctions')
->callback(fn(Message $message, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions) => $this->action($message, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions));
->inject('log')
->callback(fn(Message $message, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log) => $this->action($message, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $log));
}
/**
@@ -54,11 +56,12 @@ class Certificates extends Action
* @param Mail $queueForMails
* @param Event $queueForEvents
* @param Func $queueForFunctions
* @param Log $log
* @return void
* @throws Throwable
* @throws \Utopia\Database\Exception
*/
public function action(Message $message, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions): void
public function action(Message $message, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -70,6 +73,8 @@ class Certificates extends Action
$domain = new Domain($document->getAttribute('domain', ''));
$skipRenewCheck = $payload['skipRenewCheck'] ?? false;
$log->addTag('domain', $domain->get());
$this->execute($domain, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $skipRenewCheck);
}
+18 -6
View File
@@ -4,7 +4,6 @@ namespace Appwrite\Platform\Workers;
use Appwrite\Event\Event;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Utopia\Response\Model\Platform;
use Exception;
use Utopia\Audit\Audit;
use Utopia\CLI\Console;
@@ -16,6 +15,7 @@ use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Exception as DatabaseException;
use Utopia\Database\Query;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
@@ -36,17 +36,19 @@ class Databases extends Action
->inject('message')
->inject('dbForConsole')
->inject('dbForProject')
->callback(fn($message, $dbForConsole, $dbForProject) => $this->action($message, $dbForConsole, $dbForProject));
->inject('log')
->callback(fn(Message $message, Database $dbForConsole, Database $dbForProject, Log $log) => $this->action($message, $dbForConsole, $dbForProject, $log));
}
/**
* @param Message $message
* @param Database $dbForConsole
* @param Database $dbForProject
* @param Log $log
* @return void
* @throws \Exception
*/
public function action(Message $message, Database $dbForConsole, Database $dbForProject): void
public function action(Message $message, Database $dbForConsole, Database $dbForProject, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -60,18 +62,23 @@ class Databases extends Action
$document = new Document($payload['document'] ?? []);
$database = new Document($payload['database'] ?? []);
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
if ($database->isEmpty()) {
throw new Exception('Missing database');
}
match (strval($type)) {
$log->addTag('databaseId', $database->getId());
match (\strval($type)) {
DATABASE_TYPE_DELETE_DATABASE => $this->deleteDatabase($database, $project, $dbForProject),
DATABASE_TYPE_DELETE_COLLECTION => $this->deleteCollection($database, $collection, $project, $dbForProject),
DATABASE_TYPE_CREATE_ATTRIBUTE => $this->createAttribute($database, $collection, $document, $project, $dbForConsole, $dbForProject),
DATABASE_TYPE_DELETE_ATTRIBUTE => $this->deleteAttribute($database, $collection, $document, $project, $dbForConsole, $dbForProject),
DATABASE_TYPE_CREATE_INDEX => $this->createIndex($database, $collection, $document, $project, $dbForConsole, $dbForProject),
DATABASE_TYPE_DELETE_INDEX => $this->deleteIndex($database, $collection, $document, $project, $dbForConsole, $dbForProject),
default => Console::error('No database operation for type: ' . $type),
default => throw new \Exception('No database operation for type: ' . \strval($type)),
};
}
@@ -159,6 +166,7 @@ class Databases extends Action
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'available'));
} catch (\Exception $e) {
// TODO: Send non DatabaseExceptions to Sentry
Console::error($e->getMessage());
if ($e instanceof DatabaseException) {
@@ -168,6 +176,7 @@ class Databases extends Action
}
}
$dbForProject->updateDocument(
'attributes',
$attribute->getId(),
@@ -261,6 +270,7 @@ class Databases extends Action
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
}
} catch (\Exception $e) {
// TODO: Send non DatabaseExceptions to Sentry
Console::error($e->getMessage());
if ($e instanceof DatabaseException) {
@@ -388,6 +398,7 @@ class Databases extends Action
}
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available'));
} catch (\Exception $e) {
// TODO: Send non DatabaseExceptions to Sentry
Console::error($e->getMessage());
if ($e instanceof DatabaseException) {
@@ -445,6 +456,7 @@ class Databases extends Action
$dbForProject->deleteDocument('indexes', $index->getId());
$index->setAttribute('status', 'deleted');
} catch (\Exception $e) {
// TODO: Send non DatabaseExceptions to Sentry
Console::error($e->getMessage());
if ($e instanceof DatabaseException) {
@@ -581,7 +593,7 @@ class Databases extends Action
$callback($document);
}
} else {
Console::error('Failed to delete document: ' . $document->getId());
Console::warning('Failed to delete document: ' . $document->getId());
}
$count++;
}
+37 -4
View File
@@ -2,6 +2,7 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Auth\Auth;
use Executor\Executor;
use Throwable;
use Utopia\Abuse\Abuse;
@@ -20,6 +21,7 @@ use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Query;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Storage\Device;
@@ -45,14 +47,15 @@ class Deletes extends Action
->inject('getFunctionsDevice')
->inject('getBuildsDevice')
->inject('getCacheDevice')
->callback(fn ($message, $dbForConsole, callable $getProjectDB, callable $getFilesDevice, callable $getFunctionsDevice, callable $getBuildsDevice, callable $getCacheDevice) => $this->action($message, $dbForConsole, $getProjectDB, $getFilesDevice, $getFunctionsDevice, $getBuildsDevice, $getCacheDevice));
->inject('log')
->callback(fn ($message, $dbForConsole, callable $getProjectDB, callable $getFilesDevice, callable $getFunctionsDevice, callable $getBuildsDevice, callable $getCacheDevice, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $getFilesDevice, $getFunctionsDevice, $getBuildsDevice, $getCacheDevice, $log));
}
/**
* @throws Exception
* @throws Throwable
*/
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, callable $getFilesDevice, callable $getFunctionsDevice, callable $getBuildsDevice, callable $getCacheDevice): void
public function action(Message $message, Database $dbForConsole, callable $getProjectDB, callable $getFilesDevice, callable $getFunctionsDevice, callable $getBuildsDevice, callable $getCacheDevice, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -67,7 +70,10 @@ class Deletes extends Action
$document = new Document($payload['document'] ?? []);
$project = new Document($payload['project'] ?? []);
switch (strval($type)) {
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
switch (\strval($type)) {
case DELETE_TYPE_DOCUMENT:
switch ($document->getCollection()) {
case DELETE_TYPE_DATABASES:
@@ -149,8 +155,11 @@ 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);
throw new \Exception('No delete operation for type: ' . \strval($type));
break;
}
}
@@ -193,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
@@ -533,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);
}
/**
+17 -10
View File
@@ -90,6 +90,10 @@ class Functions extends Action
return;
}
$log->addTag('functionId', $function->getId());
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
if (!empty($events)) {
$limit = 30;
$sum = 30;
@@ -236,15 +240,14 @@ class Functions extends Action
string $eventData = null,
string $executionId = null,
): void {
$user ??= new Document();
$functionId = $function->getId();
$deploymentId = $function->getAttribute('deployment', '');
$user ??= new Document();
$functionId = $function->getId();
$deploymentId = $function->getAttribute('deployment', '');
$log->addTag('functionId', $functionId);
$log->addTag('projectId', $project->getId());
$log->addTag('deploymentId', $deploymentId);
/** Check if deployment exists */
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
/** Check if deployment exists */
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->getAttribute('resourceId') !== $functionId) {
throw new Exception('Deployment not found. Create deployment before trying to execute a function');
@@ -254,8 +257,12 @@ class Functions extends Action
throw new Exception('Deployment not found. Create deployment before trying to execute a function');
}
/** Check if build has exists */
$build = $dbForProject->getDocument('builds', $deployment->getAttribute('buildId', ''));
$buildId = $deployment->getAttribute('buildId', '');
$log->addTag('buildId', $buildId);
/** Check if build has exists */
$build = $dbForProject->getDocument('builds', $buildId);
if ($build->isEmpty()) {
throw new Exception('Build not found');
}
@@ -264,7 +271,7 @@ class Functions extends Action
throw new Exception('Build not ready');
}
/** Check if runtime is supported */
/** Check if runtime is supported */
$version = $function->getAttribute('version', 'v2');
$runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []);
+437
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());
}
}
}
+8 -5
View File
@@ -7,7 +7,7 @@ use Exception;
use PHPMailer\PHPMailer\PHPMailer;
use Swoole\Runtime;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Registry\Registry;
@@ -28,17 +28,19 @@ class Mails extends Action
->desc('Mails worker')
->inject('message')
->inject('register')
->callback(fn($message, $register) => $this->action($message, $register));
->inject('log')
->callback(fn(Message $message, Registry $register, Log $log) => $this->action($message, $register, $log));
}
/**
* @param Message $message
* @param Registry $register
* @param Log $log
* @throws \PHPMailer\PHPMailer\Exception
* @return void
* @throws Exception
*/
public function action(Message $message, Registry $register): void
public function action(Message $message, Registry $register, Log $log): void
{
Runtime::setHookFlags(SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP);
$payload = $message->getPayload() ?? [];
@@ -50,10 +52,11 @@ class Mails extends Action
$smtp = $payload['smtp'];
if (empty($smtp) && empty(App::getEnv('_APP_SMTP_HOST'))) {
Console::info('Skipped mail processing. No SMTP configuration has been set.');
return;
throw new Exception('Skipped mail processing. No SMTP configuration has been set.');
}
$log->addTag('type', empty($smtp) ? 'cloud' : 'smtp');
$recipient = $payload['recipient'];
$subject = $payload['subject'];
$variables = $payload['variables'];
+382 -56
View File
@@ -2,30 +2,43 @@
namespace Appwrite\Platform\Workers;
use Exception;
use Appwrite\Extend\Exception;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Helpers\ID;
use Utopia\DSN\DSN;
use Utopia\Messaging\Messages\Sms;
use Utopia\Messaging\Adapters\SMS\Mock;
use Utopia\Messaging\Adapters\SMS\Msg91;
use Utopia\Messaging\Adapters\SMS\Telesign;
use Utopia\Messaging\Adapters\SMS\TextMagic;
use Utopia\Messaging\Adapters\SMS\Twilio;
use Utopia\Messaging\Adapters\SMS\Vonage;
use Utopia\Logger\Log;
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\Adapter\Email as EmailAdapter;
use Utopia\Messaging\Adapter\Email\Mailgun;
use Utopia\Messaging\Adapter\Email\Sendgrid;
use Utopia\Messaging\Adapter\Push as PushAdapter;
use Utopia\Messaging\Adapter\Push\APNS;
use Utopia\Messaging\Adapter\Push\FCM;
use Utopia\Messaging\Adapter\SMS as SMSAdapter;
use Utopia\Messaging\Adapter\SMS\Mock;
use Utopia\Messaging\Adapter\SMS\Msg91;
use Utopia\Messaging\Adapter\SMS\Telesign;
use Utopia\Messaging\Adapter\SMS\Textmagic;
use Utopia\Messaging\Adapter\SMS\Twilio;
use Utopia\Messaging\Adapter\SMS\Vonage;
use Utopia\Messaging\Messages\Email;
use Utopia\Messaging\Messages\Push;
use Utopia\Messaging\Messages\SMS;
use Utopia\Messaging\Response;
use function Swoole\Coroutine\batch;
class Messaging extends Action
{
private ?DSN $dsn = null;
private string $user = '';
private string $secret = '';
private string $provider = '';
public static function getName(): string
{
return 'messaging';
return "messaging";
}
/**
@@ -33,75 +46,388 @@ class Messaging extends Action
*/
public function __construct()
{
$this->provider = App::getEnv('_APP_SMS_PROVIDER', '');
if (!empty($this->provider)) {
$this->dsn = new DSN($this->provider);
$this->user = $this->dsn->getUser();
$this->secret = $this->dsn->getPassword();
}
$this
->desc('Messaging worker')
->inject('message')
->callback(fn($message) => $this->action($message));
->inject('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): 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('Payload not found.');
}
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);
}
}
$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 = [
$primaryProvider->getId() => $primaryProvider
];
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) {
if (\array_key_exists($providerId, $providers)) {
$provider = $providers[$providerId];
} else {
$provider = $dbForProject->getDocument('providers', $providerId, [Query::equal('enabled', [true])]);
if ($provider->isEmpty()) {
$provider = $primaryProvider;
} else {
$providers[$providerId] = $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;
return batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex, $dbForProject) {
return function () use ($batch, $message, $provider, $adapter, $batchIndex, $dbForProject) {
$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($dbForProject, $messageData, $provider),
default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE)
};
try {
$response = new Response($provider->getAttribute('type'));
$response->fromArray($adapter->send($data));
$deliveredTotal += $response->getDeliveredTo();
$details[] = $response->getDetails();
foreach ($details as $detail) {
if ($detail['status'] === 'failure') {
$deliveryErrors[] = "Failed sending to target {$detail['recipient']} with error: {$detail['error']}";
}
// Deleting push targets when token has expired.
if ($detail['error'] === 'Expired device token.') {
$target = $dbForProject->findOne('targets', [Query::equal('identifier', [$detail['recipient']])]);
if ($target instanceof Document && !$target->isEmpty()) {
$dbForProject->deleteDocument('targets', $target->getId());
}
}
}
} catch (\Exception $e) {
$deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage();
} finally {
$batchIndex++;
return [
'deliveredTotal' => $deliveredTotal,
'deliveryErrors' => $deliveryErrors,
];
}
};
}, $batches));
};
}, \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
]
]);
$adapter = $this->sms($provider);
$maxBatchSize = $adapter->getMaxMessagesPerRequest();
$batches = \array_chunk($recipients, $maxBatchSize);
$batchIndex = 0;
batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex) {
return function () use ($batch, $message, $provider, $adapter, $batchIndex) {
$message->setAttribute('to', $batch);
$data = $this->buildSMSMessage($message, $provider);
try {
$adapter->send($data);
} catch (\Exception $e) {
Console::error('Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage()); // 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'], $credentials['templateId']),
'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']),
default => null
};
}
private function push(Document $provider): ?PushAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'mock' => new Mock('username', 'password'),
'apns' => new APNS(
$credentials['authKey'],
$credentials['authKeyId'],
$credentials['teamId'],
$credentials['bundleId'],
$credentials['endpoint']
),
'fcm' => new FCM($credentials['serviceAccountJSON']),
default => null
};
}
private function email(Document $provider): ?EmailAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'mock' => new Mock('username', 'password'),
'mailgun' => new Mailgun($credentials['apiKey'], $credentials['domain'], $credentials['isEuRegion']),
'sendgrid' => new Sendgrid($credentials['apiKey']),
default => null
};
}
private function buildEmailMessage(Database $dbForProject, Document $message, Document $provider): Email
{
$fromName = $provider['options']['fromName'];
$fromEmail = $provider['options']['fromEmail'];
$replyToEmail = null;
$replyToName = null;
if (isset($provider['options']['replyToName']) && isset($provider['options']['replyToEmail'])) {
$replyToName = $provider['options']['replyToName'];
$replyToEmail = $provider['options']['replyToEmail'];
}
$message = new SMS(
to: [$payload['recipient']],
content: $payload['message'],
from: $from,
);
$data = $message['data'] ?? [];
$ccTargets = $data['cc'] ?? [];
$bccTargets = $data['bcc'] ?? [];
$cc = [];
$bcc = [];
try {
$sms->send($message);
} catch (\Exception $error) {
throw new Exception('Error sending message: ' . $error->getMessage(), 500);
if (\count($ccTargets) > 0) {
$ccTargets = $dbForProject->find('targets', [Query::equal('identifier', $ccTargets)]);
foreach ($ccTargets as $ccTarget) {
$cc[] = ['email' => $ccTarget['identifier']];
}
}
if (\count($bccTargets) > 0) {
$bccTargets = $dbForProject->find('targets', [Query::equal('identifier', $bccTargets)]);
foreach ($bccTargets as $bccTarget) {
$bcc[] = ['email' => $bccTarget['identifier']];
}
}
$to = $message['to'];
$subject = $data['subject'];
$content = $data['content'];
$html = $data['html'];
return new Email($to, $subject, $content, $fromName, $fromEmail, $replyToName, $replyToEmail, $cc, $bcc, 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);
}
}
+12 -4
View File
@@ -17,6 +17,7 @@ use Appwrite\Role;
use Utopia\CLI\Console;
use Utopia\Database\Database;
use Utopia\Database\Helpers\ID;
use Utopia\Logger\Log;
use Utopia\Migration\Destinations\Appwrite as DestinationsAppwrite;
use Utopia\Migration\Resource;
use Utopia\Migration\Source;
@@ -46,17 +47,19 @@ class Migrations extends Action
->inject('message')
->inject('dbForProject')
->inject('dbForConsole')
->callback(fn(Message $message, Database $dbForProject, Database $dbForConsole) => $this->action($message, $dbForProject, $dbForConsole));
->inject('log')
->callback(fn(Message $message, Database $dbForProject, Database $dbForConsole, Log $log) => $this->action($message, $dbForProject, $dbForConsole, $log));
}
/**
* @param Message $message
* @param Database $dbForProject
* @param Database $dbForConsole
* @param Log $log
* @return void
* @throws Exception
*/
public function action(Message $message, Database $dbForProject, Database $dbForConsole): void
public function action(Message $message, Database $dbForProject, Database $dbForConsole, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -82,7 +85,9 @@ class Migrations extends Action
return;
}
$this->processMigration($project, $migration);
$log->addTag('projectId', $project->getId());
$this->processMigration($project, $migration, $log);
}
/**
@@ -228,6 +233,7 @@ class Migrations extends Action
/**
* @param Document $project
* @param Document $migration
* @param Log $log
* @return void
* @throws Authorization
* @throws Conflict
@@ -235,7 +241,7 @@ class Migrations extends Action
* @throws Structure
* @throws \Utopia\Database\Exception
*/
protected function processMigration(Document $project, Document $migration): void
protected function processMigration(Document $project, Document $migration, Log $log): void
{
/**
* @var Document $migrationDocument
@@ -252,6 +258,8 @@ class Migrations extends Action
$migrationDocument->setAttribute('status', 'processing');
$this->updateMigrationDocument($migrationDocument, $projectDocument);
$log->addTag('type', $migrationDocument->getAttribute('source'));
$source = $this->processSource($migrationDocument->getAttribute('source'), $migrationDocument->getAttribute('credentials'));
$source->report();
+7 -2
View File
@@ -5,6 +5,7 @@ namespace Appwrite\Platform\Workers;
use Exception;
use Utopia\App;
use Utopia\Database\Document;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
@@ -25,15 +26,17 @@ class Webhooks extends Action
$this
->desc('Webhooks worker')
->inject('message')
->callback(fn ($message) => $this->action($message));
->inject('log')
->callback(fn (Message $message, Log $log) => $this->action($message, $log));
}
/**
* @param Message $message
* @param Log $log
* @return void
* @throws Exception
*/
public function action(Message $message): void
public function action(Message $message, Log $log): void
{
$payload = $message->getPayload() ?? [];
@@ -46,6 +49,8 @@ class Webhooks extends Action
$project = new Document($payload['project']);
$user = new Document($payload['user'] ?? []);
$log->addTag('projectId', $project->getId());
foreach ($project->getAttribute('webhooks', []) as $webhook) {
if (array_intersect($webhook->getAttribute('events', []), $events)) {
$this->execute($events, $webhookPayload, $webhook, $user, $project);
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -0,0 +1,21 @@
<?php
namespace Appwrite\Utopia\Database\Validator\Queries;
class Topics extends Base
{
public const ALLOWED_ATTRIBUTES = [
'name',
'description',
'total'
];
/**
* Expression constructor
*
*/
public function __construct()
{
parent::__construct('topics', self::ALLOWED_ATTRIBUTES);
}
}
+31 -4
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())
+2 -2
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;
}
+3 -3
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'] = [];
@@ -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;
}
}
@@ -4,6 +4,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
use Utopia\Storage\Compression\Compression;
class Bucket extends Model
{
@@ -68,7 +69,7 @@ class Bucket extends Model
])
->addRule('compression', [
'type' => self::TYPE_STRING,
'description' => 'Compression algorithm choosen for compression. Will be one of ' . COMPRESSION_TYPE_NONE . ', [' . COMPRESSION_TYPE_GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . COMPRESSION_TYPE_ZSTD . '](https://en.wikipedia.org/wiki/Zstd).',
'description' => 'Compression algorithm choosen for compression. Will be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd).',
'default' => '',
'example' => 'gzip',
'array' => false
@@ -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;
}
}
@@ -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;
}
+46 -22
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'
],
]);
}
/**
@@ -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;
}
}
@@ -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;
}
}
@@ -40,6 +40,12 @@ class Token extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('securityPhrase', [
'type' => self::TYPE_STRING,
'description' => 'Security phrase of a token. Empty if security phrase was not requested when creating a token. It includes randomly generated phrase which is also sent in the external resource such as email.',
'default' => '',
'example' => 'Golden Fox',
])
;
}
@@ -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;
}
}
@@ -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.',
+3 -3
View File
@@ -72,6 +72,7 @@ class Executor
) {
$runtimeId = "$projectId-$deploymentId-build";
$route = "/runtimes";
$timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900);
$params = [
'runtimeId' => $runtimeId,
'source' => $source,
@@ -84,10 +85,9 @@ class Executor
'cpus' => $this->cpus,
'memory' => $this->memory,
'version' => $version,
'timeout' => $timeout,
];
$timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900);
$response = $this->call(self::METHOD_POST, $route, [ 'x-opr-runtime-id' => $runtimeId ], $params, true, $timeout);
$status = $response['headers']['status-code'];
@@ -111,7 +111,7 @@ class Executor
string $projectId,
callable $callback
) {
$timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900);
$timeout = (int) App::getEnv('_APP_FUNCTIONS_BUILD_TIMEOUT', 900);
$runtimeId = "$projectId-$deploymentId-build";
$route = "/runtimes/{$runtimeId}/logs";