= 300) { error_log("Matrix webhook failed for ticket #{$id}. HTTP {$httpCode}: {$response}"); } } private static function notifyUsers(): array { $raw = $GLOBALS['config']['MATRIX_NOTIFY_USERS'] ?? ''; return array_values(array_filter(array_map('trim', explode(',', $raw)))); } // ─── Public event methods ───────────────────────────────────────────────── /** * New ticket created (manual or automated/API). */ public static function sendTicketNotification($ticketId, array $ticketData, string $trigger = 'manual'): void { preg_match('/^\[([^\]]+)\]/', $ticketData['title'] ?? '', $m); $source = $m[1] ?? ($trigger === 'automated' ? 'Automated' : 'Manual'); self::fire([ 'event' => 'ticket_created', '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' => self::notifyUsers(), ]); } /** * Ticket status changed. * * @param string|int $ticketId * @param string $oldStatus * @param string $newStatus * @param string $ticketTitle * @param string|null $changedByDisplay Display name of the user who changed status */ public static function sendStatusChangeNotification($ticketId, string $oldStatus, string $newStatus, string $ticketTitle, ?string $changedByDisplay = null): void { self::fire([ 'event' => 'status_changed', 'ticket_id' => $ticketId, 'title' => $ticketTitle, 'old_status' => $oldStatus, 'new_status' => $newStatus, 'changed_by' => $changedByDisplay, 'url' => UrlHelper::ticketUrl($ticketId), 'notify_users' => self::notifyUsers(), ]); } /** * New comment posted (non-mention; use sendMentionNotification for @mentions). * * @param string|int $ticketId * @param string $ticketTitle * @param string $commentText Plain text (first 200 chars will be sent) * @param string|null $authorDisplay Display name of commenter * @param bool $isInternal True if the comment is internal-only */ public static function sendCommentNotification($ticketId, string $ticketTitle, string $commentText, ?string $authorDisplay = null, bool $isInternal = false): void { // Skip if this is an internal-only comment — only the assignee/admin need to know $notifyUsers = self::notifyUsers(); if (empty($notifyUsers)) { return; } self::fire([ 'event' => 'comment_added', 'ticket_id' => $ticketId, 'title' => $ticketTitle, 'author' => $authorDisplay, 'preview' => mb_strimwidth($commentText, 0, 200, '…'), 'is_internal' => $isInternal, 'url' => UrlHelper::ticketUrl($ticketId), 'notify_users' => $notifyUsers, ]); } /** * @mention detected in a comment. * * @param string|int $ticketId * @param string $ticketTitle * @param string $commentText * @param string|null $authorDisplay * @param array $mentionedMatrixIds Matrix user IDs derived from @usernames */ public static function sendMentionNotification($ticketId, string $ticketTitle, string $commentText, ?string $authorDisplay, array $mentionedMatrixIds): void { if (empty($mentionedMatrixIds)) { return; } self::fire([ 'event' => 'mention', 'ticket_id' => $ticketId, 'title' => $ticketTitle, 'author' => $authorDisplay, 'preview' => mb_strimwidth($commentText, 0, 200, '…'), 'url' => UrlHelper::ticketUrl($ticketId), 'notify_users' => $mentionedMatrixIds, ]); } /** * Notify all watchers of a ticket about an update event. * * Fetches watchers from the DB, resolves their Matrix IDs via Synapse, * and fires the appropriate event notification with them in notify_users. * * @param \mysqli $conn * @param string|int $ticketId * @param string $ticketTitle * @param string $event One of: status_changed, comment_added, assigned * @param array $extraData Merged into the payload (old_status/new_status, author, etc.) * @param int|null $excludeUserId Don't notify the actor themselves */ public static function notifyWatchers(\mysqli $conn, $ticketId, string $ticketTitle, string $event, array $extraData = [], ?int $excludeUserId = null): void { $webhookUrl = $GLOBALS['config']['MATRIX_WEBHOOK_URL'] ?? null; $domain = $GLOBALS['config']['MATRIX_DOMAIN'] ?? null; if (!$webhookUrl || !$domain) { return; } // Fetch watcher usernames, excluding the actor so they don't notify themselves if ($excludeUserId !== null) { $sql = "SELECT u.username FROM ticket_watchers tw JOIN users u ON tw.user_id = u.user_id WHERE tw.ticket_id = ? AND tw.user_id != ?"; $stmt = $conn->prepare($sql); $stmt->bind_param("ii", $ticketId, $excludeUserId); } else { $sql = "SELECT u.username FROM ticket_watchers tw JOIN users u ON tw.user_id = u.user_id WHERE tw.ticket_id = ?"; $stmt = $conn->prepare($sql); $stmt->bind_param("i", $ticketId); } $stmt->execute(); $result = $stmt->get_result(); $stmt->close(); $usernames = []; while ($row = $result->fetch_assoc()) { $usernames[] = $row['username']; } if (empty($usernames)) { return; } // Resolve to Matrix IDs — skip users without Synapse accounts $matrixIds = SynapseHelper::resolveUsernames($usernames); if (empty($matrixIds)) { return; } // Remove the global notify list duplicates and build payload $allNotify = array_unique(array_merge($matrixIds, self::notifyUsers())); $payload = array_merge($extraData, [ 'event' => $event, 'ticket_id' => $ticketId, 'title' => $ticketTitle, 'url' => UrlHelper::ticketUrl($ticketId), 'notify_users' => array_values($allNotify), ]); self::fire($payload); } /** * Ticket assigned (or reassigned) to a user. * * @param string|int $ticketId * @param string $ticketTitle * @param string|null $assigneeName Display name of new assignee * @param string|null $assigneeMatrix Matrix user ID of new assignee (to DM) * @param string|null $changedByDisplay */ public static function sendAssignmentNotification($ticketId, string $ticketTitle, ?string $assigneeName, ?string $assigneeMatrix, ?string $changedByDisplay = null): void { $notifyUsers = self::notifyUsers(); // Also notify the assignee directly if we know their Matrix ID if ($assigneeMatrix && !in_array($assigneeMatrix, $notifyUsers, true)) { $notifyUsers[] = $assigneeMatrix; } if (empty($notifyUsers)) { return; } self::fire([ 'event' => 'assigned', 'ticket_id' => $ticketId, 'title' => $ticketTitle, 'assignee' => $assigneeName, 'changed_by' => $changedByDisplay, 'url' => UrlHelper::ticketUrl($ticketId), 'notify_users' => $notifyUsers, ]); } } ?>