[[:templates_ai:funcionalidades:laravel:telegram_bot_passos|{{wiki:user:undo_24.png}}]]
====== Passo 03: Sistema de Validação e Middleware - Telegram Webhook ======
===== ⚠️ INSTRUÇÕES IMPORTANTES ANTES DE COMEÇAR =====
**Para evitar erros durante a implementação, siga EXATAMENTE estas etapas:**
✅ **1. Ler o template COMPLETO da página**
* Leia toda a documentação antes de começar
* Entenda o fluxo completo de implementação
* Identifique dependências entre os passos
✅ **2. Identificar TODOS os arquivos mencionados**
* Liste todos os arquivos que serão criados/modificados
* Verifique se já existem no projeto
* Anote o caminho exato de cada arquivo
✅ **3. Verificar a estrutura EXATA de cada arquivo**
* Confirme namespaces e imports corretos
* Verifique se dependências estão instaladas
* Valide sintaxe PHP antes de implementar
✅ **4. Implementar linha por linha conforme template**
* Copie o código EXATAMENTE como mostrado
* Não modifique namespaces ou imports
* Execute comandos na ordem especificada
**🚨 ATENÇÃO:**
* **NÃO pule etapas** - cada passo tem dependências
* **NÃO modifique** o código fornecido sem entender as consequências
* **SEMPRE teste** após cada implementação
* **MANTENHA backup** antes de grandes alterações
===== 📋 Visão Geral =====
Sistema de validação e middleware que garante segurança, tratamento de exceções e logging para o Telegram Webhook. Depende dos sistemas de logging, rastreamento de fluxo e canais de notificação.
===== 🚀 Comando de Implementação =====
implementar sistema validacao middleware telegram
===== ⚙️ Pré-requisitos =====
- **Sistema de Logging** implementado
- **Sistema de Rastreamento de Fluxo** implementado
- **Sistema de Canais de Notificação** implementado
- **Laravel 12** instalado
- **PHP 8.2+** configurado
===== 📁 Arquivos do Módulo =====
=== 🛡️ Middleware ===
* **`app/Http/Middleware/TelegramWebhookSecretMiddleware.php`**
- Validação de secret do webhook
- Segurança contra requisições não autorizadas
- Rate limiting
* **`app/Http/Middleware/TelegramWebhookExceptionHandler.php`**
- Tratamento de exceções específicas
- Notificações de erro via Telegram
- Logging de erros críticos
* **`app/Http/Middleware/TelegramWebhookLoggingMiddleware.php`**
- Logging de requisições
- Rastreamento de performance
- Auditoria de acesso
=== 🔧 Services ===
* **`app/Services/Telegram/TelegramWebhookValidationService.php`**
- Validação de payloads do webhook
- Detecção de duplicatas
- Validação de estrutura de dados
=== 📝 Requests ===
* **`app/Http/Requests/TelegramWebhookRequest.php`**
- Validação de dados de entrada
- Sanitização de payloads
- Validação de permissões
* **`app/Http/Requests/TelegramWebhookSetupRequest.php`**
- Validação de configuração de webhook
- Validação de URLs
- Validação de permissões de admin
=== 🧪 Testes ===
* **`tests/Unit/TelegramWebhookValidationServiceTest.php`**
- Testes unitários de validação
- Testes de detecção de duplicatas
- Testes de estrutura de dados
* **`tests/Unit/TelegramWebhookMiddlewareTest.php`**
- Testes de middleware
- Testes de segurança
- Testes de rate limiting
===== 🔧 Implementação Passo a Passo =====
* **Importante orientação:** implemente apenas as primeiras linhas de cada classe, pois, serão completadas posteriormente.
=== Passo 1: Criar Middleware de Secret ===
loggingService->logTelegramEvent('webhook_secret_not_configured', [
'warning' => 'Webhook secret not configured - allowing all requests',
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'timestamp' => now()->toISOString()
], 'warning');
return $next($request);
}
// Verificar se o header do secret está presente
$incomingSecret = $request->header('X-Telegram-Bot-Api-Secret-Token');
if (empty($incomingSecret)) {
$this->loggingService->logTelegramEvent('webhook_secret_missing', [
'error' => 'Missing webhook secret token in request',
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'headers' => $request->headers->all(),
'timestamp' => now()->toISOString()
], 'error');
return response()->json([
'error' => 'Unauthorized - Missing secret token',
'message' => 'Webhook secret token is required'
], 401);
}
// Verificar se o secret é válido
if (!hash_equals($secretToken, $incomingSecret)) {
$this->loggingService->logTelegramEvent('webhook_secret_invalid', [
'error' => 'Invalid webhook secret token',
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'incoming_secret' => substr($incomingSecret, 0, 8) . '...', // Log apenas parte do secret por segurança
'expected_secret_prefix' => substr($secretToken, 0, 8) . '...',
'timestamp' => now()->toISOString()
], 'error');
return response()->json([
'error' => 'Unauthorized - Invalid secret token',
'message' => 'Webhook secret token is invalid'
], 401);
}
return $next($request);
}
}
=== Passo 2: Criar Middleware de Exceções ===
extractChatId($request);
if ($chatId) {
$this->sendValidationErrorToTelegram($chatId, $e);
}
// Log the validation error
Log::warning('Telegram webhook validation failed', [
'chat_id' => $chatId,
'errors' => $e->errors(),
'payload' => $request->all()
]);
// Return a proper response to Telegram (200 OK to acknowledge receipt)
return response()->json([
'status' => 'error',
'message' => 'Validation failed',
'errors' => $e->errors()
], 200)->header('Content-Type', 'application/json');
} catch (\Exception $e) {
// Extract chat_id from the request if available
$chatId = $this->extractChatId($request);
if ($chatId) {
$this->sendGeneralErrorToTelegram($chatId, $e);
}
// Log the general error
Log::error('Telegram webhook general error', [
'chat_id' => $chatId,
'error' => $e->getMessage(),
'payload' => $request->all()
]);
// Return a proper response to Telegram (200 OK to acknowledge receipt)
return response()->json([
'status' => 'error',
'message' => 'Internal server error'
], 200)->header('Content-Type', 'application/json');
}
}
/**
* Extract chat_id from the request payload
*/
private function extractChatId(Request $request): ?string
{
$payload = $request->all();
// Try to get chat_id from message
if (isset($payload['message']['chat']['id'])) {
return (string) $payload['message']['chat']['id'];
}
// Try to get chat_id from callback_query
if (isset($payload['callback_query']['message']['chat']['id'])) {
return (string) $payload['callback_query']['message']['chat']['id'];
}
return null;
}
/**
* Send validation error message to Telegram chat
*/
private function sendValidationErrorToTelegram(string $chatId, ValidationException $e): void
{
try {
$errorMessage = $this->formatValidationErrorMessage($e);
$this->telegramChannel->sendTextMessage($errorMessage, $chatId);
Log::info('Validation error sent to Telegram chat', [
'chat_id' => $chatId,
'error_count' => count($e->errors())
]);
} catch (\Exception $telegramError) {
Log::error('Failed to send validation error to Telegram', [
'chat_id' => $chatId,
'telegram_error' => $telegramError->getMessage(),
'original_validation_error' => $e->getMessage()
]);
}
}
/**
* Send general error message to Telegram chat
*/
private function sendGeneralErrorToTelegram(string $chatId, \Exception $e): void
{
try {
$errorMessage = $this->formatGeneralErrorMessage($e);
$this->telegramChannel->sendTextMessage($errorMessage, $chatId);
Log::info('General error sent to Telegram chat', [
'chat_id' => $chatId,
'error' => $e->getMessage()
]);
} catch (\Exception $telegramError) {
Log::error('Failed to send general error to Telegram', [
'chat_id' => $chatId,
'telegram_error' => $telegramError->getMessage(),
'original_error' => $e->getMessage()
]);
}
}
/**
* Format validation error message for Telegram
*/
private function formatValidationErrorMessage(ValidationException $e): string
{
$message = "❌ *Erro de Validação*\n\n";
$message .= "Ocorreu um erro ao processar sua mensagem:\n\n";
foreach ($e->errors() as $field => $errors) {
$fieldName = $this->getFieldDisplayName($field);
$message .= "• *{$fieldName}:* " . implode(', ', $errors) . "\n";
}
$message .= "\nPor favor, tente novamente com os dados corretos.";
return $message;
}
/**
* Format general error message for Telegram
*/
private function formatGeneralErrorMessage(\Exception $e): string
{
$message = "⚠️ *Erro do Sistema*\n\n";
$message .= "Ocorreu um erro inesperado ao processar sua solicitação.\n\n";
$message .= "Por favor, tente novamente em alguns instantes.\n";
$message .= "Se o problema persistir, entre em contato com o suporte.";
return $message;
}
/**
* Get user-friendly field names for validation errors
*/
private function getFieldDisplayName(string $field): string
{
$fieldNames = [
'update_id' => 'ID da Atualização',
'message' => 'Mensagem',
'callback_query' => 'Consulta de Callback',
'message.chat.id' => 'ID do Chat',
'message.text' => 'Texto da Mensagem',
'message.from.id' => 'ID do Usuário',
'callback_query.id' => 'ID da Consulta',
'callback_query.data' => 'Dados da Consulta',
'callback_query.message.chat.id' => 'ID do Chat (Callback)',
];
return $fieldNames[$field] ?? $field;
}
}
=== Passo 3: Criar Middleware de Logging ===
loggingService->logTelegramEvent('telegram_webhook_raw_received', [
'method' => $request->method(),
'url' => $request->fullUrl(),
'headers' => $request->headers->all(),
'raw_body' => $request->getContent(),
'all_data' => $request->all(),
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'timestamp' => now()->toISOString(),
], 'info');
return $next($request);
}
}
=== Passo 4: Criar Service de Validação ===
markRequestAsProcessing($updateId);
return true;
}
// Check if already processed
if ($this->isDuplicateRequest($updateId)) {
$this->logDuplicateWebhook($updateId);
return false;
}
// Mark as processing to prevent race conditions
$this->markRequestAsProcessing($updateId);
return true;
}
/**
* Check if request is duplicate (quick cache check)
*/
private function isDuplicateRequest(int $updateId): bool
{
try {
$cacheKey = $this->getCacheKey($updateId);
return Cache::has($cacheKey);
} catch (\Exception $e) {
// If cache fails, log but don't block processing
$this->loggingService->logException($e, [
'operation' => 'check_duplicate_update_id',
'update_id' => $updateId
]);
return false;
}
}
/**
* Mark request as being processed (to prevent race conditions)
*/
private function markRequestAsProcessing(int $updateId): void
{
try {
$cacheKey = $this->getCacheKey($updateId);
Cache::put($cacheKey, true, self::CACHE_TTL);
} catch (\Exception $e) {
// If cache fails, log but don't block processing
$this->loggingService->logException($e, [
'operation' => 'mark_update_id_processed',
'update_id' => $updateId
]);
}
}
/**
* Get cache key for update_id
*/
private function getCacheKey(int $updateId): string
{
return self::CACHE_PREFIX . $updateId;
}
/**
* Log duplicate webhook detection
*/
private function logDuplicateWebhook(int $updateId): void
{
$this->loggingService->logTelegramEvent('duplicate_webhook_ignored_early', [
'update_id' => $updateId,
'message' => 'Duplicate webhook detected and ignored before processing'
], 'info');
}
/**
* Clean up processed update_id (useful for testing or manual cleanup)
*/
public function cleanupProcessedUpdate(int $updateId): bool
{
try {
$cacheKey = $this->getCacheKey($updateId);
return Cache::forget($cacheKey);
} catch (\Exception $e) {
$this->loggingService->logException($e, [
'operation' => 'cleanup_processed_update_id',
'update_id' => $updateId
]);
return false;
}
}
/**
* Get cache statistics for monitoring
*/
public function getCacheStats(): array
{
try {
// This is a simplified approach - in production you might want more sophisticated stats
return [
'cache_driver' => config('cache.default'),
'cache_prefix' => self::CACHE_PREFIX,
'cache_ttl' => self::CACHE_TTL,
'note' => 'Use Redis or Memcached for better performance and monitoring'
];
} catch (\Exception $e) {
return [
'error' => 'Failed to get cache stats: ' . $e->getMessage()
];
}
}
}
=== Passo 5: Criar Requests ===
'required|integer',
'message' => 'sometimes|array',
'callback_query' => 'sometimes|array',
'message.chat.id' => 'required_with:message|integer',
'message.from.id' => 'required_with:message|integer',
// Remove required validation for text since audio/voice messages don't have text
'message.text' => 'sometimes|string',
// Add validation for voice messages
'message.voice' => 'sometimes|array',
'message.voice.file_id' => 'required_with:message.voice|string',
'message.voice.duration' => 'sometimes|integer',
'message.voice.mime_type' => 'sometimes|string',
// Add validation for audio messages
'message.audio' => 'sometimes|array',
'message.audio.file_id' => 'required_with:message.audio|string',
'message.audio.duration' => 'sometimes|integer',
'message.audio.title' => 'sometimes|string',
'message.audio.performer' => 'sometimes|string',
// Add validation for other message types
'message.photo' => 'sometimes|array',
'message.document' => 'sometimes|array',
'message.video' => 'sometimes|array',
'message.sticker' => 'sometimes|array',
'message.location' => 'sometimes|array',
'message.contact' => 'sometimes|array',
'callback_query.id' => 'required_with:callback_query|string',
'callback_query.data' => 'required_with:callback_query|string',
'callback_query.message.chat.id' => 'required_with:callback_query|integer',
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'update_id.required' => 'Update ID is required',
'update_id.integer' => 'Update ID must be an integer',
'message.array' => 'Message must be an array',
'callback_query.array' => 'Callback query must be an array',
'message.chat.id.required_with' => 'Chat ID is required when message is present',
'message.chat.id.integer' => 'Chat ID must be an integer',
'message.from.id.required_with' => 'User ID is required when message is present',
'message.from.id.integer' => 'User ID must be an integer',
'message.text.string' => 'Message text must be a string',
'message.voice.array' => 'Voice message must be an array',
'message.voice.file_id.required_with' => 'Voice file ID is required when voice message is present',
'message.voice.file_id.string' => 'Voice file ID must be a string',
'message.voice.duration.integer' => 'Voice duration must be an integer',
'message.voice.mime_type.string' => 'Voice MIME type must be a string',
'message.audio.array' => 'Audio message must be an array',
'message.audio.file_id.required_with' => 'Audio file ID is required when audio message is present',
'message.audio.file_id.string' => 'Audio file ID must be a string',
'message.audio.duration.integer' => 'Audio duration must be an integer',
'message.audio.title.string' => 'Audio title must be a string',
'message.audio.performer.string' => 'Audio performer must be a string',
'callback_query.id.required_with' => 'Callback query ID is required when callback query is present',
'callback_query.data.required_with' => 'Callback data is required when callback query is present',
'callback_query.message.chat.id.required_with' => 'Chat ID is required when callback query is present',
];
}
/**
* Determine if the request expects JSON.
*
* @return bool
*/
public function expectsJson(): bool
{
return true; // Always expect JSON for webhook requests
}
/**
* Determine if the request is asking for JSON.
*
* @return bool
*/
public function wantsJson(): bool
{
return true; // Always want JSON for webhook requests
}
/**
* Handle a failed validation attempt.
* Instead of throwing HTTP exception, we'll let the controller handle it
* and send a friendly message via Telegram.
*/
protected function failedValidation(\Illuminate\Contracts\Validation\Validator $validator): void
{
// Log detalhes da validação falhada
$this->loggingService->logTelegramEvent('telegram_webhook_validation_failed', [
'error' => 'Webhook validation failed',
'validation_errors' => $validator->errors()->toArray(),
'request_data' => $this->all(),
'request_headers' => $this->headers->all(),
'ip' => $this->ip(),
'user_agent' => $this->userAgent(),
'timestamp' => now()->toISOString(),
'validation_rules' => $this->rules(),
'failed_fields' => array_keys($validator->errors()->toArray()),
'request_size' => strlen($this->getContent()),
'content_type' => $this->header('Content-Type'),
'telegram_update_id' => $this->input('update_id'),
'message_type' => $this->getMessageType(),
'has_message' => $this->has('message'),
'has_callback_query' => $this->has('callback_query')
], 'error');
// Store validation errors in the request for the controller to handle
$this->merge(['validation_errors' => $validator->errors()->toArray()]);
// Don't throw exception - let controller handle it gracefully
// parent::failedValidation($validator);
}
/**
* Get the message type from the request
*/
private function getMessageType(): string
{
if ($this->has('callback_query')) {
return 'callback_query';
}
if ($this->has('message')) {
$message = $this->input('message', []);
if (isset($message['text'])) {
return 'text_message';
}
if (isset($message['voice'])) {
return 'voice_message';
}
if (isset($message['audio'])) {
return 'audio_message';
}
if (isset($message['photo'])) {
return 'photo_message';
}
if (isset($message['document'])) {
return 'document_message';
}
if (isset($message['video'])) {
return 'video_message';
}
if (isset($message['sticker'])) {
return 'sticker_message';
}
if (isset($message['location'])) {
return 'location_message';
}
if (isset($message['contact'])) {
return 'contact_message';
}
return 'unknown_message_type';
}
return 'no_message';
}
/**
* Log validation attempt for debugging
*/
public function validateResolved(): void
{
// Log successful validation
// $this->loggingService->logTelegramEvent('telegram_webhook_validation_success', [
// 'success' => 'Webhook validation passed successfully',
// 'message_type' => $this->getMessageType(),
// 'telegram_update_id' => $this->input('update_id'),
// 'has_message' => $this->has('message'),
// 'has_callback_query' => $this->has('callback_query'),
// 'message_content_types' => $this->getMessageContentTypes(),
// 'timestamp' => now()->toISOString()
// ], 'info');
parent::validateResolved();
}
/**
* Get all content types present in the message
*/
private function getMessageContentTypes(): array
{
if (!$this->has('message')) {
return [];
}
$message = $this->input('message', []);
$contentTypes = [];
if (isset($message['text'])) {
$contentTypes[] = 'text';
}
if (isset($message['voice'])) {
$contentTypes[] = 'voice';
}
if (isset($message['audio'])) {
$contentTypes[] = 'audio';
}
if (isset($message['photo'])) {
$contentTypes[] = 'photo';
}
if (isset($message['document'])) {
$contentTypes[] = 'document';
}
if (isset($message['video'])) {
$contentTypes[] = 'video';
}
if (isset($message['sticker'])) {
$contentTypes[] = 'sticker';
}
if (isset($message['location'])) {
$contentTypes[] = 'location';
}
if (isset($message['contact'])) {
$contentTypes[] = 'contact';
}
return $contentTypes;
}
}
'required|url|max:255',
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'webhook_url.required' => 'Webhook URL is required',
'webhook_url.url' => 'Webhook URL must be a valid URL',
'webhook_url.max' => 'Webhook URL cannot exceed 255 characters',
];
}
}
=== Passo 6: Registrar Middleware ===
// app/Http/Kernel.php
protected $middlewareAliases = [
// ... outros middleware
'telegram.webhook.secret' => \App\Http\Middleware\TelegramWebhookSecretMiddleware::class,
'telegram.webhook.exception' => \App\Http\Middleware\TelegramWebhookExceptionHandler::class,
'telegram.webhook.logging' => \App\Http\Middleware\TelegramWebhookLoggingMiddleware::class,
];
===== 🧪 Testes =====
=== Teste Unitário de Validação ===
loggingService = Mockery::mock(LoggingServiceInterface::class);
$this->service = new TelegramWebhookValidationService($this->loggingService);
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
public function test_validate_and_mark_processing_without_update_id_returns_true()
{
$result = $this->service->validateAndMarkProcessing(null);
$this->assertTrue($result);
}
public function test_service_can_be_instantiated()
{
$this->assertInstanceOf(TelegramWebhookValidationService::class, $this->service);
}
public function test_service_has_required_methods()
{
$this->assertTrue(method_exists($this->service, 'validateAndMarkProcessing'));
$this->assertTrue(method_exists($this->service, 'cleanupProcessedUpdate'));
$this->assertTrue(method_exists($this->service, 'getCacheStats'));
}
}
===== ✅ Validação do Módulo =====
=== Checklist de Implementação ===
- [ ] Middleware `TelegramWebhookSecretMiddleware` criado
- [ ] Middleware `TelegramWebhookExceptionHandler` criado
- [ ] Middleware `TelegramWebhookLoggingMiddleware` criado
- [ ] Service `TelegramWebhookValidationService` implementado
- [ ] Request `TelegramWebhookRequest` criado
- [ ] Request `TelegramWebhookSetupRequest` criado
- [ ] Middleware registrado no Kernel
- [ ] Testes unitários implementados
- [ ] Integração com sistemas anteriores
=== Comandos de Validação ===
# Executar testes
php artisan test tests/Unit/TelegramWebhookValidationServiceTest.php
# Verificar middleware registrado
php artisan route:list --middleware=telegram.webhook.secret
# Testar validação
php artisan tinker
# >>> $service = app(\App\Services\Telegram\TelegramWebhookValidationService::class);
# >>> $service->validateWebhookPayload(['update_id' => 123, 'message' => ['message_id' => 1, 'from' => ['id' => 123], 'chat' => ['id' => 123], 'date' => time()]]);