diff --git a/backend/api/request-otp.php b/backend/api/request-otp.php index 172ef5f..d1c43b1 100644 --- a/backend/api/request-otp.php +++ b/backend/api/request-otp.php @@ -34,6 +34,7 @@ require_once __DIR__ . '/../includes/Redis.php'; require_once __DIR__ . '/../includes/RateLimit.php'; require_once __DIR__ . '/../includes/Auth.php'; require_once __DIR__ . '/../includes/Logger.php'; +require_once __DIR__ . '/../includes/WhatsApp.php'; // Authenticate — requires app key (Flutter app) Auth::requireAuth('app'); @@ -100,7 +101,20 @@ if (!$rateLimit->checkIp($clientIp, 'request-otp', 30, 60)) { $otpCode = str_pad((string) random_int(0, 9999), 4, '0', STR_PAD_LEFT); // Determine delivery method -$method = ($deviceType === 'ios') ? 'sms' : 'flash_call'; +$method = 'flash_call'; // Default fallback +$whatsappAvailable = false; + +try { + $whatsappAvailable = WhatsAppClient::isAvailable($phone); +} catch (\Throwable $e) { + error_log('WhatsApp check failed: ' . $e->getMessage()); +} + +if ($whatsappAvailable) { + $method = 'whatsapp'; +} else { + $method = ($deviceType === 'ios') ? 'sms' : 'flash_call'; +} $db = Database::getInstance(); $redis = RedisClient::getInstance(); @@ -142,6 +156,43 @@ try { VALUES (?, ?, ?, 'pending', ?, 'flash_call', ?)" ); $stmt->execute([$phone, $otpCode, $callerId, $device['device_id'], $expiresAt]); + } else if ($method === 'whatsapp') { + // WhatsApp delivery + $expiresAt = date('Y-m-d H:i:s', time() + OTP_EXPIRE_SECONDS); + $stmt = $db->prepare( + "INSERT INTO otp_requests (phone, otp_code, caller_id, status, method, expires_at) + VALUES (?, ?, '', 'pending_whatsapp', 'whatsapp', ?)" + ); + $stmt->execute([$phone, $otpCode, $expiresAt]); + $otpId = $db->lastInsertId(); + + // Try to generate premium dynamic base64 OTP image + $imagePngBase64 = null; + try { + $imagePngBase64 = WhatsAppClient::generateOtpImageBase64($otpCode); + } catch (\Throwable $e) { + error_log('Failed to generate OTP image: ' . $e->getMessage()); + } + + // Message caption / body + $messageText = "رمز التحقق الخاص بك هو: " . $otpCode . "\nيرجى إدخاله في التطبيق لإكمال العملية."; + + $sent = false; + try { + if ($imagePngBase64) { + // Send premium image message with caption + $sent = WhatsAppClient::sendMessage($phone, "رمز التحقق الخاص بك هو: " . $otpCode, $imagePngBase64); + } else { + // Fallback to text message + $sent = WhatsAppClient::sendMessage($phone, $messageText); + } + } catch (\Throwable $e) { + error_log('WhatsApp sendMessage error: ' . $e->getMessage()); + } + + if (!$sent) { + throw new \Exception('Failed to send OTP via WhatsApp'); + } } else { // SMS delivery — no specific caller_id needed for the OTP request $expiresAt = date('Y-m-d H:i:s', time() + OTP_EXPIRE_SECONDS); diff --git a/backend/config.php b/backend/config.php index 1c8b517..813aca7 100644 --- a/backend/config.php +++ b/backend/config.php @@ -39,5 +39,10 @@ define('CALLER_ID_PREFIX', '+96279'); define('LOG_REQUESTS', true); define('LOG_FILE', __DIR__ . '/logs/api.log'); +// WhatsApp Gateway Configuration +define('WHATSAPP_GATEWAY_URL', getenv('WHATSAPP_GATEWAY_URL') ?: 'http://localhost:3732'); +define('WHATSAPP_WEBHOOK_SECRET', getenv('WHATSAPP_WEBHOOK_SECRET') ?: 'flash_call_otp_webhook_secret_key'); +define('WHATSAPP_SESSION_KEY', getenv('WHATSAPP_SESSION_KEY') ?: 'flash_call_otp'); + // Timezone date_default_timezone_set('Asia/Amman'); diff --git a/backend/database.sql b/backend/database.sql index f584ef9..62976a4 100644 --- a/backend/database.sql +++ b/backend/database.sql @@ -15,8 +15,8 @@ CREATE TABLE IF NOT EXISTS otp_requests ( phone VARCHAR(20) NOT NULL, otp_code VARCHAR(6) NOT NULL, caller_id VARCHAR(20) NOT NULL DEFAULT '', - status ENUM('pending','pending_sms','calling','completed','failed','expired','verified') NOT NULL DEFAULT 'pending', - method ENUM('flash_call','sms') NOT NULL DEFAULT 'flash_call', + status ENUM('pending','pending_sms','pending_whatsapp','calling','completed','failed','expired','verified') NOT NULL DEFAULT 'pending', + method ENUM('flash_call','sms','whatsapp','telegram') NOT NULL DEFAULT 'flash_call', device_id VARCHAR(50) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP, diff --git a/backend/includes/WhatsApp.php b/backend/includes/WhatsApp.php new file mode 100644 index 0000000..c872167 --- /dev/null +++ b/backend/includes/WhatsApp.php @@ -0,0 +1,158 @@ + self::$sessionKey, + 'phone' => $cleanPhone + ]); + + $ch = curl_init(self::$gatewayUrl . '/api/contacts/check'); + 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: ' . self::$secret + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200 && $response) { + $data = json_decode($response, true); + if (isset($data['status']) && $data['status'] === 'success') { + return isset($data['data']['exists']) && $data['data']['exists'] === true; + } + } + return false; + } + + /** + * Send OTP message via WhatsApp Gateway. + * + * @param string $phone Destination phone number + * @param string $message Message text + * @param string|null $imageBase64 Optional base64 image data + * @return bool True if successfully sent + */ + public static function sendMessage($phone, $message, $imageBase64 = null) { + self::init(); + + $cleanPhone = preg_replace('/[^\d]/', '', $phone); + + $payloadData = [ + 'session_key' => self::$sessionKey, + 'phone' => $cleanPhone, + 'message' => $message + ]; + + if ($imageBase64) { + $payloadData['image'] = $imageBase64; + } + + $payload = json_encode($payloadData); + + $ch = curl_init(self::$gatewayUrl . '/api/messages/send'); + 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: ' . self::$secret + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200 && $response) { + $data = json_decode($response, true); + return isset($data['status']) && $data['status'] === 'success'; + } + return false; + } + + /** + * Generate dynamic base64-encoded image containing OTP code using GD library. + * + * @param string $otp 4-digit OTP code + * @return string Base64 encoded PNG image + */ + public static function generateOtpImageBase64($otp) { + // Create a 300x100 image + $im = imagecreatetruecolor(300, 100); + if (!$im) { + return ''; + } + + // Colors + $bgColor = imagecolorallocate($im, 240, 244, 248); // Soft grey-blue + $textColor = imagecolorallocate($im, 33, 37, 41); // Dark charcoal + $accentColor = imagecolorallocate($im, 13, 110, 253); // Premium blue + $noiseColor = imagecolorallocate($im, 200, 210, 220); // Light noise + + // Fill background + imagefill($im, 0, 0, $bgColor); + + // Draw some obfuscation lines / background noise + for ($i = 0; $i < 6; $i++) { + imageline($im, random_int(0, 300), random_int(0, 100), random_int(0, 300), random_int(0, 100), $noiseColor); + } + + // Draw some random dots + for ($i = 0; $i < 100; $i++) { + imagesetpixel($im, random_int(0, 300), random_int(0, 100), $noiseColor); + } + + // Header text (smaller) + imagestring($im, 3, 20, 15, "Verification Code:", $textColor); + + // Large OTP text (using larger font index 5 or custom size if possible) + // Split OTP and draw characters with varying Y positions and styling to make OCR harder + $chars = str_split($otp); + $x = 90; + foreach ($chars as $char) { + $y = random_int(35, 45); + imagestring($im, 5, $x, $y, $char, $accentColor); + $x += 30; + } + + // Draw a bounding border + imagerectangle($im, 0, 0, 299, 99, $noiseColor); + + // Capture output + ob_start(); + imagepng($im); + $imageData = ob_get_clean(); + imagedestroy($im); + + return base64_encode($imageData); + } +}