210 lines
7.5 KiB
PHP
210 lines
7.5 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Setting;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Str;
|
|
|
|
class ChatWebhookService
|
|
{
|
|
public function ensureRegistered(): bool
|
|
{
|
|
return (bool) ($this->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);
|
|
}
|
|
}
|