Guia completo para implementação do sistema de Telegram Webhook em Laravel, baseado no projeto Rei do Óleo. Este sistema inclui processamento de mensagens, validação de webhooks, tratamento de erros e sistema de comandos unificado.
implementar telegram webhook laravel completo
Antes de implementar, certifique-se de ter:
O sistema será implementado com a seguinte estrutura:
app/
├── Http/
│ ├── Controllers/Api/
│ │ ├── TelegramWebhookController.php
│ │ └── TelegramStatsController.php
│ ├── Middleware/
│ │ ├── TelegramWebhookSecretMiddleware.php
│ │ ├── TelegramWebhookExceptionHandler.php
│ │ └── TelegramWebhookLoggingMiddleware.php
│ ├── Requests/
│ │ ├── TelegramWebhookRequest.php
│ │ └── TelegramWebhookSetupRequest.php
│ └── Resources/
│ └── TelegramWebhookResource.php
├── Services/
│ ├── TelegramWebhookService.php
│ ├── TelegramBotService.php
│ ├── TelegramMessageProcessorService.php
│ └── Telegram/
│ ├── TelegramWebhookValidationService.php
│ └── TelegramMessageProcessorService.php
└── Console/Commands/
└── TestTelegramWebhook.php
1. Configurar variáveis de ambiente (.env):
TELEGRAM_ENABLED=true TELEGRAM_BOT_TOKEN=seu_bot_token_aqui TELEGRAM_WEBHOOK_SECRET=seu_secret_aqui TELEGRAM_RECIPIENTS=123456789,987654321
2. Configurar services.php:
'telegram' => [
'enabled' => env('TELEGRAM_ENABLED', false),
'bot_token' => env('TELEGRAM_BOT_TOKEN'),
'webhook_secret' => env('TELEGRAM_WEBHOOK_SECRET', ''),
'recipients' => explode(',', env('TELEGRAM_RECIPIENTS', '')),
],
Próximos passos serão adicionados gradualmente…
1. Webhook não recebe mensagens:
2. Erro de validação:
3. Comandos não funcionam:
As próximas seções serão adicionadas gradualmente:
<?php namespace App\Contracts; use Illuminate\Http\Request; interface LoggingServiceInterface { /** * Log API requests and responses */ public function logApiRequest(Request $request, array $context = []): void; /** * Log API responses */ public function logApiResponse(int $statusCode, array $response, float $duration, array $context = []): void; /** * Log business operations */ public function logBusinessOperation(string $operation, array $data, string $status = 'success', array $context = []): void; /** * Log security events */ public function logSecurityEvent(string $event, array $data, string $level = 'warning', array $context = []): void; /** * Log performance metrics */ public function logPerformance(string $operation, float $duration, array $metrics = [], array $context = []): void; /** * Log audit trail */ public function logAudit(string $action, string $model, int $modelId, array $changes = [], array $context = []): void; /** * Log Telegram bot events */ public function logTelegramEvent(string $event, array $data, string $level = 'info', array $context = []): void; /** * Log WhatsApp events */ public function logWhatsAppEvent(string $event, array $data, string $level = 'info', array $context = []): void; /** * Log exceptions with context */ public function logException(\Throwable $exception, array $context = []): void; /** * Get log statistics */ public function getLogStats(): array; }
<?php namespace App\Services; use App\Contracts\LoggingServiceInterface; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Auth; use Illuminate\Http\Request; use Spatie\Activitylog\ActivityLogger; class LoggingService implements LoggingServiceInterface { /** * Log API requests and responses */ public function logApiRequest(Request $request, array $context = []): void { $logData = array_merge([ 'request_id' => uniqid('api_', true), 'method' => $request->method(), 'url' => $request->fullUrl(), 'ip' => $request->ip(), 'user_agent' => $request->userAgent(), 'user_id' => $request->user()?->id, 'headers' => $this->sanitizeHeaders($request->headers->all()), 'timestamp' => now()->toISOString(), ], $context); if ($request->method() !== 'GET') { $logData['body'] = $this->sanitizeBody($request->all()); } Log::channel('api')->info('API Request', $logData); } /** * Log API responses */ public function logApiResponse(int $statusCode, array $response, float $duration, array $context = []): void { $logData = array_merge([ 'status_code' => $statusCode, 'duration_ms' => round($duration, 2), 'response_size' => strlen(json_encode($response)), 'timestamp' => now()->toISOString(), ], $context); if ($statusCode >= 400) { $logData['error_response'] = $response; Log::channel('api')->error('API Error Response', $logData); } else { Log::channel('api')->info('API Response', $logData); } } /** * Log business operations */ public function logBusinessOperation(string $operation, array $data, string $status = 'success', array $context = []): void { $logData = array_merge([ 'operation' => $operation, 'status' => $status, 'user_id' => Auth::id(), 'timestamp' => now()->toISOString(), ], $data, $context); Log::channel('business')->info('Business Operation', $logData); } /** * Log security events */ public function logSecurityEvent(string $event, array $data, string $level = 'warning', array $context = []): void { $logData = array_merge([ 'event' => $event, 'level' => $level, 'user_id' => Auth::id(), 'ip' => request()->ip(), 'user_agent' => request()->userAgent(), 'timestamp' => now()->toISOString(), ], $data, $context); Log::channel('security')->$level('Security Event', $logData); } /** * Log performance metrics */ public function logPerformance(string $operation, float $duration, array $metrics = [], array $context = []): void { $logData = array_merge([ 'operation' => $operation, 'duration_ms' => round($duration, 2), 'memory_usage' => memory_get_usage(true), 'memory_peak' => memory_get_peak_usage(true), 'timestamp' => now()->toISOString(), ], $metrics, $context); Log::channel('performance')->info('Performance Metric', $logData); } /** * Log audit trail */ public function logAudit(string $action, string $model, int $modelId, array $changes = [], array $context = []): void { $logData = array_merge([ 'action' => $action, 'model' => $model, 'model_id' => $modelId, 'changes' => $changes, 'user_id' => Auth::id(), 'timestamp' => now()->toISOString(), ], $context); Log::channel('audit')->info('Audit Trail', $logData); } /** * Log Telegram bot events */ public function logTelegramEvent(string $event, array $data, string $level = 'info', array $context = []): void { $logData = array_merge([ 'event' => $event, 'level' => $level, 'timestamp' => now()->toISOString(), ], $data, $context); Log::channel('telegram')->$level('Telegram Event', $logData); } /** * Log WhatsApp events */ public function logWhatsAppEvent(string $event, array $data, string $level = 'info', array $context = []): void { $logData = array_merge([ 'event' => $event, 'level' => $level, 'timestamp' => now()->toISOString(), ], $data, $context); Log::channel('whatsapp')->$level('WhatsApp Event', $logData); } /** * Log exceptions with context */ public function logException(\Throwable $exception, array $context = []): void { $logData = array_merge([ 'exception' => get_class($exception), 'message' => $exception->getMessage(), 'file' => $exception->getFile(), 'line' => $exception->getLine(), 'trace' => $exception->getTraceAsString(), 'user_id' => Auth::id(), 'timestamp' => now()->toISOString(), ], $context); Log::channel('errors')->error('Exception', $logData); } /** * Get log statistics */ public function getLogStats(): array { return [ 'channels' => ['api', 'business', 'security', 'performance', 'audit', 'telegram', 'whatsapp', 'errors'], 'timestamp' => now()->toISOString(), ]; } /** * Sanitize headers for logging */ private function sanitizeHeaders(array $headers): array { $sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-csrf-token']; foreach ($sensitiveHeaders as $header) { if (isset($headers[$header])) { $headers[$header] = ['***REDACTED***']; } } return $headers; } /** * Sanitize request body for logging */ private function sanitizeBody(array $body): array { $sensitiveFields = ['password', 'token', 'secret', 'key', 'api_key']; foreach ($sensitiveFields as $field) { if (isset($body[$field])) { $body[$field] = '***REDACTED***'; } } return $body; } }
<?php namespace App\Contracts; interface MessageFlowTrackerInterface { // Core tracking methods public function trackMethod(string $className, string $methodName, array $inputData = [], array $outputData = []): void; public function endMethod(string $className, string $methodName, array $outputData = []): void; // Report generation public function generateFlowReport(array $payload): string; public function generateUserReport(array $payload): string; // Pipeline control public function startTracking(string $messageId): void; public function endTracking(): void; public function clearTrace(): void; // Data retrieval public function getExecutedMethods(): array; public function getExpectedMethods(): array; public function getMethodData(): array; // Utility methods public function identifyMessageType(array $payload): string; public function saveTrace(): void; public function isEnabled(): bool; }
<?php namespace App\Services; use App\Contracts\MessageFlowTrackerInterface; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Cache; class MessageFlowTrackerService implements MessageFlowTrackerInterface { private array $executedMethods = []; private array $expectedMethods = []; private array $methodData = []; private array $timestamps = []; private string $messageId; private bool $isEnabled; public function __construct() { $this->isEnabled = config('message-flow.enabled', false); $this->messageId = uniqid('msg_', true); // Define o fluxo esperado para cada tipo de mensagem $this->expectedMethods = [ 'text' => [ 'TelegramMessageProcessorService.processWebhookPayload', 'TelegramMessageProcessorService.processTextMessage', 'TelegramBotService.processMessage', 'TelegramChannel.sendMessageWithKeyboard' ], 'voice' => [ 'TelegramMessageProcessorService.processWebhookPayload', 'TelegramMessageProcessorService.processVoiceMessage', 'SpeechToTextService.convertVoiceToText', 'TelegramBotService.processMessage', 'TelegramChannel.sendMessageWithKeyboard' ], 'callback_query' => [ 'TelegramMessageProcessorService.processWebhookPayload', 'TelegramMessageProcessorService.processCallbackQuery', 'TelegramBotService.processCallbackQuery', 'TelegramChannel.sendTextMessage' ] ]; } /** * Track method execution */ public function trackMethod(string $className, string $methodName, array $inputData = [], array $outputData = []): void { if (!$this->isEnabled) { return; } $methodKey = "{$className}.{$methodName}"; $this->executedMethods[] = $methodKey; $this->timestamps[$methodKey] = microtime(true); $this->methodData[$methodKey] = [ 'input' => $inputData, 'output' => $outputData, 'timestamp' => now()->toISOString() ]; } /** * End method tracking */ public function endMethod(string $className, string $methodName, array $outputData = []): void { if (!$this->isEnabled) { return; } $methodKey = "{$className}.{$methodName}"; if (isset($this->methodData[$methodKey])) { $this->methodData[$methodKey]['output'] = $outputData; $this->methodData[$methodKey]['duration'] = microtime(true) - $this->timestamps[$methodKey]; } } /** * Generate flow report */ public function generateFlowReport(array $payload): string { if (!$this->isEnabled) { return 'Message flow tracking is disabled'; } $messageType = $this->identifyMessageType($payload); $expected = $this->expectedMethods[$messageType] ?? []; $missing = array_diff($expected, $this->executedMethods); $extra = array_diff($this->executedMethods, $expected); $report = "=== MESSAGE FLOW REPORT ===\n"; $report .= "Message Type: {$messageType}\n"; $report .= "Message ID: {$this->messageId}\n"; $report .= "Executed Methods: " . count($this->executedMethods) . "\n"; $report .= "Expected Methods: " . count($expected) . "\n"; $report .= "Missing Methods: " . count($missing) . "\n"; $report .= "Extra Methods: " . count($extra) . "\n\n"; if (!empty($missing)) { $report .= "MISSING METHODS:\n"; foreach ($missing as $method) { $report .= " - {$method}\n"; } $report .= "\n"; } if (!empty($extra)) { $report .= "EXTRA METHODS:\n"; foreach ($extra as $method) { $report .= " - {$method}\n"; } $report .= "\n"; } return $report; } /** * Generate user-friendly report */ public function generateUserReport(array $payload): string { if (!$this->isEnabled) { return 'Tracking disabled'; } $messageType = $this->identifyMessageType($payload); $expected = $this->expectedMethods[$messageType] ?? []; $missing = array_diff($expected, $this->executedMethods); if (empty($missing)) { return "✅ Message processed successfully through all expected steps"; } $report = "⚠️ Message processing incomplete:\n"; $report .= "Missing steps: " . count($missing) . "\n"; foreach ($missing as $method) { $report .= "• " . str_replace('.', ' → ', $method) . "\n"; } return $report; } /** * Start tracking */ public function startTracking(string $messageId): void { $this->messageId = $messageId; $this->clearTrace(); } /** * End tracking */ public function endTracking(): void { if ($this->isEnabled) { $this->saveTrace(); } } /** * Clear trace */ public function clearTrace(): void { $this->executedMethods = []; $this->methodData = []; $this->timestamps = []; } /** * Get executed methods */ public function getExecutedMethods(): array { return $this->executedMethods; } /** * Get expected methods */ public function getExpectedMethods(): array { return $this->expectedMethods; } /** * Get method data */ public function getMethodData(): array { return $this->methodData; } /** * Identify message type */ public function identifyMessageType(array $payload): string { if (isset($payload['callback_query'])) { return 'callback_query'; } if (isset($payload['message'])) { $message = $payload['message']; if (isset($message['text'])) { return 'text'; } if (isset($message['voice'])) { return 'voice'; } if (isset($message['audio'])) { return 'audio'; } } return 'unknown'; } /** * Save trace to cache */ public function saveTrace(): void { if (!$this->isEnabled) { return; } try { $traceData = [ 'message_id' => $this->messageId, 'executed_methods' => $this->executedMethods, 'method_data' => $this->methodData, 'timestamp' => now()->toISOString() ]; Cache::put("message_trace_{$this->messageId}", $traceData, 60); // 1 hour } catch (\Exception $e) { Log::error('Failed to save message trace', [ 'error' => $e->getMessage(), 'message_id' => $this->messageId ]); } } /** * Check if tracking is enabled */ public function isEnabled(): bool { return $this->isEnabled; } }
<?php namespace App\Contracts; interface NotificationChannelInterface { /** * Send text message * * @param string $message * @param string|null $recipient * @return array */ public function sendTextMessage(string $message, ?string $recipient = null): array; /** * Send notification with data * * @param array $data * @return array */ public function sendNotification(array $data): array; /** * Test connection * * @return array */ public function testConnection(): array; /** * Get channel name * * @return string */ public function getChannelName(): string; /** * Check if channel is enabled * * @return bool */ public function isEnabled(): bool; }
<?php namespace App\Contracts; /** * Interface para métodos de tracking de mensagens * Esta interface define os métodos que devem estar disponíveis * para qualquer classe que precise rastrear execução de métodos */ interface MessageTrackingInterface { /** * Inicia o tracking de um método */ public function trackMethod(string $methodName, array $inputData = [], array $outputData = []): void; /** * Finaliza o tracking de um método */ public function endMethod(string $methodName, array $outputData = []): void; /** * Inicializa o sistema de tracking */ public function initializeTracking(): void; }
<?php // app/Providers/LoggingServiceProvider.php namespace App\Providers; use Illuminate\Support\ServiceProvider; use App\Contracts\LoggingServiceInterface; use App\Services\LoggingService; class LoggingServiceProvider extends ServiceProvider { public function register(): void { $this->app->bind(LoggingServiceInterface::class, LoggingService::class); } public function boot(): void { // Publish configuration if needed } }
<?php // app/Providers/MessageFlowServiceProvider.php namespace App\Providers; use App\Contracts\MessageFlowTrackerInterface; use App\Services\MessageFlowTrackerService; use Illuminate\Support\ServiceProvider; class MessageFlowServiceProvider extends ServiceProvider { public function register(): void { $this->app->singleton(MessageFlowTrackerInterface::class, MessageFlowTrackerService::class); } public function boot(): void { // Publish configuration if needed } }
// Adicionar no array 'providers' do config/app.php 'providers' => [ // ... outros providers App\Providers\LoggingServiceProvider::class, App\Providers\MessageFlowServiceProvider::class, // ... outros providers ],
<?php return [ // ... outras configurações 'telegram' => [ 'enabled' => env('TELEGRAM_ENABLED', false), 'bot_token' => env('TELEGRAM_BOT_TOKEN'), 'webhook_secret' => env('TELEGRAM_WEBHOOK_SECRET', ''), 'recipients' => array_filter(explode(',', env('TELEGRAM_RECIPIENTS', ''))), ], // ... outras configurações ];
# ============================================================================ # TELEGRAM BOT CONFIGURATION # ============================================================================ # Enable/Disable Telegram Bot TELEGRAM_ENABLED=true # Bot Token from @BotFather TELEGRAM_BOT_TOKEN=your_bot_token_here # Webhook Secret for Security (optional but recommended) TELEGRAM_WEBHOOK_SECRET=your_webhook_secret_here # Recipients (Chat IDs separated by commas) TELEGRAM_RECIPIENTS=123456789,987654321 # ============================================================================ # MESSAGE FLOW TRACKING CONFIGURATION # ============================================================================ # Enable/Disable Message Flow Tracking MESSAGE_FLOW_TRACKING_ENABLED=true # Output Configuration MESSAGE_FLOW_SEND_TO_USER=true MESSAGE_FLOW_LOG_TO_FILE=true MESSAGE_FLOW_CACHE_TRACES=true # Performance Thresholds (milliseconds) MESSAGE_FLOW_SLOW_THRESHOLD=100 MESSAGE_FLOW_VERY_SLOW_THRESHOLD=500 # Memory Thresholds (MB) MESSAGE_FLOW_MEMORY_WARNING=50 MESSAGE_FLOW_MEMORY_CRITICAL=100 # Storage TTL (minutes) MESSAGE_FLOW_CACHE_TTL=1440 MESSAGE_FLOW_DB_TTL=10080
<?php return [ // ... outras configurações 'channels' => [ // ... outros canais 'api' => [ 'driver' => 'daily', 'path' => storage_path('logs/api.log'), 'level' => 'info', 'days' => 30, ], 'business' => [ 'driver' => 'daily', 'path' => storage_path('logs/business.log'), 'level' => 'info', 'days' => 90, ], 'security' => [ 'driver' => 'daily', 'path' => storage_path('logs/security.log'), 'level' => 'warning', 'days' => 365, ], 'performance' => [ 'driver' => 'daily', 'path' => storage_path('logs/performance.log'), 'level' => 'info', 'days' => 30, ], 'audit' => [ 'driver' => 'daily', 'path' => storage_path('logs/audit.log'), 'level' => 'info', 'days' => 365, ], 'telegram' => [ 'driver' => 'daily', 'path' => storage_path('logs/telegram.log'), 'level' => 'info', 'days' => 30, ], 'whatsapp' => [ 'driver' => 'daily', 'path' => storage_path('logs/whatsapp.log'), 'level' => 'info', 'days' => 30, ], 'errors' => [ 'driver' => 'daily', 'path' => storage_path('logs/errors.log'), 'level' => 'error', 'days' => 30, ], ], ];
<?php namespace App\Services; use App\Contracts\LoggingServiceInterface; use App\Contracts\MessageFlowTrackerInterface; use App\Contracts\MessageTrackingInterface; use App\Services\Channels\TelegramChannel; class TelegramMessageProcessorService implements MessageTrackingInterface { public function __construct( private TelegramBotService $telegramBotService, private TelegramChannel $telegramChannel, private LoggingServiceInterface $loggingService, private MessageFlowTrackerInterface $flowTracker, private ?SpeechToTextService $speechService = null ) { $this->initializeTracking(); } /** * Process webhook payload */ public function processWebhookPayload(array $payload): array { // Inicia tracking $this->trackMethod('processWebhookPayload', ['payload' => $payload]); try { // Check if it's a callback query (button click) if (isset($payload['callback_query'])) { $result = $this->processCallbackQuery($payload['callback_query']); $this->endMethod('processWebhookPayload', ['result' => $result]); return $result; } // Verify if it's a message if (!isset($payload['message'])) { $result = [ 'success' => false, 'status' => 'ignored', 'message' => 'No message in payload' ]; $this->endMethod('processWebhookPayload', ['result' => $result]); return $result; } $message = $payload['message']; // Process different message types if (isset($message['text'])) { $result = $this->processTextMessage($message); $this->endMethod('processWebhookPayload', ['result' => $result]); return $result; } if (isset($message['voice'])) { $result = $this->processVoiceMessage($message); $this->endMethod('processWebhookPayload', ['result' => $result]); return $result; } if (isset($message['audio'])) { $result = $this->processAudioMessage($message); $this->endMethod('processWebhookPayload', ['result' => $result]); return $result; } $result = $this->createIgnoredResult('Unsupported message type'); $this->endMethod('processWebhookPayload', ['result' => $result]); return $result; } catch (\Exception $e) { $result = [ 'success' => false, 'status' => 'error', 'message' => 'Internal server error', 'error' => $e->getMessage() ]; $this->endMethod('processWebhookPayload', ['result' => $result, 'error' => $e->getMessage()]); $this->loggingService->logException($e, [ 'operation' => 'telegram_webhook_processing', 'payload' => $payload ]); return $result; } } /** * Process text message */ private function processTextMessage(array $message): array { $this->trackMethod('processTextMessage', ['message' => $message]); $result = $this->telegramBotService->processMessage($message); $result['status'] = 'success'; $result['message'] = 'Text message processed'; $result['input_type'] = 'text'; $this->endMethod('processTextMessage', ['result' => $result]); return $result; } /** * Process voice message */ private function processVoiceMessage(array $message): array { $this->trackMethod('processVoiceMessage', ['message' => $message]); try { $chatId = $message['chat']['id']; if (!$this->speechService) { $result = $this->createIgnoredResult('Speech service not available'); $this->endMethod('processVoiceMessage', ['result' => $result]); return $result; } // Convert voice to text $voiceFileId = $message['voice']['file_id']; $text = $this->speechService->convertVoiceToText($voiceFileId); if (empty($text)) { $result = $this->createIgnoredResult('Could not convert voice to text'); $this->endMethod('processVoiceMessage', ['result' => $result]); return $result; } // Process the converted text as a regular message $textMessage = array_merge($message, ['text' => $text]); $result = $this->processTextMessage($textMessage); $result['input_type'] = 'voice'; $result['converted_text'] = $text; $this->endMethod('processVoiceMessage', ['result' => $result]); return $result; } catch (\Exception $e) { $result = [ 'success' => false, 'status' => 'error', 'message' => 'Voice processing failed', 'error' => $e->getMessage() ]; $this->endMethod('processVoiceMessage', ['result' => $result, 'error' => $e->getMessage()]); return $result; } } /** * Process audio message */ private function processAudioMessage(array $message): array { $this->trackMethod('processAudioMessage', ['message' => $message]); try { $chatId = $message['chat']['id']; if (!$this->speechService) { $result = $this->createIgnoredResult('Speech service not available'); $this->endMethod('processAudioMessage', ['result' => $result]); return $result; } // Convert audio to text $audioFileId = $message['audio']['file_id']; $text = $this->speechService->convertAudioToText($audioFileId); if (empty($text)) { $result = $this->createIgnoredResult('Could not convert audio to text'); $this->endMethod('processAudioMessage', ['result' => $result]); return $result; } // Process the converted text as a regular message $textMessage = array_merge($message, ['text' => $text]); $result = $this->processTextMessage($textMessage); $result['input_type'] = 'audio'; $result['converted_text'] = $text; $this->endMethod('processAudioMessage', ['result' => $result]); return $result; } catch (\Exception $e) { $result = [ 'success' => false, 'status' => 'error', 'message' => 'Audio processing failed', 'error' => $e->getMessage() ]; $this->endMethod('processAudioMessage', ['result' => $result, 'error' => $e->getMessage()]); return $result; } } /** * Process callback query */ private function processCallbackQuery(array $callbackQuery): array { $this->trackMethod('processCallbackQuery', ['callback_query' => $callbackQuery]); $result = $this->telegramBotService->processCallbackQuery($callbackQuery); $result['status'] = 'success'; $result['message'] = 'Callback query processed'; $result['input_type'] = 'callback_query'; $this->endMethod('processCallbackQuery', ['result' => $result]); return $result; } /** * Create ignored result */ private function createIgnoredResult(string $message): array { return [ 'success' => false, 'status' => 'ignored', 'message' => $message, 'type' => 'ignored' ]; } // ======================================== // MessageTrackingInterface Implementation // ======================================== /** * Inicializa o sistema de tracking */ public function initializeTracking(): void { // Tracking já é inicializado no construtor } /** * Inicia o tracking de um método */ public function trackMethod(string $methodName, array $inputData = [], array $outputData = []): void { $className = class_basename($this); $this->flowTracker->trackMethod($className, $methodName, $inputData, $outputData); } /** * Finaliza o tracking de um método */ public function endMethod(string $methodName, array $outputData = []): void { $className = class_basename($this); $this->flowTracker->endMethod($className, $methodName, $outputData); } }
Continue para a próxima seção quando estiver pronto!
<?php namespace App\Services\Telegram; use App\Services\Telegram\Commands\UnifiedCommandSystem; use App\Services\Telegram\Commands\CommandResult; use App\Services\Channels\TelegramChannel; use App\Services\SpeechToTextService; use App\Contracts\LoggingServiceInterface; class TelegramMessageProcessorService { public function __construct( private UnifiedCommandSystem $commandSystem, private TelegramAuthorizationService $authorizationService, private ?SpeechToTextService $speechService = null, private ?TelegramChannel $telegramChannel = null, private ?LoggingServiceInterface $loggingService = null ) {} public function processMessage(array $payload): array { try { // Check if it's a callback query (button click) if (isset($payload['callback_query'])) { return $this->processCallbackQuery($payload['callback_query']); } // Verify if it's a message if (!isset($payload['message'])) { $result = [ 'success' => false, 'status' => 'ignored', 'message' => 'No message in payload' ]; return $result; } $message = $payload['message']; // Process different message types if (isset($message['text'])) { return $this->processTextMessage($message); } if (isset($message['voice'])) { return $this->processVoiceMessage($message); } if (isset($message['audio'])) { return $this->processAudioMessage($message); } return $this->createIgnoredResult('Unsupported message type'); } catch (\Exception $e) { $result = [ 'success' => false, 'status' => 'error', 'message' => 'Internal server error', 'error' => $e->getMessage() ]; if ($this->loggingService) { $this->loggingService->logException($e, [ 'operation' => 'telegram_webhook_processing', 'payload' => $payload ]); } return $result; } } private function processTextMessage(array $message): array { $chatId = $message['chat']['id']; $text = $message['text'] ?? ''; if (empty($text)) { return $this->createEmptyMessageResponse($chatId); } // Process command through unified system $result = $this->commandSystem->processCommand($text, $this->getContextFromMessage($message)); if ($result->isSuccess()) { return $this->createSuccessResponse($chatId, $result); } else { return $this->createCommandNotFoundResponse($chatId, $result); } } private function processVoiceMessage(array $message): array { try { $chatId = $message['chat']['id']; $voice = $message['voice']; // Check if speech service is available if (!$this->speechService) { if ($this->telegramChannel) { $this->telegramChannel->sendTextMessage( "❌ Serviço de reconhecimento de voz não está disponível. Envie uma mensagem de texto.", (string) $chatId ); } return $this->createVoiceConversionErrorResponse($chatId); } // Send processing message if ($this->telegramChannel) { $this->telegramChannel->sendTextMessage( "🎤 Processando mensagem de voz...", (string) $chatId ); } // Download voice file $voiceFilePath = $this->downloadVoiceFile($voice['file_id']); if (!$voiceFilePath) { return $this->createVoiceConversionErrorResponse($chatId); } // Convert voice to text with error handling try { $text = $this->speechService->convertVoiceToText($voiceFilePath); } catch (\Exception $speechException) { if ($this->loggingService) { $this->loggingService->logException($speechException, [ 'operation' => 'speech_to_text_conversion', 'chat_id' => $chatId, 'file_path' => $voiceFilePath ]); } // Send error message to user if ($this->telegramChannel) { $this->telegramChannel->sendTextMessage( "❌ Erro ao processar mensagem de voz. Tente novamente ou envie uma mensagem de texto.", (string) $chatId ); } return $this->createVoiceConversionErrorResponse($chatId); } if (!$text) { if ($this->loggingService) { $this->loggingService->logException(new \Exception('Failed to convert voice to text'), [ 'chat_id' => $chatId, 'file_path' => $voiceFilePath ]); } // Send error message to user if ($this->telegramChannel) { $this->telegramChannel->sendTextMessage( "❌ Não foi possível reconhecer o texto da mensagem de voz. Tente novamente.", (string) $chatId ); } return $this->createVoiceConversionErrorResponse($chatId); } // Clean up voice file if (file_exists($voiceFilePath)) { unlink($voiceFilePath); } // Send recognized text to user if ($this->telegramChannel) { $this->telegramChannel->sendTextMessage( "🎯 Texto reconhecido: *{$text}*", (string) $chatId ); } // Update context for voice processing $context = $this->getContextFromMessage($message); $context['type'] = 'voice'; $context['original_voice'] = $text; // Process converted text through unified system $result = $this->commandSystem->processCommand($text, $context); if ($result->isSuccess()) { return $this->createSuccessResponse($chatId, $result); } else { return $this->createCommandNotFoundResponse($chatId, $result); } } catch (\Exception $e) { if ($this->loggingService) { $this->loggingService->logException($e, [ 'operation' => 'voice_message_processing', 'chat_id' => $message['chat']['id'] ?? null, 'message' => $message ]); } return $this->createVoiceConversionErrorResponse($chatId); } } private function processCallbackQuery(array $callbackQuery): array { $chatId = $callbackQuery['message']['chat']['id'] ?? 0; $callbackData = $callbackQuery['data'] ?? ''; if (empty($callbackData)) { return $this->createEmptyCallbackResponse($chatId); } // Process callback through unified system $result = $this->commandSystem->processCommand($callbackData, $this->getContextFromCallbackQuery($callbackQuery)); if ($result->isSuccess()) { return $this->createSuccessResponse($chatId, $result); } else { return $this->createCallbackErrorResponse($chatId, $result); } } private function getContextFromMessage(array $message): array { // Check if this is a callback query if (isset($message['callback_query'])) { return $this->getContextFromCallbackQuery($message['callback_query']); } // Regular message return [ 'chat_id' => $message['chat']['id'], 'user_id' => $message['from']['id'] ?? null, 'type' => $this->determineMessageType($message), 'timestamp' => $message['date'] ?? time(), 'user_permissions' => $this->getUserPermissions($message) ]; } private function getContextFromCallbackQuery(array $callbackQuery): array { return [ 'chat_id' => $callbackQuery['message']['chat']['id'] ?? 0, 'user_id' => $callbackQuery['from']['id'] ?? null, 'type' => 'callback_query', 'timestamp' => $callbackQuery['date'] ?? time(), 'user_permissions' => $this->getUserPermissions($callbackQuery) ]; } private function determineMessageType(array $message): string { if (isset($message['callback_query'])) { return 'callback_query'; } if (isset($message['voice'])) { return 'voice'; } if (isset($message['audio'])) { return 'audio'; } if (isset($message['text'])) { return 'text'; } return 'unknown'; } private function getUserPermissions(array $message): array { // Extract user permissions from message or callback query $userId = null; if (isset($message['from']['id'])) { // Regular message $userId = $message['from']['id']; } elseif (isset($message['callback_query']['from']['id'])) { // Callback query wrapped in payload $userId = $message['callback_query']['from']['id']; } if (!$userId) { return ['user']; } // TODO: Implement actual permission checking // For now, return default permissions return ['user']; } private function createSuccessResponse(int $chatId, CommandResult $result): array { $data = $result->getData(); return [ 'success' => true, 'chat_id' => $chatId, 'type' => $result->getType(), 'data' => $data, 'command_info' => $result->getCommandMatch() ? [ 'command_id' => $result->getCommandMatch()->getCommand()->getId(), 'confidence' => $result->getCommandMatch()->getConfidence() ] : null ]; } private function createCommandNotFoundResponse(int $chatId, CommandResult $result): array { $data = $result->getData(); $fallbackMessage = $data['fallback_message'] ?? 'Comando não encontrado'; // Send the fallback message to the user in Telegram chat if ($this->telegramChannel) { $this->telegramChannel->sendTextMessage( $fallbackMessage, (string) $chatId ); } return [ 'success' => false, 'chat_id' => $chatId, 'type' => 'command_not_found', 'message' => $fallbackMessage, 'suggestions' => $data['suggestions'] ?? [], 'data' => $data ]; } private function createEmptyMessageResponse(int $chatId): array { return [ 'success' => false, 'chat_id' => $chatId, 'type' => 'empty_message', 'message' => 'Mensagem vazia recebida.', 'data' => [] ]; } private function createVoiceConversionErrorResponse(int $chatId): array { return [ 'success' => false, 'chat_id' => $chatId, 'type' => 'voice_conversion_error', 'message' => 'Erro ao converter mensagem de voz.', 'data' => [] ]; } private function createEmptyCallbackResponse(int $chatId): array { return [ 'success' => false, 'chat_id' => $chatId, 'type' => 'empty_callback', 'message' => 'Dados de callback vazios.', 'data' => [] ]; } private function createCallbackErrorResponse(int $chatId, CommandResult $result): array { return [ 'success' => false, 'chat_id' => $chatId, 'type' => 'callback_error', 'message' => 'Erro ao processar callback.', 'data' => $result->getData() ]; } private function createIgnoredResult(string $message): array { return [ 'success' => false, 'status' => 'ignored', 'message' => $message ]; } // Additional methods for audio processing and file downloads... // (processAudioMessage, downloadVoiceFile, downloadAudioFile, etc.) }
<?php namespace App\Repositories; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Cache; class TelegramRepository { private const CACHE_TTL = 3600; // 1 hour private const CACHE_PREFIX = 'telegram:'; /** * Get authorized users */ public function getAuthorizedUsers(): array { return Config::get('services.telegram.recipients', []); } /** * Check if user is authorized */ public function isUserAuthorized(int $chatId): bool { $authorizedUsers = $this->getAuthorizedUsers(); return in_array($chatId, $authorizedUsers); } /** * Get bot configuration */ public function getBotConfig(): array { return [ 'enabled' => Config::get('services.telegram.enabled', false), 'bot_token' => Config::get('services.telegram.bot_token'), 'recipients' => $this->getAuthorizedUsers(), ]; } /** * Cache webhook info */ public function cacheWebhookInfo(array $webhookInfo): void { Cache::put( self::CACHE_PREFIX . 'webhook_info', $webhookInfo, self::CACHE_TTL ); } /** * Get cached webhook info */ public function getCachedWebhookInfo(): ?array { return Cache::get(self::CACHE_PREFIX . 'webhook_info'); } /** * Clear webhook cache */ public function clearWebhookCache(): void { Cache::forget(self::CACHE_PREFIX . 'webhook_info'); } /** * Cache message processing result */ public function cacheMessageResult(int $chatId, array $result): void { $key = self::CACHE_PREFIX . "message_result:{$chatId}"; Cache::put($key, $result, self::CACHE_TTL); } /** * Get cached message result */ public function getCachedMessageResult(int $chatId): ?array { $key = self::CACHE_PREFIX . "message_result:{$chatId}"; return Cache::get($key); } /** * Store webhook log */ public function storeWebhookLog(array $payload, array $result): void { $logData = [ 'timestamp' => now()->toISOString(), 'payload' => $payload, 'result' => $result, 'ip' => request()->ip(), 'user_agent' => request()->userAgent(), ]; // Store in cache for recent logs $logs = Cache::get(self::CACHE_PREFIX . 'recent_logs', []); $logs[] = $logData; // Keep only last 100 logs if (count($logs) > 100) { $logs = array_slice($logs, -100); } Cache::put(self::CACHE_PREFIX . 'recent_logs', $logs, self::CACHE_TTL); } /** * Get recent webhook logs */ public function getRecentWebhookLogs(int $limit = 50): array { $logs = Cache::get(self::CACHE_PREFIX . 'recent_logs', []); return array_slice($logs, -$limit); } /** * Get webhook statistics */ public function getWebhookStats(): array { $logs = $this->getRecentWebhookLogs(1000); $stats = [ 'total_requests' => count($logs), 'successful_requests' => 0, 'failed_requests' => 0, 'ignored_requests' => 0, 'message_requests' => 0, 'callback_requests' => 0, ]; foreach ($logs as $log) { $result = $log['result'] ?? []; $status = $result['status'] ?? 'unknown'; switch ($status) { case 'success': $stats['successful_requests']++; break; case 'error': $stats['failed_requests']++; break; case 'ignored': $stats['ignored_requests']++; break; } // Count request types if (isset($log['payload']['message'])) { $stats['message_requests']++; } if (isset($log['payload']['callback_query'])) { $stats['callback_requests']++; } } return $stats; } }
<?php namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Http\Requests\TelegramWebhookRequest; use App\Http\Requests\TelegramWebhookSetupRequest; use App\Http\Resources\TelegramWebhookResource; use App\Services\TelegramBotService; use App\Services\TelegramWebhookService; use App\Services\Telegram\TelegramMessageProcessorService; use App\Services\Telegram\TelegramWebhookValidationService; use App\Services\Channels\TelegramChannel; use App\Contracts\LoggingServiceInterface; use App\Contracts\MessageFlowTrackerInterface; use Illuminate\Http\JsonResponse; class TelegramWebhookController extends Controller { public function __construct( private TelegramBotService $telegramBotService, private TelegramChannel $telegramChannel, private TelegramWebhookService $webhookService, private TelegramMessageProcessorService $messageProcessor, private TelegramWebhookValidationService $webhookValidationService, private LoggingServiceInterface $loggingService, private MessageFlowTrackerInterface $flowTracker ) {} /** * Handle Telegram webhook */ public function handle(TelegramWebhookRequest $request): JsonResponse { $startTime = microtime(true); // Inicializa o tracker com ID único $this->flowTracker->startTracking(uniqid('webhook_', true)); try { // Check if validation failed if ($request->has('validation_errors')) { $validationErrors = $request->input('validation_errors'); $errorMessage = $this->createValidationErrorMessage($validationErrors); // Send friendly error message to user via Telegram $this->sendFriendlyErrorMessage($request->all(), $errorMessage); return TelegramWebhookResource::ignored('Validation failed - friendly message sent to user') ->response() ->setStatusCode(200); } $payload = $request->validated(); // Validate payload structure first $validation = $this->webhookService->validatePayload($payload); if (!$validation['valid']) { // Send friendly error message to user via Telegram $this->sendFriendlyErrorMessage($payload, $validation['message']); return TelegramWebhookResource::ignored($validation['message']) ->response() ->setStatusCode(200); } // Validate webhook and mark as processing (after payload validation) $updateId = $payload['update_id'] ?? null; $skipDuplicateCheck = app()->environment('testing'); if (!$this->webhookValidationService->validateAndMarkProcessing($updateId, $skipDuplicateCheck)) { return TelegramWebhookResource::success('Duplicate webhook ignored', [ 'success' => true, 'status' => 'ignored', 'message' => 'Duplicate webhook ignored', 'update_id' => $updateId ]) ->response() ->setStatusCode(200); } // Process the webhook payload with timeout protection $result = $this->processWithTimeout(function () use ($payload) { return $this->messageProcessor->processWebhookPayload($payload); }, 25); // 25 seconds timeout // Gera relatório do fluxo APÓS o processamento $flowReport = $this->flowTracker->generateFlowReport($payload); $userReport = $this->flowTracker->generateUserReport($payload); // Finaliza o tracking $this->flowTracker->endTracking(); // Se habilitado, envia relatório para o usuário if (config('message-flow.tracking.send_to_user', false)) { $this->sendFlowReportToUser($payload, $userReport); } if (config('message-flow.enabled', false)) { $this->loggingService->logTelegramEvent('message_flow_report', [ 'flow_report' => $flowReport, 'user_report' => $userReport, 'payload' => $payload ], 'info'); } // Check if the result is valid and has the expected structure if (!is_array($result) || !isset($result['success'])) { $this->loggingService->logTelegramEvent('telegram_webhook_invalid_result', [ 'error' => 'Invalid result structure from message processor', 'result' => $result, 'payload' => $payload, 'telegram_update_id' => $request->input('update_id'), 'timestamp' => now()->toISOString() ], 'error'); // Send friendly error message to user $this->sendFriendlyErrorMessage($payload, 'Desculpe, ocorreu um erro interno. Tente novamente em alguns instantes.'); return TelegramWebhookResource::error('Invalid processing result') ->response() ->setStatusCode(500); } // Check if this is a normal response (not an error) - check status first $isNormalResponse = isset($result['type']) && in_array($result['type'], [ 'command_not_found', 'ignored', 'unauthorized', 'unsupported', 'empty_message', 'voice_conversion_error', 'audio_conversion_error' ]); if ($isNormalResponse) { // Send fallback message to user for command_not_found if ($result['type'] === 'command_not_found') { $fallbackMessage = $this->createCommandNotFoundFallbackMessage($result); $this->sendFriendlyErrorMessage($payload, $fallbackMessage); } // Use appropriate resource method based on status if (isset($result['status']) && $result['status'] === 'ignored') { return TelegramWebhookResource::ignored($result['message'] ?? 'Message ignored', $result) ->response() ->setStatusCode(200); } return TelegramWebhookResource::success($result['message'] ?? 'Message processed', $result) ->response() ->setStatusCode(200); } // Check if the message was processed successfully if ($result['success']) { return TelegramWebhookResource::success($result['message'] ?? 'Message processed successfully', $result) ->response() ->setStatusCode(200); } // Handle actual errors (return 500) $this->loggingService->logTelegramEvent('telegram_webhook_processing_failed', [ 'error' => 'Message processing failed', 'error_message' => $result['message'] ?? 'Unknown error', 'result' => $result, 'payload' => $payload, 'telegram_update_id' => $request->input('update_id'), 'timestamp' => now()->toISOString() ], 'error'); // Send friendly error message to user $this->sendFriendlyErrorMessage($payload, $result['message'] ?? 'Desculpe, ocorreu um erro ao processar sua mensagem. Tente novamente.'); return TelegramWebhookResource::error($result['message'] ?? 'Message processing failed', $result) ->response() ->setStatusCode(500); } catch (\Exception $e) { $duration = (microtime(true) - $startTime) * 1000; $this->loggingService->logException($e, [ 'operation' => 'telegram_webhook_processing', 'chat_id' => $request->input('message.chat.id'), 'user_id' => $request->input('message.from.id'), 'processing_time_ms' => round($duration, 2) ]); // Send friendly error message to user even for exceptions $this->sendFriendlyErrorMessage($request->all(), 'Desculpe, ocorreu um erro inesperado. Nossa equipe foi notificada.'); return TelegramWebhookResource::error('Internal server error') ->response() ->setStatusCode(500); } } /** * Set webhook URL for Telegram bot */ public function setWebhook(TelegramWebhookSetupRequest $request): JsonResponse { try { $webhookUrl = $request->validated()['webhook_url']; $result = $this->webhookService->setWebhook($webhookUrl); if (!$result['success']) { return TelegramWebhookResource::error($result['message'], $result) ->response() ->setStatusCode(400); } return TelegramWebhookResource::success($result['message'], $result) ->response() ->setStatusCode(200); } catch (\Exception $e) { $this->loggingService->logException($e, [ 'operation' => 'telegram_webhook_setup', 'webhook_url' => $request->validated()['webhook_url'] ?? 'unknown' ]); return TelegramWebhookResource::error('Internal server error') ->response() ->setStatusCode(500); } } /** * Get webhook info */ public function getWebhookInfo(): JsonResponse { try { $this->loggingService->logTelegramEvent('telegram_webhook_info_request', [ 'action' => 'get_webhook_info' ], 'info'); $result = $this->webhookService->getWebhookInfo(); if (!$result['success']) { $this->loggingService->logTelegramEvent('telegram_webhook_info_failed', [ 'error' => $result['message'], 'result' => $result ], 'error'); return TelegramWebhookResource::error($result['message'], $result) ->response() ->setStatusCode(400); } return TelegramWebhookResource::success('Webhook info retrieved', $result) ->response() ->setStatusCode(200); } catch (\Exception $e) { $this->loggingService->logException($e, [ 'operation' => 'telegram_webhook_info' ]); return TelegramWebhookResource::error('Internal server error') ->response() ->setStatusCode(500); } } /** * Delete webhook */ public function deleteWebhook(): JsonResponse { try { $this->loggingService->logTelegramEvent('telegram_webhook_deletion', [ 'action' => 'delete_webhook' ], 'info'); $result = $this->webhookService->deleteWebhook(); if (!$result['success']) { $this->loggingService->logTelegramEvent('telegram_webhook_deletion_failed', [ 'error' => $result['message'], 'result' => $result ], 'error'); return TelegramWebhookResource::error($result['message'], $result) ->response() ->setStatusCode(400); } $this->loggingService->logTelegramEvent('telegram_webhook_deletion_success', [ 'result' => $result ], 'success'); return TelegramWebhookResource::success($result['message'], $result) ->response() ->setStatusCode(200); } catch (\Exception $e) { $this->loggingService->logException($e, [ 'operation' => 'telegram_webhook_deletion' ]); return TelegramWebhookResource::error('Internal server error') ->response() ->setStatusCode(500); } } /** * Test bot functionality */ public function test(): JsonResponse { try { $this->loggingService->logTelegramEvent('telegram_bot_test', [ 'action' => 'test_bot' ], 'info'); $result = $this->webhookService->testBot(); if (!$result['success']) { $this->loggingService->logTelegramEvent('telegram_bot_test_failed', [ 'error' => $result['message'], 'result' => $result ], 'error'); return TelegramWebhookResource::error($result['message'], $result) ->response() ->setStatusCode(400); } $this->loggingService->logTelegramEvent('telegram_bot_test_success', [ 'result' => $result ], 'success'); return TelegramWebhookResource::success($result['message'], $result) ->response() ->setStatusCode(200); } catch (\Exception $e) { $this->loggingService->logException($e, [ 'operation' => 'telegram_bot_test' ]); return TelegramWebhookResource::error('Test failed: ' . $e->getMessage()) ->response() ->setStatusCode(500); } } // ... (métodos auxiliares privados serão adicionados na próxima seção) }
<?php namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Http\Resources\TelegramWebhookResource; use App\Services\TelegramLoggingService; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; class TelegramStatsController extends Controller { public function __construct( private TelegramLoggingService $loggingService ) {} /** * Get webhook statistics */ public function getStats(): JsonResponse { try { $stats = $this->loggingService->getWebhookStats(); return TelegramWebhookResource::success('Statistics retrieved successfully', [ 'statistics' => $stats ]) ->response() ->setStatusCode(200); } catch (\Exception $e) { return TelegramWebhookResource::error('Failed to retrieve statistics: ' . $e->getMessage()) ->response() ->setStatusCode(500); } } /** * Get recent webhook logs */ public function getRecentLogs(Request $request): JsonResponse { try { $limit = $request->get('limit', 50); $limit = min(max((int) $limit, 1), 100); // Limit between 1 and 100 $logs = $this->loggingService->getRecentLogs($limit); return TelegramWebhookResource::success('Recent logs retrieved successfully', [ 'logs' => $logs, 'count' => count($logs), 'limit' => $limit ]) ->response() ->setStatusCode(200); } catch (\Exception $e) { return TelegramWebhookResource::error('Failed to retrieve logs: ' . $e->getMessage()) ->response() ->setStatusCode(500); } } /** * Get webhook health status */ public function getHealthStatus(): JsonResponse { try { $stats = $this->loggingService->getWebhookStats(); // Calculate health metrics $totalRequests = $stats['total_requests'] ?? 0; $successfulRequests = $stats['successful_requests'] ?? 0; $failedRequests = $stats['failed_requests'] ?? 0; $successRate = $totalRequests > 0 ? ($successfulRequests / $totalRequests) * 100 : 0; $errorRate = $totalRequests > 0 ? ($failedRequests / $totalRequests) * 100 : 0; $healthStatus = [ 'status' => $successRate >= 90 ? 'healthy' : ($successRate >= 70 ? 'warning' : 'critical'), 'success_rate' => round($successRate, 2), 'error_rate' => round($errorRate, 2), 'total_requests' => $totalRequests, 'successful_requests' => $successfulRequests, 'failed_requests' => $failedRequests, 'ignored_requests' => $stats['ignored_requests'] ?? 0, 'message_requests' => $stats['message_requests'] ?? 0, 'callback_requests' => $stats['callback_requests'] ?? 0, 'last_updated' => now()->toISOString(), ]; return TelegramWebhookResource::success('Health status retrieved successfully', [ 'health' => $healthStatus ]) ->response() ->setStatusCode(200); } catch (\Exception $e) { return TelegramWebhookResource::error('Failed to retrieve health status: ' . $e->getMessage()) ->response() ->setStatusCode(500); } } }
// ============================================================================= // TELEGRAM BOT ROUTES // ============================================================================= Route::prefix('telegram')->group(function () { //Route::middleware('telegram.webhook.secret')->group(function () { Route::post('/webhook', [TelegramWebhookController::class, 'handle']); // POST /api/telegram/webhook //}); // Outras rotas sem validação de secret (gerenciamento interno) Route::post('/set-webhook', [TelegramWebhookController::class, 'setWebhook']); // POST /api/telegram/set-webhook Route::get('/webhook-info', [TelegramWebhookController::class, 'getWebhookInfo']); // GET /api/telegram/webhook-info Route::delete('/webhook', [TelegramWebhookController::class, 'deleteWebhook']); // DELETE /api/telegram/webhook // Testing and monitoring Route::post('/test', [TelegramWebhookController::class, 'test']); // POST /api/telegram/test // Statistics and monitoring Route::prefix('stats')->group(function () { Route::get('/', [TelegramStatsController::class, 'getStats']); // GET /api/telegram/stats Route::get('/logs', [TelegramStatsController::class, 'getRecentLogs']); // GET /api/telegram/stats/logs Route::get('/health', [TelegramStatsController::class, 'getHealthStatus']); // GET /api/telegram/stats/health }); });
<?php namespace App\Services; use Illuminate\Support\Facades\Http; use App\Contracts\LoggingServiceInterface; class TelegramWebhookService { private string $botToken; private string $apiUrl; public function __construct( private LoggingServiceInterface $loggingService ) { $this->botToken = config('services.telegram.bot_token'); $this->apiUrl = "https://api.telegram.org/bot{$this->botToken}"; } /** * Set webhook URL for Telegram bot */ public function setWebhook(string $webhookUrl): array { try { $response = Http::post("{$this->apiUrl}/setWebhook", [ 'url' => $webhookUrl ]); if ($response->successful()) { $data = $response->json(); $result = [ 'success' => true, 'message' => 'Webhook set successfully', 'data' => $data ]; $this->loggingService->logBusinessOperation('telegram_webhook_setup_success', [ 'webhook_url' => $webhookUrl, 'result' => $result ], 'success'); return $result; } $result = [ 'success' => false, 'message' => 'Failed to set webhook', 'error' => $response->json() ]; $this->loggingService->logBusinessOperation('telegram_webhook_setup_failed', [ 'webhook_url' => $webhookUrl, 'error' => $result['error'] ], 'error'); return $result; } catch (\Exception $e) { $result = [ 'success' => false, 'message' => 'Internal server error', 'error' => $e->getMessage() ]; $this->loggingService->logException($e, [ 'operation' => 'telegram_webhook_setup', 'webhook_url' => $webhookUrl ]); return $result; } } /** * Get webhook info */ public function getWebhookInfo(): array { try { $this->loggingService->logBusinessOperation('telegram_webhook_info_request', [ 'action' => 'get_webhook_info' ], 'info'); $response = Http::get("{$this->apiUrl}/getWebhookInfo"); if ($response->successful()) { $data = $response->json(); $result = [ 'success' => true, 'data' => $data ]; $this->loggingService->logBusinessOperation('telegram_webhook_info_success', [ 'result' => $result ], 'success'); return $result; } $result = [ 'success' => false, 'message' => 'Failed to get webhook info', 'error' => $response->json() ]; $this->loggingService->logBusinessOperation('telegram_webhook_info_failed', [ 'error' => $result['error'] ], 'error'); return $result; } catch (\Exception $e) { $this->loggingService->logException($e, [ 'operation' => 'telegram_webhook_info' ]); return [ 'success' => false, 'message' => 'Internal server error', 'error' => $e->getMessage() ]; } } /** * Delete webhook */ public function deleteWebhook(): array { try { $this->loggingService->logBusinessOperation('telegram_webhook_deletion_attempt', [ 'action' => 'delete_webhook' ], 'info'); $response = Http::post("{$this->apiUrl}/deleteWebhook"); if ($response->successful()) { $data = $response->json(); $result = [ 'success' => true, 'message' => 'Webhook deleted successfully', 'data' => $data ]; $this->loggingService->logBusinessOperation('telegram_webhook_deletion_success', [ 'result' => $result ], 'success'); return $result; } $result = [ 'success' => false, 'message' => 'Failed to delete webhook', 'error' => $response->json() ]; $this->loggingService->logBusinessOperation('telegram_webhook_deletion_failed', [ 'error' => $result['error'] ], 'error'); return $result; } catch (\Exception $e) { $result = [ 'success' => false, 'message' => 'Internal server error', 'error' => $e->getMessage() ]; $this->loggingService->logException($e, [ 'operation' => 'telegram_webhook_deletion' ]); return $result; } } /** * Test bot functionality */ public function testBot(): array { try { $this->loggingService->logBusinessOperation('telegram_bot_test_attempt', [ 'action' => 'test_bot' ], 'info'); $recipients = config('services.telegram.recipients', []); if (empty($recipients)) { $result = [ 'success' => false, 'message' => 'No recipients configured' ]; $this->loggingService->logBusinessOperation('telegram_bot_test_failed', [ 'error' => 'No recipients configured' ], 'error'); return $result; } $results = []; foreach ($recipients as $recipient) { $result = $this->sendTestMessage($recipient); $results[$recipient] = $result; } $successCount = count(array_filter($results, fn($r) => $r['success'])); $result = [ 'success' => $successCount > 0, 'message' => 'Test completed', 'sent_to' => $successCount, 'total_recipients' => count($recipients), 'results' => $results ]; $this->loggingService->logBusinessOperation('telegram_bot_test_success', [ 'sent_to' => $successCount, 'total_recipients' => count($recipients), 'result' => $result ], 'success'); return $result; } catch (\Exception $e) { $result = [ 'success' => false, 'message' => 'Test failed: ' . $e->getMessage() ]; $this->loggingService->logException($e, [ 'operation' => 'telegram_bot_test' ]); return $result; } } /** * Send test message to specific recipient */ private function sendTestMessage(string $recipient): array { try { $testMessage = "🧪 *Teste do Bot*\n\n" . "Este é um teste do bot de relatórios do Rei do Óleo.\n" . "Se você recebeu esta mensagem, o bot está funcionando!\n\n" . "Use `/help` para ver os comandos disponíveis.\n\n" . "⏰ Teste realizado em: " . now()->format('d/m/Y H:i:s'); $response = Http::post("{$this->apiUrl}/sendMessage", [ 'chat_id' => $recipient, 'text' => $testMessage, 'parse_mode' => 'Markdown' ]); if ($response->successful()) { $this->loggingService->logBusinessOperation('test_message_sent_successfully', [ 'chat_id' => $recipient, 'message_type' => 'test' ], 'info'); return [ 'success' => true, 'message' => 'Test message sent successfully' ]; } return [ 'success' => false, 'error' => $response->json()['description'] ?? 'Unknown error' ]; } catch (\Exception $e) { $this->loggingService->logException($e, [ 'operation' => 'telegram_test_message_send', 'recipient' => $recipient ]); return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * Validate webhook payload */ public function validatePayload(array $payload): array { if (!isset($payload['update_id'])) { return [ 'valid' => false, 'message' => 'Missing update_id in payload' ]; } if (!isset($payload['message']) && !isset($payload['callback_query'])) { return [ 'valid' => false, 'message' => 'No message or callback_query in payload' ]; } // Check if it's a message and validate message structure if (isset($payload['message'])) { $message = $payload['message']; // Check if message has required fields if (!isset($message['chat']['id']) || !isset($message['from']['id'])) { return [ 'valid' => false, 'message' => 'Message missing required chat or user information' ]; } // Check if message has any content (text, voice, audio, photo, etc.) $hasContent = isset($message['text']) || isset($message['voice']) || isset($message['audio']) || isset($message['photo']) || isset($message['document']) || isset($message['video']) || isset($message['sticker']) || isset($message['location']) || isset($message['contact']); if (!$hasContent) { return [ 'valid' => false, 'message' => 'Message has no recognizable content' ]; } } return [ 'valid' => true, 'type' => isset($payload['callback_query']) ? 'callback_query' : 'message' ]; } }
<?php namespace App\Services; use App\Services\Telegram\Commands\UnifiedCommandSystem; use App\Services\Telegram\TelegramAuthorizationService; use App\Services\Telegram\TelegramMenuBuilder; use App\Contracts\LoggingServiceInterface; use App\Contracts\MessageFlowTrackerInterface; use App\Contracts\MessageTrackingInterface; class TelegramBotService implements MessageTrackingInterface { public function __construct( private UnifiedCommandSystem $commandSystem, private TelegramAuthorizationService $authorizationService, private TelegramMenuBuilder $menuBuilder, private LoggingServiceInterface $loggingService, private MessageFlowTrackerInterface $flowTracker ) { $this->initializeTracking(); } /** * Process incoming message from Telegram webhook */ public function processMessage(array $message): array { $this->trackMethod('processMessage', ['message' => $message]); try { $chatId = $message['chat']['id']; $text = $message['text'] ?? ''; $from = $message['from'] ?? []; // Check if user is authorized if (!$this->authorizationService->isAuthorizedUser($chatId)) { $result = $this->menuBuilder->buildUnauthorizedMessage($chatId); $this->endMethod('processMessage', ['result' => $result, 'status' => 'unauthorized']); return $result; } // Process command using UnifiedCommandSystem $context = [ 'chat_id' => $chatId, 'user_id' => $from['id'] ?? null, 'type' => 'text', 'timestamp' => time(), 'user_permissions' => ['user'] // TODO: Get real permissions ]; $result = $this->commandSystem->processCommand($text, $context); if ($result->isSuccess()) { $data = $result->getData(); $response = [ 'success' => true, 'chat_id' => $chatId, 'type' => $result->getType(), 'data' => $data, 'command_info' => $result->getCommandMatch() ? [ 'command_id' => $result->getCommandMatch()->getCommand()->getId(), 'confidence' => $result->getCommandMatch()->getConfidence() ] : null ]; $this->endMethod('processMessage', ['result' => $response, 'status' => 'success']); return $response; } else { $data = $result->getData(); $fallbackMessage = $data['fallback_message'] ?? 'Comando não encontrado'; $response = [ 'success' => false, 'chat_id' => $chatId, 'type' => 'command_not_found', 'message' => $fallbackMessage, 'suggestions' => $data['suggestions'] ?? [], 'data' => $data ]; $this->endMethod('processMessage', ['result' => $response, 'status' => 'command_not_found']); return $response; } } catch (\Exception $e) { $this->loggingService->logException($e, [ 'operation' => 'telegram_message_processing', 'chat_id' => $message['chat']['id'] ?? null, 'user_id' => $message['from']['id'] ?? null, 'message' => $message ]); $errorResponse = [ 'success' => false, 'chat_id' => $chatId, 'type' => 'error', 'message' => 'Erro interno do sistema', 'data' => [] ]; $this->endMethod('processMessage', ['result' => $errorResponse, 'error' => $e->getMessage()]); return $errorResponse; } } /** * Process callback query from inline keyboard buttons */ public function processCallbackQuery(array $callbackQuery): array { try { $chatId = $callbackQuery['message']['chat']['id']; $messageId = $callbackQuery['message']['message_id'] ?? null; $callbackData = $callbackQuery['data'] ?? ''; $from = $callbackQuery['from'] ?? []; // Check if user is authorized if (!$this->authorizationService->isAuthorizedUser($chatId)) { return $this->menuBuilder->buildUnauthorizedMessage($chatId); } // Process callback using UnifiedCommandSystem $context = [ 'chat_id' => $chatId, 'user_id' => $from['id'] ?? null, 'type' => 'callback_query', 'timestamp' => time(), 'user_permissions' => ['user'], // TODO: Get real permissions 'callback_data' => $callbackData ]; $result = $this->commandSystem->processCommand($callbackData, $context); if ($result->isSuccess()) { $data = $result->getData(); return [ 'success' => true, 'chat_id' => $chatId, 'type' => $result->getType(), 'data' => $data, 'command_info' => $result->getCommandMatch() ? [ 'command_id' => $result->getCommandMatch()->getCommand()->getId(), 'confidence' => $result->getCommandMatch()->getConfidence() ] : null ]; } else { $data = $result->getData(); $fallbackMessage = $data['fallback_message'] ?? 'Ação não encontrada'; return [ 'success' => false, 'chat_id' => $chatId, 'type' => 'callback_not_found', 'message' => $fallbackMessage, 'suggestions' => $data['suggestions'] ?? [], 'data' => $data ]; } } catch (\Exception $e) { $this->loggingService->logException($e, [ 'operation' => 'telegram_callback_processing', 'chat_id' => $callbackQuery['message']['chat']['id'] ?? null, 'user_id' => $callbackQuery['from']['id'] ?? null, 'callback_query' => $callbackQuery ]); return [ 'success' => false, 'chat_id' => $chatId, 'type' => 'error', 'message' => 'Erro interno do sistema', 'data' => [] ]; } } /** * Send error message to user via Telegram */ public function sendErrorMessage(int $chatId, string $errorMessage): array { try { // Create keyboard with helpful options $keyboard = [ [ ['text' => '❓ Ajuda', 'callback_data' => 'help'], ['text' => '🏠 Menu Principal', 'callback_data' => 'main_menu'] ], [ ['text' => '📋 Comandos', 'callback_data' => 'commands_list'], ['text' => '🔄 Tentar Novamente', 'callback_data' => 'retry'] ] ]; // Send error message with helpful keyboard using menuBuilder $result = $this->menuBuilder->buildMainMenu($chatId); $this->loggingService->logTelegramEvent('telegram_error_message_sent', [ 'chat_id' => $chatId, 'error_message' => $errorMessage, 'result' => $result ], 'info'); return [ 'success' => true, 'chat_id' => $chatId, 'type' => 'error_message_sent', 'message' => 'Error message sent to user', 'data' => $result ]; } catch (\Exception $e) { $this->loggingService->logException($e, [ 'operation' => 'send_error_message', 'chat_id' => $chatId, 'error_message' => $errorMessage ]); return [ 'success' => false, 'chat_id' => $chatId, 'type' => 'error_message_failed', 'message' => 'Failed to send error message', 'data' => [] ]; } } // ======================================== // MessageTrackingInterface Implementation // ======================================== /** * Inicializa o sistema de tracking */ public function initializeTracking(): void { // Tracking já é inicializado no construtor } /** * Inicia o tracking de um método */ public function trackMethod(string $methodName, array $inputData = [], array $outputData = []): void { $className = class_basename($this); $this->flowTracker->trackMethod($className, $methodName, $inputData, $outputData); } /** * Finaliza o tracking de um método */ public function endMethod(string $methodName, array $outputData = []): void { $className = class_basename($this); $this->flowTracker->endMethod($className, $methodName, $outputData); } }
<?php namespace App\Services\Channels; use App\Contracts\NotificationChannelInterface; use App\Contracts\LoggingServiceInterface; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use App\Contracts\MessageFlowTrackerInterface; use App\Contracts\MessageTrackingInterface; class TelegramChannel implements NotificationChannelInterface, MessageTrackingInterface { private string $botToken; private string $apiUrl; private array $recipients; public function __construct( private MessageFlowTrackerInterface $flowTracker, private ?LoggingServiceInterface $loggingService = null ) { $this->initializeTracking(); $this->botToken = config('services.telegram.bot_token', ''); $this->apiUrl = "https://api.telegram.org/bot{$this->botToken}"; $this->recipients = config('services.telegram.recipients', []); } /** * Send text message via Telegram */ public function sendTextMessage(string $message, ?string $recipient = null): array { $this->trackMethod('sendTextMessage', ['message' => $message, 'recipient' => $recipient]); if (!$this->isEnabled()) { $result = [ 'success' => false, 'error' => 'Telegram channel is disabled' ]; $this->endMethod('sendTextMessage', ['result' => $result, 'status' => 'disabled']); return $result; } try { if ($recipient) { return $this->sendToRecipient($recipient, $message); } // Send to all configured recipients if (empty($this->recipients)) { $result = [ 'success' => false, 'error' => 'No Telegram recipients configured' ]; $this->endMethod('sendTextMessage', ['result' => $result, 'status' => 'no_recipients']); return $result; } $results = []; foreach ($this->recipients as $recipient) { $results[$recipient] = $this->sendToRecipient($recipient, $message); } $successCount = count(array_filter($results, fn($r) => $r['success'])); $result = [ 'success' => $successCount > 0, 'sent_to' => $successCount, 'total_recipients' => count($results), 'results' => $results ]; $this->endMethod('sendTextMessage', ['result' => $result, 'status' => 'success']); return $result; } catch (\Exception $e) { Log::error('Telegram channel error', [ 'error' => $e->getMessage(), 'message' => $message ]); $result = [ 'success' => false, 'error' => $e->getMessage() ]; $this->endMethod('sendTextMessage', ['result' => $result, 'error' => $e->getMessage()]); return $result; } } /** * Send message with inline keyboard via Telegram */ public function sendMessageWithKeyboard(string $message, string $chatId, array $keyboard): array { $this->trackMethod('sendMessageWithKeyboard', ['message' => $message, 'chat_id' => $chatId, 'keyboard' => $keyboard]); if (!$this->isEnabled()) { $result = [ 'success' => false, 'error' => 'Telegram channel is disabled' ]; $this->endMethod('sendMessageWithKeyboard', ['result' => $result, 'status' => 'disabled']); return $result; } try { $response = Http::post("{$this->apiUrl}/sendMessage", [ 'chat_id' => $chatId, 'text' => $message, 'parse_mode' => 'Markdown', 'reply_markup' => [ 'inline_keyboard' => $keyboard ] ]); if ($response->successful()) { $data = $response->json(); $result = [ 'success' => true, 'message_id' => $data['result']['message_id'] ?? null, 'response' => $data ]; $this->endMethod('sendMessageWithKeyboard', ['result' => $result, 'status' => 'success']); return $result; } $result = [ 'success' => false, 'error' => $response->json()['description'] ?? 'Unknown error', 'status' => $response->status() ]; $this->endMethod('sendMessageWithKeyboard', ['result' => $result, 'status' => 'http_error']); return $result; } catch (\Exception $e) { Log::error('Telegram keyboard message error', [ 'error' => $e->getMessage(), 'message' => $message, 'chat_id' => $chatId ]); $result = [ 'success' => false, 'error' => $e->getMessage() ]; $this->endMethod('sendMessageWithKeyboard', ['result' => $result, 'error' => $e->getMessage()]); return $result; } } /** * Answer callback query (for inline keyboard buttons) */ public function answerCallbackQuery(string $callbackQueryId, ?string $text = null, bool $showAlert = false): array { if (!$this->isEnabled()) { return [ 'success' => false, 'error' => 'Telegram channel is disabled' ]; } try { $data = [ 'callback_query_id' => $callbackQueryId ]; if ($text) { $data['text'] = $text; } if ($showAlert) { $data['show_alert'] = $showAlert; } $response = Http::post("{$this->apiUrl}/answerCallbackQuery", $data); if ($response->successful()) { $data = $response->json(); return [ 'success' => true, 'response' => $data ]; } return [ 'success' => false, 'error' => $response->json()['description'] ?? 'Unknown error', 'status' => $response->status() ]; } catch (\Exception $e) { Log::error('Telegram answer callback query error', [ 'error' => $e->getMessage(), 'callback_query_id' => $callbackQueryId ]); return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * Check if Telegram channel is enabled */ public function isEnabled(): bool { return config('services.telegram.enabled', true) && !empty($this->botToken) && !empty($this->recipients); } /** * Send message to specific recipient */ private function sendToRecipient(string $chatId, string $message): array { try { $response = Http::post("{$this->apiUrl}/sendMessage", [ 'chat_id' => $chatId, 'text' => $message, 'parse_mode' => 'Markdown' ]); if ($response->successful()) { $data = $response->json(); return [ 'success' => true, 'message_id' => $data['result']['message_id'] ?? null, 'response' => $data ]; } return [ 'success' => false, 'error' => $response->json()['description'] ?? 'Unknown error', 'status' => $response->status() ]; } catch (\Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } // ======================================== // MessageTrackingInterface Implementation // ======================================== /** * Inicializa o sistema de tracking */ public function initializeTracking(): void { // Tracking já é inicializado no construtor } /** * Inicia o tracking de um método */ public function trackMethod(string $methodName, array $inputData = [], array $outputData = []): void { $className = class_basename($this); $this->flowTracker->trackMethod($className, $methodName, $inputData, $outputData); } /** * Finaliza o tracking de um método */ public function endMethod(string $methodName, array $outputData = []): void { $className = class_basename($this); $this->flowTracker->endMethod($className, $methodName, $outputData); } }
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use App\Contracts\LoggingServiceInterface; use Symfony\Component\HttpFoundation\Response; class TelegramWebhookSecretMiddleware { public function __construct( private LoggingServiceInterface $loggingService ) {} /** * Handle an incoming request. */ public function handle(Request $request, Closure $next): Response { $secretToken = config('services.telegram.webhook_secret'); // Se não há secret configurado, permite a requisição (modo de desenvolvimento) if (empty($secretToken)) { $this->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); } }
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use Illuminate\Validation\ValidationException; use App\Services\Channels\TelegramChannel; use Illuminate\Support\Facades\Log; use Symfony\Component\HttpFoundation\Response; class TelegramWebhookExceptionHandler { public function __construct( private TelegramChannel $telegramChannel ) {} /** * Handle an incoming request. */ public function handle(Request $request, Closure $next): Response { try { return $next($request); } catch (ValidationException $e) { // Extract chat_id from the request if available $chatId = $this->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; } }
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use App\Contracts\LoggingServiceInterface; use Symfony\Component\HttpFoundation\Response; class TelegramWebhookLoggingMiddleware { public function __construct( private LoggingServiceInterface $loggingService ) {} /** * Handle an incoming request. */ public function handle(Request $request, Closure $next): Response { // Log the raw request before any validation $this->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); } }
<?php namespace App\Services\Telegram; use App\Contracts\LoggingServiceInterface; use Illuminate\Support\Facades\Cache; class TelegramWebhookValidationService { private const CACHE_PREFIX = 'telegram_update_processed_'; private const CACHE_TTL = 300; // 5 minutes public function __construct( private LoggingServiceInterface $loggingService ) {} /** * Check if webhook is duplicate and mark as processing */ public function validateAndMarkProcessing(?int $updateId, bool $skipDuplicateCheck = false): bool { if (!$updateId) { return true; // No update_id, allow processing } // Skip duplicate check if requested (useful for testing) if ($skipDuplicateCheck) { $this->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() ]; } } }
// Adicionar no método configure() do bootstrap/app.php $middleware->alias([ 'telegram.webhook.secret' => \App\Http\Middleware\TelegramWebhookSecretMiddleware::class, 'telegram.webhook.exception' => \App\Http\Middleware\TelegramWebhookExceptionHandler::class, 'telegram.webhook.logging' => \App\Http\Middleware\TelegramWebhookLoggingMiddleware::class, ]);
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; use App\Contracts\LoggingServiceInterface; class TelegramWebhookRequest extends FormRequest { public function __construct( private LoggingServiceInterface $loggingService ) { parent::__construct(); } /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; // Webhook requests are always authorized } /** * Get the validation rules that apply to the request. */ public function rules(): array { return [ 'update_id' => '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. */ public function expectsJson(): bool { return true; // Always expect JSON for webhook requests } /** * Determine if the request is asking for JSON. */ 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; } }
<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class TelegramWebhookSetupRequest extends FormRequest { /** * Determine if the user is authorized to make this request. */ public function authorize(): bool { return true; } /** * Get the validation rules that apply to the request. */ public function rules(): array { return [ 'webhook_url' => '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', ]; } }
<?php namespace App\Http\Resources; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; class TelegramWebhookResource extends JsonResource { /** * Transform the resource into an array. */ public function toArray(Request $request): array { return [ 'status' => $this->resource['status'] ?? 'success', 'message' => $this->resource['message'] ?? 'Webhook processed successfully', 'data' => $this->resource['data'] ?? null, 'result' => $this->resource['result'] ?? null, 'timestamp' => now()->toISOString(), ]; } /** * Create a success response */ public static function success(string $message = 'Webhook processed successfully', array $data = []): static { return new static([ 'status' => 'success', 'message' => $message, 'data' => $data, ]); } /** * Create an error response */ public static function error(string $message = 'Webhook processing failed', array $data = []): static { return new static([ 'status' => 'error', 'message' => $message, 'data' => $data, ]); } /** * Create an ignored response */ public static function ignored(string $message = 'Webhook ignored', array $data = []): static { return new static([ 'status' => 'ignored', 'message' => $message, 'data' => $data, ]); } }
<?php namespace Tests\Unit\Services\Telegram; use App\Services\Telegram\TelegramWebhookValidationService; use App\Contracts\LoggingServiceInterface; use Mockery; use PHPUnit\Framework\TestCase; class TelegramWebhookValidationServiceTest extends TestCase { private TelegramWebhookValidationService $service; private LoggingServiceInterface $loggingService; protected function setUp(): void { parent::setUp(); $this->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')); } }
#!/bin/bash # telegram-webhook-install.sh # Script para instalação automática do sistema de Telegram Webhook set -e echo "🚀 Instalando Sistema de Telegram Webhook..." # Cores para output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Função para log log() { echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" } error() { echo -e "${RED}[ERROR]${NC} $1" exit 1 } success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } # Verificar se está no diretório do Laravel if [ ! -f "artisan" ]; then error "Este script deve ser executado na raiz do projeto Laravel" fi log "Verificando pré-requisitos..." # Verificar PHP if ! command -v php &> /dev/null; then error "PHP não está instalado" fi # Verificar Composer if ! command -v composer &> /dev/null; then error "Composer não está instalado" fi # Verificar Redis (opcional) if ! command -v redis-cli &> /dev/null; then warning "Redis não encontrado. Recomendado para melhor performance." fi success "Pré-requisitos verificados" # 1. Instalar dependências log "Instalando dependências do Composer..." composer install --no-dev --optimize-autoloader # 2. Configurar variáveis de ambiente log "Configurando variáveis de ambiente..." if [ ! -f ".env" ]; then error "Arquivo .env não encontrado. Execute 'cp .env.example .env' primeiro" fi # Adicionar configurações do Telegram se não existirem if ! grep -q "TELEGRAM_ENABLED" .env; then echo "" >> .env echo "# Telegram Bot Configuration" >> .env echo "TELEGRAM_ENABLED=true" >> .env echo "TELEGRAM_BOT_TOKEN=your_bot_token_here" >> .env echo "TELEGRAM_WEBHOOK_SECRET=your_webhook_secret_here" >> .env echo "TELEGRAM_RECIPIENTS=123456789,987654321" >> .env success "Configurações do Telegram adicionadas ao .env" else warning "Configurações do Telegram já existem no .env" fi # 3. Executar migrações log "Executando migrações..." php artisan migrate --force # 4. Limpar e otimizar cache log "Otimizando aplicação..." php artisan config:cache php artisan route:cache php artisan view:cache # 5. Registrar comando Artisan log "Registrando comando de teste..." if ! grep -q "TestTelegramWebhook" app/Console/Kernel.php; then # Adicionar comando ao Kernel se necessário echo "Comando já registrado automaticamente pelo Laravel" fi # 6. Criar diretórios necessários log "Criando diretórios necessários..." mkdir -p storage/logs/telegram mkdir -p storage/app/telegram chmod -R 775 storage/logs/telegram chmod -R 775 storage/app/telegram # 7. Configurar permissões log "Configurando permissões..." chmod -R 775 storage chmod -R 775 bootstrap/cache # 8. Testar instalação log "Testando instalação..." # Verificar se as rotas estão funcionando if php artisan route:list | grep -q "telegram/webhook"; then success "Rotas do Telegram registradas com sucesso" else error "Falha ao registrar rotas do Telegram" fi # 9. Exibir informações de configuração echo "" echo "==========================================" echo "🎉 INSTALAÇÃO CONCLUÍDA COM SUCESSO!" echo "==========================================" echo "" echo "📋 Próximos passos:" echo "" echo "1. Configure suas credenciais no arquivo .env:" echo " - TELEGRAM_BOT_TOKEN: Token do seu bot do Telegram" echo " - TELEGRAM_WEBHOOK_SECRET: Secret para validação do webhook" echo " - TELEGRAM_RECIPIENTS: IDs dos usuários que receberão mensagens" echo "" echo "2. Configure o webhook do Telegram:" echo " curl -X POST \"https://api.telegram.org/bot<SEU_BOT_TOKEN>/setWebhook\" \\" echo " -H \"Content-Type: application/json\" \\" echo " -d '{\"url\": \"https://seudominio.com/api/telegram/webhook\"}'" echo "" echo "3. Teste o sistema:" echo " php artisan telegram:test-webhook --chat-id=123456789 --message=\"teste\"" echo "" echo "4. Monitore os logs:" echo " tail -f storage/logs/telegram/webhook.log" echo "" echo "📚 Documentação completa disponível em:" echo " docs/dokuwiki/telegram_webhook_implementation_guide.txt" echo "" success "Sistema de Telegram Webhook instalado e configurado!"
Parabéns! O sistema de Telegram Webhook está completamente implementado!
✅ Seção 1: Controllers e Rotas - TelegramWebhookController completo - TelegramStatsController para monitoramento - Rotas da API configuradas
✅ Seção 2: Services e Processamento - TelegramWebhookService para gerenciamento - TelegramBotService para processamento - TelegramChannel para comunicação
✅ Seção 3: Middlewares e Validação - Middleware de segurança com secret - Middleware de tratamento de exceções - Middleware de logging - Serviço de validação de duplicatas
✅ Seção 4: Requests e Resources - TelegramWebhookRequest com validação completa - TelegramWebhookSetupRequest para configuração - TelegramWebhookResource para respostas padronizadas
✅ Seção 5: Testes e Comandos - Comando Artisan para testes - Testes unitários - Testes de integração - Cobertura completa de funcionalidades
✅ Seção 6: Scripts de Instalação - Script de instalação automática - Script de configuração do webhook - Script de monitoramento - Docker Compose para desenvolvimento - Makefile para comandos rápidos
1. Configure suas credenciais no arquivo `.env` 2. Execute o script de instalação: `bash scripts/telegram-webhook-install.sh` 3. Configure o webhook: `make webhook-setup` 4. Teste o sistema: `make test` 5. Monitore o funcionamento: `make monitor`
Sistema pronto para produção! 🚀
—
## 📋 DEPENDÊNCIAS ADICIONAIS IMPORTANTES
### Modelos Telegram
<?php namespace App\Models\Telegram; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Factories\HasFactory; use App\Contracts\Telegram\Commands\CommandInterface; class TelegramCommand extends Model implements CommandInterface { use HasFactory; protected $fillable = [ 'command_id', 'aliases', 'description', 'action_handler', 'action_method', 'action_parameters', 'permissions', 'category', 'voice_settings', 'natural_language', 'fallback', 'is_active', 'priority' ]; protected $casts = [ 'aliases' => 'array', 'action_parameters' => 'array', 'permissions' => 'array', 'voice_settings' => 'array', 'natural_language' => 'array', 'fallback' => 'array', 'is_active' => 'boolean', 'priority' => 'integer' ]; public function getId(): string { return $this->command_id; } public function getAliases(): array { return $this->aliases ?? []; } public function getDescription(): string { return $this->description ?? ''; } public function getAction(): array { return [ 'handler' => $this->action_handler, 'method' => $this->action_method, 'parameters' => $this->action_parameters ?? [] ]; } public function getPermissions(): array { return $this->permissions ?? ['all']; } public function getCategory(): string { return $this->category ?? 'general'; } public function getVoiceSettings(): array { return $this->voice_settings ?? [ 'enabled' => false, 'priority' => 1, 'noise_reduction' => false, 'language' => ['pt'] ]; } public function getNaturalLanguage(): array { return $this->natural_language ?? []; } public function getFallback(): array { return $this->fallback ?? [ 'message' => 'Desculpe, não entendi esse comando.', 'suggestions' => [] ]; } public function canHandle(string $input): bool { $input = strtolower(trim($input)); // Check exact aliases foreach ($this->getAliases() as $alias) { if (strtolower($alias) === $input) { return true; } } // Check natural language patterns foreach ($this->getNaturalLanguage() as $pattern) { if (str_contains(strtolower($pattern), $input) || str_contains($input, strtolower($pattern))) { return true; } } return false; } public function getConfidence(string $input): float { $input = strtolower(trim($input)); $maxConfidence = 0.0; // Exact match - highest confidence foreach ($this->getAliases() as $alias) { if (strtolower($alias) === $input) { return 1.0; } } // Natural language match - calculate similarity foreach ($this->getNaturalLanguage() as $pattern) { $similarity = $this->calculateSimilarity($input, strtolower($pattern)); $maxConfidence = max($maxConfidence, $similarity); } return $maxConfidence; } private function calculateSimilarity(string $input, string $pattern): float { // Simple similarity calculation using Levenshtein distance $levenshtein = levenshtein($input, $pattern); $maxLength = max(strlen($input), strlen($pattern)); if ($maxLength === 0) return 1.0; return 1 - ($levenshtein / $maxLength); } public function scopeActive($query) { return $query->where('is_active', true); } public function scopeByCategory($query, string $category) { return $query->where('category', $category); } public function scopeByPermission($query, array $userPermissions) { return $query->where(function ($q) use ($userPermissions) { $q->whereJsonContains('permissions', 'all') ->orWhereJsonContains('permissions', $userPermissions); }); } }
### Interfaces de Comandos Telegram
<?php namespace App\Contracts\Telegram\Commands; interface CommandInterface { public function getId(): string; public function getAliases(): array; public function getDescription(): string; public function getAction(): array; public function getPermissions(): array; public function getCategory(): string; public function getVoiceSettings(): array; public function getNaturalLanguage(): array; public function getFallback(): array; public function canHandle(string $input): bool; public function getConfidence(string $input): float; }
<?php namespace App\Contracts\Telegram\Commands; interface CommandRegistryInterface { public function findCommand(string $input, array $context = []): ?CommandMatch; public function getAllCommands(): array; public function getCommandsByCategory(string $category): array; public function reloadCommands(): void; public function addCommand(array $commandConfig): void; public function removeCommand(string $commandId): void; }
<?php namespace App\Contracts\Telegram; interface TelegramCommandHandlerInterface { /** * Handle the command */ public function handle(int $chatId, array $params = []): array; /** * Get command name */ public function getCommandName(): string; /** * Get command description */ public function getCommandDescription(): string; /** * Check if handler can handle the command */ public function canHandle(string $command): bool; }
### Migrations
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { /** * Run the migrations. */ public function up(): void { Schema::create('telegram_commands', function (Blueprint $table) { $table->id(); $table->string('command_id')->unique(); $table->json('aliases'); $table->text('description'); $table->string('action_handler'); $table->string('action_method'); $table->json('action_parameters')->nullable(); $table->json('permissions'); $table->string('category'); $table->json('voice_settings')->nullable(); $table->json('natural_language')->nullable(); $table->json('fallback')->nullable(); $table->boolean('is_active')->default(true); $table->integer('priority')->default(1); $table->timestamps(); $table->index(['category', 'is_active']); $table->index(['command_id', 'is_active']); $table->index('priority'); }); } /** * Reverse the migrations. */ public function down(): void { Schema::dropIfExists('telegram_commands'); } };
### Serviços de Comandos
<?php namespace App\Services\Telegram\Commands; use App\Contracts\Telegram\Commands\CommandMatch; use App\Services\Telegram\Commands\Repositories\CommandConfigRepository; use App\Services\Telegram\Commands\Cache\CommandCache; use App\Services\Telegram\Commands\Learning\CommandLearning; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\App; class UnifiedCommandSystem { public function __construct( private CommandRegistry $registry, private CommandCache $cache, private CommandLearning $learning, private CommandConfigRepository $configRepo ) {} public function processCommand(string $input, array $context = []): CommandResult { try { $cacheKey = $this->generateCacheKey($input, $context); // 1. Check cache first if ($cached = $this->cache->get($input, $context)) { $this->learning->recordHit($input, $cached); return $this->createResult($cached, 'cache_hit'); } // 2. Process command through registry $result = $this->registry->findCommand($input, $context); if (!$result) { return $this->createFallbackResult($input, $context); } // 3. Cache result $this->cache->put($input, $context, $result); // 4. Learn from usage $this->learning->recordUsage($input, $result); // 5. Execute command $executionResult = $this->executeCommand($result, $context); return $this->createResult($result, 'success', $executionResult); } catch (\Exception $e) { Log::error('Failed to process command', [ 'input' => $input, 'context' => $context, 'error' => $e->getMessage() ]); return $this->createErrorResult($input, $e->getMessage()); } } public function addCommand(array $commandConfig): CommandResult { try { $this->registry->addCommand($commandConfig); $this->cache->clear(); return new CommandResult(true, 'Command added successfully', $commandConfig); } catch (\Exception $e) { return new CommandResult(false, $e->getMessage(), $commandConfig); } } public function removeCommand(string $commandId): CommandResult { try { $this->registry->removeCommand($commandId); $this->cache->clear(); return new CommandResult(true, 'Command removed successfully', ['command_id' => $commandId]); } catch (\Exception $e) { return new CommandResult(false, $e->getMessage(), ['command_id' => $commandId]); } } public function reloadCommands(): CommandResult { try { $this->registry->reloadCommands(); $this->cache->clear(); return new CommandResult(true, 'Commands reloaded successfully'); } catch (\Exception $e) { return new CommandResult(false, $e->getMessage()); } } private function generateCacheKey(string $input, array $context): string { return md5($input . serialize($context)); } private function createResult(CommandMatch $match, string $source, array $executionResult = []): CommandResult { return new CommandResult( true, 'Command processed successfully', [ 'command' => $match->getCommand()->getId(), 'confidence' => $match->getConfidence(), 'source' => $source, 'execution' => $executionResult ] ); } private function createFallbackResult(string $input, array $context): CommandResult { return new CommandResult( false, 'No command found for input', [ 'input' => $input, 'context' => $context, 'suggestions' => $this->getSuggestions($input) ] ); } private function createErrorResult(string $input, string $error): CommandResult { return new CommandResult( false, 'Error processing command: ' . $error, [ 'input' => $input, 'error' => $error ] ); } private function executeCommand(CommandMatch $match, array $context): array { $command = $match->getCommand(); $action = $command->getAction(); $handler = App::make($action['handler']); $method = $action['method']; $parameters = $action['parameters'] ?? []; return $handler->$method($context, $parameters); } private function getSuggestions(string $input): array { $allCommands = $this->registry->getAllCommands(); $suggestions = []; foreach ($allCommands as $command) { $similarity = $command->getConfidence($input); if ($similarity > 0.3) { $suggestions[] = [ 'command' => $command->getId(), 'aliases' => $command->getAliases(), 'confidence' => $similarity ]; } } usort($suggestions, fn($a, $b) => $b['confidence'] <=> $a['confidence']); return array_slice($suggestions, 0, 5); } }
### Comandos Artisan Adicionais
<?php namespace Database\Seeders; use Illuminate\Database\Seeder; use App\Models\Telegram\TelegramCommand; class TelegramCommandSeeder extends Seeder { public function run(): void { $commands = [ [ 'command_id' => 'start', 'aliases' => ['start', 'inicio', 'começar'], 'description' => 'Inicia o bot e mostra o menu principal', 'action_handler' => 'App\Services\Telegram\Handlers\StartCommandHandler', 'action_method' => 'handle', 'action_parameters' => [], 'permissions' => ['all'], 'category' => 'general', 'voice_settings' => [ 'enabled' => true, 'priority' => 1, 'noise_reduction' => false, 'language' => ['pt'] ], 'natural_language' => [ 'iniciar', 'começar', 'menu principal', 'ajuda' ], 'fallback' => [ 'message' => 'Use /start para iniciar o bot', 'suggestions' => ['/start', '/help', '/menu'] ], 'is_active' => true, 'priority' => 1 ], [ 'command_id' => 'help', 'aliases' => ['help', 'ajuda', 'comandos'], 'description' => 'Mostra a lista de comandos disponíveis', 'action_handler' => 'App\Services\Telegram\Handlers\HelpCommandHandler', 'action_method' => 'handle', 'action_parameters' => [], 'permissions' => ['all'], 'category' => 'general', 'voice_settings' => [ 'enabled' => true, 'priority' => 1, 'noise_reduction' => false, 'language' => ['pt'] ], 'natural_language' => [ 'ajuda', 'comandos', 'como usar', 'instruções' ], 'fallback' => [ 'message' => 'Use /help para ver os comandos disponíveis', 'suggestions' => ['/help', '/start', '/menu'] ], 'is_active' => true, 'priority' => 2 ], [ 'command_id' => 'status', 'aliases' => ['status', 'estado', 'situação'], 'description' => 'Mostra o status do sistema', 'action_handler' => 'App\Services\Telegram\Handlers\StatusCommandHandler', 'action_method' => 'handle', 'action_parameters' => [], 'permissions' => ['admin', 'manager'], 'category' => 'admin', 'voice_settings' => [ 'enabled' => true, 'priority' => 2, 'noise_reduction' => false, 'language' => ['pt'] ], 'natural_language' => [ 'status', 'estado', 'situação', 'como está' ], 'fallback' => [ 'message' => 'Comando de status disponível apenas para administradores', 'suggestions' => ['/help', '/start'] ], 'is_active' => true, 'priority' => 3 ] ]; foreach ($commands as $command) { TelegramCommand::updateOrCreate( ['command_id' => $command['command_id']], $command ); } } }
### Script de Instalação Atualizado
<code bash> #!/bin/bash
# Telegram Webhook Installation Script # Instala todas as dependências necessárias para o sistema de Telegram Webhook
set -e
echo “🚀 Iniciando instalação do Telegram Webhook…”
# Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color
# Functions print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Check if we're in the right directory if [ ! -f “composer.json” ]; then
print_error "Execute este script no diretório raiz do projeto Laravel" exit 1
fi
print_status “Verificando dependências…”
# Install Composer dependencies print_status “Instalando dependências do Composer…” composer install –no-dev –optimize-autoloader
# Copy environment file if [ ! -f “.env” ]; then
print_status "Criando arquivo .env..." cp .env.example .env print_warning "Configure suas credenciais no arquivo .env"
fi
# Generate application key print_status “Gerando chave da aplicação…” php artisan key:generate
# Run migrations print_status “Executando migrations…” php artisan migrate –force
# Run seeders print_status “Executando seeders…” php artisan db:seed –class=TelegramCommandSeeder
# Clear and cache configuration print_status “Limpando e otimizando cache…” php artisan config:clear php artisan config:cache php artisan route:cache php artisan view:cache
# Create necessary directories print_status “Criando diretórios necessários…” mkdir -p storage/logs/telegram mkdir -p storage/logs/api mkdir -p storage/logs/business mkdir -p storage/logs/security mkdir -p storage/logs/performance mkdir -p storage/logs/audit mkdir -p storage/logs/whatsapp mkdir -p storage/logs/errors
# Set permissions print_status “Configurando permissões…” chmod -R 775 storage chmod -R 775 bootstrap/cache
# Test the installation print_status “Testando instalação…”
# Test database connection if php artisan tinker –execute=“echo 'Database connection: ' . (DB::connection()→getPdo() ? 'OK' : 'FAILED');” | grep -q “OK”; then
print_success "Conexão com banco de dados: OK"
else
print_error "Falha na conexão com banco de dados" exit 1
fi
# Test Telegram configuration if php artisan tinker –execute=“echo 'Telegram config: ' . (config('services.telegram.bot_token') ? 'OK' : 'MISSING');” | grep -q “OK”; then
print_success "Configuração do Telegram: OK"
else
print_warning "Configure TELEGRAM_BOT_TOKEN no arquivo .env"
fi
print_success “Instalação concluída com sucesso! 🎉” print_status “Próximos passos:” echo “1. Configure suas credenciais no arquivo .env” echo “2. Configure o webhook: php artisan telegram:webhook-setup” echo “3. Teste o sistema: php artisan telegram:test” echo “4. Monitore os logs: tail -f storage/logs/telegram.log”