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();
+
+
+
+
+
+
+ Create in Mattermost → Integrations → Incoming Webhooks
+
+
+
+
@@ -338,6 +357,7 @@ function toggleChannelFields() {
const chatIdField = document.getElementById('chat_id');
const discordWebhook = document.getElementById('discord_webhook');
const slackWebhook = document.getElementById('slack_webhook');
+ const mattermostWebhook = document.getElementById('mattermost_webhook');
const genericWebhook = document.getElementById('generic_webhook_url');
// Remove required from all
@@ -346,6 +366,7 @@ function toggleChannelFields() {
chatIdField.removeAttribute('required');
discordWebhook.removeAttribute('required');
slackWebhook.removeAttribute('required');
+ if (mattermostWebhook) mattermostWebhook.removeAttribute('required');
if (genericWebhook) genericWebhook.removeAttribute('required');
// Hide all fields
@@ -353,6 +374,7 @@ function toggleChannelFields() {
document.getElementById('telegram_fields').classList.add('hidden');
document.getElementById('discord_fields').classList.add('hidden');
document.getElementById('slack_fields').classList.add('hidden');
+ document.getElementById('mattermost_fields').classList.add('hidden');
document.getElementById('webhook_fields').classList.add('hidden');
// Hide test button by default
@@ -379,6 +401,12 @@ function toggleChannelFields() {
slackWebhook.setAttribute('required', 'required');
slackWebhook.focus();
break;
+ case 'mattermost':
+ if (mattermostWebhook) {
+ mattermostWebhook.setAttribute('required', 'required');
+ mattermostWebhook.focus();
+ }
+ break;
case 'webhook':
if (genericWebhook) {
genericWebhook.setAttribute('required', 'required');
@@ -434,6 +462,17 @@ if (addChannelForm) {
}
}
+ // Validate Mattermost webhook
+ if (channelType === 'mattermost') {
+ const webhookUrl = document.getElementById('mattermost_webhook').value.trim();
+ if (!webhookUrl) {
+ e.preventDefault();
+ alert('Please enter the Mattermost webhook URL');
+ document.getElementById('mattermost_webhook').focus();
+ return false;
+ }
+ }
+
// Validate Generic webhook
if (channelType === 'webhook') {
const webhookUrl = document.getElementById('generic_webhook_url').value.trim();
@@ -517,6 +556,13 @@ function testChannel(channelType, existingConfig = null) {
errorMessage = 'Please enter a Slack webhook URL';
}
break;
+ case 'mattermost':
+ const mattermostWebhook = document.getElementById('mattermost_webhook').value.trim();
+ if (!mattermostWebhook) {
+ isValid = false;
+ errorMessage = 'Please enter a Mattermost webhook URL';
+ }
+ break;
case 'webhook':
const genericWebhook = document.getElementById('generic_webhook_url').value.trim();
if (!genericWebhook) {
@@ -570,6 +616,9 @@ function testChannel(channelType, existingConfig = null) {
case 'slack':
formData.append('slack_webhook_url', existingConfig.webhook_url);
break;
+ case 'mattermost':
+ formData.append('mattermost_webhook_url', existingConfig.webhook_url);
+ break;
case 'webhook':
formData.append('webhook_url', existingConfig.webhook_url);
break;
@@ -590,6 +639,9 @@ function testChannel(channelType, existingConfig = null) {
case 'slack':
formData.append('slack_webhook_url', document.getElementById('slack_webhook').value);
break;
+ case 'mattermost':
+ formData.append('mattermost_webhook_url', document.getElementById('mattermost_webhook').value);
+ break;
case 'webhook':
formData.append('webhook_url', document.getElementById('generic_webhook_url').value);
break;
diff --git a/database/migrations/000_initial_schema_v1.1.0.sql b/database/migrations/000_initial_schema_v1.1.0.sql
index 1fa5573..418ad92 100644
--- a/database/migrations/000_initial_schema_v1.1.0.sql
+++ b/database/migrations/000_initial_schema_v1.1.0.sql
@@ -182,7 +182,7 @@ CREATE TABLE IF NOT EXISTS user_notifications (
CREATE TABLE IF NOT EXISTS notification_channels (
id INT AUTO_INCREMENT PRIMARY KEY,
notification_group_id INT NOT NULL,
- channel_type ENUM('email', 'telegram', 'discord', 'slack', 'webhook') NOT NULL,
+ channel_type ENUM('email', 'telegram', 'discord', 'slack', 'mattermost', 'webhook') NOT NULL,
channel_config JSON NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
diff --git a/database/migrations/019_add_webhook_channel_type.sql b/database/migrations/019_add_webhook_channel_type.sql
index 9482969..10e3cef 100644
--- a/database/migrations/019_add_webhook_channel_type.sql
+++ b/database/migrations/019_add_webhook_channel_type.sql
@@ -1,5 +1,5 @@
--- Add 'webhook' to the channel_type ENUM in notification_channels table
--- This allows custom webhook integrations like Mattermost
+-- Add 'webhook' and 'mattermost' to the channel_type ENUM in notification_channels table
+-- This allows custom webhook integrations and Mattermost support
ALTER TABLE notification_channels
-MODIFY COLUMN channel_type ENUM('email', 'telegram', 'discord', 'slack', 'webhook') NOT NULL;
+MODIFY COLUMN channel_type ENUM('email', 'telegram', 'discord', 'slack', 'mattermost', 'webhook') NOT NULL;