Replace Discord webhook notifications with Matrix (hookshot)
- Add helpers/NotificationHelper.php: shared Matrix webhook sender that reads MATRIX_WEBHOOK_URL and MATRIX_NOTIFY_USERS from config - Remove sendDiscordWebhook() from TicketController; call NotificationHelper::sendTicketNotification() instead - Replace 60-line Discord embed block in create_ticket_api.php with a single NotificationHelper call - config/config.php: DISCORD_WEBHOOK_URL → MATRIX_WEBHOOK_URL + new MATRIX_NOTIFY_USERS key (comma-separated Matrix user IDs) - .env.example: updated env var names and comments Payload sent to hookshot includes notify_users array so the JS transform can build proper @mention links for each user. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
12
.env.example
12
.env.example
@@ -7,10 +7,16 @@ DB_USER=tinkertickets
|
|||||||
DB_PASS=your_password_here
|
DB_PASS=your_password_here
|
||||||
DB_NAME=ticketing_system
|
DB_NAME=ticketing_system
|
||||||
|
|
||||||
# Discord Webhook (optional - for notifications)
|
# Matrix Webhook (optional - for notifications via matrix-hookshot)
|
||||||
DISCORD_WEBHOOK_URL=
|
# Set to your hookshot generic webhook URL, e.g.:
|
||||||
|
# https://matrix.lotusguild.org/webhook/<uuid>
|
||||||
|
MATRIX_WEBHOOK_URL=
|
||||||
|
|
||||||
# Application Domain (required for Discord webhook links)
|
# Matrix users to @mention on every new ticket (comma-separated Matrix user IDs)
|
||||||
|
# e.g. @jared:matrix.lotusguild.org,@alice:matrix.lotusguild.org
|
||||||
|
MATRIX_NOTIFY_USERS=
|
||||||
|
|
||||||
|
# Application Domain (required for Matrix webhook ticket links)
|
||||||
# Set this to your public domain (e.g., t.lotusguild.org)
|
# Set this to your public domain (e.g., t.lotusguild.org)
|
||||||
APP_DOMAIN=
|
APP_DOMAIN=
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ $GLOBALS['config'] = [
|
|||||||
'ASSETS_URL' => '/assets', // Assets URL
|
'ASSETS_URL' => '/assets', // Assets URL
|
||||||
'API_URL' => '/api', // API URL
|
'API_URL' => '/api', // API URL
|
||||||
|
|
||||||
// Discord webhook
|
// Matrix webhook (hookshot generic webhook URL)
|
||||||
'DISCORD_WEBHOOK_URL' => $envVars['DISCORD_WEBHOOK_URL'] ?? null,
|
'MATRIX_WEBHOOK_URL' => $envVars['MATRIX_WEBHOOK_URL'] ?? null,
|
||||||
|
// Comma-separated Matrix user IDs to @mention on new tickets (e.g. @jared:matrix.lotusguild.org)
|
||||||
|
'MATRIX_NOTIFY_USERS' => $envVars['MATRIX_NOTIFY_USERS'] ?? '',
|
||||||
|
|
||||||
// Domain settings for external integrations (webhooks, links, etc.)
|
// Domain settings for external integrations (webhooks, links, etc.)
|
||||||
// Set APP_DOMAIN in .env to override
|
// Set APP_DOMAIN in .env to override
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ require_once dirname(__DIR__) . '/models/UserModel.php';
|
|||||||
require_once dirname(__DIR__) . '/models/WorkflowModel.php';
|
require_once dirname(__DIR__) . '/models/WorkflowModel.php';
|
||||||
require_once dirname(__DIR__) . '/models/TemplateModel.php';
|
require_once dirname(__DIR__) . '/models/TemplateModel.php';
|
||||||
require_once dirname(__DIR__) . '/helpers/UrlHelper.php';
|
require_once dirname(__DIR__) . '/helpers/UrlHelper.php';
|
||||||
|
require_once dirname(__DIR__) . '/helpers/NotificationHelper.php';
|
||||||
|
|
||||||
class TicketController {
|
class TicketController {
|
||||||
private $ticketModel;
|
private $ticketModel;
|
||||||
@@ -110,8 +111,8 @@ class TicketController {
|
|||||||
$GLOBALS['auditLog']->logTicketCreate($userId, $result['ticket_id'], $ticketData);
|
$GLOBALS['auditLog']->logTicketCreate($userId, $result['ticket_id'], $ticketData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send Discord webhook notification for new ticket
|
// Send Matrix notification for new ticket
|
||||||
$this->sendDiscordWebhook($result['ticket_id'], $ticketData);
|
NotificationHelper::sendTicketNotification($result['ticket_id'], $ticketData, 'manual');
|
||||||
|
|
||||||
// Redirect to the new ticket
|
// Redirect to the new ticket
|
||||||
header("Location: " . $GLOBALS['config']['BASE_URL'] . "/ticket/" . $result['ticket_id']);
|
header("Location: " . $GLOBALS['config']['BASE_URL'] . "/ticket/" . $result['ticket_id']);
|
||||||
@@ -195,107 +196,5 @@ class TicketController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function sendDiscordWebhook($ticketId, $ticketData) {
|
|
||||||
$webhookUrl = $GLOBALS['config']['DISCORD_WEBHOOK_URL'] ?? null;
|
|
||||||
if (empty($webhookUrl)) {
|
|
||||||
error_log("Discord webhook URL not configured, skipping webhook for ticket creation");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create ticket URL using validated host
|
|
||||||
$ticketUrl = UrlHelper::ticketUrl($ticketId);
|
|
||||||
|
|
||||||
// Map priorities to Discord colors (matching API endpoint)
|
|
||||||
$priorityColors = [
|
|
||||||
1 => 0xDC3545, // P1 Critical - Red
|
|
||||||
2 => 0xFD7E14, // P2 High - Orange
|
|
||||||
3 => 0x0DCAF0, // P3 Medium - Cyan
|
|
||||||
4 => 0x198754, // P4 Low - Green
|
|
||||||
5 => 0x6C757D // P5 Info - Gray
|
|
||||||
];
|
|
||||||
|
|
||||||
// Priority labels for display
|
|
||||||
$priorityLabels = [
|
|
||||||
1 => "P1 - Critical",
|
|
||||||
2 => "P2 - High",
|
|
||||||
3 => "P3 - Medium",
|
|
||||||
4 => "P4 - Low",
|
|
||||||
5 => "P5 - Info"
|
|
||||||
];
|
|
||||||
|
|
||||||
$priority = (int)($ticketData['priority'] ?? 4);
|
|
||||||
$color = $priorityColors[$priority] ?? 0x6C757D;
|
|
||||||
$priorityLabel = $priorityLabels[$priority] ?? "P{$priority}";
|
|
||||||
|
|
||||||
$title = $ticketData['title'] ?? 'Untitled';
|
|
||||||
$category = $ticketData['category'] ?? 'General';
|
|
||||||
$type = $ticketData['type'] ?? 'Issue';
|
|
||||||
$status = $ticketData['status'] ?? 'Open';
|
|
||||||
|
|
||||||
// Extract hostname from title for cleaner display
|
|
||||||
preg_match('/^\[([^\]]+)\]/', $title, $hostnameMatch);
|
|
||||||
$sourceHost = $hostnameMatch[1] ?? 'Manual';
|
|
||||||
|
|
||||||
$embed = [
|
|
||||||
'title' => 'New Ticket Created',
|
|
||||||
'description' => "**#{$ticketId}** - {$title}",
|
|
||||||
'url' => $ticketUrl,
|
|
||||||
'color' => $color,
|
|
||||||
'fields' => [
|
|
||||||
[
|
|
||||||
'name' => 'Priority',
|
|
||||||
'value' => $priorityLabel,
|
|
||||||
'inline' => true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Category',
|
|
||||||
'value' => $category,
|
|
||||||
'inline' => true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Type',
|
|
||||||
'value' => $type,
|
|
||||||
'inline' => true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Status',
|
|
||||||
'value' => $status,
|
|
||||||
'inline' => true
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => 'Source',
|
|
||||||
'value' => $sourceHost,
|
|
||||||
'inline' => true
|
|
||||||
]
|
|
||||||
],
|
|
||||||
'footer' => [
|
|
||||||
'text' => 'Tinker Tickets | Manual Entry'
|
|
||||||
],
|
|
||||||
'timestamp' => date('c')
|
|
||||||
];
|
|
||||||
|
|
||||||
$payload = [
|
|
||||||
'embeds' => [$embed]
|
|
||||||
];
|
|
||||||
|
|
||||||
// Send webhook
|
|
||||||
$ch = curl_init($webhookUrl);
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
||||||
|
|
||||||
$webhookResult = curl_exec($ch);
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
$curlError = curl_error($ch);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($curlError) {
|
|
||||||
error_log("Discord webhook cURL error: {$curlError}");
|
|
||||||
} elseif ($httpCode !== 204 && $httpCode !== 200) {
|
|
||||||
error_log("Discord webhook failed for ticket #{$ticketId}. HTTP Code: {$httpCode}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -227,66 +227,12 @@ if ($stmt->execute()) {
|
|||||||
$stmt->close();
|
$stmt->close();
|
||||||
$conn->close();
|
$conn->close();
|
||||||
|
|
||||||
// Discord webhook notification
|
// Matrix webhook notification
|
||||||
if (isset($envVars['DISCORD_WEBHOOK_URL']) && !empty($envVars['DISCORD_WEBHOOK_URL'])) {
|
require_once __DIR__ . '/helpers/NotificationHelper.php';
|
||||||
$discord_webhook_url = $envVars['DISCORD_WEBHOOK_URL'];
|
NotificationHelper::sendTicketNotification($ticket_id, [
|
||||||
|
'title' => $title,
|
||||||
// Map priorities to Discord colors (decimal format)
|
'priority' => $priority,
|
||||||
$priorityColors = [
|
'category' => $category,
|
||||||
"1" => 0xDC3545, // P1 Critical - Red
|
'type' => $type,
|
||||||
"2" => 0xFD7E14, // P2 High - Orange
|
'status' => $status,
|
||||||
"3" => 0x0DCAF0, // P3 Medium - Cyan
|
], 'automated');
|
||||||
"4" => 0x198754, // P4 Low - Green
|
|
||||||
"5" => 0x6C757D // P5 Info - Gray
|
|
||||||
];
|
|
||||||
|
|
||||||
// Priority labels for display
|
|
||||||
$priorityLabels = [
|
|
||||||
"1" => "P1 - Critical",
|
|
||||||
"2" => "P2 - High",
|
|
||||||
"3" => "P3 - Medium",
|
|
||||||
"4" => "P4 - Low",
|
|
||||||
"5" => "P5 - Info"
|
|
||||||
];
|
|
||||||
|
|
||||||
// Create ticket URL using validated host
|
|
||||||
$ticketUrl = UrlHelper::ticketUrl($ticket_id);
|
|
||||||
|
|
||||||
// Extract hostname from title for cleaner display
|
|
||||||
preg_match('/^\[([^\]]+)\]/', $title, $hostnameMatch);
|
|
||||||
$sourceHost = $hostnameMatch[1] ?? 'Unknown';
|
|
||||||
|
|
||||||
$discord_data = [
|
|
||||||
"embeds" => [[
|
|
||||||
"title" => "New Ticket Created",
|
|
||||||
"description" => "**#{$ticket_id}** - {$title}",
|
|
||||||
"url" => $ticketUrl,
|
|
||||||
"color" => $priorityColors[$priority] ?? 0x6C757D,
|
|
||||||
"fields" => [
|
|
||||||
["name" => "Priority", "value" => $priorityLabels[$priority] ?? "P{$priority}", "inline" => true],
|
|
||||||
["name" => "Category", "value" => $category, "inline" => true],
|
|
||||||
["name" => "Type", "value" => $type, "inline" => true],
|
|
||||||
["name" => "Status", "value" => $status, "inline" => true],
|
|
||||||
["name" => "Source", "value" => $sourceHost, "inline" => true]
|
|
||||||
],
|
|
||||||
"footer" => [
|
|
||||||
"text" => "Tinker Tickets | Automated Alert"
|
|
||||||
],
|
|
||||||
"timestamp" => date('c')
|
|
||||||
]]
|
|
||||||
];
|
|
||||||
|
|
||||||
$ch = curl_init($discord_webhook_url);
|
|
||||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
|
||||||
curl_setopt($ch, CURLOPT_POST, 1);
|
|
||||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($discord_data));
|
|
||||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
||||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
||||||
$webhookResult = curl_exec($ch);
|
|
||||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
||||||
curl_close($ch);
|
|
||||||
|
|
||||||
if ($httpCode !== 204 && $httpCode !== 200) {
|
|
||||||
error_log("Discord webhook failed for ticket #{$ticket_id}. HTTP Code: {$httpCode}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
58
helpers/NotificationHelper.php
Normal file
58
helpers/NotificationHelper.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
require_once dirname(__DIR__) . '/helpers/UrlHelper.php';
|
||||||
|
|
||||||
|
class NotificationHelper {
|
||||||
|
/**
|
||||||
|
* Send a Matrix webhook notification for a new ticket.
|
||||||
|
*
|
||||||
|
* @param string $ticketId Ticket ID (9-digit string)
|
||||||
|
* @param array $ticketData Ticket fields (title, priority, category, type, status, ...)
|
||||||
|
* @param string $trigger 'manual' (web UI) or 'automated' (API)
|
||||||
|
*/
|
||||||
|
public static function sendTicketNotification($ticketId, $ticketData, $trigger = 'manual') {
|
||||||
|
$webhookUrl = $GLOBALS['config']['MATRIX_WEBHOOK_URL'] ?? null;
|
||||||
|
if (empty($webhookUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse notify users from config (comma-separated Matrix user IDs)
|
||||||
|
$notifyRaw = $GLOBALS['config']['MATRIX_NOTIFY_USERS'] ?? '';
|
||||||
|
$notifyUsers = array_values(array_filter(array_map('trim', explode(',', $notifyRaw))));
|
||||||
|
|
||||||
|
// Extract hostname from [hostname] prefix in title
|
||||||
|
preg_match('/^\[([^\]]+)\]/', $ticketData['title'] ?? '', $m);
|
||||||
|
$source = $m[1] ?? ($trigger === 'automated' ? 'Automated' : 'Manual');
|
||||||
|
|
||||||
|
$payload = [
|
||||||
|
'ticket_id' => $ticketId,
|
||||||
|
'title' => $ticketData['title'] ?? 'Untitled',
|
||||||
|
'priority' => (int)($ticketData['priority'] ?? 4),
|
||||||
|
'category' => $ticketData['category'] ?? 'General',
|
||||||
|
'type' => $ticketData['type'] ?? 'Issue',
|
||||||
|
'status' => $ticketData['status'] ?? 'Open',
|
||||||
|
'source' => $source,
|
||||||
|
'url' => UrlHelper::ticketUrl($ticketId),
|
||||||
|
'trigger' => $trigger,
|
||||||
|
'notify_users' => $notifyUsers,
|
||||||
|
];
|
||||||
|
|
||||||
|
$ch = curl_init($webhookUrl);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, 1);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||||
|
|
||||||
|
$response = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$curlError = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($curlError) {
|
||||||
|
error_log("Matrix webhook cURL error for ticket #{$ticketId}: {$curlError}");
|
||||||
|
} elseif ($httpCode < 200 || $httpCode >= 300) {
|
||||||
|
error_log("Matrix webhook failed for ticket #{$ticketId}. HTTP {$httpCode}: {$response}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
Reference in New Issue
Block a user