mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
feat(notifications): add Console and Webhook provider adapters
Console adapter persists alerts directly to the project database. Webhook adapter dispatches signed JSON payloads (HMAC-SHA256) to subscriber URLs.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Messaging\Adapter;
|
||||
|
||||
use Appwrite\Messaging\Messages\Console as ConsoleMessage;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Helpers\ID;
|
||||
use Utopia\Database\Helpers\Permission;
|
||||
use Utopia\Database\Helpers\Role;
|
||||
use Utopia\Messaging\Adapter;
|
||||
use Utopia\Messaging\Message;
|
||||
use Utopia\Messaging\Response;
|
||||
|
||||
class Console extends Adapter
|
||||
{
|
||||
protected const NAME = 'Console';
|
||||
protected const TYPE = 'console';
|
||||
protected const MESSAGE_TYPE = ConsoleMessage::class;
|
||||
|
||||
public function __construct(protected Database $database)
|
||||
{
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return static::NAME;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::TYPE;
|
||||
}
|
||||
|
||||
public function getMessageType(): string
|
||||
{
|
||||
return static::MESSAGE_TYPE;
|
||||
}
|
||||
|
||||
public function getMaxMessagesPerRequest(): int
|
||||
{
|
||||
return 1000;
|
||||
}
|
||||
|
||||
public function send(Message $message): array
|
||||
{
|
||||
if (!$message instanceof ConsoleMessage) {
|
||||
throw new \Exception('Invalid message type.');
|
||||
}
|
||||
|
||||
return $this->process($message);
|
||||
}
|
||||
|
||||
protected function process(ConsoleMessage $message): array
|
||||
{
|
||||
$response = new Response($this->getType());
|
||||
$delivered = 0;
|
||||
|
||||
foreach ($message->getRecipients() as $recipient) {
|
||||
$userId = $recipient['userId'] ?? '';
|
||||
$teamId = $recipient['teamId'] ?? '';
|
||||
$key = $userId !== '' ? $userId : $teamId;
|
||||
|
||||
try {
|
||||
$document = new Document([
|
||||
'$id' => $message->getMessageId() ?? ID::unique(),
|
||||
'$permissions' => $this->buildPermissions($userId, $teamId),
|
||||
'messageId' => $message->getMessageId(),
|
||||
'type' => $message->getType(),
|
||||
'channel' => self::TYPE,
|
||||
'userId' => $userId !== '' ? $userId : null,
|
||||
'teamId' => $teamId !== '' ? $teamId : null,
|
||||
'projectId' => $message->getProjectId(),
|
||||
'title' => $message->getTitle(),
|
||||
'body' => $message->getBody(),
|
||||
]);
|
||||
|
||||
$this->database->createDocument('alerts', $document);
|
||||
$delivered++;
|
||||
$response->addResult($key);
|
||||
} catch (\Throwable $error) {
|
||||
$response->addResult($key, $error->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$response->setDeliveredTo($delivered);
|
||||
return $response->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string>
|
||||
*/
|
||||
private function buildPermissions(string $userId, string $teamId): array
|
||||
{
|
||||
$permissions = [];
|
||||
if ($userId !== '') {
|
||||
$permissions[] = Permission::read(Role::user($userId));
|
||||
$permissions[] = Permission::update(Role::user($userId));
|
||||
$permissions[] = Permission::delete(Role::user($userId));
|
||||
}
|
||||
if ($teamId !== '') {
|
||||
$permissions[] = Permission::read(Role::team($teamId));
|
||||
$permissions[] = Permission::update(Role::team($teamId, 'owner'));
|
||||
$permissions[] = Permission::delete(Role::team($teamId, 'owner'));
|
||||
}
|
||||
return $permissions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Messaging\Adapter;
|
||||
|
||||
use Appwrite\Messaging\Messages\Webhook as WebhookMessage;
|
||||
use Utopia\Messaging\Adapter;
|
||||
use Utopia\Messaging\Message;
|
||||
use Utopia\Messaging\Response;
|
||||
|
||||
class Webhook extends Adapter
|
||||
{
|
||||
protected const NAME = 'Webhook';
|
||||
protected const TYPE = 'webhook';
|
||||
protected const MESSAGE_TYPE = WebhookMessage::class;
|
||||
|
||||
protected const SIGNATURE_HEADER = 'X-Appwrite-Webhook-Signature';
|
||||
protected const TIMESTAMP_HEADER = 'X-Appwrite-Webhook-Timestamp';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return static::NAME;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return static::TYPE;
|
||||
}
|
||||
|
||||
public function getMessageType(): string
|
||||
{
|
||||
return static::MESSAGE_TYPE;
|
||||
}
|
||||
|
||||
public function getMaxMessagesPerRequest(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
public function send(Message $message): array
|
||||
{
|
||||
if (!$message instanceof WebhookMessage) {
|
||||
throw new \Exception('Invalid message type.');
|
||||
}
|
||||
|
||||
return $this->process($message);
|
||||
}
|
||||
|
||||
protected function process(WebhookMessage $message): array
|
||||
{
|
||||
$response = new Response($this->getType());
|
||||
$body = \json_encode($message->getPayload(), JSON_THROW_ON_ERROR);
|
||||
$timestamp = (string) \time();
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
self::TIMESTAMP_HEADER . ': ' . $timestamp,
|
||||
];
|
||||
|
||||
$secret = $message->getSigningSecret();
|
||||
if ($secret !== null && $secret !== '') {
|
||||
$signature = \hash_hmac('sha256', $timestamp . '.' . $body, $secret);
|
||||
$headers[] = self::SIGNATURE_HEADER . ': sha256=' . $signature;
|
||||
}
|
||||
|
||||
foreach ($message->getHeaders() as $name => $value) {
|
||||
$headers[] = $name . ': ' . $value;
|
||||
}
|
||||
|
||||
$delivered = 0;
|
||||
foreach ($message->getUrls() as $url) {
|
||||
$result = $this->dispatch('POST', $url, $headers, $body, $message->getTimeout());
|
||||
if ($result['statusCode'] >= 200 && $result['statusCode'] < 300 && empty($result['error'])) {
|
||||
$delivered++;
|
||||
$response->addResult($url);
|
||||
} else {
|
||||
$response->addResult($url, $result['error'] ?: ('HTTP ' . $result['statusCode']));
|
||||
}
|
||||
}
|
||||
|
||||
$response->setDeliveredTo($delivered);
|
||||
return $response->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $headers
|
||||
* @return array{statusCode: int, response: string|null, error: string|null}
|
||||
*/
|
||||
protected function dispatch(string $method, string $url, array $headers, string $body, int $timeout): array
|
||||
{
|
||||
$handle = \curl_init();
|
||||
\curl_setopt_array($handle, [
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $body,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => $timeout,
|
||||
CURLOPT_CONNECTTIMEOUT => \min(10, $timeout),
|
||||
CURLOPT_USERAGENT => 'Appwrite Webhook',
|
||||
]);
|
||||
|
||||
$output = \curl_exec($handle);
|
||||
$statusCode = (int) \curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
|
||||
$error = \curl_error($handle);
|
||||
\curl_close($handle);
|
||||
|
||||
return [
|
||||
'statusCode' => $statusCode,
|
||||
'response' => \is_string($output) ? $output : null,
|
||||
'error' => $error !== '' ? $error : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Messaging\Messages;
|
||||
|
||||
use Utopia\Messaging\Message;
|
||||
|
||||
class Console implements Message
|
||||
{
|
||||
/**
|
||||
* @param array<int, array{userId?: string, teamId?: string}> $recipients
|
||||
*/
|
||||
public function __construct(
|
||||
protected array $recipients,
|
||||
protected string $title,
|
||||
protected string $body,
|
||||
protected string $type = 'info',
|
||||
protected ?string $messageId = null,
|
||||
protected ?string $projectId = null,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{userId?: string, teamId?: string}>
|
||||
*/
|
||||
public function getRecipients(): array
|
||||
{
|
||||
return $this->recipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{userId?: string, teamId?: string}>
|
||||
*/
|
||||
public function getTo(): array
|
||||
{
|
||||
return $this->recipients;
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function getBody(): string
|
||||
{
|
||||
return $this->body;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getMessageId(): ?string
|
||||
{
|
||||
return $this->messageId;
|
||||
}
|
||||
|
||||
public function getProjectId(): ?string
|
||||
{
|
||||
return $this->projectId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Messaging\Messages;
|
||||
|
||||
use Utopia\Messaging\Message;
|
||||
|
||||
class Webhook implements Message
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $urls
|
||||
* @param array<string, mixed> $payload
|
||||
* @param array<string, string> $headers
|
||||
*/
|
||||
public function __construct(
|
||||
protected array $urls,
|
||||
protected array $payload,
|
||||
protected ?string $signingSecret = null,
|
||||
protected array $headers = [],
|
||||
protected int $timeout = 30,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getUrls(): array
|
||||
{
|
||||
return $this->urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias used by the base adapter to bound max messages per request.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getTo(): array
|
||||
{
|
||||
return $this->urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getPayload(): array
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
public function getSigningSecret(): ?string
|
||||
{
|
||||
return $this->signingSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
public function getTimeout(): int
|
||||
{
|
||||
return $this->timeout;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user