278 lines
11 KiB
PHP
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);
|
||
|
|
}
|
||
|
|
}
|