[[: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()]]);