#!/usr/bin/env php true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 4, CURLOPT_TIMEOUT => 30, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_USERAGENT => facebookEventImporterUserAgent(), CURLOPT_HTTPHEADER => [ 'Accept: application/json', 'Authorization: Bearer '.$token, ], ]); $body = curl_exec($ch); $errno = curl_errno($ch); $error = curl_error($ch); $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); if (PHP_VERSION_ID < 80000) curl_close($ch); if ($errno) throw new RuntimeException($error ?: 'Graph API request failed.'); } else { $context = stream_context_create([ 'http' => [ 'method' => 'GET', 'timeout' => 30, 'ignore_errors' => true, 'header' => "Accept: application/json\r\nAuthorization: Bearer ".$token."\r\nUser-Agent: ".facebookEventImporterUserAgent()."\r\n", ], ]); $body = @file_get_contents($url, false, $context); $statusLine = $http_response_header[0] ?? ''; $status = preg_match('/\s(\d{3})\s/', $statusLine, $match) ? (int)$match[1] : 0; if ($body === false) throw new RuntimeException($statusLine ?: 'Graph API request failed.'); } $json = json_decode((string)$body, true); if (!is_array($json)) throw new RuntimeException('Graph API returned invalid JSON.'); if ($status < 200 || $status >= 300 || isset($json['error'])) { $err = $json['error'] ?? []; $message = is_array($err) ? ($err['message'] ?? 'Graph API error') : 'Graph API error'; $code = is_array($err) && isset($err['code']) ? ' (code '.$err['code'].')' : ''; throw new RuntimeException($message.$code); } return $json; } function fbCliDateToTimestamp(string $date, string $fallback): int { $date = trim($date); $ts = strtotime($date !== '' ? $date : $fallback); if ($ts === false) throw new RuntimeException("Invalid date: ".$date); return $ts; } function fbCliDiscoverPages(array $options, string $token, string $version): array { if (empty($options['discover-pages'])) return []; $center = trim((string)($options['center'] ?? '')); if ($center === '' || !preg_match('/^-?\d+(?:\.\d+)?,-?\d+(?:\.\d+)?$/', $center)) { throw new RuntimeException('--discover-pages requires --center=LAT,LNG.'); } $area = trim((string)($options['area'] ?? '')); $query = trim((string)($options['query'] ?? $area)); if ($query === '') throw new RuntimeException('--discover-pages requires --query or --area.'); $limit = max(1, min(100, (int)($options['place-limit'] ?? 50))); $distance = max(1, (int)($options['distance'] ?? 25000)); $json = fbCliGraphRequest('search', [ 'type' => 'place', 'q' => $query, 'center' => $center, 'distance' => $distance, 'fields' => FB_GRAPH_PLACE_FIELDS, 'limit' => $limit, ], $token, $version); $ids = []; foreach (($json['data'] ?? []) as $place) { if (!empty($place['id'])) $ids[(string)$place['id']] = (string)$place['id']; } fbCliLine('Discovered '.count($ids).' Page/place ID'.(count($ids) === 1 ? '' : 's').' from Graph place search.'); return array_values($ids); } function fbCliFetchPageEvents(string $pageId, string $token, string $version, int $since, int $until, int $remaining): array { $events = []; $after = ''; while ($remaining > count($events)) { $params = [ 'fields' => FB_GRAPH_EVENT_FIELDS, 'since' => $since, 'until' => $until, 'limit' => min(100, $remaining - count($events)), ]; if ($after !== '') $params['after'] = $after; $json = fbCliGraphRequest(rawurlencode($pageId).'/events', $params, $token, $version); foreach (($json['data'] ?? []) as $event) { if (is_array($event)) $events[] = $event + ['_source_page_id' => $pageId]; } $after = (string)($json['paging']['cursors']['after'] ?? ''); if ($after === '' || empty($json['data'])) break; } return $events; } function fbCliFetchEvent(string $eventId, string $token, string $version): array { $json = fbCliGraphRequest(rawurlencode($eventId), ['fields' => FB_GRAPH_EVENT_FIELDS], $token, $version); return $json + ['_source_event_id' => $eventId]; } function fbCliStageGraphEvent(array $event, string $area, bool $downloadImages, bool $dryRun, array &$summary): void { $candidate = facebookGraphEventToCandidate($event, $area); $title = $candidate['title'] ?: '(untitled)'; if ($dryRun) { $summary['dry_run']++; fbCliLine('DRY RUN: '.$title.' | '.($candidate['event_date'] ?: 'no date').' | '.($candidate['source_url'] ?: 'no source URL')); return; } $result = facebookEventStageCandidate($candidate, $downloadImages); if ($result['ok']) { $summary[$result['action']] = ($summary[$result['action']] ?? 0) + 1; fbCliLine(strtoupper(str_replace('_', ' ', $result['action'])).': #'.$result['id'].' '.$title); } else { $summary['failed']++; fbCliLine('FAILED: '.$title.' - '.$result['error']); } } $options = fbCliOptions($argv); if (isset($options['help']) || $options === []) { fbCliUsage(); exit(0); } $token = trim((string)($options['access-token'] ?? getenv('FACEBOOK_GRAPH_ACCESS_TOKEN') ?: '')); if ($token === '') { fwrite(STDERR, "Missing Graph API access token. Use --access-token or FACEBOOK_GRAPH_ACCESS_TOKEN.\n\n"); fbCliUsage(); exit(2); } try { $version = fbCliGraphVersion($options); $area = trim((string)($options['area'] ?? '')); $limit = max(1, (int)($options['limit'] ?? 100)); $downloadImages = !isset($options['no-images']); $dryRun = isset($options['dry-run']); $summary = ['created' => 0, 'updated' => 0, 'already_imported' => 0, 'dry_run' => 0, 'failed' => 0, 'graph_failed' => 0]; $pageIds = array_values(array_unique(array_merge( fbCliValues($options, 'page-id'), fbCliFileValues($options, 'pages-file'), fbCliDiscoverPages($options, $token, $version) ))); $eventIds = array_values(array_unique(array_merge( fbCliValues($options, 'event-id'), fbCliFileValues($options, 'events-file') ))); if (!$pageIds && !$eventIds) { throw new RuntimeException('Provide at least one --page-id, --pages-file, --event-id, --events-file, or --discover-pages source.'); } $since = fbCliDateToTimestamp((string)($options['since'] ?? ''), 'today'); $until = fbCliDateToTimestamp((string)($options['until'] ?? ''), '+1 year'); $staged = 0; foreach ($eventIds as $eventId) { if ($staged >= $limit) break; fbCliLine('Fetching Graph event: '.$eventId); try { $event = fbCliFetchEvent($eventId, $token, $version); fbCliStageGraphEvent($event, $area, $downloadImages, $dryRun, $summary); $staged++; } catch (Throwable $e) { $summary['graph_failed']++; fbCliLine('GRAPH FAILED: event '.$eventId.' - '.$e->getMessage()); } } foreach ($pageIds as $pageId) { if ($staged >= $limit) break; $remaining = $limit - $staged; fbCliLine('Fetching Graph Page events: '.$pageId); try { $events = fbCliFetchPageEvents($pageId, $token, $version, $since, $until, $remaining); } catch (Throwable $e) { $summary['graph_failed']++; fbCliLine('GRAPH FAILED: page '.$pageId.' - '.$e->getMessage()); continue; } foreach ($events as $event) { if ($staged >= $limit) break; fbCliStageGraphEvent($event, $area, $downloadImages, $dryRun, $summary); $staged++; } } fbCliLine(); fbCliLine('Summary:'); fbCliLine(' Created: '.$summary['created']); fbCliLine(' Updated: '.$summary['updated']); fbCliLine(' Already imported: '.$summary['already_imported']); if ($dryRun) fbCliLine(' Dry-run candidates: '.$summary['dry_run']); fbCliLine(' Stage failures: '.$summary['failed']); fbCliLine(' Graph failures: '.$summary['graph_failed']); fbCliLine(); fbCliLine('Review staged imports at: /admin/event_imports.php'); } catch (Throwable $e) { fwrite(STDERR, "Error: ".$e->getMessage()."\n"); exit(1); }