Files
api_arrahn/app/Http/Controllers/WhatsappWebhookController.php
T
ismailmasseran 7fb7f75050
Build Docker Image / build-backend (push) Successful in 18s
Feature/whatsapp blast (#16)
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Reviewed-on: #16
2026-09-24 11:27:48 +08:00

123 lines
3.8 KiB
PHP

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
/**
* Meta WhatsApp Cloud webhook: verify challenge + acknowledge events.
* Sufficient for production onboarding; delivery statuses are logged only.
*/
class WhatsappWebhookController extends Controller
{
/**
* GET /api/whatsapp/webhook
* Meta sends hub.mode, hub.verify_token, hub.challenge.
*/
public function verify(Request $request)
{
$mode = $request->query('hub_mode', $request->query('hub.mode'));
$token = $request->query('hub_verify_token', $request->query('hub.verify_token'));
$challenge = $request->query('hub_challenge', $request->query('hub.challenge'));
$expected = (string) config('whatsapp.verify_token');
if ($mode === 'subscribe' && $expected !== '' && hash_equals($expected, (string) $token)) {
Log::info('WhatsApp webhook verified');
return response((string) $challenge, 200)
->header('Content-Type', 'text/plain');
}
Log::warning('WhatsApp webhook verify failed', [
'mode' => $mode,
'token_match' => $expected !== '' && hash_equals($expected, (string) $token),
]);
return response('Forbidden', 403);
}
/**
* POST /api/whatsapp/webhook
* Acknowledge quickly; optionally check X-Hub-Signature-256 when app secret is set.
*/
public function handle(Request $request)
{
$raw = $request->getContent();
if (!$this->signatureOk($request, $raw)) {
Log::warning('WhatsApp webhook invalid signature');
return response('Invalid signature', 403);
}
$payload = json_decode($raw, true);
if (!is_array($payload)) {
return response('OK', 200);
}
try {
$this->logEvents($payload);
} catch (\Throwable $e) {
Log::error('WhatsApp webhook log failed: ' . $e->getMessage());
}
return response('OK', 200);
}
protected function signatureOk(Request $request, $raw)
{
$secret = trim((string) config('whatsapp.app_secret'));
if ($secret === '') {
return true;
}
$header = (string) $request->header('X-Hub-Signature-256', '');
if ($header === '' || strpos($header, 'sha256=') !== 0) {
return false;
}
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
return hash_equals($expected, $header);
}
protected function logEvents(array $payload)
{
$entries = $payload['entry'] ?? [];
if (!is_array($entries)) {
return;
}
foreach ($entries as $entry) {
$changes = $entry['changes'] ?? [];
if (!is_array($changes)) {
continue;
}
foreach ($changes as $change) {
$value = $change['value'] ?? [];
if (!is_array($value)) {
continue;
}
foreach ($value['statuses'] ?? [] as $status) {
Log::info('WhatsApp status', [
'id' => $status['id'] ?? null,
'status' => $status['status'] ?? null,
'recipient' => $status['recipient_id'] ?? null,
'errors' => $status['errors'] ?? null,
]);
}
foreach ($value['messages'] ?? [] as $message) {
Log::info('WhatsApp inbound', [
'from' => $message['from'] ?? null,
'type' => $message['type'] ?? null,
'id' => $message['id'] ?? null,
]);
}
}
}
}
}