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.
This commit is contained in:
@@ -463,7 +463,9 @@ class NotificationGroupController extends Controller
|
|||||||
'email' => 'Email',
|
'email' => 'Email',
|
||||||
'telegram' => 'Telegram',
|
'telegram' => 'Telegram',
|
||||||
'discord' => 'Discord',
|
'discord' => 'Discord',
|
||||||
'slack' => 'Slack'
|
'slack' => 'Slack',
|
||||||
|
'mattermost' => 'Mattermost',
|
||||||
|
'webhook' => 'Webhook'
|
||||||
];
|
];
|
||||||
|
|
||||||
$channelName = $channelNames[$channelType] ?? ucfirst($channelType);
|
$channelName = $channelNames[$channelType] ?? ucfirst($channelType);
|
||||||
@@ -532,6 +534,17 @@ class NotificationGroupController extends Controller
|
|||||||
}
|
}
|
||||||
return ['webhook_url' => $webhookUrl];
|
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':
|
case 'webhook':
|
||||||
$webhookUrl = trim($data['webhook_url'] ?? '');
|
$webhookUrl = trim($data['webhook_url'] ?? '');
|
||||||
if (empty($webhookUrl) || !filter_var($webhookUrl, FILTER_VALIDATE_URL)) {
|
if (empty($webhookUrl) || !filter_var($webhookUrl, FILTER_VALIDATE_URL)) {
|
||||||
|
|||||||
107
app/Services/Channels/MattermostChannel.php
Normal file
107
app/Services/Channels/MattermostChannel.php
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Channels;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use App\Services\Logger;
|
||||||
|
|
||||||
|
class MattermostChannel implements NotificationChannelInterface
|
||||||
|
{
|
||||||
|
private Client $client;
|
||||||
|
private Logger $logger;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use App\Services\Channels\EmailChannel;
|
|||||||
use App\Services\Channels\TelegramChannel;
|
use App\Services\Channels\TelegramChannel;
|
||||||
use App\Services\Channels\DiscordChannel;
|
use App\Services\Channels\DiscordChannel;
|
||||||
use App\Services\Channels\SlackChannel;
|
use App\Services\Channels\SlackChannel;
|
||||||
|
use App\Services\Channels\MattermostChannel;
|
||||||
use App\Services\Channels\WebhookChannel;
|
use App\Services\Channels\WebhookChannel;
|
||||||
|
|
||||||
class NotificationService
|
class NotificationService
|
||||||
@@ -19,6 +20,7 @@ class NotificationService
|
|||||||
'telegram' => new TelegramChannel(),
|
'telegram' => new TelegramChannel(),
|
||||||
'discord' => new DiscordChannel(),
|
'discord' => new DiscordChannel(),
|
||||||
'slack' => new SlackChannel(),
|
'slack' => new SlackChannel(),
|
||||||
|
'mattermost' => new MattermostChannel(),
|
||||||
'webhook' => new WebhookChannel(),
|
'webhook' => new WebhookChannel(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,9 +77,9 @@ ob_start();
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
||||||
<?php foreach ($group['channels'] as $channel):
|
<?php foreach ($group['channels'] as $channel):
|
||||||
$config = json_decode($channel['channel_config'], true);
|
$config = json_decode($channel['channel_config'], true);
|
||||||
$icons = ['email' => 'fa-envelope', 'telegram' => 'fa-telegram', 'discord' => 'fa-discord', 'slack' => 'fa-slack', 'webhook' => 'fa-link'];
|
$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', 'webhook' => 'fas'];
|
$iconClasses = ['email' => 'fas', 'telegram' => 'fab', 'discord' => 'fab', 'slack' => 'fab', 'mattermost' => 'fab', 'webhook' => 'fas'];
|
||||||
$colors = ['email' => 'blue', 'telegram' => 'blue', 'discord' => 'indigo', 'slack' => 'teal', 'webhook' => 'purple'];
|
$colors = ['email' => 'blue', 'telegram' => 'blue', 'discord' => 'indigo', 'slack' => 'teal', 'mattermost' => 'green', 'webhook' => 'purple'];
|
||||||
$icon = $icons[$channel['channel_type']] ?? 'fa-bell';
|
$icon = $icons[$channel['channel_type']] ?? 'fa-bell';
|
||||||
$iconClass = $iconClasses[$channel['channel_type']] ?? 'fas';
|
$iconClass = $iconClasses[$channel['channel_type']] ?? 'fas';
|
||||||
$color = $colors[$channel['channel_type']] ?? 'gray';
|
$color = $colors[$channel['channel_type']] ?? 'gray';
|
||||||
@@ -157,6 +157,7 @@ ob_start();
|
|||||||
<option value="telegram">Telegram</option>
|
<option value="telegram">Telegram</option>
|
||||||
<option value="discord">Discord</option>
|
<option value="discord">Discord</option>
|
||||||
<option value="slack">Slack</option>
|
<option value="slack">Slack</option>
|
||||||
|
<option value="mattermost">Mattermost</option>
|
||||||
<option value="webhook">Webhook (Custom)</option>
|
<option value="webhook">Webhook (Custom)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,6 +243,24 @@ ob_start();
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mattermost Fields -->
|
||||||
|
<div id="mattermost_fields" class="hidden space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="mattermost_webhook" class="block text-sm font-medium text-gray-700 mb-1.5">
|
||||||
|
Webhook URL
|
||||||
|
</label>
|
||||||
|
<input type="text"
|
||||||
|
id="mattermost_webhook"
|
||||||
|
name="mattermost_webhook_url"
|
||||||
|
class="w-full px-3 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary transition-colors text-sm font-mono"
|
||||||
|
placeholder="https://your-mattermost.com/hooks/..."
|
||||||
|
autocomplete="off">
|
||||||
|
<p class="mt-1.5 text-xs text-gray-500">
|
||||||
|
Create in Mattermost → Integrations → Incoming Webhooks
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Generic Webhook Fields -->
|
<!-- Generic Webhook Fields -->
|
||||||
<div id="webhook_fields" class="hidden space-y-4">
|
<div id="webhook_fields" class="hidden space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -338,6 +357,7 @@ function toggleChannelFields() {
|
|||||||
const chatIdField = document.getElementById('chat_id');
|
const chatIdField = document.getElementById('chat_id');
|
||||||
const discordWebhook = document.getElementById('discord_webhook');
|
const discordWebhook = document.getElementById('discord_webhook');
|
||||||
const slackWebhook = document.getElementById('slack_webhook');
|
const slackWebhook = document.getElementById('slack_webhook');
|
||||||
|
const mattermostWebhook = document.getElementById('mattermost_webhook');
|
||||||
const genericWebhook = document.getElementById('generic_webhook_url');
|
const genericWebhook = document.getElementById('generic_webhook_url');
|
||||||
|
|
||||||
// Remove required from all
|
// Remove required from all
|
||||||
@@ -346,6 +366,7 @@ function toggleChannelFields() {
|
|||||||
chatIdField.removeAttribute('required');
|
chatIdField.removeAttribute('required');
|
||||||
discordWebhook.removeAttribute('required');
|
discordWebhook.removeAttribute('required');
|
||||||
slackWebhook.removeAttribute('required');
|
slackWebhook.removeAttribute('required');
|
||||||
|
if (mattermostWebhook) mattermostWebhook.removeAttribute('required');
|
||||||
if (genericWebhook) genericWebhook.removeAttribute('required');
|
if (genericWebhook) genericWebhook.removeAttribute('required');
|
||||||
|
|
||||||
// Hide all fields
|
// Hide all fields
|
||||||
@@ -353,6 +374,7 @@ function toggleChannelFields() {
|
|||||||
document.getElementById('telegram_fields').classList.add('hidden');
|
document.getElementById('telegram_fields').classList.add('hidden');
|
||||||
document.getElementById('discord_fields').classList.add('hidden');
|
document.getElementById('discord_fields').classList.add('hidden');
|
||||||
document.getElementById('slack_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');
|
document.getElementById('webhook_fields').classList.add('hidden');
|
||||||
|
|
||||||
// Hide test button by default
|
// Hide test button by default
|
||||||
@@ -379,6 +401,12 @@ function toggleChannelFields() {
|
|||||||
slackWebhook.setAttribute('required', 'required');
|
slackWebhook.setAttribute('required', 'required');
|
||||||
slackWebhook.focus();
|
slackWebhook.focus();
|
||||||
break;
|
break;
|
||||||
|
case 'mattermost':
|
||||||
|
if (mattermostWebhook) {
|
||||||
|
mattermostWebhook.setAttribute('required', 'required');
|
||||||
|
mattermostWebhook.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'webhook':
|
case 'webhook':
|
||||||
if (genericWebhook) {
|
if (genericWebhook) {
|
||||||
genericWebhook.setAttribute('required', 'required');
|
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
|
// Validate Generic webhook
|
||||||
if (channelType === 'webhook') {
|
if (channelType === 'webhook') {
|
||||||
const webhookUrl = document.getElementById('generic_webhook_url').value.trim();
|
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';
|
errorMessage = 'Please enter a Slack webhook URL';
|
||||||
}
|
}
|
||||||
break;
|
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':
|
case 'webhook':
|
||||||
const genericWebhook = document.getElementById('generic_webhook_url').value.trim();
|
const genericWebhook = document.getElementById('generic_webhook_url').value.trim();
|
||||||
if (!genericWebhook) {
|
if (!genericWebhook) {
|
||||||
@@ -570,6 +616,9 @@ function testChannel(channelType, existingConfig = null) {
|
|||||||
case 'slack':
|
case 'slack':
|
||||||
formData.append('slack_webhook_url', existingConfig.webhook_url);
|
formData.append('slack_webhook_url', existingConfig.webhook_url);
|
||||||
break;
|
break;
|
||||||
|
case 'mattermost':
|
||||||
|
formData.append('mattermost_webhook_url', existingConfig.webhook_url);
|
||||||
|
break;
|
||||||
case 'webhook':
|
case 'webhook':
|
||||||
formData.append('webhook_url', existingConfig.webhook_url);
|
formData.append('webhook_url', existingConfig.webhook_url);
|
||||||
break;
|
break;
|
||||||
@@ -590,6 +639,9 @@ function testChannel(channelType, existingConfig = null) {
|
|||||||
case 'slack':
|
case 'slack':
|
||||||
formData.append('slack_webhook_url', document.getElementById('slack_webhook').value);
|
formData.append('slack_webhook_url', document.getElementById('slack_webhook').value);
|
||||||
break;
|
break;
|
||||||
|
case 'mattermost':
|
||||||
|
formData.append('mattermost_webhook_url', document.getElementById('mattermost_webhook').value);
|
||||||
|
break;
|
||||||
case 'webhook':
|
case 'webhook':
|
||||||
formData.append('webhook_url', document.getElementById('generic_webhook_url').value);
|
formData.append('webhook_url', document.getElementById('generic_webhook_url').value);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ CREATE TABLE IF NOT EXISTS user_notifications (
|
|||||||
CREATE TABLE IF NOT EXISTS notification_channels (
|
CREATE TABLE IF NOT EXISTS notification_channels (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
notification_group_id INT NOT NULL,
|
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,
|
channel_config JSON NOT NULL,
|
||||||
is_active BOOLEAN DEFAULT TRUE,
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-- Add 'webhook' to the channel_type ENUM in notification_channels table
|
-- Add 'webhook' and 'mattermost' to the channel_type ENUM in notification_channels table
|
||||||
-- This allows custom webhook integrations like Mattermost
|
-- This allows custom webhook integrations and Mattermost support
|
||||||
|
|
||||||
ALTER TABLE notification_channels
|
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user