Files
crm.clientright.ru/modules/ModComments/actions/N8nSupportWebhook.php
Fedor 01c4fe80b5 chore: snapshot current working tree changes
Save all currently accumulated repository changes as a backup snapshot for Gitea so no local work is lost.
2026-03-26 14:19:01 +03:00

278 lines
11 KiB
PHP

<?php
class ModComments_N8nSupportWebhook {
const WEBHOOK_URL = 'https://n8n.clientright.ru/webhook/N8N_SUPPORT_WEBHOOK_REPLAY_CRM';
const LOG_FILE = 'logs/modcomments_n8n_webhook.log';
const CONNECT_TIMEOUT_SEC = 4;
const TIMEOUT_SEC = 12;
const MAX_BINARY_BYTES = 15728640; // 15MB
private static $queue = array();
private static $shutdownRegistered = false;
public static function queueFromComment(Vtiger_Request $request, $commentId, $authorUserId) {
global $adb;
$commentId = (int)$commentId;
$authorUserId = (int)$authorUserId;
if ($commentId <= 0) return;
$relatedTo = (int)$request->get('related_to');
$commentType = (string)$request->get('commenttype');
$isPrivate = (int)$request->get('is_private') ? 1 : 0;
$message = (string)$request->getRaw('commentcontent');
$createdTime = null;
$rct = $adb->pquery('SELECT createdtime FROM vtiger_crmentity WHERE crmid = ?', array($commentId));
if ($rct && $adb->num_rows($rct) > 0) {
$createdTime = (string)$adb->query_result($rct, 0, 'createdtime');
}
$parent = self::getParentInfo($relatedTo);
$contactId = isset($parent['contact_id']) ? (int)$parent['contact_id'] : 0;
$contact = $contactId > 0 ? self::getContactInfo($contactId) : null;
$attachments = self::getAttachmentsForComment($commentId);
$payloadBase = array(
'event' => 'crm_comment_created',
'ts' => date('c'),
'comment_id' => $commentId,
'comment_createdtime' => $createdTime,
'channel' => $commentType,
'message' => $message,
'is_private' => $isPrivate,
'author_user_id' => $authorUserId,
'parent' => $parent,
'contact_id' => $contactId,
'contact' => $contact,
'attachments' => $attachments,
);
if (empty($attachments)) {
self::enqueueJob($payloadBase);
} else {
foreach ($attachments as $attachment) {
$payload = $payloadBase;
$payload['attachment'] = $attachment;
$filePath = isset($attachment['local_path']) ? $attachment['local_path'] : null;
$fileName = (isset($attachment['storedname']) && $attachment['storedname'] !== '')
? $attachment['storedname']
: (isset($attachment['name']) ? $attachment['name'] : null);
$mimeType = isset($attachment['type']) ? $attachment['type'] : null;
if ($filePath && is_file($filePath) && is_readable($filePath)) {
$size = @filesize($filePath);
if ($size !== false && $size <= self::MAX_BINARY_BYTES) {
self::enqueueJob($payload, $filePath, $fileName, $mimeType);
continue;
}
}
self::enqueueJob($payload);
}
}
self::registerShutdown();
}
private static function enqueueJob($payload, $filePath = null, $fileName = null, $mimeType = null) {
self::$queue[] = array(
'url' => self::WEBHOOK_URL,
'payload' => $payload,
'file_path' => $filePath,
'file_name' => $fileName,
'mime_type' => $mimeType,
);
}
private static function registerShutdown() {
if (self::$shutdownRegistered) return;
self::$shutdownRegistered = true;
register_shutdown_function(array(__CLASS__, 'flushQueue'));
}
public static function flushQueue() {
if (empty(self::$queue)) return;
if (function_exists('fastcgi_finish_request')) {
@fastcgi_finish_request();
}
foreach (self::$queue as $job) {
$res = self::sendWebhook($job['url'], $job['payload'], $job['file_path'], $job['file_name'], $job['mime_type']);
self::log('Webhook result: ' . json_encode($res, JSON_UNESCAPED_UNICODE));
}
}
private static function sendWebhook($url, $payload, $filePath = null, $fileName = null, $mimeType = null) {
$payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE);
if ($payloadJson === false) {
return array('ok' => false, 'error' => 'json_encode_failed');
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::CONNECT_TIMEOUT_SEC);
curl_setopt($ch, CURLOPT_TIMEOUT, self::TIMEOUT_SEC);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
$isMultipart = $filePath && is_file($filePath) && is_readable($filePath);
if ($isMultipart) {
$postFields = array(
'payload' => $payloadJson,
'file' => new CURLFile($filePath, $mimeType ? $mimeType : 'application/octet-stream', $fileName ? $fileName : basename($filePath)),
);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
} else {
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json; charset=utf-8'));
curl_setopt($ch, CURLOPT_POSTFIELDS, $payloadJson);
}
$resp = curl_exec($ch);
$errno = curl_errno($ch);
$error = $errno ? curl_error($ch) : null;
$code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return array(
'ok' => ($errno === 0) && ($code >= 200 && $code < 300),
'http_code' => $code,
'curl_errno' => $errno,
'curl_error' => $error,
'multipart' => $isMultipart,
'resp_len' => is_string($resp) ? strlen($resp) : 0,
);
}
private static function getParentInfo($parentId) {
global $adb;
$parentId = (int)$parentId;
if ($parentId <= 0) return array('id' => 0, 'module' => null, 'contact_id' => 0);
$module = null;
$r = $adb->pquery('SELECT setype FROM vtiger_crmentity WHERE crmid = ? AND deleted = 0', array($parentId));
if ($r && $adb->num_rows($r) > 0) {
$module = (string)$adb->query_result($r, 0, 'setype');
}
$contactId = 0;
$ticketId = 0;
$projectId = 0;
if ($module === 'Contacts') {
$contactId = $parentId;
} elseif ($module === 'HelpDesk') {
$ticketId = $parentId;
$r2 = $adb->pquery('SELECT contact_id FROM vtiger_troubletickets WHERE ticketid = ?', array($parentId));
if ($r2 && $adb->num_rows($r2) > 0) {
$contactId = (int)$adb->query_result($r2, 0, 'contact_id');
}
} elseif ($module === 'Project') {
$projectId = $parentId;
$r2 = $adb->pquery('SELECT linktoaccountscontacts FROM vtiger_project WHERE projectid = ?', array($parentId));
if ($r2 && $adb->num_rows($r2) > 0) {
$contactId = (int)$adb->query_result($r2, 0, 'linktoaccountscontacts');
}
}
return array(
'id' => $parentId,
'module' => $module,
'contact_id' => (int)$contactId,
'ticket_id' => (int)$ticketId,
'project_id' => (int)$projectId,
);
}
private static function getContactInfo($contactId) {
global $adb;
$contactId = (int)$contactId;
if ($contactId <= 0) return null;
$r = $adb->pquery(
"SELECT c.mobile, cf.cf_2616\n" .
"FROM vtiger_contactdetails c\n" .
"INNER JOIN vtiger_crmentity e ON e.crmid = c.contactid AND e.deleted = 0\n" .
"LEFT JOIN vtiger_contactscf cf ON cf.contactid = c.contactid\n" .
"WHERE c.contactid = ?",
array($contactId)
);
if (!$r || $adb->num_rows($r) === 0) {
self::log('Contact info not found or query failed: contactid=' . $contactId);
return array('id' => $contactId, 'mobile' => null, 'cf_2616' => null);
}
return array(
'id' => $contactId,
'mobile' => (string)$adb->query_result($r, 0, 'mobile'),
'cf_2616' => $adb->query_result($r, 0, 'cf_2616'),
);
}
private static function getAttachmentsForComment($commentId) {
global $adb;
$commentId = (int)$commentId;
if ($commentId <= 0) return array();
$siteUrl = null;
if (isset($GLOBALS['site_URL']) && $GLOBALS['site_URL']) {
$siteUrl = rtrim((string)$GLOBALS['site_URL'], '/');
}
$out = array();
$r = $adb->pquery(
"SELECT a.attachmentsid, a.name, a.type, a.path, a.storedname\n" .
"FROM vtiger_seattachmentsrel r\n" .
"INNER JOIN vtiger_attachments a ON a.attachmentsid = r.attachmentsid\n" .
"WHERE r.crmid = ?",
array($commentId)
);
if (!$r) return array();
$rows = $adb->num_rows($r);
for ($i = 0; $i < $rows; $i++) {
$aid = (int)$adb->query_result($r, $i, 'attachmentsid');
$name = (string)$adb->query_result($r, $i, 'name');
$type = (string)$adb->query_result($r, $i, 'type');
$path = (string)$adb->query_result($r, $i, 'path');
$stored = (string)$adb->query_result($r, $i, 'storedname');
$download = 'index.php?module=ModComments&action=DownloadFile&record=' . $commentId . '&fileid=' . $aid;
$downloadAbs = $siteUrl ? ($siteUrl . '/' . $download) : null;
$localPath = null;
if ($path && preg_match('#^https?://#i', $path)) {
$localPath = null;
} else {
$candidate1 = $path . $aid . '_' . ($stored ? $stored : $name);
$candidate2 = $path . $aid . '_' . $name;
if (@is_file($candidate1)) {
$localPath = $candidate1;
} elseif (@is_file($candidate2)) {
$localPath = $candidate2;
}
}
$out[] = array(
'attachmentsid' => $aid,
'name' => $name,
'type' => $type,
'path' => $path,
'storedname' => $stored,
'download_url' => $download,
'download_url_abs' => $downloadAbs,
'local_path' => $localPath,
);
}
return $out;
}
private static function log($msg) {
$line = date('Y-m-d H:i:s') . ' ' . $msg . PHP_EOL;
@file_put_contents(self::LOG_FILE, $line, FILE_APPEND);
}
}