diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index e478884529..2de922117b 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -3251,7 +3251,12 @@ App::patch('/v1/messaging/messages/email/:messageId') throw new Exception(Exception::MESSAGE_NOT_FOUND); } - if ($status !== MessageStatus::DRAFT && \count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { + if ( + $status !== MessageStatus::DRAFT + && \count($topics ?? $message->getAttribute('topics', [])) === 0 + && \count($users ?? $message->getAttribute('users', [])) === 0 + && \count($targets ?? $message->getAttribute('targets', [])) === 0 + ) { throw new Exception(Exception::MESSAGE_MISSING_TARGET); } @@ -3264,18 +3269,21 @@ App::patch('/v1/messaging/messages/email/:messageId') throw new Exception(Exception::MESSAGE_ALREADY_SENT); case MessageStatus::FAILED: throw new Exception(Exception::MESSAGE_ALREADY_FAILED); - case MessageStatus::SCHEDULED: - if (\is_null($scheduledAt) && \is_null($currentScheduledAt)) { - throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE); - } - break; } - if (!\is_null($currentScheduledAt) && $currentScheduledAt < new \DateTime()) { + if ( + $status === MessageStatus::SCHEDULED + && \is_null($scheduledAt) + && \is_null($currentScheduledAt) + ) { + throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE); + } + + if (!\is_null($currentScheduledAt) && new \DateTime($currentScheduledAt) < new \DateTime()) { throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED); } - if (\is_null($currentScheduledAt && !\is_null($scheduledAt))) { + if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) { $schedule = $dbForConsole->createDocument('schedules', new Document([ 'region' => App::getEnv('_APP_REGION', 'default'), 'resourceType' => 'message', @@ -3292,18 +3300,18 @@ App::patch('/v1/messaging/messages/email/:messageId') if (!\is_null($currentScheduledAt)) { $schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId')); + $scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED; if ($schedule->isEmpty()) { throw new Exception(Exception::SCHEDULE_NOT_FOUND); } + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('active', $scheduledStatus); + if (!\is_null($scheduledAt)) { - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('schedule', $scheduledAt) - ->setAttribute('active', $status === MessageStatus::SCHEDULED); - } else { - $schedule->setAttribute('active', $status === MessageStatus::SCHEDULED); + $schedule->setAttribute('schedule', $scheduledAt); } $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule); @@ -3402,7 +3410,12 @@ App::patch('/v1/messaging/messages/sms/:messageId') throw new Exception(Exception::MESSAGE_NOT_FOUND); } - if ($status !== MessageStatus::DRAFT && \count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { + if ( + $status !== MessageStatus::DRAFT + && \count($topics ?? $message->getAttribute('topics', [])) === 0 + && \count($users ?? $message->getAttribute('users', [])) === 0 + && \count($targets ?? $message->getAttribute('targets', [])) === 0 + ) { throw new Exception(Exception::MESSAGE_MISSING_TARGET); } @@ -3415,18 +3428,21 @@ App::patch('/v1/messaging/messages/sms/:messageId') throw new Exception(Exception::MESSAGE_ALREADY_SENT); case MessageStatus::FAILED: throw new Exception(Exception::MESSAGE_ALREADY_FAILED); - case MessageStatus::SCHEDULED: - if (\is_null($scheduledAt) && \is_null($message->getAttribute('scheduledAt'))) { - throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE); - } - break; } - if (!is_null($currentScheduledAt) && $currentScheduledAt < new \DateTime()) { + if ( + $status === MessageStatus::SCHEDULED + && \is_null($scheduledAt) + && \is_null($currentScheduledAt) + ) { + throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE); + } + + if (!\is_null($currentScheduledAt) && new \DateTime($currentScheduledAt) < new \DateTime()) { throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED); } - if (\is_null($currentScheduledAt && !\is_null($scheduledAt))) { + if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) { $schedule = $dbForConsole->createDocument('schedules', new Document([ 'region' => App::getEnv('_APP_REGION', 'default'), 'resourceType' => 'message', @@ -3443,18 +3459,18 @@ App::patch('/v1/messaging/messages/sms/:messageId') if (!\is_null($currentScheduledAt)) { $schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId')); + $scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED; if ($schedule->isEmpty()) { throw new Exception(Exception::SCHEDULE_NOT_FOUND); } + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('active', $scheduledStatus); + if (!\is_null($scheduledAt)) { - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('schedule', $scheduledAt) - ->setAttribute('active', $status === MessageStatus::SCHEDULED); - } else { - $schedule->setAttribute('active', $status === MessageStatus::SCHEDULED); + $schedule->setAttribute('schedule', $scheduledAt); } $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule); @@ -3546,6 +3562,17 @@ App::patch('/v1/messaging/messages/push/:messageId') throw new Exception(Exception::MESSAGE_NOT_FOUND); } + if ( + $status !== MessageStatus::DRAFT + && \count($topics ?? $message->getAttribute('topics', [])) === 0 + && \count($users ?? $message->getAttribute('users', [])) === 0 + && \count($targets ?? $message->getAttribute('targets', [])) === 0 + ) { + throw new Exception(Exception::MESSAGE_MISSING_TARGET); + } + + $currentScheduledAt = $message->getAttribute('scheduledAt'); + switch ($message->getAttribute('status')) { case MessageStatus::PROCESSING: throw new Exception(Exception::MESSAGE_ALREADY_PROCESSING); @@ -3555,17 +3582,19 @@ App::patch('/v1/messaging/messages/push/:messageId') throw new Exception(Exception::MESSAGE_ALREADY_FAILED); } - if ($status !== MessageStatus::DRAFT && \count($topics) === 0 && \count($users) === 0 && \count($targets) === 0) { - throw new Exception(Exception::MESSAGE_MISSING_TARGET); + if ( + $status === MessageStatus::SCHEDULED + && \is_null($scheduledAt) + && \is_null($currentScheduledAt) + ) { + throw new Exception(Exception::MESSAGE_MISSING_SCHEDULE); } - $currentScheduledAt = $message->getAttribute('scheduledAt'); - - if (!is_null($currentScheduledAt) && $currentScheduledAt < new \DateTime()) { + if (!\is_null($currentScheduledAt) && new \DateTime($currentScheduledAt) < new \DateTime()) { throw new Exception(Exception::MESSAGE_ALREADY_SCHEDULED); } - if (\is_null($currentScheduledAt && !\is_null($scheduledAt))) { + if (\is_null($currentScheduledAt) && !\is_null($scheduledAt)) { $schedule = $dbForConsole->createDocument('schedules', new Document([ 'region' => App::getEnv('_APP_REGION', 'default'), 'resourceType' => 'message', @@ -3582,18 +3611,18 @@ App::patch('/v1/messaging/messages/push/:messageId') if (!\is_null($currentScheduledAt)) { $schedule = $dbForConsole->getDocument('schedules', $message->getAttribute('scheduleId')); + $scheduledStatus = ($status ?? $message->getAttribute('status')) === MessageStatus::SCHEDULED; if ($schedule->isEmpty()) { throw new Exception(Exception::SCHEDULE_NOT_FOUND); } + $schedule + ->setAttribute('resourceUpdatedAt', DateTime::now()) + ->setAttribute('active', $scheduledStatus); + if (!\is_null($scheduledAt)) { - $schedule - ->setAttribute('resourceUpdatedAt', DateTime::now()) - ->setAttribute('schedule', $scheduledAt) - ->setAttribute('active', $status === MessageStatus::SCHEDULED); - } else { - $schedule->setAttribute('active', $status === MessageStatus::SCHEDULED); + $schedule->setAttribute('schedule', $scheduledAt); } $dbForConsole->updateDocument('schedules', $schedule->getId(), $schedule); diff --git a/src/Appwrite/Platform/Tasks/ScheduleMessages.php b/src/Appwrite/Platform/Tasks/ScheduleMessages.php index bc9b6d37d2..05b094ac58 100644 --- a/src/Appwrite/Platform/Tasks/ScheduleMessages.php +++ b/src/Appwrite/Platform/Tasks/ScheduleMessages.php @@ -17,8 +17,8 @@ use function Swoole\Coroutine\run; class ScheduleMessages extends ScheduleBase { - public const UPDATE_TIMER = 10; // seconds - public const ENQUEUE_TIMER = 60; // seconds + public const UPDATE_TIMER = 3; // seconds + public const ENQUEUE_TIMER = 4; // seconds public static function getName(): string { @@ -37,14 +37,14 @@ class ScheduleMessages extends ScheduleBase continue; } - $now = DateTime::now(); - $scheduledAt = DateTime::formatTz($schedule['schedule']); + $now = new \DateTime(); + $scheduledAt = new \DateTime($schedule['schedule']); if ($scheduledAt > $now) { continue; } - \go(function () use ($schedule, $pools, $dbForConsole) { + \go(function () use ($now, $schedule, $pools, $dbForConsole) { $queue = $pools->get('queue')->pop(); $connection = $queue->getResource(); $queueForMessaging = new Messaging($connection); diff --git a/src/Appwrite/Utopia/Response/Model/Message.php b/src/Appwrite/Utopia/Response/Model/Message.php index fe1703a0d9..f6502bd75a 100644 --- a/src/Appwrite/Utopia/Response/Model/Message.php +++ b/src/Appwrite/Utopia/Response/Model/Message.php @@ -97,8 +97,8 @@ class Message extends Model ->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.', + 'default' => 'draft', + 'example' => 'Message status can be one of the following: draft, processing, scheduled, sent, or failed.', ]); } diff --git a/tests/e2e/Services/Messaging/MessagingBase.php b/tests/e2e/Services/Messaging/MessagingBase.php index be8499ce07..4f2b5ffba9 100644 --- a/tests/e2e/Services/Messaging/MessagingBase.php +++ b/tests/e2e/Services/Messaging/MessagingBase.php @@ -5,6 +5,7 @@ namespace Tests\E2E\Services\Messaging; use Appwrite\Messaging\Status as MessageStatus; use Tests\E2E\Client; use Utopia\App; +use Utopia\Database\DateTime; use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Role; @@ -876,6 +877,258 @@ trait MessagingBase return $message; } + public function testScheduledMessage(): void + { + // Create user + $response = $this->client->call(Client::METHOD_POST, '/users', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'userId' => ID::unique(), + 'email' => uniqid() . "@example.com", + 'password' => 'password', + 'name' => 'Messaging User 1', + ]); + + $targetId = $response['body']['targets'][0]['$id']; + + // Create scheduled message + $message = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'messageId' => ID::unique(), + 'targets' => [$targetId], + 'subject' => 'New blog post', + 'content' => 'Check out the new blog post at http://localhost', + 'status' => MessageStatus::SCHEDULED, + 'scheduledAt' => DateTime::addSeconds(new \DateTime(), 3), + ]); + + $this->assertEquals(201, $message['headers']['status-code']); + $this->assertEquals(MessageStatus::SCHEDULED, $message['body']['status']); + + \sleep(8); + + $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $message['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(MessageStatus::FAILED, $message['body']['status']); + + /** + * Test for FAILURE + */ + $message = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'messageId' => ID::unique(), + 'targets' => [$targetId], + 'subject' => 'New blog post', + 'content' => 'Check out the new blog post at http://localhost', + 'status' => MessageStatus::SCHEDULED, + ]); + + $this->assertEquals(400, $message['headers']['status-code']); + } + + public function testScheduledToDraftMessage(): void + { + // Create user + $response = $this->client->call(Client::METHOD_POST, '/users', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'userId' => ID::unique(), + 'email' => uniqid() . "@example.com", + 'password' => 'password', + 'name' => 'Messaging User 1', + ]); + + $targetId = $response['body']['targets'][0]['$id']; + + // Create scheduled message + $message = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'messageId' => ID::unique(), + 'targets' => [$targetId], + 'subject' => 'New blog post', + 'content' => 'Check out the new blog post at http://localhost', + 'status' => MessageStatus::SCHEDULED, + 'scheduledAt' => DateTime::addSeconds(new \DateTime(), 5), + ]); + + $this->assertEquals(201, $message['headers']['status-code']); + $this->assertEquals(MessageStatus::SCHEDULED, $message['body']['status']); + + $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $message['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'status' => MessageStatus::DRAFT, + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(MessageStatus::DRAFT, $message['body']['status']); + + \sleep(8); + + $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $message['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(MessageStatus::DRAFT, $message['body']['status']); + } + + public function testDraftToScheduledMessage(): void + { + // Create user + $response = $this->client->call(Client::METHOD_POST, '/users', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'userId' => ID::unique(), + 'email' => uniqid() . "@example.com", + 'password' => 'password', + 'name' => 'Messaging User 1', + ]); + + $targetId = $response['body']['targets'][0]['$id']; + + // Create draft message + $message = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'messageId' => ID::unique(), + 'targets' => [$targetId], + 'subject' => 'New blog post', + 'content' => 'Check out the new blog post at http://localhost', + 'status' => MessageStatus::DRAFT, + ]); + + $this->assertEquals(201, $message['headers']['status-code']); + $this->assertEquals(MessageStatus::DRAFT, $message['body']['status']); + + $response = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $message['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'status' => MessageStatus::SCHEDULED, + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $message['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'status' => MessageStatus::SCHEDULED, + 'scheduledAt' => DateTime::addSeconds(new \DateTime(), 3), + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(MessageStatus::SCHEDULED, $message['body']['status']); + + \sleep(8); + + $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $message['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(MessageStatus::FAILED, $message['body']['status']); + } + + public function testUpdateScheduledAt(): void + { + // Create user + $response = $this->client->call(Client::METHOD_POST, '/users', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'userId' => ID::unique(), + 'email' => uniqid() . "@example.com", + 'password' => 'password', + 'name' => 'Messaging User 1', + ]); + + $targetId = $response['body']['targets'][0]['$id']; + + // Create scheduled message + $message = $this->client->call(Client::METHOD_POST, '/messaging/messages/email', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'messageId' => ID::unique(), + 'targets' => [$targetId], + 'subject' => 'New blog post', + 'content' => 'Check out the new blog post at http://localhost', + 'status' => MessageStatus::SCHEDULED, + 'scheduledAt' => DateTime::addSeconds(new \DateTime(), 3), + ]); + + $this->assertEquals(201, $message['headers']['status-code']); + $this->assertEquals(MessageStatus::SCHEDULED, $message['body']['status']); + + $scheduledAt = DateTime::addSeconds(new \DateTime(), 10); + + $message = $this->client->call(Client::METHOD_PATCH, '/messaging/messages/email/' . $message['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'scheduledAt' => $scheduledAt, + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + + \sleep(8); + + $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $message['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(MessageStatus::SCHEDULED, $message['body']['status']); + + \sleep(8); + + $message = $this->client->call(Client::METHOD_GET, '/messaging/messages/' . $message['body']['$id'], [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $message['headers']['status-code']); + $this->assertEquals(MessageStatus::FAILED, $message['body']['status']); + } + public function testSendEmail() { if (empty(App::getEnv('_APP_MESSAGE_EMAIL_TEST_DSN'))) {