Files
coruna-lab/app/Http/Controllers/Hooks/TelegramWebhookController.php
T
2026-08-11 02:26:43 +08:00

179 lines
6.6 KiB
PHP

<?php
namespace App\Http\Controllers\Hooks;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Telegram\TelegramBotContext;
use App\Telegram\TelegramRegistrar;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Nutgram\Laravel\RunningMode\LaravelWebhook;
use Psr\Log\LoggerInterface;
use SergiX44\Nutgram\Configuration;
use SergiX44\Nutgram\Nutgram;
use Throwable;
class TelegramWebhookController extends Controller
{
public function __invoke(Request $request): Response
{
app(TelegramBotContext::class)->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<string, mixed> $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));
}
}
}