Commit 8ec37422 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Refactor WhatsApp: instances call manager hub instead of Meta directly

Instances no longer hold WhatsApp API credentials. They send messages
via the el-captain-manager messaging hub API using MESSAGING_HUB_URL
and MESSAGING_HUB_KEY env vars. The hub handles Meta Graph API calls,
rate limiting, and delivery tracking centrally.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent effca1ea
......@@ -10,17 +10,13 @@
class WhatsAppService
{
private string $apiVersion;
private string $phoneNumberId;
private string $accessToken;
private string $baseUrl;
private string $hubUrl;
private string $hubKey;
public function __construct()
{
$this->apiVersion = (string) (config('services.whatsapp.api_version') ?? 'v25.0');
$this->phoneNumberId = (string) (config('services.whatsapp.phone_number_id') ?? '');
$this->accessToken = (string) (config('services.whatsapp.access_token') ?? '');
$this->baseUrl = "https://graph.facebook.com/{$this->apiVersion}/{$this->phoneNumberId}";
$this->hubUrl = rtrim((string) (config('services.messaging_hub.url') ?? ''), '/');
$this->hubKey = (string) (config('services.messaging_hub.key') ?? '');
}
public function sendTemplate(
......@@ -31,30 +27,19 @@ public function sendTemplate(
?int $academyId = null,
?int $senderId = null,
): WhatsAppMessage {
$to = $this->formatPhone($to);
$payload = [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'template',
'template' => [
'name' => $templateName,
'language' => ['code' => $languageCode],
],
];
if (!empty($components)) {
$payload['template']['components'] = $components;
}
$response = $this->sendRequest($payload);
$response = $this->sendToHub([
'phone' => $to,
'message_type' => 'template',
'template_name' => $templateName,
'template_language' => $languageCode,
'components' => $components,
]);
return $this->logMessage(
academyId: $academyId,
to: $to,
type: MessageType::Template,
content: $templateName,
payload: $payload,
response: $response,
senderId: $senderId,
);
......@@ -65,28 +50,18 @@ public function sendText(
string $body,
?int $academyId = null,
?int $senderId = null,
bool $previewUrl = false,
): WhatsAppMessage {
$to = $this->formatPhone($to);
$payload = [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'text',
'text' => [
'preview_url' => $previewUrl,
'body' => $body,
],
];
$response = $this->sendRequest($payload);
$response = $this->sendToHub([
'phone' => $to,
'message_type' => 'text',
'content' => $body,
]);
return $this->logMessage(
academyId: $academyId,
to: $to,
type: MessageType::Text,
content: $body,
payload: $payload,
response: $response,
senderId: $senderId,
);
......@@ -99,29 +74,18 @@ public function sendImage(
?int $academyId = null,
?int $senderId = null,
): WhatsAppMessage {
$to = $this->formatPhone($to);
$payload = [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'image',
'image' => [
'link' => $imageUrl,
],
];
if ($caption) {
$payload['image']['caption'] = $caption;
}
$response = $this->sendRequest($payload);
$response = $this->sendToHub([
'phone' => $to,
'message_type' => 'image',
'media_url' => $imageUrl,
'content' => $caption,
]);
return $this->logMessage(
academyId: $academyId,
to: $to,
type: MessageType::Image,
content: $caption ?? $imageUrl,
payload: $payload,
response: $response,
senderId: $senderId,
);
......@@ -135,42 +99,26 @@ public function sendDocument(
?int $academyId = null,
?int $senderId = null,
): WhatsAppMessage {
$to = $this->formatPhone($to);
$payload = [
'messaging_product' => 'whatsapp',
'to' => $to,
'type' => 'document',
'document' => [
'link' => $documentUrl,
],
];
if ($filename) {
$payload['document']['filename'] = $filename;
}
if ($caption) {
$payload['document']['caption'] = $caption;
}
$response = $this->sendRequest($payload);
$response = $this->sendToHub([
'phone' => $to,
'message_type' => 'document',
'media_url' => $documentUrl,
'filename' => $filename,
]);
return $this->logMessage(
academyId: $academyId,
to: $to,
type: MessageType::Document,
content: $caption ?? $filename ?? $documentUrl,
payload: $payload,
response: $response,
senderId: $senderId,
);
}
public function sendBulkTemplate(
public function sendBulkText(
array $recipients,
string $templateName,
string $languageCode = 'en_US',
array $components = [],
string $body,
?int $academyId = null,
?int $senderId = null,
): array {
......@@ -178,7 +126,7 @@ public function sendBulkTemplate(
foreach ($recipients as $phone) {
try {
$msg = $this->sendTemplate($phone, $templateName, $languageCode, $components, $academyId, $senderId);
$msg = $this->sendText($phone, $body, $academyId, $senderId);
if ($msg->status === MessageStatus::Failed) {
$results['failed']++;
} else {
......@@ -187,16 +135,18 @@ public function sendBulkTemplate(
$results['messages'][] = $msg;
} catch (\Throwable $e) {
$results['failed']++;
Log::warning("WhatsApp bulk send failed for {$phone}: {$e->getMessage()}");
Log::warning("Messaging hub bulk send failed for {$phone}: {$e->getMessage()}");
}
}
return $results;
}
public function sendBulkText(
public function sendBulkTemplate(
array $recipients,
string $body,
string $templateName,
string $languageCode = 'en_US',
array $components = [],
?int $academyId = null,
?int $senderId = null,
): array {
......@@ -204,7 +154,7 @@ public function sendBulkText(
foreach ($recipients as $phone) {
try {
$msg = $this->sendText($phone, $body, $academyId, $senderId);
$msg = $this->sendTemplate($phone, $templateName, $languageCode, $components, $academyId, $senderId);
if ($msg->status === MessageStatus::Failed) {
$results['failed']++;
} else {
......@@ -213,7 +163,7 @@ public function sendBulkText(
$results['messages'][] = $msg;
} catch (\Throwable $e) {
$results['failed']++;
Log::warning("WhatsApp bulk text failed for {$phone}: {$e->getMessage()}");
Log::warning("Messaging hub bulk template failed for {$phone}: {$e->getMessage()}");
}
}
......@@ -222,30 +172,25 @@ public function sendBulkText(
public function isConfigured(): bool
{
return !empty($this->phoneNumberId) && !empty($this->accessToken);
return !empty($this->hubUrl) && !empty($this->hubKey);
}
private function sendRequest(array $payload): array
private function sendToHub(array $data): array
{
if (!$this->isConfigured()) {
return ['error' => ['message' => 'WhatsApp API not configured']];
return ['success' => false, 'error' => 'Messaging hub not configured'];
}
$response = Http::withToken($this->accessToken)
->timeout(30)
->post("{$this->baseUrl}/messages", $payload);
$data = $response->json();
try {
$response = Http::timeout(30)
->withHeaders(['X-Messaging-Key' => $this->hubKey])
->post("{$this->hubUrl}/api/messaging/send", $data);
if (!$response->successful()) {
Log::error('WhatsApp API error', [
'status' => $response->status(),
'body' => $data,
'payload_to' => $payload['to'] ?? null,
]);
return $response->json() ?? ['success' => false, 'error' => 'Empty response'];
} catch (\Throwable $e) {
Log::error('Messaging hub request failed', ['error' => $e->getMessage()]);
return ['success' => false, 'error' => $e->getMessage()];
}
return $data ?? [];
}
private function logMessage(
......@@ -253,44 +198,26 @@ private function logMessage(
string $to,
MessageType $type,
string $content,
array $payload,
array $response,
?int $senderId,
): WhatsAppMessage {
$waMessageId = $response['messages'][0]['id'] ?? null;
$status = $waMessageId ? MessageStatus::Sent : MessageStatus::Failed;
$errorMessage = null;
if (!$waMessageId) {
$errorMessage = $response['error']['message']
?? $response['error']['error_data']['details'] ?? 'Unknown error';
}
$success = $response['success'] ?? false;
$status = $success ? MessageStatus::Sent : MessageStatus::Failed;
$externalId = $response['message_id'] ?? null;
$errorMessage = $success ? null : ($response['error'] ?? 'Unknown error');
return WhatsAppMessage::create([
'academy_id' => $academyId ?? (int) app('current_academy')?->id,
'wa_message_id' => $waMessageId,
'wa_message_id' => $externalId,
'phone_number' => $to,
'type' => $type->value,
'content' => mb_substr($content, 0, 2000),
'status' => $status->value,
'error_message' => $errorMessage,
'payload' => $payload,
'payload' => $response,
'response' => $response,
'sent_by' => $senderId ?? auth()->id(),
'sent_at' => $waMessageId ? now() : null,
'sent_at' => $success ? now() : null,
]);
}
private function formatPhone(string $phone): string
{
$phone = preg_replace('/[\s\-\(\)\+]/', '', $phone);
if (str_starts_with($phone, '0')) {
$phone = '20' . substr($phone, 1);
} elseif (strlen($phone) === 10) {
$phone = '20' . $phone;
}
return $phone;
}
}
......@@ -42,4 +42,9 @@
'webhook_verify_token' => env('WHATSAPP_WEBHOOK_VERIFY_TOKEN', ''),
],
'messaging_hub' => [
'url' => env('MESSAGING_HUB_URL', ''),
'key' => env('MESSAGING_HUB_KEY', ''),
],
];
......@@ -17,7 +17,7 @@ chmod -R 775 storage bootstrap/cache
# Build .env from Docker env vars (CapRover passes them as container env)
: > .env
env | grep -E "^(APP_|DB_|LOG_|SESSION_|CACHE_|QUEUE_|FILESYSTEM_|BROADCAST_|MAIL_|TRUSTED_|BCRYPT_|PLATFORM_|ADMIN_|ACADEMY_|RUN_|WHATSAPP_)" | sort | sed 's/=\(.*\)/="\1"/' >> .env
env | grep -E "^(APP_|DB_|LOG_|SESSION_|CACHE_|QUEUE_|FILESYSTEM_|BROADCAST_|MAIL_|TRUSTED_|BCRYPT_|PLATFORM_|ADMIN_|ACADEMY_|RUN_|WHATSAPP_|MESSAGING_)" | sort | sed 's/=\(.*\)/="\1"/' >> .env
# Ensure APP_KEY line exists (key:generate needs it to write to)
if ! grep -q '^APP_KEY=' .env 2>/dev/null; then
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment