Merge branch 'main' of https://github.com/appwrite/appwrite into sync-with-main

This commit is contained in:
Christy Jacob
2024-06-20 15:26:17 +00:00
306 changed files with 4215 additions and 1224 deletions
+1 -1
View File
@@ -263,7 +263,7 @@ class Phpass extends Hash
*/
$itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$output = '$2a$';
$output .= chr(ord('0') + $options['iteration_count_log2'] / 10);
$output .= chr(ord('0') + intval($options['iteration_count_log2'] / 10));
$output .= chr(ord('0') + $options['iteration_count_log2'] % 10);
$output .= '$';
$i = 0;
-9
View File
@@ -160,15 +160,6 @@ class Apple extends OAuth2
*/
public function getUserName(string $accessToken): string
{
if (
isset($this->claims['email']) &&
!empty($this->claims['email']) &&
isset($this->claims['email_verified']) &&
$this->claims['email_verified'] === 'true'
) {
return $this->claims['email'];
}
return '';
}
-8
View File
@@ -27,14 +27,6 @@ class Compose
}
}
/**
* @return string
*/
public function getVersion(): string
{
return (isset($this->compose['version'])) ? $this->compose['version'] : '';
}
/**
* @return Service[]
*/
+23 -10
View File
@@ -3,6 +3,7 @@
namespace Appwrite\Event;
use Utopia\Database\Document;
use Utopia\DSN\DSN;
use Utopia\Queue\Client;
use Utopia\Queue\Connection;
@@ -107,18 +108,30 @@ class Database extends Event
*/
public function trigger(): string|bool
{
$this->setQueue($this->getProject()->getAttribute('database'));
try {
$dsn = new DSN($this->getProject()->getAttribute('database'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $this->getProject()->getAttribute('database'));
}
$this->setQueue($dsn->getHost());
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
'project' => $this->project,
'user' => $this->user,
'type' => $this->type,
'collection' => $this->collection,
'document' => $this->document,
'database' => $this->database,
'events' => Event::generateEvents($this->getEvent(), $this->getParams())
]);
try {
$result = $client->enqueue([
'project' => $this->project,
'user' => $this->user,
'type' => $this->type,
'collection' => $this->collection,
'document' => $this->document,
'database' => $this->database,
'events' => Event::generateEvents($this->getEvent(), $this->getParams())
]);
return $result;
} catch (\Throwable $th) {
return false;
}
}
}
+28 -1
View File
@@ -49,6 +49,7 @@ class Event
protected string $class = '';
protected string $event = '';
protected array $params = [];
protected array $sensitive = [];
protected array $payload = [];
protected array $context = [];
protected ?Document $project = null;
@@ -158,12 +159,17 @@ class Event
* Set payload for this event.
*
* @param array $payload
* @param array $sensitive
* @return self
*/
public function setPayload(array $payload): self
public function setPayload(array $payload, array $sensitive = []): self
{
$this->payload = $payload;
foreach ($sensitive as $key) {
$this->sensitive[$key] = true;
}
return $this;
}
@@ -177,6 +183,19 @@ class Event
return $this->payload;
}
public function getRealtimePayload(): array
{
$payload = [];
foreach ($this->payload as $key => $value) {
if (!isset($this->sensitive[$key])) {
$payload[$key] = $value;
}
}
return $payload;
}
/**
* Set context for this event.
*
@@ -239,6 +258,13 @@ class Event
return $this;
}
public function setParamSensitive(string $key): self
{
$this->sensitive[$key] = true;
return $this;
}
/**
* Get param of event.
*
@@ -291,6 +317,7 @@ class Event
public function reset(): self
{
$this->params = [];
$this->sensitive = [];
return $this;
}
-1
View File
@@ -58,7 +58,6 @@ class Usage extends Event
public function trigger(): string|bool
{
$client = new Client($this->queue, $this->connection);
return $client->enqueue([
'project' => $this->getProject(),
'reduce' => $this->reduce,
+1 -1
View File
@@ -34,7 +34,7 @@ class Event extends Validator
public function isValid($value): bool
{
$events = Config::getParam('events', []);
$parts = \explode('.', $value);
$parts = \explode('.', $value ?? '');
$count = \count($parts);
if ($count < 2 || $count > 7) {
@@ -13,7 +13,7 @@ class FunctionEvent extends Event
*/
public function isValid($value): bool
{
if (str_starts_with($value, 'functions.')) {
if (str_starts_with($value ?? false, 'functions.')) {
$this->message = 'Triggering a function on a function event is not allowed.';
return false;
}
+15 -1
View File
@@ -155,6 +155,7 @@ class Exception extends \Exception
public const FUNCTION_NOT_FOUND = 'function_not_found';
public const FUNCTION_RUNTIME_UNSUPPORTED = 'function_runtime_unsupported';
public const FUNCTION_ENTRYPOINT_MISSING = 'function_entrypoint_missing';
public const FUNCTION_SYNCHRONOUS_TIMEOUT = 'function_synchronous_timeout';
/** Deployments */
public const DEPLOYMENT_NOT_FOUND = 'deployment_not_found';
@@ -198,6 +199,9 @@ class Exception extends \Exception
public const ATTRIBUTE_VALUE_INVALID = 'attribute_value_invalid';
public const ATTRIBUTE_TYPE_INVALID = 'attribute_type_invalid';
/** Relationship */
public const RELATIONSHIP_VALUE_INVALID = 'relationship_value_invalid';
/** Indexes */
public const INDEX_NOT_FOUND = 'index_not_found';
public const INDEX_LIMIT_EXCEEDED = 'index_limit_exceeded';
@@ -300,11 +304,21 @@ class Exception extends \Exception
protected array $errors = [];
protected bool $publish;
public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int $code = null, \Throwable $previous = null)
public function __construct(string $type = Exception::GENERAL_UNKNOWN, string $message = null, int|string $code = null, \Throwable $previous = null)
{
$this->errors = Config::getParam('errors');
$this->type = $type;
$this->code = $code ?? $this->errors[$type]['code'];
// Mark string errors like HY001 from PDO as 500 errors
if(\is_string($this->code)) {
if (\is_numeric($this->code)) {
$this->code = (int) $this->code;
} else {
$this->code = 500;
}
}
$this->message = $message ?? $this->errors[$type]['description'];
$this->publish = $this->errors[$type]['publish'] ?? ($this->code >= 500);
+5 -2
View File
@@ -16,11 +16,14 @@ class Hooks
/**
* @param mixed[] $params
* @return mixed
*/
public function trigger(string $name, array $params = [])
public function trigger(string $name, array $params = []): mixed
{
if (isset(self::$hooks[$name])) {
call_user_func_array(self::$hooks[$name], $params);
return call_user_func_array(self::$hooks[$name], $params);
}
return null;
}
}
+2
View File
@@ -84,6 +84,8 @@ abstract class Migration
'1.5.3' => 'V20',
'1.5.4' => 'V20',
'1.5.5' => 'V20',
'1.5.6' => 'V20',
'1.5.7' => 'V20',
];
/**
+2 -4
View File
@@ -2,15 +2,13 @@
namespace Appwrite\Platform;
use Appwrite\Platform\Services\Tasks;
use Appwrite\Platform\Services\Workers;
use Appwrite\Platform\Modules\Core;
use Utopia\Platform\Platform;
class Appwrite extends Platform
{
public function __construct()
{
$this->addService('tasks', new Tasks());
$this->addService('workers', new Workers());
parent::__construct(new Core());
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Appwrite\Platform\Modules;
use Appwrite\Platform\Services\Tasks;
use Appwrite\Platform\Services\Workers;
use Utopia\Platform\Module;
class Core extends Module
{
public function __construct()
{
$this->addService('tasks', new Tasks());
$this->addService('workers', new Workers());
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ class Tasks extends Service
{
public function __construct()
{
$this->type = self::TYPE_CLI;
$this->type = Service::TYPE_TASK;
$this
->addAction(Doctor::getName(), new Doctor())
->addAction(Install::getName(), new Install())
+1 -1
View File
@@ -20,7 +20,7 @@ class Workers extends Service
{
public function __construct()
{
$this->type = self::TYPE_WORKER;
$this->type = Service::TYPE_WORKER;
$this
->addAction(Audits::getName(), new Audits())
->addAction(Builds::getName(), new Builds())
+1 -1
View File
@@ -49,6 +49,7 @@ class Maintenance extends Action
$this->foreachProject($dbForConsole, function (Document $project) use ($queueForDeletes, $usageStatsRetentionHourly) {
$queueForDeletes->setProject($project);
$this->notifyDeleteTargets($queueForDeletes);
$this->notifyDeleteExecutionLogs($queueForDeletes);
$this->notifyDeleteAbuseLogs($queueForDeletes);
$this->notifyDeleteAuditLogs($queueForDeletes);
@@ -60,7 +61,6 @@ class Maintenance extends Action
$this->renewCertificates($dbForConsole, $queueForCertificates);
$this->notifyDeleteCache($cacheRetention, $queueForDeletes);
$this->notifyDeleteSchedules($schedulesDeletionRetention, $queueForDeletes);
$this->notifyDeleteTargets($queueForDeletes);
}, $interval, $delay);
}
+2 -14
View File
@@ -2,11 +2,11 @@
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\Text;
use Utopia\Validator\WhiteList;
class QueueCount extends Action
@@ -21,19 +21,7 @@ class QueueCount extends Action
{
$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
]), 'Queue name')
->param('name', '', new Text(100), 'Queue name')
->param('type', '', new WhiteList([
'success',
'failed',
+2 -15
View File
@@ -2,12 +2,11 @@
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;
use Utopia\Validator\Text;
use Utopia\Validator\Wildcard;
class QueueRetry extends Action
@@ -22,19 +21,7 @@ class QueueRetry extends Action
{
$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
]), 'Queue name')
->param('name', '', new Text(100), 'Queue name')
->param('limit', 0, new Wildcard(), 'jobs limit', true)
->inject('queue')
->callback(fn ($name, $limit, $queue) => $this->action($name, $limit, $queue));
+5
View File
@@ -15,6 +15,7 @@ use Appwrite\SDK\Language\Kotlin;
use Appwrite\SDK\Language\Node;
use Appwrite\SDK\Language\PHP;
use Appwrite\SDK\Language\Python;
use Appwrite\SDK\Language\ReactNative;
use Appwrite\SDK\Language\REST;
use Appwrite\SDK\Language\Ruby;
use Appwrite\SDK\Language\Swift;
@@ -159,6 +160,10 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
$config = new Flutter();
$config->setPackageName('appwrite');
break;
case 'react-native':
$config = new ReactNative();
$config->setNPMPackage('react-native-appwrite');
break;
case 'flutter-dev':
$config = new Flutter();
$config->setPackageName('appwrite_dev');
+3 -1
View File
@@ -262,7 +262,7 @@ class Builds extends Action
Console::execute('mkdir -p ' . $tmpDirectory . '/' . $rootDirectory, '', $stdout, $stderr);
// Merge template into user repo
Console::execute('cp -rfn ' . $tmpTemplateDirectory . '/' . $templateRootDirectory . '/* ' . $tmpDirectory . '/' . $rootDirectory, '', $stdout, $stderr);
Console::execute('rsync -av --exclude \'.git\' ' . $tmpTemplateDirectory . '/' . $templateRootDirectory . '/ ' . $tmpDirectory . '/' . $rootDirectory, '', $stdout, $stderr);
// Commit and push
$exit = Console::execute('git config --global user.email "team@appwrite.io" && git config --global user.name "Appwrite" && cd ' . $tmpDirectory . ' && git add . && git commit -m "Create \'' . \escapeshellcmd($function->getAttribute('name', '')) . '\' function" && git push origin ' . \escapeshellcmd($branchName), '', $stdout, $stderr);
@@ -447,6 +447,8 @@ class Builds extends Action
throw new \Exception('Build not found', 404);
}
$logs = \mb_substr($logs, 0, null, 'UTF-8'); // Get only valid UTF8 part - removes leftover half-multibytes causing SQL errors
$build = $build->setAttribute('logs', $build->getAttribute('logs', '') . $logs);
$build = $dbForProject->updateDocument('builds', $build->getId(), $build);
@@ -135,9 +135,9 @@ class Certificates extends Action
try {
// Email for alerts is required by LetsEncrypt
$email = System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS');
$email = System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'));
if (empty($email)) {
throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate.');
throw new Exception('You must set a valid security email address (_APP_EMAIL_CERTIFICATES) to issue an SSL certificate.');
}
// Validate domain and DNS records. Skip if job is forced
@@ -474,7 +474,7 @@ class Certificates extends Action
->setBody($body)
->setName('Appwrite Administrator')
->setVariables($emailVariables)
->setRecipient(System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'))
->setRecipient(System::getEnv('_APP_EMAIL_CERTIFICATES', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS')))
->trigger();
}
+5 -1
View File
@@ -116,6 +116,11 @@ class Databases extends Action
*/
$attribute = $dbForProject->getDocument('attributes', $attribute->getId());
if ($attribute->isEmpty()) {
// Attribute was deleted before job was processed
return;
}
$collectionId = $collection->getId();
$key = $attribute->getAttribute('key', '');
$type = $attribute->getAttribute('type', '');
@@ -176,7 +181,6 @@ class Databases extends Action
}
}
$dbForProject->updateDocument(
'attributes',
$attribute->getId(),
+56 -23
View File
@@ -12,6 +12,7 @@ use Utopia\Audit\Audit;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\CLI\Console;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
@@ -21,6 +22,7 @@ use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Query;
use Utopia\DSN\DSN;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
@@ -441,7 +443,7 @@ class Deletes extends Action
* @param Document $document
* @return void
* @throws Authorization
* @throws \Utopia\Database\Exception
* @throws DatabaseException
* @throws Conflict
* @throws Restricted
* @throws Structure
@@ -469,25 +471,48 @@ class Deletes extends Action
* @return void
* @throws Exception
* @throws Authorization
* @throws \Utopia\Database\Exception
* @throws DatabaseException
*/
private function deleteProject(Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, Document $document): void
{
$projectId = $document->getId();
$projectInternalId = $document->getInternalId();
// Delete project tables
try {
$dsn = new DSN($document->getAttribute('database', 'console'));
} catch (\InvalidArgumentException) {
// TODO: Temporary until all projects are using shared tables
$dsn = new DSN('mysql://' . $document->getAttribute('database', 'console'));
}
$dbForProject = $getProjectDB($document);
while (true) {
$collections = $dbForProject->listCollections();
$projectCollectionIds = [
...\array_keys(Config::getParam('collections', [])['projects']),
Audit::COLLECTION,
TimeLimit::COLLECTION,
];
if (empty($collections)) {
break;
}
$limit = \count($projectCollectionIds) + 25;
while (true) {
$collections = $dbForProject->listCollections($limit);
foreach ($collections as $collection) {
$dbForProject->deleteCollection($collection->getId());
if ($dsn->getHost() !== DATABASE_SHARED_TABLES || !\in_array($collection->getId(), $projectCollectionIds)) {
$dbForProject->deleteCollection($collection->getId());
} else {
$this->deleteByGroup($collection->getId(), [], database: $dbForProject);
}
}
if ($dsn->getHost() === DATABASE_SHARED_TABLES) {
$collectionsIds = \array_map(fn ($collection) => $collection->getId(), $collections);
if (empty(\array_diff($collectionsIds, $projectCollectionIds))) {
break;
}
} elseif (empty($collections)) {
break;
}
}
@@ -523,17 +548,16 @@ class Deletes extends Action
Query::equal('projectInternalId', [$projectInternalId]),
], $dbForConsole);
// Delete VCS commments
// Delete VCS comments
$this->deleteByGroup('vcsComments', [
Query::equal('projectInternalId', [$projectInternalId]),
], $dbForConsole);
// Delete metadata tables
try {
// Delete metadata table
if ($dsn->getHost() !== DATABASE_SHARED_TABLES) {
$dbForProject->deleteCollection('_metadata');
} 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.
} else {
$this->deleteByGroup('_metadata', [], $dbForProject);
}
// Delete all storage directories
@@ -660,9 +684,11 @@ class Deletes extends Action
$dbForProject = $getProjectDB($project);
$timeLimit = new TimeLimit("", 0, 1, $dbForProject);
$abuse = new Abuse($timeLimit);
$status = $abuse->cleanup($abuseRetention);
if (!$status) {
throw new Exception('Failed to delete Abuse logs for project ' . $projectId);
try {
$abuse->cleanup($abuseRetention);
} catch (DatabaseException $e) {
Console::error('Failed to delete abuse logs for project ' . $projectId . ': ' . $e->getMessage());
}
}
@@ -678,9 +704,11 @@ class Deletes extends Action
$projectId = $project->getId();
$dbForProject = $getProjectDB($project);
$audit = new Audit($dbForProject);
$status = $audit->cleanup($auditRetention);
if (!$status) {
throw new Exception('Failed to delete Audit logs for project' . $projectId);
try {
$audit->cleanup($auditRetention);
} catch (DatabaseException $e) {
Console::error('Failed to delete audit logs for project ' . $projectId . ': ' . $e->getMessage());
}
}
@@ -935,7 +963,12 @@ class Deletes extends Action
while ($sum === $limit) {
$chunk++;
$results = $database->find($collection, \array_merge([Query::limit($limit)], $queries));
try {
$results = $database->find($collection, [Query::limit($limit), ...$queries]);
} catch (DatabaseException $e) {
Console::error('Failed to find documents for collection ' . $collection . ': ' . $e->getMessage());
return;
}
$sum = count($results);
+76 -5
View File
@@ -199,6 +199,70 @@ class Functions extends Action
}
}
/**
* @param string $message
* @param Document $function
* @param string $trigger
* @param string $path
* @param string $method
* @param Document $user
* @param string|null $jwt
* @param string|null $event
* @throws Exception
*/
private function fail(
string $message,
Database $dbForProject,
Document $function,
string $trigger,
string $path,
string $method,
Document $user,
string $jwt = null,
string $event = null,
): void {
$headers['x-appwrite-trigger'] = $trigger;
$headers['x-appwrite-event'] = $event ?? '';
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
$headersFiltered = [];
foreach ($headers as $key => $value) {
if (\in_array(\strtolower($key), FUNCTION_ALLOWLIST_HEADERS_REQUEST)) {
$headersFiltered[] = ['name' => $key, 'value' => $value];
}
}
$executionId = ID::unique();
$execution = new Document([
'$id' => $executionId,
'$permissions' => $user->isEmpty() ? [] : [Permission::read(Role::user($user->getId()))],
'functionInternalId' => $function->getInternalId(),
'functionId' => $function->getId(),
'deploymentInternalId' => '',
'deploymentId' => '',
'trigger' => $trigger,
'status' => 'failed',
'responseStatusCode' => 0,
'responseHeaders' => [],
'requestPath' => $path,
'requestMethod' => $method,
'requestHeaders' => $headersFiltered,
'errors' => $message,
'logs' => '',
'duration' => 0.0,
'search' => implode(' ', [$function->getId(), $executionId]),
]);
if ($function->getAttribute('logging')) {
$execution = $dbForProject->createDocument('executions', $execution);
}
if ($execution->isEmpty()) {
throw new Exception('Failed to create execution');
}
}
/**
* @param Log $log
* @param Database $dbForProject
@@ -252,11 +316,15 @@ class Functions extends Action
$deployment = $dbForProject->getDocument('deployments', $deploymentId);
if ($deployment->getAttribute('resourceId') !== $functionId) {
throw new Exception('Deployment not found. Create deployment before trying to execute a function');
$errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.';
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
}
if ($deployment->isEmpty()) {
throw new Exception('Deployment not found. Create deployment before trying to execute a function');
$errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.';
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
}
$buildId = $deployment->getAttribute('buildId', '');
@@ -266,11 +334,15 @@ class Functions extends Action
/** Check if build has exists */
$build = $dbForProject->getDocument('builds', $buildId);
if ($build->isEmpty()) {
throw new Exception('Build not found');
$errorMessage = 'The execution could not be completed because a corresponding deployment was not found. A function deployment needs to be created before it can be executed. Please create a deployment for your function and try again.';
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
}
if ($build->getAttribute('status') !== 'ready') {
throw new Exception('Build not ready');
$errorMessage = 'The execution could not be completed because the build is not ready. Please wait for the build to complete and try again.';
$this->fail($errorMessage, $dbForProject, $function, $trigger, $path, $method, $user, $jwt, $event);
return;
}
/** Check if runtime is supported */
@@ -296,7 +368,6 @@ class Functions extends Action
$headers['x-appwrite-user-id'] = $user->getId() ?? '';
$headers['x-appwrite-user-jwt'] = $jwt ?? '';
/** Create execution or update execution status */
/** Create execution or update execution status */
$execution = $dbForProject->getDocument('executions', $executionId ?? '');
if ($execution->isEmpty()) {
+35 -19
View File
@@ -25,7 +25,7 @@ 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\TextMagic;
use Utopia\Messaging\Adapter\SMS\Twilio;
use Utopia\Messaging\Adapter\SMS\Vonage;
use Utopia\Messaging\Messages\Email;
@@ -35,6 +35,7 @@ use Utopia\Messaging\Messages\SMS;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\Storage\Device;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
use Utopia\System\System;
@@ -42,6 +43,8 @@ use function Swoole\Coroutine\batch;
class Messaging extends Action
{
private ?Local $localDevice = null;
public static function getName(): string
{
return 'messaging';
@@ -58,9 +61,8 @@ class Messaging extends Action
->inject('log')
->inject('dbForProject')
->inject('deviceForFiles')
->inject('deviceForLocalFiles')
->inject('queueForUsage')
->callback(fn (Message $message, Log $log, Database $dbForProject, Device $deviceForFiles, Device $deviceForLocalFiles, Usage $queueForUsage) => $this->action($message, $log, $dbForProject, $deviceForFiles, $deviceForLocalFiles, $queueForUsage));
->callback(fn (Message $message, Log $log, Database $dbForProject, Device $deviceForFiles, Usage $queueForUsage) => $this->action($message, $log, $dbForProject, $deviceForFiles, $queueForUsage));
}
/**
@@ -68,7 +70,6 @@ class Messaging extends Action
* @param Log $log
* @param Database $dbForProject
* @param Device $deviceForFiles
* @param Device $deviceForLocalFiles
* @param Usage $queueForUsage
* @return void
* @throws \Exception
@@ -78,7 +79,6 @@ class Messaging extends Action
Log $log,
Database $dbForProject,
Device $deviceForFiles,
Device $deviceForLocalFiles,
Usage $queueForUsage
): void {
Runtime::setHookFlags(SWOOLE_HOOK_ALL ^ SWOOLE_HOOK_TCP);
@@ -101,7 +101,7 @@ class Messaging extends Action
case MESSAGE_SEND_TYPE_EXTERNAL:
$message = $dbForProject->getDocument('messages', $payload['messageId']);
$this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $deviceForLocalFiles, );
$this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $project);
break;
default:
throw new \Exception('Unknown message type: ' . $type);
@@ -112,7 +112,7 @@ class Messaging extends Action
Database $dbForProject,
Document $message,
Device $deviceForFiles,
Device $deviceForLocalFiles,
Document $project,
): void {
$topicIds = $message->getAttribute('topics', []);
$targetIds = $message->getAttribute('targets', []);
@@ -218,8 +218,8 @@ class Messaging extends Action
/**
* @var array<array> $results
*/
$results = batch(\array_map(function ($providerId) use ($identifiers, &$providers, $default, $message, $dbForProject, $deviceForFiles, $deviceForLocalFiles) {
return function () use ($providerId, $identifiers, &$providers, $default, $message, $dbForProject, $deviceForFiles, $deviceForLocalFiles) {
$results = batch(\array_map(function ($providerId) use ($identifiers, &$providers, $default, $message, $dbForProject, $deviceForFiles, $project) {
return function () use ($providerId, $identifiers, &$providers, $default, $message, $dbForProject, $deviceForFiles, $project) {
if (\array_key_exists($providerId, $providers)) {
$provider = $providers[$providerId];
} else {
@@ -246,8 +246,8 @@ class Messaging extends Action
$adapter->getMaxMessagesPerRequest()
);
return batch(\array_map(function ($batch) use ($message, $provider, $adapter, $dbForProject, $deviceForFiles, $deviceForLocalFiles) {
return function () use ($batch, $message, $provider, $adapter, $dbForProject, $deviceForFiles, $deviceForLocalFiles) {
return batch(\array_map(function ($batch) use ($message, $provider, $adapter, $dbForProject, $deviceForFiles, $project) {
return function () use ($batch, $message, $provider, $adapter, $dbForProject, $deviceForFiles, $project) {
$deliveredTotal = 0;
$deliveryErrors = [];
$messageData = clone $message;
@@ -256,7 +256,7 @@ class Messaging extends Action
$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, $deviceForFiles, $deviceForLocalFiles),
MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($dbForProject, $messageData, $provider, $deviceForFiles, $project),
default => throw new \Exception('Provider with the requested ID is of the incorrect type')
};
@@ -354,8 +354,8 @@ class Messaging extends Action
$path = $file->getAttribute('path', '');
if ($deviceForLocalFiles->exists($path)) {
$deviceForLocalFiles->delete($path);
if ($this->getLocalDevice($project)->exists($path)) {
$this->getLocalDevice($project)->delete($path);
}
}
}
@@ -441,17 +441,24 @@ class Messaging extends Action
try {
$adapter->send($data);
$countryCode = $adapter->getCountryCode($message['to'][0] ?? '');
if (!empty($countryCode)) {
$queueForUsage
->addMetric(str_replace('{countryCode}', $countryCode, METRIC_MESSAGES_COUNTRY_CODE), 1);
}
$queueForUsage
->addMetric(METRIC_MESSAGES, 1)
->setProject($project)
->trigger();
} catch (\Throwable $e) {
throw new \Exception('Failed sending to targets with error: ' . $e->getMessage());
} catch (\Throwable $th) {
throw new \Exception('Failed sending to targets with error: ' . $th->getMessage());
}
};
}, $batches));
}
private function getSmsAdapter(Document $provider): ?SMSAdapter
{
$credentials = $provider->getAttribute('credentials');
@@ -459,7 +466,7 @@ class Messaging extends Action
return match ($provider->getAttribute('provider')) {
'mock' => new Mock('username', 'password'),
'twilio' => new Twilio($credentials['accountSid'], $credentials['authToken']),
'textmagic' => new Textmagic($credentials['username'], $credentials['apiKey']),
'textmagic' => new TextMagic($credentials['username'], $credentials['apiKey']),
'telesign' => new Telesign($credentials['customerId'], $credentials['apiKey']),
'msg91' => new Msg91($credentials['senderId'], $credentials['authKey'], $credentials['templateId']),
'vonage' => new Vonage($credentials['apiKey'], $credentials['apiSecret']),
@@ -517,7 +524,7 @@ class Messaging extends Action
Document $message,
Document $provider,
Device $deviceForFiles,
Device $deviceForLocalFiles,
Document $project,
): Email {
$fromName = $provider['options']['fromName'] ?? null;
$fromEmail = $provider['options']['fromEmail'] ?? null;
@@ -579,7 +586,7 @@ class Messaging extends Action
}
if ($deviceForFiles->getType() !== Storage::DEVICE_LOCAL) {
$deviceForFiles->transfer($path, $path, $deviceForLocalFiles);
$deviceForFiles->transfer($path, $path, $this->getLocalDevice($project));
}
$attachment = new Attachment(
@@ -651,4 +658,13 @@ class Messaging extends Action
$badge
);
}
private function getLocalDevice($project): Local
{
if($this->localDevice === null) {
$this->localDevice = new Local(APP_STORAGE_UPLOADS . '/app-' . $project->getId());
}
return $this->localDevice;
}
}
+9 -1
View File
@@ -16,6 +16,7 @@ use Utopia\Database\Exception\Restricted;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Logger\Log;
use Utopia\Logger\Log\Breadcrumb;
use Utopia\Migration\Destinations\Appwrite as DestinationsAppwrite;
use Utopia\Migration\Exception as MigrationException;
use Utopia\Migration\Source;
@@ -85,6 +86,7 @@ class Migrations extends Action
return;
}
$log->addTag('migrationId', $migration->getId());
$log->addTag('projectId', $project->getId());
$this->processMigration($project, $migration, $log);
@@ -256,6 +258,7 @@ class Migrations extends Action
$migrationDocument = $this->dbForProject->getDocument('migrations', $migration->getId());
$migrationDocument->setAttribute('stage', 'processing');
$migrationDocument->setAttribute('status', 'processing');
$log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'processing'", \microtime(true)));
$this->updateMigrationDocument($migrationDocument, $projectDocument);
$log->addTag('type', $migrationDocument->getAttribute('source'));
@@ -277,6 +280,7 @@ class Migrations extends Action
/** Start Transfer */
$migrationDocument->setAttribute('stage', 'migrating');
$log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'migrating'", \microtime(true)));
$this->updateMigrationDocument($migrationDocument, $projectDocument);
$transfer->run($migrationDocument->getAttribute('resources'), function () use ($migrationDocument, $transfer, $projectDocument) {
$migrationDocument->setAttribute('resourceData', json_encode($transfer->getCache()));
@@ -291,6 +295,7 @@ class Migrations extends Action
if (!empty($sourceErrors) || !empty($destinationErrors)) {
$migrationDocument->setAttribute('status', 'failed');
$migrationDocument->setAttribute('stage', 'finished');
$log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'finished' and failed", \microtime(true)));
$errorMessages = [];
foreach ($sourceErrors as $error) {
@@ -303,6 +308,7 @@ class Migrations extends Action
}
$migrationDocument->setAttribute('errors', $errorMessages);
$log->addExtra('migrationErrors', json_encode($errorMessages));
$this->updateMigrationDocument($migrationDocument, $projectDocument);
return;
@@ -310,6 +316,7 @@ class Migrations extends Action
$migrationDocument->setAttribute('status', 'completed');
$migrationDocument->setAttribute('stage', 'finished');
$log->addBreadcrumb(new Breadcrumb("debug", "migration", "Migration hit stage 'finished' and succeeded", \microtime(true)));
} catch (\Throwable $th) {
Console::error($th->getMessage());
@@ -338,6 +345,7 @@ class Migrations extends Action
}
$migrationDocument->setAttribute('errors', $errorMessages);
$log->addTag('migrationErrors', json_encode($errorMessages));
}
} finally {
if ($tempAPIKey) {
@@ -347,7 +355,7 @@ class Migrations extends Action
$this->updateMigrationDocument($migrationDocument, $projectDocument);
if ($migrationDocument->getAttribute('status', '') == 'failed') {
throw new Exception(implode("\n", $migrationDocument->getAttribute('errors', [])));
throw new Exception("Migration failed");
}
}
}
+1 -1
View File
@@ -104,7 +104,7 @@ class Webhooks extends Action
\curl_setopt($ch, CURLOPT_USERAGENT, \sprintf(
APP_USERAGENT,
System::getEnv('_APP_VERSION', 'UNKNOWN'),
System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY)
System::getEnv('_APP_EMAIL_SECURITY', System::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS', APP_EMAIL_SECURITY))
));
\curl_setopt(
$ch,
@@ -96,15 +96,15 @@ class OpenAPI3 extends Format
];
if (isset($output['components']['securitySchemes']['Project'])) {
$output['components']['securitySchemes']['Project']['x-appwrite'] = ['demo' => '5df5acd0d48c2'];
$output['components']['securitySchemes']['Project']['x-appwrite'] = ['demo' => '<YOUR_PROJECT_ID>'];
}
if (isset($output['components']['securitySchemes']['Key'])) {
$output['components']['securitySchemes']['Key']['x-appwrite'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2'];
$output['components']['securitySchemes']['Key']['x-appwrite'] = ['demo' => '<YOUR_API_KEY>'];
}
if (isset($output['securityDefinitions']['JWT'])) {
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...'];
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
}
if (isset($output['components']['securitySchemes']['Locale'])) {
@@ -93,15 +93,15 @@ class Swagger2 extends Format
];
if (isset($output['securityDefinitions']['Project'])) {
$output['securityDefinitions']['Project']['x-appwrite'] = ['demo' => '5df5acd0d48c2'];
$output['securityDefinitions']['Project']['x-appwrite'] = ['demo' => '<YOUR_PROJECT_ID>'];
}
if (isset($output['securityDefinitions']['Key'])) {
$output['securityDefinitions']['Key']['x-appwrite'] = ['demo' => '919c2d18fb5d4...a2ae413da83346ad2'];
$output['securityDefinitions']['Key']['x-appwrite'] = ['demo' => '<YOUR_API_KEY>'];
}
if (isset($output['securityDefinitions']['JWT'])) {
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ...'];
$output['securityDefinitions']['JWT']['x-appwrite'] = ['demo' => '<YOUR_JWT>'];
}
if (isset($output['securityDefinitions']['Locale'])) {
+15 -4
View File
@@ -67,9 +67,6 @@ class Comment
];
}
//TODO: Update link to documentation
$text .= "**Your function has been automatically deployed.** Learn more about Appwrite [Function Deployments](https://appwrite.io/docs/functions).\n\n";
foreach ($projects as $projectId => $project) {
$text .= "**{$project['name']}** `{$projectId}`\n\n";
$text .= "| Function | ID | Status | Action |\n";
@@ -79,6 +76,18 @@ class Comment
$hostname = System::getEnv('_APP_DOMAIN');
foreach ($project['functions'] as $functionId => $function) {
if ($function['status'] === 'waiting' || $function['status'] === 'processing' || $function['status'] === 'building') {
$text .= "**Your function deployment is in progress. Please check back in a few minutes for the updated status.**\n\n";
} elseif ($function['status'] === 'ready') {
$text .= "**Your function has been successfully deployed.**\n\n";
} else {
$text .= "**Your function deployment has failed. Please check the logs for more details and retry.**\n\n";
}
$text .= "Project name: **{$project['name']}** \nProject ID: `{$projectId}`\n\n";
$text .= "| Function | ID | Status | Action |\n";
$text .= "| :- | :- | :- | :- |\n";
$generateImage = function (string $status) use ($protocol, $hostname) {
$extention = $status === 'building' ? 'gif' : 'png';
$imagesUrl = $protocol . '://' . $hostname . '/images/vcs/';
@@ -106,7 +115,9 @@ class Comment
$text .= "\n\n";
}
//TODO: Update did you know section
$functionUrl = $protocol . '://' . $hostname . '/console/project-' . $projectId . '/functions/function-' . $functionId;
$text .= "Only deployments on the production branch are activated automatically. If you'd like to activate this deployment, navigate to [your deployments]($functionUrl). Learn more about Appwrite [Function deployments](https://appwrite.io/docs/functions).\n\n";
$tip = $this->tips[array_rand($this->tips)];
$text .= "> **💡 Did you know?** \n " . $tip . "\n\n";
+8 -3
View File
@@ -2,6 +2,7 @@
namespace Executor;
use Appwrite\Extend\Exception as AppwriteException;
use Exception;
use Utopia\System\System;
@@ -193,7 +194,6 @@ class Executor
'path' => $path,
'method' => $method,
'headers' => $headers,
'image' => $image,
'source' => $source,
'entrypoint' => $entrypoint,
@@ -311,6 +311,8 @@ class Executor
$responseType = $responseHeaders['content-type'] ?? '';
$responseStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_errno($ch);
$curlErrorMessage = curl_error($ch);
if ($decode) {
switch (substr($responseType, 0, strpos($responseType, ';'))) {
@@ -327,8 +329,11 @@ class Executor
}
}
if ((curl_errno($ch)/* || 200 != $responseStatus*/)) {
throw new Exception(curl_error($ch) . ' with status code ' . $responseStatus, $responseStatus);
if ($curlError) {
if ($curlError == CURLE_OPERATION_TIMEDOUT) {
throw new AppwriteException(AppwriteException::FUNCTION_SYNCHRONOUS_TIMEOUT);
}
throw new Exception($curlErrorMessage . ' with status code ' . $responseStatus, $responseStatus);
}
curl_close($ch);