From 774379f107cfbcd578159398588e6936b1e6e1bd Mon Sep 17 00:00:00 2001 From: Hosteroid Date: Tue, 21 Oct 2025 14:33:22 +0300 Subject: [PATCH] Add Mattermost notification channel support Introduces Mattermost as a new notification channel, including backend service integration, controller validation, UI form fields, and updates to channel type enums in the database schema and migrations. This enables users to configure and send notifications via Mattermost webhooks. --- .../NotificationGroupController.php | 15 ++- app/Services/Channels/MattermostChannel.php | 107 ++++++++++++++++++ app/Services/NotificationService.php | 2 + app/Views/groups/edit.php | 58 +++++++++- .../migrations/000_initial_schema_v1.1.0.sql | 2 +- .../019_add_webhook_channel_type.sql | 6 +- 6 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 app/Services/Channels/MattermostChannel.php diff --git a/app/Controllers/NotificationGroupController.php b/app/Controllers/NotificationGroupController.php index 067979d..a171174 100644 --- a/app/Controllers/NotificationGroupController.php +++ b/app/Controllers/NotificationGroupController.php @@ -463,7 +463,9 @@ class NotificationGroupController extends Controller 'email' => 'Email', 'telegram' => 'Telegram', 'discord' => 'Discord', - 'slack' => 'Slack' + 'slack' => 'Slack', + 'mattermost' => 'Mattermost', + 'webhook' => 'Webhook' ]; $channelName = $channelNames[$channelType] ?? ucfirst($channelType); @@ -532,6 +534,17 @@ class NotificationGroupController extends Controller } return ['webhook_url' => $webhookUrl]; + case 'mattermost': + $webhookUrl = trim($data['mattermost_webhook_url'] ?? ''); + if (empty($webhookUrl) || !filter_var($webhookUrl, FILTER_VALIDATE_URL)) { + return null; + } + // Validate Mattermost webhook URL format + if (!str_contains($webhookUrl, '/hooks/')) { + return null; + } + return ['webhook_url' => $webhookUrl]; + case 'webhook': $webhookUrl = trim($data['webhook_url'] ?? ''); if (empty($webhookUrl) || !filter_var($webhookUrl, FILTER_VALIDATE_URL)) { diff --git a/app/Services/Channels/MattermostChannel.php b/app/Services/Channels/MattermostChannel.php new file mode 100644 index 0000000..ba66734 --- /dev/null +++ b/app/Services/Channels/MattermostChannel.php @@ -0,0 +1,107 @@ +client = new Client(['timeout' => 10]); + $this->logger = new Logger('mattermost_channel'); + } + + public function send(array $config, string $message, array $data = []): bool + { + if (!isset($config['webhook_url'])) { + return false; + } + + try { + // Mattermost expects a simple text payload or attachments + $payload = [ + 'text' => $message + ]; + + // Add attachments for richer formatting if domain data is available + if (isset($data['domain'])) { + $color = $this->getColorByDaysLeft($data['days_left'] ?? null); + + $payload['attachments'] = [ + [ + 'color' => $color, + 'title' => '🔔 Domain Expiration Alert', + 'text' => $message, + 'fields' => [ + [ + 'short' => true, + 'title' => 'Domain', + 'value' => $data['domain'] + ], + [ + 'short' => true, + 'title' => 'Days Left', + 'value' => $data['days_left'] ?? 'N/A' + ], + [ + 'short' => true, + 'title' => 'Expiration Date', + 'value' => $data['expiration_date'] ?? 'N/A' + ], + [ + 'short' => true, + 'title' => 'Registrar', + 'value' => $data['registrar'] ?? 'N/A' + ] + ], + 'footer' => 'Domain Monitor', + 'ts' => time() + ] + ]; + } + + $response = $this->client->post($config['webhook_url'], [ + 'json' => $payload + ]); + + $ok = $response->getStatusCode() === 200; + if ($ok) { + $this->logger->info('Mattermost message sent', [ + 'status' => $response->getStatusCode() + ]); + } else { + $this->logger->error('Mattermost non-200 status', [ + 'status' => $response->getStatusCode() + ]); + } + return $ok; + } catch (\Exception $e) { + $this->logger->error('Mattermost send failed', [ + 'exception' => $e->getMessage() + ]); + return false; + } + } + + private function getColorByDaysLeft(?int $daysLeft): string + { + if ($daysLeft === null) { + return '#36a64f'; // Green + } + + if ($daysLeft <= 0) { + return '#ff0000'; // Red - expired + } elseif ($daysLeft <= 1) { + return '#ff6600'; // Orange - critical + } elseif ($daysLeft <= 7) { + return '#ffaa00'; // Yellow - warning + } else { + return '#36a64f'; // Green - ok + } + } +} diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php index f518224..066e589 100644 --- a/app/Services/NotificationService.php +++ b/app/Services/NotificationService.php @@ -6,6 +6,7 @@ use App\Services\Channels\EmailChannel; use App\Services\Channels\TelegramChannel; use App\Services\Channels\DiscordChannel; use App\Services\Channels\SlackChannel; +use App\Services\Channels\MattermostChannel; use App\Services\Channels\WebhookChannel; class NotificationService @@ -19,6 +20,7 @@ class NotificationService 'telegram' => new TelegramChannel(), 'discord' => new DiscordChannel(), 'slack' => new SlackChannel(), + 'mattermost' => new MattermostChannel(), 'webhook' => new WebhookChannel(), ]; } diff --git a/app/Views/groups/edit.php b/app/Views/groups/edit.php index 3e89c20..f0c7497 100644 --- a/app/Views/groups/edit.php +++ b/app/Views/groups/edit.php @@ -77,9 +77,9 @@ ob_start();
'fa-envelope', 'telegram' => 'fa-telegram', 'discord' => 'fa-discord', 'slack' => 'fa-slack', 'webhook' => 'fa-link']; - $iconClasses = ['email' => 'fas', 'telegram' => 'fab', 'discord' => 'fab', 'slack' => 'fab', 'webhook' => 'fas']; - $colors = ['email' => 'blue', 'telegram' => 'blue', 'discord' => 'indigo', 'slack' => 'teal', 'webhook' => 'purple']; + $icons = ['email' => 'fa-envelope', 'telegram' => 'fa-telegram', 'discord' => 'fa-discord', 'slack' => 'fa-slack', 'mattermost' => 'fa-comments', 'webhook' => 'fa-link']; + $iconClasses = ['email' => 'fas', 'telegram' => 'fab', 'discord' => 'fab', 'slack' => 'fab', 'mattermost' => 'fab', 'webhook' => 'fas']; + $colors = ['email' => 'blue', 'telegram' => 'blue', 'discord' => 'indigo', 'slack' => 'teal', 'mattermost' => 'green', 'webhook' => 'purple']; $icon = $icons[$channel['channel_type']] ?? 'fa-bell'; $iconClass = $iconClasses[$channel['channel_type']] ?? 'fas'; $color = $colors[$channel['channel_type']] ?? 'gray'; @@ -157,6 +157,7 @@ ob_start(); +
@@ -242,6 +243,24 @@ ob_start(); + + +