linkConverterService = $linkConverterService; $this->settingService = $settingService; } /** * Handle incoming Zalo Bot webhook request. */ public function handleWebhook(array $payload): void { Log::info('Zalo Webhook payload received', $payload); // Check if message structure exists (mimics Telegram format) $message = $payload['message'] ?? null; if (!$message) { return; } $chatId = $message['chat']['id'] ?? null; $text = $message['text'] ?? ''; if (!$chatId || empty($text)) { return; } // Get sender name (fallback to "Bạn") $fromName = trim( ($message['from']['first_name'] ?? '') . ' ' . ($message['from']['last_name'] ?? '') ); if (empty($fromName)) { $fromName = $message['from']['username'] ?? 'Bạn'; } // Find Shopee links $urls = $this->extractShopeeUrls($text); if (empty($urls)) { return; } foreach ($urls as $url) { $this->processAndReplyLink($chatId, $fromName, $url); } } /** * Process a single Shopee link and reply to the chat. */ protected function processAndReplyLink(string $chatId, string $fromName, string $url): void { try { // 1. Resolve redirects if it's a short URL (e.g. s.shopee.vn or shp.ee) $resolvedUrl = $this->resolveShortUrl($url); // 2. Extract product name from resolved URL $productName = $this->extractProductName($resolvedUrl) ?? 'Sản phẩm Shopee'; // 3. Estimate commission $commission = $this->estimateCommission($resolvedUrl); // 4. Convert link to our local affiliate short link $convertResult = $this->linkConverterService->process($resolvedUrl); if ($convertResult && $convertResult->isSuccess) { $shortLink = $convertResult->affiliateUrl; // 5. Construct the message template matching competitor's chatbot $replyText = "Hi @{$fromName}, Link của bạn đây ạ:\n\n" . "📦 {$productName}\n" . "🔗 Link: {$shortLink}\n" . "💵 Hoa hồng ước tính: {$commission}\n\n" . "⚠️ Lưu ý: Để hoa hồng hợp lệ, KHÔNG mua qua Livestream, KHÔNG xem video/live bất kỳ sau khi click & XÓA sản phẩm ra khỏi giỏ hàng TRƯỚC KHI bấm link!"; // 6. Send reply message $this->sendMessage($chatId, $replyText); } else { Log::error("Failed to convert Shopee URL via Zalo: " . ($convertResult->errorMessage ?? 'Unknown error')); } } catch (\Exception $e) { Log::error("Error processing link in ZaloBotService: " . $e->getMessage()); } } /** * Extract all Shopee URLs from text. */ public function extractShopeeUrls(string $text): array { $pattern = '/(?:https?:\/\/)?(?:[a-zA-Z0-9-]+\.)?shopee\.vn\/[^\s]+|(?:https?:\/\/)?shp\.ee\/[^\s]+/i'; if (preg_match_all($pattern, $text, $matches)) { $urls = []; foreach ($matches[0] as $url) { if (!str_starts_with(strtolower($url), 'http://') && !str_starts_with(strtolower($url), 'https://')) { $url = 'https://' . $url; } $urls[] = $url; } return $urls; } return []; } /** * Resolve short URLs (s.shopee.vn or shp.ee) to get the final redirected URL. */ public function resolveShortUrl(string $url): string { if (str_contains($url, '/an_redir') || (!str_contains($url, 's.shopee.vn') && !str_contains($url, 'shp.ee'))) { return $url; } try { $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_NOBODY, true); // Get headers only curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); // Check redirect location manually curl_setopt($ch, CURLOPT_TIMEOUT, 5); curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); $response = curl_exec($ch); $info = curl_getinfo($ch); curl_close($ch); if ($info['http_code'] >= 300 && $info['http_code'] < 400) { if (preg_match('/[Ll]ocation:\s*(.+)/', $response, $matches)) { $resolvedUrl = trim($matches[1]); // Recursively resolve redirect if it's still a redirect link return $this->resolveShortUrl($resolvedUrl); } } } catch (\Exception $e) { Log::warning("Could not resolve short URL redirect: " . $e->getMessage()); } return $url; } /** * Extract product/shop name from Shopee URL. */ public function extractProductName(string $url): ?string { $parsedUrl = parse_url($url); if (!isset($parsedUrl['path'])) { return null; } $path = urldecode($parsedUrl['path']); $path = trim($path, '/'); // Match typical Shopee product paths ending with -i.SHOP_ID.ITEM_ID if (preg_match('/^(.*?)-i\.\d+\.\d+$/', $path, $matches)) { $slug = $matches[1]; $name = str_replace(['-', '_'], ' ', $slug); return trim($name); } // Shop pages / Usernames if (!str_contains($path, '/')) { $name = str_replace(['-', '_'], ' ', $path); return trim($name); } return null; } /** * Predictable estimate of product commission based on the item ID. */ public function estimateCommission(string $url): string { $itemId = 0; if (preg_match('/-i\.\d+\.(\d+)$/', $url, $matches)) { $itemId = (int)$matches[1]; } if ($itemId === 0) { $itemId = crc32($url); } // Predictable single estimate between 4.0% and 7.5% based on itemId or crc32 $rate = 4.0 + (abs($itemId) % 36) / 10.0; return number_format($rate, 1) . '%'; } /** * Send message using Zalo Bot Creator HTTP API. */ public function sendMessage(string $chatId, string $text): bool { $token = $this->settingService->get('zalo_bot_token') ?? env('ZALO_BOT_TOKEN'); if (!$token) { Log::error('Zalo Bot Token is not configured. Cannot send message.'); return false; } $apiUrl = "https://bot-api.zaloplatforms.com/bot{$token}/sendMessage"; Log::info('Sending message to Zalo Bot', [ 'chat_id' => $chatId, 'url' => "https://bot-api.zaloplatforms.com/bot[HIDDEN]/sendMessage" ]); $response = Http::timeout(10)->post($apiUrl, [ 'chat_id' => $chatId, 'text' => $text, ]); if ($response->successful()) { Log::info('Message sent successfully to Zalo Bot'); return true; } Log::error('Failed to send message to Zalo Bot', [ 'status' => $response->status(), 'body' => $response->body() ]); return false; } }