config = $config; $this->logger = \OC::$server->getLogger(); $this->dbService = $dbService; } /** * Check if Google Analytics is configured */ public function isConfigured(): bool { $clientId = $this->config->getAppValue('google_client_id', AppInfo::APP_NAME); $clientSecret = $this->config->getAppValue('google_client_secret', AppInfo::APP_NAME); $refreshToken = $this->config->getAppValue('google_refresh_token', AppInfo::APP_NAME); return !empty($clientId) && !empty($clientSecret) && !empty($refreshToken); } /** * Check if LLM service is configured */ public function isLLMConfigured(): bool { $apiKey = $this->config->getAppValue('anthropic_api_key', AppInfo::APP_NAME); return !empty($apiKey); } /** * Get client by slug */ public function getClientBySlug(string $slug): ?ClientConfig { $clients = $this->getClients(); foreach ($clients as $client) { if ($client->getSlug() === $slug) { return $client; } } return null; } /** * Get all active clients */ public function getActiveClients(): array { $clients = $this->getClients(); return array_filter($clients, fn($c) => $c->isActive()); } /** * Get all clients */ private function getClients(): array { $json = $this->config->getAppValue('clients_json', AppInfo::APP_NAME); if (empty($json)) { return []; } $data = json_decode($json, true); if (!is_array($data['clients'] ?? null)) { return []; } return array_map(fn($c) => ClientConfig::fromJson($c), $data['clients']); } /** * Get client count */ public function getClientCount(): int { return count($this->getClients()); } /** * Fetch GA4 data */ public function fetchGA4Data(ClientConfig $client, string $dateRange = '7d'): array { $this->logger->info("Fetching GA4 data for: {$client->getName()}"); try { // Get fresh access token $accessToken = $this->getFreshAccessToken(); // Build API request URL $propertyId = $client->getPropertyId(); $url = self::GA_API_BASE . "/properties/{$propertyId}:runReport"; // Build request body $requestBody = [ 'dateRanges' => [ [ 'startDate' => $this->getDateRange($dateRange, 'start'), 'endDate' => $this->getDateRange($dateRange, 'end'), ] ], 'metrics' => [ 'sessions', 'totalUsers', 'conversions', 'eventCount' ], 'dimensions' => [ 'date', 'sessionDefaultChannelGroup', 'pagePath' ] ]; // Make HTTP request $response = $this->makeGARequest($url, $accessToken, $requestBody); if (!$response) { throw new \Exception('Failed to fetch GA4 data'); } $responseData = $this->parseGAResponse($response); if (!$responseData) { throw new \Exception('Invalid GA4 response format'); } $this->logger->info("GA4 data fetched successfully"); return $responseData; } catch (\Exception $e) { $this->logger->error("GA4 fetch failed: {$e->getMessage()}"); throw $e; } } /** * Get fresh access token */ private function getFreshAccessToken(): string { $refreshToken = $this->config->getAppValue('google_refresh_token', AppInfo::APP_NAME); if (empty($refreshToken)) { throw new \Exception('Refresh token not configured'); } try { $response = $this->refreshAccessToken($refreshToken); if (!$response || empty($response['access_token'])) { throw new \Exception('Failed to refresh access token'); } $this->logger->info("Access token refreshed"); return $response['access_token']; } catch (\Exception $e) { $this->logger->error("Token refresh failed: {$e->getMessage()}"); // Check if it's an expired token error if (str_contains($e->getMessage(), 'invalid_grant')) { throw new TokenExpiredException('Refresh token expired - re-run OAuth setup'); } throw $e; } } /** * Refresh access token using refresh token */ private function refreshAccessToken(string $refreshToken): ?array { $clientId = $this->config->getAppValue('google_client_id', AppInfo::APP_NAME); $clientSecret = $this->config->getAppValue('google_client_secret', AppInfo::APP_NAME); $url = self::TOKEN_REFRESH_URL; $data = [ 'client_id' => $clientId, 'client_secret' => $clientSecret, 'refresh_token' => $refreshToken, 'grant_type' => 'refresh_token' ]; $response = $this->makeHttpPOST($url, $data, []); if (!$response) { return null; } return json_decode($response, true); } /** * Make HTTP POST request */ private function makeHttpPOST(string $url, array $data, array $headers = []): ?string { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge([ 'Content-Type: application/json' ], $headers)); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { $this->logger->error("Curl error: " . curl_error($ch)); curl_close($ch); return null; } if ($httpCode !== 200) { $this->logger->error("HTTP {$httpCode} from: {$url}"); } curl_close($ch); return $httpCode === 200 ? $response : null; } /** * Make GA4 API request with auth */ private function makeGARequest(string $url, string $accessToken, array $body): ?array { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 'Content-Type: application/json', 'Authorization: Bearer ' . $accessToken ]); curl_setopt($ch, CURLOPT_TIMEOUT, 30); $response = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (curl_errno($ch)) { $this->logger->error("GA4 API curl error: " . curl_error($ch)); curl_close($ch); return null; } if ($httpCode !== 200) { $this->logger->error("GA4 API HTTP {$httpCode}"); } curl_close($ch); return $httpCode === 200 ? json_decode($response, true) : null; } /** * Parse GA4 response */ private function parseGAResponse(string $response): ?array { $decoded = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { $this->logger->error("JSON decode error"); return null; } // Check for runReport response if (!isset($decoded['runs'])) { $this->logger->error("Invalid GA4 response format - missing runs"); return null; } // Process runs $runs = $decoded['runs'] ?? []; // Extract data from runs (GA4 returns array of run objects) $data = []; foreach ($runs as $run) { if (isset($run['rows'])) { foreach ($run['rows'] as $row) { foreach ($row['dimensionValues'] ?? [] as $i => $value) { $data[$i] = $value; } foreach ($row['metricValues'] ?? [] as $i => $value) { $data[$i] = $value; } } } } return empty($data) ? null : [ 'dates' => $data, 'metrics' => $this->extractMetrics($runs) ]; } /** * Extract metrics from GA4 response */ private function extractMetrics(array $runs): array { $metrics = [ 'sessions' => [], 'totalUsers' => [], 'conversions' => [], 'eventCount' => [] ]; foreach ($runs as $run) { if (isset($run['rows'])) { foreach ($run['rows'] as $row) { foreach ($row['metricValues'] ?? [] as $i => $value) { $metric = $row['metricValues'][$i]['name'] ?? null; if ($metric === 'sessions') { $metrics['sessions'][] = $value['value'] ?? 0; } elseif ($metric === 'totalUsers') { $metrics['totalUsers'][] = $value['value'] ?? 0; } elseif ($metric === 'conversions') { $metrics['conversions'][] = $value['value'] ?? 0; } elseif ($metric === 'eventCount') { $metrics['eventCount'][] = $value['value'] ?? 0; } } } } } return $metrics; } /** * Get date range (last N days) */ private function getDateRange(string $range, string $type): string { $days = (int)str_replace(['d', 'w', 'm'], '', $range); $date = new \DateTime(); if ($type === 'start') { $date->modify("-{$days} days"); } else { $date->modify("-{$days} days"); } return $date->format('Y-m-d'); } /** * Save report to Nextcloud */ public function saveReport(ClientConfig $client, string $markdown): array { $basePath = $client->getWebdavConfig()['base_path'] ?? '/AIGeneratedReports'; $filename = "Report_" . date('Y-m-d') . ".md"; $fullPath = $basePath . '/' . $filename; $this->logger->info("Saving report to: {$fullPath}"); // Use Nextcloud file API $folder = $this->ensureNextcloudFolder($basePath); if (!$folder) { throw new \Exception("Failed to create Nextcloud folder: {$basePath}"); } $file = $folder->newFile($filename); $file->putContent($markdown); $this->logger->info("Report saved: {$fullPath}"); return [ 'file_path' => $fullPath, 'file_size' => strlen($markdown) ]; } /** * Get report by ID */ public function getReportById(int $id): ?array { $report = $this->dbService->getReportById($id); if (!$report) { return null; } return [ 'id' => $report['id'], 'client_id' => $report['client_id'], 'client_name' => $report['client_name'], 'report_date' => $report['report_date'], 'file_path' => $report['file_path'], 'file_size' => $report['file_size'], 'created_at' => $report['created_at'] ]; } /** * Get all reports */ public function getAllReports(): array { $reports = $this->dbService->getAllReports(); return array_map(function($r) { return [ 'id' => $r['id'], 'client_id' => $r['client_id'], 'client_name' => $r['client_name'], 'report_date' => $r['report_date'], 'file_path' => $r['file_path'], 'file_size' => $r['file_size'], 'created_at' => $r['created_at'] ]; }, $reports); } /** * Generate reports for all active clients */ public function generateForAllClients(): array { $clients = $this->getActiveClients(); $results = []; foreach ($clients as $client) { try { $this->generateReportForClient($client); $results[$client->getSlug()] = 'success'; } catch (\Exception $e) { $results[$client->getSlug()] = "error: {$e->getMessage()}"; } } return $results; } /** * Generate report for a single client */ private function generateReportForClient(ClientConfig $client): void { $this->logger->info("Generating report for: {$client->getName()}"); $rawData = $this->fetchGA4Data($client, '7d'); // Process with DataProcessor $processor = new \OCA\AnalyticsHub\Service\DataProcessor($this->config); $processed = $processor->process($rawData, $client); // Generate with LLM $llmService = new \OCA\AnalyticsHub\Service\LLMService($this->config); $markdown = $llmService->generate($processed, $client); // Save to Nextcloud $result = $this->saveReport($client, $markdown); $this->logger->info("Report generated and saved"); } /** * Get last report time */ public function getLastReportTime(): ?string { $report = $this->dbService->getLatestReport(); return $report ? $report['created_at'] : null; } /** * Ensure Nextcloud folder exists */ private function ensureNextcloudFolder(string $path) ?\OCP\Files\Folder { $userFolder = \OC::$server->getUserFolder(); if (!$userFolder->nodeExists($path)) { $this->logger->info("Creating Nextcloud folder: {$path}"); return $userFolder->newFolder($path); } return $userFolder->get($path); } }