Merge branch '1.5.x' of https://github.com/appwrite/appwrite into feat-rc-sdks

This commit is contained in:
Torsten Dittmann
2024-02-15 11:32:15 +00:00
82 changed files with 1421 additions and 679 deletions
+1 -1
View File
@@ -279,7 +279,7 @@ class Firebase extends OAuth2
$role = \json_decode($role, true);
return $role;
} catch (\Exception $e) {
} catch (\Throwable $e) {
if ($e->getCode() !== 404) {
throw $e;
}
+9 -10
View File
@@ -244,7 +244,9 @@ class Exception extends \Exception
public const REALTIME_POLICY_VIOLATION = 'realtime_policy_violation';
/** Health */
public const QUEUE_SIZE_EXCEEDED = 'queue_size_exceeded';
public const HEALTH_QUEUE_SIZE_EXCEEDED = 'health_queue_size_exceeded';
public const HEALTH_CERTIFICATE_EXPIRED = 'health_certificate_expired';
public const HEALTH_INVALID_HOST = 'health_invalid_host';
/** Provider */
public const PROVIDER_NOT_FOUND = 'provider_not_found';
@@ -264,6 +266,8 @@ class Exception extends \Exception
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_PROCESSING = 'message_already_processing';
public const MESSAGE_ALREADY_FAILED = 'message_already_failed';
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';
@@ -276,21 +280,16 @@ class Exception extends \Exception
protected string $type = '';
protected array $errors = [];
protected bool $publish = true;
protected bool $publish;
public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int $code = null, \Throwable $previous = null)
{
$this->errors = Config::getParam('errors');
$this->type = $type;
$this->code = $code ?? $this->errors[$type]['code'];
$this->message = $message ?? $this->errors[$type]['description'];
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;
$this->code = $code ?? $this->code;
$this->publish = $this->errors[$type]['publish'] ?? ($this->code >= 500);
parent::__construct($this->message, $this->code, $previous);
}
+1 -1
View File
@@ -401,7 +401,7 @@ abstract class Migration
try {
$stmt->execute();
} catch (\Exception $e) {
} catch (\Throwable $e) {
Console::warning($e->getMessage());
}
}
+14
View File
@@ -6,6 +6,11 @@ use Utopia\Validator;
class CNAME extends Validator
{
/**
* @var mixed
*/
protected mixed $logs;
/**
* @var string
*/
@@ -27,6 +32,14 @@ class CNAME extends Validator
return 'Invalid CNAME record';
}
/**
* @return mixed
*/
public function getLogs(): mixed
{
return $this->logs;
}
/**
* Check if CNAME record target value matches selected target
*
@@ -42,6 +55,7 @@ class CNAME extends Validator
try {
$records = \dns_get_record($domain, DNS_CNAME);
$this->logs = $records;
} catch (\Throwable $th) {
return false;
}
+7 -14
View File
@@ -3,6 +3,7 @@
namespace Appwrite\Platform\Services;
use Appwrite\Platform\Tasks\CalcTierStats;
use Appwrite\Platform\Tasks\CreateInfMetric;
use Appwrite\Platform\Tasks\DeleteOrphanedProjects;
use Appwrite\Platform\Tasks\DevGenerateTranslations;
use Appwrite\Platform\Tasks\Doctor;
@@ -12,6 +13,8 @@ use Appwrite\Platform\Tasks\Install;
use Appwrite\Platform\Tasks\Maintenance;
use Appwrite\Platform\Tasks\Migrate;
use Appwrite\Platform\Tasks\PatchRecreateRepositoriesDocuments;
use Appwrite\Platform\Tasks\QueueCount;
use Appwrite\Platform\Tasks\QueueRetry;
use Appwrite\Platform\Tasks\SDKs;
use Appwrite\Platform\Tasks\SSL;
use Appwrite\Platform\Tasks\ScheduleFunctions;
@@ -21,7 +24,6 @@ use Appwrite\Platform\Tasks\Upgrade;
use Appwrite\Platform\Tasks\Vars;
use Appwrite\Platform\Tasks\Version;
use Appwrite\Platform\Tasks\VolumeSync;
use Appwrite\Platform\Tasks\CreateInfMetric;
use Utopia\Platform\Service;
class Tasks extends Service
@@ -30,19 +32,8 @@ class Tasks extends Service
{
$this->type = self::TYPE_CLI;
$this
->addAction(Version::getName(), new Version())
->addAction(Vars::getName(), new Vars())
->addAction(SSL::getName(), new SSL())
->addAction(Hamster::getName(), new Hamster())
->addAction(Doctor::getName(), new Doctor())
->addAction(Install::getName(), new Install())
->addAction(Upgrade::getName(), new Upgrade())
->addAction(Maintenance::getName(), new Maintenance())
->addAction(Migrate::getName(), new Migrate())
->addAction(SDKs::getName(), new SDKs())
->addAction(VolumeSync::getName(), new VolumeSync())
->addAction(Specs::getName(), new Specs())
->addAction(CalcTierStats::getName(), new CalcTierStats())
->addAction(CreateInfMetric::getName(), new CreateInfMetric())
->addAction(DeleteOrphanedProjects::getName(), new DeleteOrphanedProjects())
->addAction(DevGenerateTranslations::getName(), new DevGenerateTranslations())
->addAction(Doctor::getName(), new Doctor())
@@ -51,7 +42,10 @@ class Tasks extends Service
->addAction(Install::getName(), new Install())
->addAction(Maintenance::getName(), new Maintenance())
->addAction(Migrate::getName(), new Migrate())
->addAction(Migrate::getName(), new Migrate())
->addAction(PatchRecreateRepositoriesDocuments::getName(), new PatchRecreateRepositoriesDocuments())
->addAction(QueueCount::getName(), new QueueCount())
->addAction(QueueRetry::getName(), new QueueRetry())
->addAction(SDKs::getName(), new SDKs())
->addAction(SSL::getName(), new SSL())
->addAction(ScheduleFunctions::getName(), new ScheduleFunctions())
@@ -61,7 +55,6 @@ class Tasks extends Service
->addAction(Vars::getName(), new Vars())
->addAction(Version::getName(), new Version())
->addAction(VolumeSync::getName(), new VolumeSync())
->addAction(CreateInfMetric::getName(), new CreateInfMetric())
;
}
}
@@ -2,7 +2,6 @@
namespace Appwrite\Platform\Tasks;
use Exception;
use League\Csv\CannotInsertRecord;
use Utopia\App;
use Utopia\Database\Document;
@@ -200,7 +199,7 @@ class CalcTierStats extends Action
$mail->Body = "Please find the daily cloud report atttached";
$mail->send();
Console::success('Email has been sent!');
} catch (Exception $e) {
} catch (\Throwable $e) {
Console::error("Message could not be sent. Mailer Error: {$mail->ErrorInfo}");
}
}
@@ -2,7 +2,6 @@
namespace Appwrite\Platform\Tasks;
use Exception;
use League\Csv\CannotInsertRecord;
use Utopia\App;
use Utopia\Platform\Action;
@@ -180,7 +179,7 @@ class GetMigrationStats extends Action
$mail->Body = "Please find the migration report atttached";
$mail->send();
Console::success('Email has been sent!');
} catch (Exception $e) {
} catch (\Throwable $e) {
Console::error("Message could not be sent. Mailer Error: {$mail->ErrorInfo}");
}
}
+3 -4
View File
@@ -3,7 +3,6 @@
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Hamster as EventHamster;
use Exception;
use Utopia\App;
use Utopia\Platform\Action;
use Utopia\CLI\Console;
@@ -120,7 +119,7 @@ class Hamster extends Action
->setType(EventHamster::TYPE_ORGANISATION)
->setOrganization($organization)
->trigger();
} catch (Exception $e) {
} catch (\Throwable $e) {
Console::error($e->getMessage());
}
});
@@ -135,7 +134,7 @@ class Hamster extends Action
->setType(EventHamster::TYPE_PROJECT)
->setProject($project)
->trigger();
} catch (Exception $e) {
} catch (\Throwable $e) {
Console::error($e->getMessage());
}
});
@@ -150,7 +149,7 @@ class Hamster extends Action
->setType(EventHamster::TYPE_USER)
->setUser($user)
->trigger();
} catch (Exception $e) {
} catch (\Throwable $e) {
Console::error($e->getMessage());
}
});
+2 -1
View File
@@ -36,6 +36,7 @@ class Maintenance extends Action
// # of days in seconds (1 day = 86400s)
$interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400');
$delay = (int) App::getEnv('_APP_MAINTENANCE_DELAY', '0');
$usageStatsRetentionHourly = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_HOURLY', '8640000'); //100 days
$cacheRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_CACHE', '2592000'); // 30 days
$schedulesDeletionRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_SCHEDULES', '86400'); // 1 Day
@@ -60,7 +61,7 @@ class Maintenance extends Action
$this->notifyDeleteCache($cacheRetention, $queueForDeletes);
$this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes);
$this->notifyDeleteTargets($queueForDeletes);
}, $interval);
}, $interval, $delay);
}
protected function foreachProject(Database $dbForConsole, callable $callback): void
@@ -0,0 +1,70 @@
<?php
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Event;
use Utopia\CLI\Console;
use Utopia\Platform\Action;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
use Utopia\Validator\WhiteList;
class QueueCount extends Action
{
public static function getName(): string
{
return 'queue-count';
}
public function __construct()
{
$this
->desc('Return the number of from a specific queue identified by the name parameter with a specific type')
->param('name', '', new WhiteList([
Event::DATABASE_QUEUE_NAME,
Event::DELETE_QUEUE_NAME,
Event::AUDITS_QUEUE_NAME,
Event::MAILS_QUEUE_NAME,
Event::FUNCTIONS_QUEUE_NAME,
Event::USAGE_QUEUE_NAME,
Event::WEBHOOK_QUEUE_NAME,
Event::CERTIFICATES_QUEUE_NAME,
Event::BUILDS_QUEUE_NAME,
Event::MESSAGING_QUEUE_NAME,
Event::MIGRATIONS_QUEUE_NAME,
Event::HAMSTER_QUEUE_NAME
]), 'Queue name')
->param('type', '', new WhiteList([
'success',
'failed',
'processing',
]), 'Queue type')
->inject('queue')
->callback(fn ($name, $type, $queue) => $this->action($name, $type, $queue));
}
/**
* @param string $name The name of the queue to count the jobs from
* @param string $type The type of jobs to count
* @param Connection $queue
*/
public function action(string $name, string $type, Connection $queue): void
{
if (!$name) {
Console::error('Missing required parameter $name');
return;
}
$queueClient = new Client($name, $queue);
$count = match ($type) {
'success' => $queueClient->countSuccessfulJobs(),
'failed' => $queueClient->countFailedJobs(),
'processing' => $queueClient->countProcessingJobs(),
default => 0
};
Console::log("Queue: '{$name}' has {$count} {$type} jobs.");
}
}
@@ -0,0 +1,64 @@
<?php
namespace Appwrite\Platform\Tasks;
use Appwrite\Event\Event;
use Utopia\CLI\Console;
use Utopia\Platform\Action;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
use Utopia\Validator\WhiteList;
class QueueRetry extends Action
{
public static function getName(): string
{
return 'queue-retry';
}
public function __construct()
{
$this
->desc('Retry failed jobs from a specific queue identified by the name parameter')
->param('name', '', new WhiteList([
Event::DATABASE_QUEUE_NAME,
Event::DELETE_QUEUE_NAME,
Event::AUDITS_QUEUE_NAME,
Event::MAILS_QUEUE_NAME,
Event::FUNCTIONS_QUEUE_NAME,
Event::USAGE_QUEUE_NAME,
Event::WEBHOOK_CLASS_NAME,
Event::CERTIFICATES_QUEUE_NAME,
Event::BUILDS_QUEUE_NAME,
Event::MESSAGING_QUEUE_NAME,
Event::MIGRATIONS_QUEUE_NAME,
Event::HAMSTER_CLASS_NAME
]), 'Queue name')
->inject('queue')
->callback(fn ($name, $queue) => $this->action($name, $queue));
}
/**
* @param string $name The name of the queue to retry jobs from
* @param Connection $queue
*/
public function action(string $name, Connection $queue): void
{
if (!$name) {
Console::error('Missing required parameter $name');
return;
}
$queueClient = new Client($name, $queue);
if ($queueClient->countFailedJobs() === 0) {
Console::error('No failed jobs found.');
return;
}
Console::log('Retrying failed jobs...');
$queueClient->retry();
}
}
+1 -5
View File
@@ -18,8 +18,6 @@ use Appwrite\SDK\Language\Python;
use Appwrite\SDK\Language\REST;
use Appwrite\SDK\Language\Ruby;
use Appwrite\SDK\Language\Swift;
use Exception;
use Throwable;
use Appwrite\SDK\Language\Apple;
use Appwrite\SDK\Language\Web;
use Appwrite\SDK\SDK;
@@ -242,9 +240,7 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
try {
$sdk->generate($result);
} catch (Exception $exception) {
Console::error($exception->getMessage());
} catch (Throwable $exception) {
} catch (\Throwable $exception) {
Console::error($exception->getMessage());
}
+7 -3
View File
@@ -7,6 +7,7 @@ use Appwrite\Event\Certificate;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Document;
use Utopia\Validator\Boolean;
use Utopia\Validator\Hostname;
class SSL extends Action
@@ -21,19 +22,22 @@ class SSL extends Action
$this
->desc('Validate server certificates')
->param('domain', App::getEnv('_APP_DOMAIN', ''), new Hostname(), 'Domain to generate certificate for. If empty, main domain will be used.', true)
->param('skip-check', true, new Boolean(true), 'If DNS and renew check should be skipped. Defaults to true, and when true, all jobs will result in certificate generation attempt.', true)
->inject('queueForCertificates')
->callback(fn (string $domain, Certificate $queueForCertificates) => $this->action($domain, $queueForCertificates));
->callback(fn (string $domain, bool|string $skipCheck, Certificate $queueForCertificates) => $this->action($domain, $skipCheck, $queueForCertificates));
}
public function action(string $domain, Certificate $queueForCertificates): void
public function action(string $domain, bool|string $skipCheck, Certificate $queueForCertificates): void
{
$skipCheck = \strval($skipCheck) === 'true';
Console::success('Scheduling a job to issue a TLS certificate for domain: ' . $domain);
$queueForCertificates
->setDomain(new Document([
'domain' => $domain
]))
->setSkipRenewCheck(true)
->setSkipRenewCheck($skipCheck)
->trigger();
}
}
+1 -1
View File
@@ -264,7 +264,7 @@ class Specs extends Action
$formatInstance
->setParam('name', APP_NAME)
->setParam('description', 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)')
->setParam('endpoint', 'https://HOSTNAME/v1')
->setParam('endpoint', 'https://cloud.appwrite.io/v1')
->setParam('version', APP_VERSION_STABLE)
->setParam('terms', $endpoint . '/policy/terms')
->setParam('support.email', $email)
+3 -4
View File
@@ -8,7 +8,6 @@ use Appwrite\Event\Usage;
use Appwrite\Messaging\Adapter\Realtime;
use Appwrite\Utopia\Response\Model\Deployment;
use Appwrite\Vcs\Comment;
use Exception;
use Swoole\Coroutine as Co;
use Executor\Executor;
use Utopia\App;
@@ -420,7 +419,7 @@ class Builds extends Action
variables: $vars,
command: $command
);
} catch (Exception $error) {
} catch (\Throwable $error) {
$err = $error;
}
}),
@@ -459,7 +458,7 @@ class Builds extends Action
}
}
);
} catch (Exception $error) {
} catch (\Throwable $error) {
if (empty($err)) {
$err = $error;
}
@@ -617,7 +616,7 @@ class Builds extends Action
'$id' => $commentId
]));
break;
} catch (Exception $err) {
} catch (\Throwable $err) {
if ($retries >= 9) {
throw $err;
}
+18 -6
View File
@@ -75,7 +75,7 @@ class Certificates extends Action
$log->addTag('domain', $domain->get());
$this->execute($domain, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $skipRenewCheck);
$this->execute($domain, $dbForConsole, $queueForMails, $queueForEvents, $queueForFunctions, $log, $skipRenewCheck);
}
/**
@@ -89,7 +89,7 @@ class Certificates extends Action
* @throws Throwable
* @throws \Utopia\Database\Exception
*/
private function execute(Domain $domain, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, bool $skipRenewCheck = false): void
private function execute(Domain $domain, Database $dbForConsole, Mail $queueForMails, Event $queueForEvents, Func $queueForFunctions, Log $log, bool $skipRenewCheck = false): void
{
/**
* 1. Read arguments and validate domain
@@ -143,11 +143,11 @@ class Certificates extends Action
if (!$skipRenewCheck) {
$mainDomain = $this->getMainDomain();
$isMainDomain = !isset($mainDomain) || $domain->get() === $mainDomain;
$this->validateDomain($domain, $isMainDomain);
$this->validateDomain($domain, $isMainDomain, $log);
}
// If certificate exists already, double-check expiry date. Skip if job is forced
if (!$skipRenewCheck && !$this->isRenewRequired($domain->get())) {
if (!$skipRenewCheck && !$this->isRenewRequired($domain->get(), $log)) {
throw new Exception('Renew isn\'t required.');
}
@@ -185,6 +185,8 @@ class Certificates extends Action
// Send email to security email
$this->notifyError($domain->get(), $e->getMessage(), $attempts, $queueForMails);
throw $e;
} finally {
// All actions result in new updatedAt date
$certificate->setAttribute('updated', DateTime::now());
@@ -252,7 +254,7 @@ class Certificates extends Action
* @return void
* @throws Exception
*/
private function validateDomain(Domain $domain, bool $isMainDomain): void
private function validateDomain(Domain $domain, bool $isMainDomain, Log $log): void
{
if (empty($domain->get())) {
throw new Exception('Missing certificate domain.');
@@ -272,8 +274,15 @@ class Certificates extends Action
}
// Verify domain with DNS records
$validationStart = \microtime(true);
$validator = new CNAME($target->get());
if (!$validator->isValid($domain->get())) {
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
$error = $validator->getLogs();
$log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error));
throw new Exception('Failed to verify domain DNS records.');
}
} else {
@@ -289,7 +298,7 @@ class Certificates extends Action
* @return bool True, if certificate needs to be renewed
* @throws Exception
*/
private function isRenewRequired(string $domain): bool
private function isRenewRequired(string $domain, Log $log): bool
{
$certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem';
if (\file_exists($certPath)) {
@@ -299,12 +308,15 @@ class Certificates extends Action
$validTo = $certData['validTo_time_t'] ?? 0;
if (empty($validTo)) {
$log->addTag('certificateDomain', $domain);
throw new Exception('Unable to read certificate file (cert.pem).');
}
// LetsEncrypt allows renewal 30 days before expiry
$expiryInAdvance = (60 * 60 * 24 * 30);
if ($validTo - $expiryInAdvance > \time()) {
$log->addTag('certificateDomain', $domain);
$log->addExtra('certificateData', \is_array($certData) ? \json_encode($certData) : \strval($certData));
return false;
}
}
+4 -4
View File
@@ -165,7 +165,7 @@ class Databases extends Action
}
$dbForProject->updateDocument('attributes', $attribute->getId(), $attribute->setAttribute('status', 'available'));
} catch (\Exception $e) {
} catch (\Throwable $e) {
// TODO: Send non DatabaseExceptions to Sentry
Console::error($e->getMessage());
@@ -269,7 +269,7 @@ class Databases extends Action
if (!$relatedAttribute->isEmpty()) {
$dbForProject->deleteDocument('attributes', $relatedAttribute->getId());
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
// TODO: Send non DatabaseExceptions to Sentry
Console::error($e->getMessage());
@@ -397,7 +397,7 @@ class Databases extends Action
throw new DatabaseException('Failed to create Index');
}
$dbForProject->updateDocument('indexes', $index->getId(), $index->setAttribute('status', 'available'));
} catch (\Exception $e) {
} catch (\Throwable $e) {
// TODO: Send non DatabaseExceptions to Sentry
Console::error($e->getMessage());
@@ -455,7 +455,7 @@ class Databases extends Action
}
$dbForProject->deleteDocument('indexes', $index->getId());
$index->setAttribute('status', 'deleted');
} catch (\Exception $e) {
} catch (\Throwable $e) {
// TODO: Send non DatabaseExceptions to Sentry
Console::error($e->getMessage());
+2 -8
View File
@@ -11,7 +11,6 @@ use Utopia\Audit\Audit;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Database\Database;
use Exception;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\DateTime;
@@ -585,7 +584,7 @@ class Deletes extends Action
// Delete metadata tables
try {
$dbForProject->deleteCollection('_metadata');
} catch (Exception) {
} catch (\Throwable) {
// Ignore: deleteCollection tries to delete a metadata entry after the collection is deleted,
// which will throw an exception here because the metadata collection is already deleted.
}
@@ -630,12 +629,7 @@ class Deletes extends Action
$teamId = $document->getAttribute('teamId');
$team = $dbForProject->getDocument('teams', $teamId);
if (!$team->isEmpty()) {
$team = $dbForProject->updateDocument(
'teams',
$teamId,
// Ensure that total >= 0
$team->setAttribute('total', \max($team->getAttribute('total', 0) - 1, 0))
);
$dbForProject->decreaseDocumentAttribute('teams', $teamId, 'total', 1, 0);
}
}
});
+3 -3
View File
@@ -340,7 +340,7 @@ class Hamster extends Action
if (!$res) {
Console::error('Failed to create event for project: ' . $project->getId());
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
Console::error('Failed to send stats for project: ' . $project->getId());
Console::error($e->getMessage());
} finally {
@@ -410,7 +410,7 @@ class Hamster extends Action
if (!$res) {
throw new \Exception('Failed to create event for organization : ' . $organization->getId());
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
Console::error($e->getMessage());
}
}
@@ -464,7 +464,7 @@ class Hamster extends Action
if (!$res) {
throw new \Exception('Failed to create user profile for user: ' . $user->getId());
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
Console::error($e->getMessage());
}
}
+1 -1
View File
@@ -130,7 +130,7 @@ class Mails extends Action
try {
$mail->send();
} catch (\Exception $error) {
} catch (\Throwable $error) {
throw new Exception('Error sending mail: ' . $error->getMessage(), 500);
}
}
+55 -20
View File
@@ -2,23 +2,22 @@
namespace Appwrite\Platform\Workers;
use Appwrite\Event\Usage;
use Appwrite\Extend\Exception;
use Appwrite\Messaging\Status as MessageStatus;
use Utopia\App;
use Utopia\CLI\Console;
use Utopia\Database\Helpers\ID;
use Utopia\DSN\DSN;
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\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Logger\Log;
use Utopia\Messaging\Adapter\Email as EmailAdapter;
use Utopia\Messaging\Adapter\Email\Mailgun;
use Utopia\Messaging\Adapter\Email\Sendgrid;
use Utopia\Messaging\Adapter\Email\SMTP;
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;
@@ -32,7 +31,8 @@ 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 Utopia\Platform\Action;
use Utopia\Queue\Message;
use function Swoole\Coroutine\batch;
@@ -53,31 +53,40 @@ class Messaging extends Action
->inject('message')
->inject('log')
->inject('dbForProject')
->callback(fn(Message $message, Log $log, Database $dbForProject) => $this->action($message, $log, $dbForProject));
->inject('queueForUsage')
->callback(fn(Message $message, Log $log, Database $dbForProject, Usage $queueForUsage) => $this->action($message, $log, $dbForProject, $queueForUsage));
}
/**
* @param Message $message
* @param Log $log
* @param Database $dbForProject
* @param Usage $queueForUsage
* @return void
* @throws Exception
*/
public function action(Message $message, Log $log, Database $dbForProject): void
public function action(Message $message, Log $log, Database $dbForProject, Usage $queueForUsage): void
{
$payload = $message->getPayload() ?? [];
if (empty($payload)) {
throw new \Exception('Payload not found.');
throw new Exception('Missing payload');
}
if (
!\is_null($payload['message'])
&& !\is_null($payload['recipients'])
&& $payload['providerType'] === MESSAGE_TYPE_SMS
) {
// Message was triggered internally
$this->processInternalSMSMessage($log, new Document($payload['message']), $payload['recipients']);
$this->processInternalSMSMessage(
new Document($payload['message']),
new Document($payload['project'] ?? []),
$payload['recipients'],
$queueForUsage,
$log,
);
} else {
$message = $dbForProject->getDocument('messages', $payload['messageId']);
@@ -254,8 +263,8 @@ class Messaging extends Action
}
}
}
} catch (\Exception $e) {
$deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage();
} catch (\Throwable $e) {
$deliveryErrors[] = 'Failed sending to targets ' . $batchIndex + 1 . ' of ' . \count($batch) . ' with error: ' . $e->getMessage();
} finally {
$batchIndex++;
@@ -279,6 +288,10 @@ class Messaging extends Action
$deliveryErrors = \array_merge($deliveryErrors, $result['deliveryErrors']);
}
if (empty($deliveryErrors) && $deliveredTotal === 0) {
$deliveryErrors[] = 'Unknown error';
}
$message->setAttribute('deliveryErrors', $deliveryErrors);
if (\count($message->getAttribute('deliveryErrors')) > 0) {
@@ -299,12 +312,26 @@ class Messaging extends Action
$dbForProject->updateDocument('messages', $message->getId(), $message);
}
private function processInternalSMSMessage(Log $log, Document $message, array $recipients): void
private function processInternalSMSMessage(Document $message, Document $project, array $recipients, Usage $queueForUsage, Log $log): void
{
if (empty(App::getEnv('_APP_SMS_PROVIDER')) || empty(App::getEnv('_APP_SMS_FROM'))) {
throw new \Exception('Skipped SMS processing. Missing "_APP_SMS_PROVIDER" or "_APP_SMS_FROM" environment variables.');
}
if ($project->isEmpty()) {
throw new Exception('Project not set in payload');
}
Console::log('Project: ' . $project->getId());
$denyList = App::getEnv('_APP_SMS_PROJECTS_DENY_LIST', '');
$denyList = explode(',', $denyList);
if (\in_array($project->getId(), $denyList)) {
Console::error('Project is in the deny list. Skipping...');
return;
}
$smsDSN = new DSN(App::getEnv('_APP_SMS_PROVIDER'));
$host = $smsDSN->getHost();
$password = $smsDSN->getPassword();
@@ -330,8 +357,8 @@ class Messaging extends Action
'apiKey' => $password
],
'telesign' => [
'username' => $user,
'password' => $password
'customerId' => $user,
'apiKey' => $password
],
'msg91' => [
'senderId' => $user,
@@ -354,16 +381,21 @@ class Messaging extends Action
$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) {
batch(\array_map(function ($batch) use ($message, $provider, $adapter, $batchIndex, $project, $queueForUsage) {
return function () use ($batch, $message, $provider, $adapter, $batchIndex, $project, $queueForUsage) {
$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
$queueForUsage
->setProject($project)
->addMetric(METRIC_MESSAGES, 1)
->trigger();
} catch (\Throwable $e) {
throw new Exception('Failed sending to targets ' . $batchIndex + 1 . '-' . \count($batch) . ' with error: ' . $e->getMessage(), 500);
}
};
}, $batches));
@@ -376,11 +408,12 @@ class Messaging extends Action
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']),
'telesign' => new Telesign($credentials['customerId'], $credentials['apiKey']),
'msg91' => new Msg91($credentials['senderId'], $credentials['authKey'], $credentials['templateId']),
'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']),
default => null
@@ -390,6 +423,7 @@ class Messaging extends Action
private function push(Document $provider): ?PushAdapter
{
$credentials = $provider->getAttribute('credentials');
return match ($provider->getAttribute('provider')) {
'mock' => new Mock('username', 'password'),
'apns' => new APNS(
@@ -407,6 +441,7 @@ class Messaging extends Action
{
$credentials = $provider->getAttribute('credentials', []);
$options = $provider->getAttribute('options', []);
return match ($provider->getAttribute('provider')) {
'mock' => new Mock('username', 'password'),
'smtp' => new SMTP(
+1 -14
View File
@@ -20,7 +20,6 @@ class Usage extends Action
];
protected const INFINITY_PERIOD = '_inf_';
protected const DEBUG_PROJECT_ID = 85293;
public static function getName(): string
{
return 'usage';
@@ -70,17 +69,6 @@ class Usage extends Action
getProjectDB: $getProjectDB
);
}
if ($project->getInternalId() == self::DEBUG_PROJECT_ID) {
var_dump([
'type' => 'payload',
'project' => $project->getInternalId(),
'database' => $project['database'] ?? '',
$payload['metrics']
]);
var_dump('==========================');
}
self::$stats[$projectId]['project'] = $project;
foreach ($payload['metrics'] ?? [] as $metric) {
if (!isset(self::$stats[$projectId]['keys'][$metric['key']])) {
@@ -91,7 +79,6 @@ class Usage extends Action
}
}
/**
* On Documents that tied by relations like functions>deployments>build || documents>collection>database || buckets>files.
* When we remove a parent document we need to deduct his children aggregation from the project scope.
@@ -231,7 +218,7 @@ class Usage extends Action
default:
break;
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
console::error("[reducer] " . " {DateTime::now()} " . " {$project->getInternalId()} " . " {$e->getMessage()}");
}
}
+1 -36
View File
@@ -57,14 +57,6 @@ class UsageHook extends Usage
try {
$dbForProject = $getProjectDB($data['project']);
if ($projectInternalId == 85293) {
var_dump([
'project' => $projectInternalId,
'database' => $database,
'time' => DateTime::now(),
'data' => $data['keys']
]);
}
foreach ($data['keys'] ?? [] as $key => $value) {
if ($value == 0) {
continue;
@@ -75,15 +67,6 @@ class UsageHook extends Usage
$id = \md5("{$time}_{$period}_{$key}");
try {
if ($projectInternalId == self::DEBUG_PROJECT_ID) {
var_dump([
'type' => 'create',
'period' => $period,
'metric' => $key,
'id' => $id,
'value' => $value
]);
}
$dbForProject->createDocument('stats_v2', new Document([
'$id' => $id,
'period' => $period,
@@ -94,15 +77,6 @@ class UsageHook extends Usage
]));
} catch (Duplicate $th) {
if ($value < 0) {
if ($projectInternalId == self::DEBUG_PROJECT_ID) {
var_dump([
'type' => 'decrease',
'period' => $period,
'metric' => $key,
'id' => $id,
'value' => $value
]);
}
$dbForProject->decreaseDocumentAttribute(
'stats_v2',
$id,
@@ -110,15 +84,6 @@ class UsageHook extends Usage
abs($value)
);
} else {
if ($projectInternalId == self::DEBUG_PROJECT_ID) {
var_dump([
'type' => 'increase',
'period' => $period,
'metric' => $key,
'id' => $id,
'value' => $value
]);
}
$dbForProject->increaseDocumentAttribute(
'stats_v2',
$id,
@@ -129,7 +94,7 @@ class UsageHook extends Usage
}
}
}
} catch (\Exception $e) {
} catch (\Throwable $e) {
console::error(DateTime::now() . ' ' . $projectInternalId . ' ' . $e->getMessage());
}
}
+19 -6
View File
@@ -216,7 +216,7 @@ abstract class Format
case 'updateEmail':
switch ($param) {
case 'status':
return 'MessageType';
return 'MessageStatus';
}
break;
case 'createSMTPProvider':
@@ -240,17 +240,24 @@ abstract class Format
break;
case 'projects':
switch ($method) {
case 'getSmsTemplate':
case 'getEmailTemplate':
case 'updateSmsTemplate':
case 'updateEmailTemplate':
case 'deleteSmsTemplate':
case 'deleteEmailTemplate':
switch ($param) {
case 'type':
return 'TemplateType';
return 'EmailTemplateType';
case 'locale':
return 'TemplateLocale';
return 'EmailTemplateLocale';
}
break;
case 'getSmsTemplate':
case 'updateSmsTemplate':
case 'deleteSmsTemplate':
switch ($param) {
case 'type':
return 'SMSTemplateType';
case 'locale':
return 'SMSTemplateLocale';
}
break;
case 'createPlatform':
@@ -327,6 +334,12 @@ abstract class Format
return 'MessagingProviderType';
}
break;
case 'createSHAUser':
switch ($param) {
case 'passwordVersion':
return 'PasswordHash';
}
break;
}
break;
}
@@ -12,7 +12,8 @@ class Users extends Base
'passwordUpdate',
'registration',
'emailVerification',
'phoneVerification'
'phoneVerification',
'labels',
];
/**
+9 -6
View File
@@ -70,18 +70,19 @@ use Appwrite\Utopia\Response\Model\Token;
use Appwrite\Utopia\Response\Model\Webhook;
use Appwrite\Utopia\Response\Model\Preferences;
use Appwrite\Utopia\Response\Model\HealthAntivirus;
use Appwrite\Utopia\Response\Model\HealthCertificate;
use Appwrite\Utopia\Response\Model\HealthQueue;
use Appwrite\Utopia\Response\Model\HealthStatus;
use Appwrite\Utopia\Response\Model\HealthTime;
use Appwrite\Utopia\Response\Model\HealthVersion;
use Appwrite\Utopia\Response\Model\MFAChallenge;
use Appwrite\Utopia\Response\Model\MFAProvider;
use Appwrite\Utopia\Response\Model\MFAProviders;
use Appwrite\Utopia\Response\Model\Installation;
use Appwrite\Utopia\Response\Model\LocaleCode;
use Appwrite\Utopia\Response\Model\MetricBreakdown;
use Appwrite\Utopia\Response\Model\Provider;
use Appwrite\Utopia\Response\Model\Message;
use Appwrite\Utopia\Response\Model\MFAFactors;
use Appwrite\Utopia\Response\Model\MFAType;
use Appwrite\Utopia\Response\Model\Subscriber;
use Appwrite\Utopia\Response\Model\Topic;
use Appwrite\Utopia\Response\Model\ProviderRepository;
@@ -169,8 +170,8 @@ class Response extends SwooleResponse
public const MODEL_PREFERENCES = 'preferences';
// MFA
public const MODEL_MFA_PROVIDER = 'mfaProvider';
public const MODEL_MFA_PROVIDERS = 'mfaProviders';
public const MODEL_MFA_TYPE = 'mfaType';
public const MODEL_MFA_FACTORS = 'mfaFactors';
public const MODEL_MFA_OTP = 'mfaTotp';
public const MODEL_MFA_CHALLENGE = 'mfaChallenge';
@@ -279,6 +280,7 @@ class Response extends SwooleResponse
public const MODEL_HEALTH_QUEUE = 'healthQueue';
public const MODEL_HEALTH_TIME = 'healthTime';
public const MODEL_HEALTH_ANTIVIRUS = 'healthAntivirus';
public const MODEL_HEALTH_CERTIFICATE = 'healthCertificate';
public const MODEL_HEALTH_STATUS_LIST = 'healthStatusList';
// Console
@@ -421,6 +423,7 @@ class Response extends SwooleResponse
->setModel(new HealthAntivirus())
->setModel(new HealthQueue())
->setModel(new HealthStatus())
->setModel(new HealthCertificate())
->setModel(new HealthTime())
->setModel(new HealthVersion())
->setModel(new Metric())
@@ -440,8 +443,8 @@ class Response extends SwooleResponse
->setModel(new TemplateEmail())
->setModel(new ConsoleVariables())
->setModel(new MFAChallenge())
->setModel(new MFAProvider())
->setModel(new MFAProviders())
->setModel(new MFAType())
->setModel(new MFAFactors())
->setModel(new Provider())
->setModel(new Message())
->setModel(new Topic())
@@ -0,0 +1,71 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class HealthCertificate extends Model
{
public function __construct()
{
$this
->addRule('name', [
'type' => self::TYPE_STRING,
'description' => 'Certificate name',
'default' => '',
'example' => '/CN=www.google.com',
])
->addRule('subjectSN', [
'type' => self::TYPE_STRING,
'description' => 'Subject SN',
'default' => 'www.google.com',
'example' => '',
])
->addRule('issuerOrganisation', [
'type' => self::TYPE_STRING,
'description' => 'Issuer organisation',
'default' => 'Google Trust Services LLC',
'example' => '',
])
->addRule('validFrom', [
'type' => self::TYPE_STRING,
'description' => 'Valid from',
'default' => '',
'example' => '1704200998',
])
->addRule('validTo', [
'type' => self::TYPE_STRING,
'description' => 'Valid to',
'default' => '',
'example' => '1711458597',
])
->addRule('signatureTypeSN', [
'type' => self::TYPE_STRING,
'description' => 'Signature type SN',
'default' => '',
'example' => 'RSA-SHA256',
])
;
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'Health Certificate';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_HEALTH_CERTIFICATE;
}
}
@@ -5,7 +5,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class MFAProviders extends Model
class MFAFactors extends Model
{
public function __construct()
{
@@ -38,7 +38,7 @@ class MFAProviders extends Model
*/
public function getName(): string
{
return 'MFAProviders';
return 'MFAFactors';
}
/**
@@ -48,6 +48,6 @@ class MFAProviders extends Model
*/
public function getType(): string
{
return Response::MODEL_MFA_PROVIDERS;
return Response::MODEL_MFA_FACTORS;
}
}
@@ -5,7 +5,7 @@ namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class MFAProvider extends Model
class MFAType extends Model
{
public function __construct()
{
@@ -39,7 +39,7 @@ class MFAProvider extends Model
*/
public function getName(): string
{
return 'MFAProvider';
return 'MFAType';
}
/**
@@ -49,6 +49,6 @@ class MFAProvider extends Model
*/
public function getType(): string
{
return Response::MODEL_MFA_PROVIDER;
return Response::MODEL_MFA_TYPE;
}
}
@@ -39,6 +39,13 @@ class Topic extends Model
'description' => 'Total count of subscribers subscribed to topic.',
'default' => 0,
'example' => 100,
])
->addRule('subscribe', [
'type' => self::TYPE_STRING,
'description' => 'Subscribe permissions.',
'default' => ['users'],
'example' => 'users',
'array' => true,
]);
}