Deploy: 2026-07-15 05:08:07

This commit is contained in:
Hamza-Ayed
2026-07-15 05:08:07 +03:00
parent 3de7764cdd
commit 66b2c0eaa9
5 changed files with 834 additions and 0 deletions
@@ -0,0 +1,235 @@
<?php
namespace App\Controllers;
use App\Core\Request;
use App\Core\Response;
use App\Models\Company;
/**
* Handles WhatsApp Cloud API (Meta Official) routes
*/
class CloudApiController extends BaseController
{
/**
* Get the Cloud API connection status for the company
*/
public function status(Request $request, Response $response)
{
$companyId = $request->company_id;
$session = \App\Core\Database::selectOne(
"SELECT * FROM meta_cloud_sessions WHERE company_id = ? LIMIT 1",
[$companyId]
);
if (!$session) {
$response->json([
'status' => 'success',
'data' => null
]);
return;
}
// Hide token from frontend
unset($session['access_token']);
unset($session['verify_token']);
$response->json([
'status' => 'success',
'data' => $session
]);
}
/**
* Connect or update a Cloud API session
*/
public function connect(Request $request, Response $response)
{
$companyId = $request->company_id;
$body = $request->getBody();
if (empty($body['phone_number_id']) || empty($body['waba_id']) || empty($body['access_token'])) {
$response->status(400)->json(['error' => 'Missing required fields']);
return;
}
$session = \App\Core\Database::selectOne(
"SELECT id FROM meta_cloud_sessions WHERE company_id = ? LIMIT 1",
[$companyId]
);
$verifyToken = 'nabeh_cloud_' . bin2hex(random_bytes(8));
if ($session) {
\App\Core\Database::execute(
"UPDATE meta_cloud_sessions SET phone_number_id = ?, waba_id = ?, access_token = ?, status = 'active' WHERE id = ?",
[$body['phone_number_id'], $body['waba_id'], $body['access_token'], $session['id']]
);
} else {
\App\Core\Database::execute(
"INSERT INTO meta_cloud_sessions (company_id, phone_number_id, waba_id, access_token, verify_token, status) VALUES (?, ?, ?, ?, ?, 'active')",
[$companyId, $body['phone_number_id'], $body['waba_id'], $body['access_token'], $verifyToken]
);
}
$updated = \App\Core\Database::selectOne(
"SELECT * FROM meta_cloud_sessions WHERE company_id = ? LIMIT 1",
[$companyId]
);
unset($updated['access_token']);
$response->json([
'status' => 'success',
'message' => 'WhatsApp Cloud API connected successfully',
'data' => $updated
]);
}
/**
* Disconnect Cloud API
*/
public function disconnect(Request $request, Response $response)
{
$companyId = $request->company_id;
\App\Core\Database::execute(
"DELETE FROM meta_cloud_sessions WHERE company_id = ?",
[$companyId]
);
$response->json([
'status' => 'success',
'message' => 'Disconnected successfully'
]);
}
/**
* Fetch templates from Meta and sync them locally
*/
public function syncTemplates(Request $request, Response $response)
{
$companyId = $request->company_id;
$session = \App\Core\Database::selectOne(
"SELECT * FROM meta_cloud_sessions WHERE company_id = ? LIMIT 1",
[$companyId]
);
if (!$session || $session['status'] !== 'active') {
$response->status(400)->json(['error' => 'Cloud API not connected']);
return;
}
try {
// Call the Node.js Cloud API Service via Gateway
$gatewayUrl = rtrim(getenv('WHATSAPP_GATEWAY_URL') ?: 'http://localhost:3722', '/');
$syncUrl = $gatewayUrl . '/api/cloud/templates/sync';
$payload = json_encode([
'waba_id' => $session['waba_id'],
'access_token' => $session['access_token']
]);
$ch = curl_init($syncUrl);
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, 15);
$res = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
$resData = json_decode($res, true);
$templates = $resData['data'] ?? [];
// Clear old templates and insert new ones
\App\Core\Database::execute("DELETE FROM message_templates WHERE company_id = ?", [$companyId]);
foreach ($templates as $tpl) {
\App\Core\Database::execute(
"INSERT INTO message_templates (company_id, meta_template_id, name, language, category, status, body_text) VALUES (?, ?, ?, ?, ?, ?, ?)",
[$companyId, $tpl['id'], $tpl['name'], $tpl['language'], strtolower($tpl['category']), strtolower($tpl['status']), 'Synced from Meta']
);
}
$response->json([
'status' => 'success',
'message' => 'Templates synced successfully',
'count' => count($templates)
]);
} else {
$response->status($httpCode)->json(['error' => 'Failed to sync templates from Meta']);
}
} catch (\Exception $e) {
$response->status(500)->json(['error' => $e->getMessage()]);
}
}
/**
* Webhook to receive incoming messages from Meta
*/
public function webhook(Request $request, Response $response)
{
// Meta GET verification request
if ($request->getMethod() === 'GET') {
$mode = $request->get('hub_mode');
$token = $request->get('hub_verify_token');
$challenge = $request->get('hub_challenge');
if ($mode === 'subscribe' && $token) {
$session = \App\Core\Database::selectOne(
"SELECT * FROM meta_cloud_sessions WHERE verify_token = ? LIMIT 1",
[$token]
);
if ($session) {
echo $challenge;
exit;
}
}
$response->status(403)->send('Forbidden');
return;
}
// Handle POST incoming messages
$body = $request->getBody();
if (isset($body['entry'][0]['changes'][0]['value']['messages'])) {
$metaMessages = $body['entry'][0]['changes'][0]['value']['messages'];
$metadata = $body['entry'][0]['changes'][0]['value']['metadata'];
$phoneNumberId = $metadata['phone_number_id'];
$session = \App\Core\Database::selectOne(
"SELECT * FROM meta_cloud_sessions WHERE phone_number_id = ? AND status = 'active' LIMIT 1",
[$phoneNumberId]
);
if ($session) {
foreach ($metaMessages as $msg) {
// Log incoming message
$msgText = $msg['text']['body'] ?? '[Media/Other]';
\App\Models\MessageLog::logMessage([
'company_id' => $session['company_id'],
'contact_phone' => $msg['from'],
'direction' => 'inbound',
'message_type' => $msg['type'],
'message_body' => $msgText,
'whatsapp_message_id' => $msg['id'],
'status' => 'read'
]);
// Note: Here you can integrate Gemini AI for auto-replies over Cloud API
// Similar to triggerAutoReply() in WhatsAppController
}
}
}
$response->status(200)->send('EVENT_RECEIVED');
}
}
+62
View File
@@ -0,0 +1,62 @@
<?php
if (php_sapi_name() !== 'cli') {
http_response_code(403);
exit('Access denied.');
}
require_once __DIR__ . '/app/bootstrap.php';
use App\Core\Database;
try {
$pdo = Database::getConnection();
echo "=== Running Database Migrations: WhatsApp Cloud API Integration ===\n";
// 1. Create meta_cloud_sessions table
$createSessionsTableSql = "
CREATE TABLE IF NOT EXISTS `meta_cloud_sessions` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`company_id` INT NOT NULL,
`phone_number_id` VARCHAR(50) NOT NULL COMMENT 'Meta Phone Number ID',
`waba_id` VARCHAR(50) NOT NULL COMMENT 'WhatsApp Business Account ID',
`access_token` VARCHAR(1024) NOT NULL COMMENT 'Encrypted Meta Access Token',
`display_phone` VARCHAR(20) DEFAULT NULL COMMENT 'Display phone number',
`status` ENUM('active', 'inactive', 'pending') DEFAULT 'pending',
`verify_token` VARCHAR(100) DEFAULT NULL COMMENT 'Webhook verification token',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE,
INDEX `idx_meta_company` (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
";
Database::execute($createSessionsTableSql);
echo "✅ Table 'meta_cloud_sessions' verified/created.\n";
// 2. Create message_templates table
$createTemplatesTableSql = "
CREATE TABLE IF NOT EXISTS `message_templates` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`company_id` INT NOT NULL,
`meta_template_id` VARCHAR(100) DEFAULT NULL,
`name` VARCHAR(255) NOT NULL,
`language` VARCHAR(10) DEFAULT 'ar',
`category` ENUM('marketing', 'utility', 'authentication') DEFAULT 'utility',
`header_type` ENUM('none', 'text', 'image', 'video', 'document') DEFAULT 'none',
`header_content` TEXT DEFAULT NULL,
`body_text` TEXT NOT NULL,
`footer_text` VARCHAR(60) DEFAULT NULL,
`buttons` JSON DEFAULT NULL,
`status` ENUM('draft', 'pending', 'approved', 'rejected') DEFAULT 'draft',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE,
INDEX `idx_template_company` (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
";
Database::execute($createTemplatesTableSql);
echo "✅ Table 'message_templates' verified/created.\n";
echo "Migration completed successfully!\n";
} catch (\Exception $e) {
echo "❌ Migration error: " . $e->getMessage() . "\n";
}
+115
View File
@@ -901,6 +901,7 @@
<!-- Sub-tabs for Channels -->
<div style="display: flex; gap: 1rem; border-bottom: 1px solid var(--card-border); margin-bottom: 1.5rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<button class="tab-btn" :class="{ 'active': channelTab === 'whatsapp' }" @click="channelTab = 'whatsapp'" x-text="lang === 'ar' ? 'واتساب' : 'WhatsApp'"></button>
<button class="tab-btn" :class="{ 'active': channelTab === 'whatsapp_cloud' }" @click="channelTab = 'whatsapp_cloud'; fetchCloudSession()" x-text="lang === 'ar' ? 'واتساب بزنس API (رسمي)' : 'WhatsApp Business API'"></button>
<button class="tab-btn" :class="{ 'active': channelTab === 'messenger' }" @click="channelTab = 'messenger'; fetchMetaSessions()" x-text="lang === 'ar' ? 'فيسبوك ماسنجر' : 'Facebook Messenger'"></button>
<button class="tab-btn" :class="{ 'active': channelTab === 'instagram' }" @click="channelTab = 'instagram'; fetchMetaSessions()" x-text="lang === 'ar' ? 'إنستغرام' : 'Instagram'"></button>
</div>
@@ -978,6 +979,52 @@
</template>
</div>
<!-- WhatsApp Cloud API Tab Content -->
<div x-show="channelTab === 'whatsapp_cloud'">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;" :style="lang === 'ar' ? 'flex-direction: row-reverse' : ''">
<h2 style="font-size: 1.4rem; margin: 0;" x-text="lang === 'ar' ? 'واتساب بزنس API (الربط الرسمي بـ Meta)' : 'WhatsApp Business API Integration'"></h2>
</div>
<p class="text-muted" style="margin-bottom: 1.5rem; font-size: 0.9rem;" x-text="lang === 'ar' ? 'اربط حساب شركتك الرسمي مع Meta مباشرة. هذا الخيار مثالي لإرسال حملات دعائية ضخمة وقوالب معتمدة بدون خطر حظر الرقم.' : 'Connect your official WhatsApp Business account with Meta. Best for massive broadcast campaigns using approved templates without ban risks.'"></p>
<div class="card" style="margin-bottom: 2rem;">
<template x-if="!cloudSession">
<div style="display: flex; flex-direction: column; gap: 1.5rem;" :style="lang === 'ar' ? 'align-items: flex-end;' : 'align-items: flex-start;'">
<div style="width: 100%; max-width: 500px;">
<label class="form-label" x-text="lang === 'ar' ? 'رقم الهاتف (Phone Number ID)' : 'Phone Number ID'"></label>
<input type="text" x-model="newCloudSession.phone_number_id" class="form-input" placeholder="e.g. 123456789012345">
</div>
<div style="width: 100%; max-width: 500px;">
<label class="form-label" x-text="lang === 'ar' ? 'رقم حساب الأعمال (WABA ID)' : 'WhatsApp Business Account ID'"></label>
<input type="text" x-model="newCloudSession.waba_id" class="form-input" placeholder="e.g. 987654321098765">
</div>
<div style="width: 100%; max-width: 500px;">
<label class="form-label" x-text="lang === 'ar' ? 'رمز الوصول الدائم (Access Token)' : 'Permanent Access Token'"></label>
<input type="password" x-model="newCloudSession.access_token" class="form-input" placeholder="EA...XXXX">
</div>
<button @click="connectCloudSession()" class="btn btn-primary" :disabled="actionLoading" style="align-self: flex-start;">
<span x-text="lang === 'ar' ? 'ربط حساب Meta' : 'Connect Meta Account'"></span>
</button>
</div>
</template>
<template x-if="cloudSession">
<div style="display: flex; flex-direction: column; gap: 1rem;" :style="lang === 'ar' ? 'align-items: flex-end;' : 'align-items: flex-start;'">
<div style="display: flex; align-items: center; gap: 1rem;">
<div style="font-size: 2rem; color: var(--success);">✓</div>
<div>
<h3 style="margin: 0; color: var(--success);" x-text="lang === 'ar' ? 'الحساب مربوط بنجاح' : 'Account Connected Successfully'"></h3>
<p class="text-muted" style="margin: 0.5rem 0 0 0;" x-text="lang === 'ar' ? 'رقم الهاتف ID: ' + cloudSession.phone_number_id : 'Phone ID: ' + cloudSession.phone_number_id"></p>
</div>
</div>
<div style="display: flex; gap: 1rem; margin-top: 1rem;">
<button @click="disconnectCloudSession()" class="btn btn-danger" :disabled="actionLoading">
<span x-text="lang === 'ar' ? 'قطع الاتصال وإزالة الحساب' : 'Disconnect Account'"></span>
</button>
</div>
</div>
</template>
</div>
</div>
<div class="grid-two" style="align-items: start;">
<!-- QR Display Card (Active selected session) -->
<div class="card" style="margin: 0;" x-show="whatsappSession && (whatsappSession.status === 'connecting' || whatsappSession.status === 'waiting_qr' || whatsappSession.status === 'connected')">
@@ -1946,6 +1993,12 @@
whatsappMaxSessions: 1,
newSessionName: '',
metaSessions: [],
cloudSession: null,
newCloudSession: {
phone_number_id: '',
waba_id: '',
access_token: ''
},
newMetaSession: {
channel_type: 'messenger',
page_id: '',
@@ -2375,6 +2428,68 @@
this.actionLoading = false;
}
},
async fetchCloudSession() {
if (!this.token) return;
try {
const response = await fetch('/api/cloud/status', {
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok && data.status === 'success') {
this.cloudSession = data.data;
}
} catch (err) {
console.error('Failed to fetch cloud session', err);
}
},
async connectCloudSession() {
if (!this.newCloudSession.phone_number_id || !this.newCloudSession.waba_id || !this.newCloudSession.access_token) {
alert(this.lang === 'ar' ? 'الرجاء تعبئة جميع الحقول' : 'Please fill all fields');
return;
}
this.actionLoading = true;
try {
const response = await fetch('/api/cloud/connect', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.token}`
},
body: JSON.stringify(this.newCloudSession)
});
const data = await response.json();
if (response.ok && data.status === 'success') {
this.cloudSession = data.data;
this.newCloudSession = { phone_number_id: '', waba_id: '', access_token: '' };
} else {
alert(data.message || data.error || 'Connection failed');
}
} catch (err) {
alert('Network error connecting Cloud API');
} finally {
this.actionLoading = false;
}
},
async disconnectCloudSession() {
if (!confirm(this.lang === 'ar' ? 'هل أنت متأكد من قطع الاتصال بحساب Meta؟' : 'Are you sure you want to disconnect Meta Account?')) {
return;
}
this.actionLoading = true;
try {
const response = await fetch('/api/cloud/disconnect', {
method: 'POST',
headers: { 'Authorization': `Bearer ${this.token}` }
});
const data = await response.json();
if (response.ok && data.status === 'success') {
this.cloudSession = null;
}
} catch (err) {
console.error('Failed to disconnect cloud session', err);
} finally {
this.actionLoading = false;
}
},
async deleteMetaSession(id) {
if (!confirm(this.lang === 'ar' ? 'هل أنت متأكد من حذف هذه القناة؟' : 'Are you sure you want to disconnect this channel?')) {
return;
+8
View File
@@ -103,6 +103,14 @@ $router->delete('/api/meta/sessions', [\App\Controllers\MetaWebhookContr
$router->get('/api/webhooks/meta', [\App\Controllers\MetaWebhookController::class, 'verify']);
$router->post('/api/webhooks/meta', [\App\Controllers\MetaWebhookController::class, 'webhook']);
// WhatsApp Cloud API (Meta Official) Routes
$router->get('/api/cloud/status', [\App\Controllers\CloudApiController::class, 'status'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/cloud/connect', [\App\Controllers\CloudApiController::class, 'connect'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]);
$router->post('/api/cloud/disconnect', [\App\Controllers\CloudApiController::class, 'disconnect'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/cloud/templates/sync', [\App\Controllers\CloudApiController::class, 'syncTemplates'],[\App\Middlewares\AuthMiddleware::class]);
$router->get('/api/cloud/webhook', [\App\Controllers\CloudApiController::class, 'webhook']); // GET verify
$router->post('/api/cloud/webhook', [\App\Controllers\CloudApiController::class, 'webhook']); // POST incoming messages
// Customer Service Agents (Staff) Routes
$router->get('/api/staff', [\App\Controllers\StaffController::class, 'index'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/staff', [\App\Controllers\StaffController::class, 'store'], [\App\Middlewares\AuthMiddleware::class, \App\Middlewares\SubscriptionMiddleware::class]);