178 lines
6.0 KiB
PHP
178 lines
6.0 KiB
PHP
<?php
|
|
|
|
namespace App\Core\Flows;
|
|
|
|
use App\Models\ConversationState;
|
|
use App\Models\MessageLog;
|
|
|
|
/**
|
|
* ConversationFlowEngine
|
|
* Orchestrates multi-stage interactive conversation flows.
|
|
*/
|
|
class ConversationFlowEngine
|
|
{
|
|
/**
|
|
* Map of registered flow names to their classes
|
|
*/
|
|
private static array $flows = [
|
|
'test_flow' => TestFlow::class,
|
|
];
|
|
|
|
/**
|
|
* Map of keyword triggers to start a specific flow
|
|
*/
|
|
private static array $startTriggers = [
|
|
'test' => 'test_flow',
|
|
'اختبار' => 'test_flow',
|
|
];
|
|
|
|
/**
|
|
* Process incoming message.
|
|
* Returns true if handled by the flow engine, false otherwise.
|
|
*/
|
|
public static function processMessage(array $session, array $msgData): bool
|
|
{
|
|
// 1. Housekeeping: remove expired sessions
|
|
ConversationState::cleanExpired();
|
|
|
|
$phone = $msgData['phone'];
|
|
$companyId = $session['company_id'];
|
|
$text = isset($msgData['body']) ? trim($msgData['body']) : '';
|
|
|
|
// 2. Lookup existing active flow
|
|
$state = ConversationState::findActive($companyId, $phone);
|
|
|
|
$flowName = '';
|
|
$currentStep = '';
|
|
$context = [];
|
|
|
|
if ($state) {
|
|
$flowName = $state['flow_name'];
|
|
$currentStep = $state['current_step'];
|
|
$context = json_decode($state['context_data'] ?: '{}', true) ?: [];
|
|
} else {
|
|
// Check if message is a starting trigger for a new flow
|
|
$normalizedText = strtolower(trim($text));
|
|
if (isset(self::$startTriggers[$normalizedText])) {
|
|
$flowName = self::$startTriggers[$normalizedText];
|
|
$currentStep = 'start';
|
|
$context = [];
|
|
}
|
|
}
|
|
|
|
// If no active flow and no trigger matches, pass control back to normal bot rules
|
|
if (empty($flowName) || !isset(self::$flows[$flowName])) {
|
|
return false;
|
|
}
|
|
|
|
// 3. User cancel flow option
|
|
$normalizedCancel = strtolower(trim($text));
|
|
if (in_array($normalizedCancel, ['إلغاء', 'خروج', 'cancel', 'exit'])) {
|
|
if ($state) {
|
|
ConversationState::deleteState($state['id']);
|
|
$cancelMsg = in_array($normalizedCancel, ['cancel', 'exit'])
|
|
? 'Interactive flow cancelled.'
|
|
: 'تم إلغاء المحادثة التفاعلية.';
|
|
self::sendReply($session, $phone, $cancelMsg);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// 4. Instantiate and execute the flow
|
|
$flowClass = self::$flows[$flowName];
|
|
/** @var BaseFlow $flowInstance */
|
|
$flowInstance = new $flowClass();
|
|
|
|
try {
|
|
$result = $flowInstance->handleStep($currentStep, $msgData, $context);
|
|
|
|
if ($result->isFinished()) {
|
|
// Flow has reached terminal state: clean up DB record
|
|
if ($state) {
|
|
ConversationState::deleteState($state['id']);
|
|
}
|
|
} else {
|
|
// Flow has next step: save or update state record (TTL: 1 hour)
|
|
ConversationState::saveState([
|
|
'company_id' => $companyId,
|
|
'contact_phone' => $phone,
|
|
'flow_name' => $flowName,
|
|
'current_step' => $result->getNextStep(),
|
|
'context_data' => json_encode($context, JSON_UNESCAPED_UNICODE),
|
|
'expires_at' => date('Y-m-d H:i:s', strtotime('+1 hour'))
|
|
]);
|
|
}
|
|
|
|
// 5. Send reply if one is provided
|
|
if ($result->getReplyText() !== '') {
|
|
self::sendReply($session, $phone, $result->getReplyText());
|
|
}
|
|
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
error_log("[ConversationFlowEngine Exception] Flow '{$flowName}' failed: " . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send outbound message reply via the Baileys Gateway
|
|
*/
|
|
private static function sendReply(array $session, string $phone, string $message): void
|
|
{
|
|
$gatewayUrl = rtrim(getenv('WHATSAPP_GATEWAY_URL') ?: 'http://localhost:3722', '/');
|
|
if (substr($gatewayUrl, -4) === '/api') {
|
|
$sendUrl = $gatewayUrl . '/messages/send';
|
|
} else {
|
|
$sendUrl = $gatewayUrl . '/api/messages/send';
|
|
}
|
|
|
|
$payload = json_encode([
|
|
'session_key' => $session['session_key'],
|
|
'phone' => $phone,
|
|
'message' => $message
|
|
]);
|
|
|
|
$ch = curl_init($sendUrl);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_POST, true);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
'Content-Type: application/json',
|
|
'X-Webhook-Secret: ' . getenv('WEBHOOK_SECRET')
|
|
]);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
|
|
|
$response = curl_exec($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
$status = 'failed';
|
|
$errorMsg = null;
|
|
$waMsgId = null;
|
|
|
|
if ($httpCode === 200) {
|
|
$status = 'sent';
|
|
$resData = json_decode($response, true);
|
|
$waMsgId = $resData['data']['key']['id'] ?? null;
|
|
} else {
|
|
$resData = json_decode($response, true);
|
|
$errorMsg = $resData['error'] ?? 'HTTP Code ' . $httpCode;
|
|
error_log("[Flow Engine Gateway Error] Failed to send: " . $errorMsg);
|
|
}
|
|
|
|
// Log the outbound auto-reply message
|
|
MessageLog::logMessage([
|
|
'company_id' => $session['company_id'],
|
|
'session_id' => $session['id'],
|
|
'contact_phone' => $phone,
|
|
'direction' => 'outbound',
|
|
'message_type' => 'text',
|
|
'message_body' => $message,
|
|
'whatsapp_message_id' => $waMsgId,
|
|
'status' => $status,
|
|
'error_message' => $errorMsg
|
|
]);
|
|
}
|
|
}
|