From 067bf582dc2a241a532dd2a659c40877d9ab9c57 Mon Sep 17 00:00:00 2001 From: Jake Barnby Date: Wed, 6 May 2026 14:46:57 +1200 Subject: [PATCH] feat(webhooks): emit pausing alerts via notifications worker to owners on email and console When a webhook is permanently paused after exceeding the max failure threshold, fan out the alert through the new Notifications worker instead of the legacy Mails worker. Recipients are filtered to project owners only and receive both an email and a console notification, with a single deduplication key per webhook + attempts so duplicate triggers collapse downstream. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Appwrite/Platform/Workers/Webhooks.php | 80 ++++++++++++++++------ 1 file changed, 60 insertions(+), 20 deletions(-) diff --git a/src/Appwrite/Platform/Workers/Webhooks.php b/src/Appwrite/Platform/Workers/Webhooks.php index a7f4595966..c5abf4f755 100644 --- a/src/Appwrite/Platform/Workers/Webhooks.php +++ b/src/Appwrite/Platform/Workers/Webhooks.php @@ -2,8 +2,8 @@ namespace Appwrite\Platform\Workers; -use Appwrite\Event\Mail; use Appwrite\Event\Message\Usage as UsageMessage; +use Appwrite\Event\Notification; use Appwrite\Event\Publisher\Usage as UsagePublisher; use Appwrite\Template\Template; use Appwrite\Usage\Context as UsageContext; @@ -36,7 +36,7 @@ class Webhooks extends Action ->inject('message') ->inject('project') ->inject('dbForPlatform') - ->inject('queueForMails') + ->inject('queueForNotifications') ->inject('publisherForUsage') ->inject('log') ->inject('plan') @@ -47,14 +47,14 @@ class Webhooks extends Action * @param Message $message * @param Document $project * @param Database $dbForPlatform - * @param Mail $queueForMails + * @param Notification $queueForNotifications * @param UsagePublisher $publisherForUsage * @param Log $log * @param array $plan * @return void * @throws Exception */ - public function action(Message $message, Document $project, Database $dbForPlatform, Mail $queueForMails, UsagePublisher $publisherForUsage, Log $log, array $plan): void + public function action(Message $message, Document $project, Database $dbForPlatform, Notification $queueForNotifications, UsagePublisher $publisherForUsage, Log $log, array $plan): void { $this->errors = []; $payload = $message->getPayload(); @@ -73,7 +73,7 @@ class Webhooks extends Action foreach ($project->getAttribute('webhooks', []) as $webhook) { if (array_intersect($webhook->getAttribute('events', []), $events)) { - $this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $queueForMails, $publisherForUsage, $plan); + $this->execute($events, $webhookPayload, $webhook, $user, $project, $dbForPlatform, $queueForNotifications, $publisherForUsage, $plan); } } @@ -89,11 +89,11 @@ class Webhooks extends Action * @param Document $user * @param Document $project * @param Database $dbForPlatform - * @param Mail $queueForMails + * @param Notification $queueForNotifications * @param array $plan * @return void */ - private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, Mail $queueForMails, UsagePublisher $publisherForUsage, array $plan): void + private function execute(array $events, string $payload, Document $webhook, Document $user, Document $project, Database $dbForPlatform, Notification $queueForNotifications, UsagePublisher $publisherForUsage, array $plan): void { if ($webhook->getAttribute('enabled') !== true) { return; @@ -171,7 +171,7 @@ class Webhooks extends Action if ($attempts >= \intval(System::getEnv('_APP_WEBHOOK_MAX_FAILED_ATTEMPTS', '10'))) { $webhook->setAttribute('enabled', false); $updatePayload['enabled'] = false; - $this->sendEmailAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $queueForMails, $plan); + $this->sendAlert($attempts, $statusCode, $webhook, $project, $dbForPlatform, $queueForNotifications, $plan); } $dbForPlatform->updateDocument('webhooks', $webhook->getId(), new Document($updatePayload)); @@ -203,26 +203,49 @@ class Webhooks extends Action * @param Document $webhook * @param Document $project * @param Database $dbForPlatform - * @param Mail $queueForMails + * @param Notification $queueForNotifications * @param array $plan * @return void */ - public function sendEmailAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForPlatform, Mail $queueForMails, array $plan): void + public function sendAlert(int $attempts, mixed $statusCode, Document $webhook, Document $project, Database $dbForPlatform, Notification $queueForNotifications, array $plan): void { $memberships = $dbForPlatform->find('memberships', [ Query::equal('teamInternalId', [$project->getAttribute('teamInternalId')]), Query::limit(APP_LIMIT_SUBQUERY) ]); - $userIds = array_column(\array_map(fn ($membership) => $membership->getArrayCopy(), $memberships), 'userId'); + // Webhook-paused alerts go only to project owners — non-owner team members do not receive them. + $ownerMemberships = \array_filter( + $memberships, + fn (Document $membership) => \in_array('owner', $membership->getAttribute('roles', []), true) + ); + + if (empty($ownerMemberships)) { + return; + } + + $userIds = \array_values(\array_unique(\array_filter(\array_map( + fn (Document $membership) => $membership->getAttribute('userId'), + $ownerMemberships + )))); + + if (empty($userIds)) { + return; + } $users = $dbForPlatform->find('users', [ Query::equal('$id', $userIds), + Query::limit(APP_LIMIT_SUBQUERY), ]); + if (empty($users)) { + return; + } + $projectId = $project->getId(); $region = $project->getAttribute('region', 'default'); $webhookId = $webhook->getId(); + $teamId = $project->getAttribute('teamId'); $template = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-webhook-failed.tpl'); @@ -241,7 +264,6 @@ class Webhooks extends Action $template->setParam('{{termsUrl}}', $plan['termsUrl'] ?? APP_EMAIL_TERMS_URL); $template->setParam('{{privacyUrl}}', $plan['privacyUrl'] ?? APP_EMAIL_PRIVACY_URL); - // TODO: Use setbodyTemplate once #7307 is merged $subject = 'Webhook deliveries have been paused'; $preview = 'Webhook deliveries to your endpoint have been paused.'; $body = Template::fromFile(__DIR__ . '/../../../../app/config/locale/templates/email-base-styled.tpl'); @@ -249,20 +271,38 @@ class Webhooks extends Action $body ->setParam('{{subject}}', $subject) ->setParam('{{message}}', $template->render()) - ->setParam('{{year}}', date("Y")); + ->setParam('{{year}}', date('Y')); - $queueForMails + $queueForNotifications ->setProject($project) ->setSubject($subject) ->setPreview($preview) - ->setBody($body->render()); + ->setBody($body->render()) + ->setDeduplicationKey('webhook:' . $webhook->getId() . ':paused:' . $attempts); foreach ($users as $user) { - $queueForMails - ->setVariables(['user' => $user->getAttribute('name', '')]) - ->setName($user->getAttribute('name', '')) - ->setRecipient($user->getAttribute('email')) - ->trigger(); + $email = $user->getAttribute('email'); + $userId = $user->getId(); + + if (!empty($email)) { + $queueForNotifications->addRecipient( + $email, + NOTIFICATION_TYPE_EMAIL, + null, + $userId, + $teamId, + ); + } + + $queueForNotifications->addRecipient( + $userId, + NOTIFICATION_TYPE_CONSOLE, + null, + $userId, + $teamId, + ); } + + $queueForNotifications->trigger(); } }