setAgent(null); return $this->handle( $request, expectedSecret: (string) config('coruna.telegram.webhook_secret', ''), resolveBot: static fn () => app(Nutgram::class), label: 'official', ); } public function agent(Request $request, User $agent): Response { if (! $agent->isEnabled() || ! $agent->hasTelegramBot()) { abort(404); } app(TelegramBotContext::class)->setAgent($agent); $secret = TelegramBotContext::webhookSecretFor($agent); return $this->handle( $request, expectedSecret: $secret, resolveBot: function () use ($agent) { if (app()->runningUnitTests()) { return app(Nutgram::class); } $bot = $this->makeAgentBot($agent); app(TelegramRegistrar::class)->registerAgent($bot, $agent); return $bot; }, label: 'agent:'.$agent->id, requireSecret: true, ); } /** * @param callable(): Nutgram $resolveBot */ private function handle( Request $request, string $expectedSecret, callable $resolveBot, string $label, bool $requireSecret = false, ): Response { $raw = $request->getContent(); $update = $request->all(); if ($update === [] && is_string($raw) && $raw !== '') { $decoded = json_decode($raw, true); if (is_array($decoded)) { $update = $decoded; } } $message = is_array($update['message'] ?? null) ? $update['message'] : []; $chatId = $message['chat']['id'] ?? ($update['callback_query']['message']['chat']['id'] ?? null); $text = is_string($message['text'] ?? null) ? $message['text'] : null; $updateId = $update['update_id'] ?? null; $header = (string) $request->header('X-Telegram-Bot-Api-Secret-Token', ''); $this->webhookLog('info', 'telegram webhook hit', [ 'bot' => $label, 'ip' => $request->ip(), 'update_id' => $updateId, 'chat_id' => $chatId, 'text' => $text, 'has_secret_header' => $header !== '', 'secret_configured' => $expectedSecret !== '', 'body_bytes' => strlen($raw), ]); if ($requireSecret || $expectedSecret !== '') { if ($expectedSecret === '' || $header === '' || ! hash_equals($expectedSecret, $header)) { $this->webhookLog('warning', 'telegram webhook rejected: bad secret', [ 'bot' => $label, 'ip' => $request->ip(), 'update_id' => $updateId, ]); abort(403, 'Invalid webhook secret'); } } try { $bot = $resolveBot(); $bot->run(); $this->webhookLog('info', 'telegram webhook handled', [ 'bot' => $label, 'update_id' => $updateId, 'chat_id' => $chatId, 'handler' => $bot->currentHandler()?->getPattern(), ]); } catch (Throwable $e) { $this->webhookLog('error', 'telegram webhook failed', [ 'bot' => $label, 'message' => $e->getMessage(), 'exception' => $e::class, 'file' => $e->getFile().':'.$e->getLine(), 'update_id' => $updateId, 'chat_id' => $chatId, 'trace' => collect(explode("\n", $e->getTraceAsString()))->take(12)->all(), ]); if (app()->runningUnitTests() && $e instanceof \InvalidArgumentException) { return response()->noContent(); } } return response()->noContent(); } private function makeAgentBot(User $agent): Nutgram { $configuration = new Configuration( apiUrl: config('nutgram.config.api_url', Configuration::DEFAULT_API_URL), botId: config('nutgram.config.bot_id'), botName: config('nutgram.config.bot_name'), testEnv: config('nutgram.config.test_env', false), isLocal: config('nutgram.config.is_local', false), clientTimeout: config('nutgram.config.timeout', Configuration::DEFAULT_CLIENT_TIMEOUT), clientOptions: config('nutgram.config.client', []), container: app(), hydrator: config('nutgram.config.hydrator', Configuration::DEFAULT_HYDRATOR), cache: app(Cache::class), logger: app(LoggerInterface::class)->channel(config('nutgram.log_channel', 'null')), localPathTransformer: config('nutgram.config.local_path_transformer'), pollingTimeout: config('nutgram.config.polling.timeout', Configuration::DEFAULT_POLLING_TIMEOUT), pollingAllowedUpdates: config('nutgram.config.polling.allowed_updates', Configuration::DEFAULT_ALLOWED_UPDATES), pollingLimit: config('nutgram.config.polling.limit', Configuration::DEFAULT_POLLING_LIMIT), enableHttp2: config('nutgram.config.enable_http2', Configuration::DEFAULT_ENABLE_HTTP2), conversationTtl: config('nutgram.config.conversation_ttl', Configuration::DEFAULT_CONVERSATION_TTL), ); $bot = new Nutgram((string) $agent->bot_token, $configuration); $secret = TelegramBotContext::webhookSecretFor($agent); $webhook = new LaravelWebhook( getToken: static fn () => request()?->header('X-Telegram-Bot-Api-Secret-Token'), secretToken: $secret, ); $webhook->setSafeMode(true); $bot->setRunningMode($webhook); return $bot; } /** @param array $context */ private function webhookLog(string $level, string $message, array $context = []): void { try { Log::{$level}($message, $context); } catch (Throwable) { error_log($message.' '.json_encode($context, JSON_UNESCAPED_UNICODE)); } } }