status(200)->setHeader('Content-Type', 'text/plain'); echo $challenge; exit; } else { $response->status(403)->json(['error' => 'Verification failed']); } } /** * Handle incoming Meta Webhook events (POST /api/webhooks/meta) */ public function webhook(Request $request, Response $response) { $rawPayload = file_get_contents('php://input'); $payload = json_decode($rawPayload, true); if (!$payload) { $response->status(400)->json(['error' => 'Invalid JSON payload']); return; } $object = $payload['object'] ?? ''; if ($object !== 'page' && $object !== 'instagram') { $response->status(200)->json(['status' => 'ignored', 'reason' => 'unsupported object type']); return; } $channelType = ($object === 'page') ? 'messenger' : 'instagram'; $entries = $payload['entry'] ?? []; foreach ($entries as $entry) { $pageId = $entry['id'] ?? ''; $messaging = $entry['messaging'] ?? []; $session = MetaSession::findByPageId($pageId, $channelType); if (!$session || $session['status'] !== 'connected') { continue; } foreach ($messaging as $messageEvent) { $senderId = $messageEvent['sender']['id'] ?? ''; $message = $messageEvent['message'] ?? []; if (empty($senderId) || empty($message)) { continue; } // Deduplicate incoming messages $mid = $message['mid'] ?? ''; if (!empty($mid)) { $alreadyLogged = \App\Core\Database::selectOne( "SELECT id FROM messages_log WHERE whatsapp_message_id = ? LIMIT 1", [$mid] ); if ($alreadyLogged) { continue; } } $text = $message['text'] ?? ''; if (empty($text)) { continue; } // 1. Find or create CRM Contact $contact = Contact::findByPhone($session['company_id'], $senderId); if (!$contact) { $prefix = ($channelType === 'messenger') ? 'FB-' : 'IG-'; $contactName = $prefix . substr($senderId, -6); try { Contact::createSecure([ 'company_id' => $session['company_id'], 'name' => $contactName, 'phone' => $senderId, 'notes' => 'Auto-created via Meta webhook' ]); } catch (\PDOException $e) { // Ignore duplicate contact error if created concurrently } } // 2. Log inbound message in history log MessageLog::logMessage([ 'company_id' => $session['company_id'], 'session_id' => null, 'meta_session_id' => $session['id'], 'contact_phone' => $senderId, 'direction' => 'inbound', 'message_type' => 'text', 'message_body' => $text, 'whatsapp_message_id' => $mid, 'status' => 'read' ]); // 3. Trigger chatbot rules auto-reply in background $this->triggerMetaAutoReply($session, $senderId, $text); } } $response->json(['status' => 'success']); } /** * Trigger auto-reply based on keyword rules or Gemini AI */ private function triggerMetaAutoReply(array $session, string $senderId, string $incomingText) { $companyId = (int)$session['company_id']; $rule = ChatbotRule::findActiveForRule($companyId); if (!$rule || !$rule['is_active']) { return; } $replyText = ''; if ($rule['trigger_type'] === 'keyword') { $keywords = array_map('trim', explode(',', strtolower($rule['keyword']))); $incomingLower = strtolower(trim($incomingText)); $matched = false; foreach ($keywords as $kw) { if ($kw !== '' && strpos($incomingLower, $kw) !== false) { $matched = true; break; } } if ($matched) { $replyText = $rule['ai_prompt']; } } elseif ($rule['trigger_type'] === 'gemini_ai') { $configuredGeminiKey = !empty($rule['gemini_api_key']) ? $rule['gemini_api_key'] : null; $apiKey = GeminiService::getGeminiApiKey($configuredGeminiKey); if (empty($apiKey)) { error_log("[Meta Chatbot Warning] Gemini API Key is not set."); return; } $systemPrompt = $rule['ai_prompt'] ?: 'You are a helpful customer support assistant.'; // Enforce language matching rule $systemPrompt .= "\n\nIMPORTANT LANGUAGE RULE: Detect the language of the incoming message. If the incoming message is in English, you MUST reply in English. If the incoming message is in Arabic, you MUST reply in Arabic. Override any default language instruction to match the user's language."; $replyText = GeminiService::generateResponse($apiKey, $systemPrompt, $incomingText); } if (!empty($replyText)) { $success = MetaService::sendMessage($session['page_access_token'], $senderId, $replyText); if ($success) { MessageLog::logMessage([ 'company_id' => $companyId, 'session_id' => null, 'meta_session_id' => $session['id'], 'contact_phone' => $senderId, 'direction' => 'outbound', 'message_type' => 'text', 'message_body' => $replyText, 'status' => 'sent' ]); } } } /** * Get Meta connection status for current company (GET /api/meta/sessions) */ public function listSessions(Request $request, Response $response) { $sessions = MetaSession::findAllByCompany($request->company_id); $response->json([ 'status' => 'success', 'data' => $sessions ]); } /** * Connect Facebook page or Instagram profile (POST /api/meta/sessions/connect) */ public function connectSession(Request $request, Response $response) { $body = $request->getBody(); $channelType = $body['channel_type'] ?? ''; $pageId = $body['page_id'] ?? ''; $pageName = $body['page_name'] ?? ''; $pageAccessToken = $body['page_access_token'] ?? ''; if (empty($channelType) || empty($pageId) || empty($pageName) || empty($pageAccessToken)) { $response->status(400)->json([ 'status' => 'error', 'message' => 'Missing channel_type, page_id, page_name, or page_access_token' ]); return; } if ($channelType !== 'messenger' && $channelType !== 'instagram') { $response->status(400)->json([ 'status' => 'error', 'message' => 'Invalid channel_type' ]); return; } try { $id = MetaSession::connectSession( $request->company_id, $channelType, $pageId, $pageName, $pageAccessToken ); $response->json([ 'status' => 'success', 'message' => 'Meta channel connected successfully', 'session_id' => $id ]); } catch (\Exception $e) { $response->status(500)->json([ 'status' => 'error', 'message' => 'Failed to connect: ' . $e->getMessage() ]); } } /** * Disconnect Meta session (DELETE /api/meta/sessions) */ public function deleteSession(Request $request, Response $response) { $body = $request->getBody(); $sessionId = $body['session_id'] ?? null; if (!$sessionId) { $response->status(400)->json([ 'status' => 'error', 'message' => 'Missing session_id' ]); return; } $session = MetaSession::find($sessionId); if (!$session || (int)$session['company_id'] !== (int)$request->company_id) { $response->status(404)->json([ 'status' => 'error', 'message' => 'Session not found' ]); return; } MetaSession::delete($sessionId); $response->json([ 'status' => 'success', 'message' => 'Meta channel disconnected successfully' ]); } }