ensureRegisteredDetailed()['ok'] ?? false); } /** * @return array{ok: bool, reason: string, message: string, webhook_url: string, attempts?: int} */ public function ensureRegisteredDetailed(int $maxAttempts = 3): array { $webhookUrl = $this->webhookUrl(); $config = app(ConfigService::class); $chatApi = $config->getString('chat_api'); $chatApiKey = $config->getString('chat_api_key'); if (! $chatApi || ! $chatApiKey) { return [ 'ok' => false, 'reason' => 'missing_chat_config', 'message' => 'تنظیمات chat_api یا chat_api_key خالی است', 'webhook_url' => $webhookUrl, ]; } $host = preg_replace('#^https?://#', '', rtrim((string) $chatApi, '/')); $secret = $this->ensureSecret(); if ($host === '' || $secret === '') { return [ 'ok' => false, 'reason' => 'invalid_host_or_secret', 'message' => 'آدرس چت یا رمز وب‌هوک نامعتبر است', 'webhook_url' => $webhookUrl, ]; } if ($this->isUnreachableWebhookUrl($webhookUrl)) { Log::warning('chat webhook register refused: unreachable/local URL', [ 'webhook_url' => $webhookUrl, 'hint' => 'Set chat_webhook_base_url to public Laravel URL (e.g. https://niazitala.ir/server) or fix APP_URL', ]); return [ 'ok' => false, 'reason' => 'unreachable_webhook_url', 'message' => 'آدرس وب‌هوک محلی/غیرقابل‌دسترس است؛ chat_webhook_base_url را روی آدرس عمومی سرور بگذارید', 'webhook_url' => $webhookUrl, ]; } $attempts = max(1, $maxAttempts); $lastReason = 'unknown'; $lastMessage = 'ثبت وب‌هوک ناموفق بود'; for ($i = 1; $i <= $attempts; $i++) { try { $response = Http::timeout(8) ->withOptions(Controller::httpClientOptions([], 8.0)) ->retry(0) ->post('https://'.$host.'/api/projects/set-webhook', [ 'api_key' => $chatApiKey, 'webhook_url' => $webhookUrl, 'webhook_secret' => $secret, ]); if ($response->successful()) { Log::info('chat webhook registered', [ 'host' => $host, 'webhook_url' => $webhookUrl, 'attempt' => $i, ]); return [ 'ok' => true, 'reason' => 'registered', 'message' => 'webhook registered', 'webhook_url' => $webhookUrl, 'attempts' => $i, ]; } $lastReason = 'chat_api_http_'.$response->status(); $lastMessage = 'سرور چت ثبت وب‌هوک را رد کرد (HTTP '.$response->status().')'; Log::warning('chat webhook register non-2xx', [ 'host' => $host, 'webhook_url' => $webhookUrl, 'status' => $response->status(), 'body' => mb_substr($response->body(), 0, 300), 'attempt' => $i, ]); } catch (\Throwable $e) { $lastReason = 'chat_api_exception'; $lastMessage = 'ارتباط با سرور چت برای ثبت وب‌هوک برقرار نشد'; Log::warning('chat webhook register failed', [ 'host' => $host, 'webhook_url' => $webhookUrl, 'error' => $e->getMessage(), 'attempt' => $i, ]); } if ($i < $attempts) { usleep(250000 * $i); } } return [ 'ok' => false, 'reason' => $lastReason, 'message' => $lastMessage, 'webhook_url' => $webhookUrl, 'attempts' => $attempts, ]; } /** * آدرس عمومی وب‌هوک — باید به لاراول بخورد نه فرانت SPA. * اولویت: chat_webhook_base_url (مثلاً https://niazitala.ir/server) سپس APP_URL. */ public function webhookUrl(): string { $base = trim(app(ConfigService::class)->getString('chat_webhook_base_url')); if ($base === '' || $base === '0' || strtolower($base) === 'false') { $base = rtrim((string) config('app.url'), '/'); } else { $base = rtrim($base, '/'); } return $base.'/api/chat/support-message-notify'; } public function isUnreachableWebhookUrl(string $url): bool { $host = parse_url($url, PHP_URL_HOST); if (! is_string($host) || $host === '') { return true; } $host = strtolower($host); return in_array($host, ['localhost', '127.0.0.1', '::1', '0.0.0.0'], true) || str_ends_with($host, '.local') || str_ends_with($host, '.internal'); } public function ensureSecret(): string { $existing = $this->sanitizeSecret(app(ConfigService::class)->getString('chat_webhook_secret')); if ($existing !== '') { return $existing; } $secret = Str::random(48); $row = Setting::where('key', 'chat_webhook_secret')->first(); if ($row) { $current = $this->sanitizeSecret((string) $row->value); if ($current === '') { $row->value = $secret; $row->save(); } else { // مقدار ذخیره‌شده کاراکتر نامعتبر هدر داشت — با نسخهٔ تمیز جایگزین کن if ($current !== trim((string) $row->value)) { $row->value = $current; $row->save(); } return $current; } } else { Setting::create([ 'key' => 'chat_webhook_secret', 'value' => $secret, 'title_fa' => 'رمز وب‌هوک چت', 'is_active' => true, 'can_edit' => 0, 'description' => 'رمز مشترک بین لاراول و سرور چت برای اعلان پیام پشتیبانی. فقط سرور؛ در صورت خالی بودن به‌صورت خودکار ساخته می‌شود.', 'type' => 'text', ]); } return $secret; } /** فقط ASCII قابل‌استفاده در هدر HTTP (بدون فاصله/خط جدید/یونیکد). */ public function sanitizeSecret(string $secret): string { $secret = trim($secret); if ($secret === '') { return ''; } // حذف کاراکترهای غیرقابل‌قبول برای header value $clean = preg_replace('/[^\x21-\x7E]/', '', $secret) ?? ''; return trim($clean); } }