- Rebuilt the CLI importer to use the official Facebook Graph API instead of HTML scraping.
Main changes: Replaced [scripts/facebook-events-import.php](/Users/tyemeclifford/Documents/GH/MyKeyser/scripts/facebook-events-import.php) with a Graph API client. It now supports --page-id, --pages-file, --event-id, --events-file, and optional area discovery via --discover-pages --center=LAT,LNG --query=.... Uses FACEBOOK_GRAPH_ACCESS_TOKEN or --access-token. Pulls Graph fields for title, description, start/end time, place/location, ticket URI, and cover.source for the main photo. Keeps the existing staged review/import workflow in /admin/event_imports.php. - Implemented event detail pages and clickable event cards. What changed: Added [event.php](/Users/tyemeclifford/Documents/GH/MyKeyser/event.php) for individual event pages. Made event cards clickable from [events.php](/Users/tyemeclifford/Documents/GH/MyKeyser/events.php), [index.php](/Users/tyemeclifford/Documents/GH/MyKeyser/index.php), and legacy [2index.php](/Users/tyemeclifford/Documents/GH/MyKeyser/2index.php). Added large event photo display plus lightbox enlargement in [assets/css/style.css](/Users/tyemeclifford/Documents/GH/MyKeyser/assets/css/style.css) and [assets/js/app.js](/Users/tyemeclifford/Documents/GH/MyKeyser/assets/js/app.js). Added eventUrl() and externalHref() helpers in [includes/functions.php](/Users/tyemeclifford/Documents/GH/MyKeyser/includes/functions.php). Added copy-event-link support on event detail pages. Updated README to list the new route.
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
require_once dirname(__DIR__).'/includes/boot.php';
|
||||
require_once dirname(__DIR__).'/includes/facebook_event_importer.php';
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
fwrite(STDERR, "This script must be run from the command line.\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const FB_GRAPH_DEFAULT_VERSION = 'v23.0';
|
||||
const FB_GRAPH_EVENT_FIELDS = 'id,name,description,start_time,end_time,place{id,name,location},cover{source},ticket_uri';
|
||||
const FB_GRAPH_PLACE_FIELDS = 'id,name,location';
|
||||
|
||||
function fbCliUsage(): void {
|
||||
echo "Facebook Graph API event importer for My Keyser\n\n";
|
||||
echo "Usage:\n";
|
||||
echo " FACEBOOK_GRAPH_ACCESS_TOKEN=... php scripts/facebook-events-import.php --area=\"Keyser, WV\" --page-id=123456789\n";
|
||||
echo " php scripts/facebook-events-import.php --access-token=... --event-id=123456789012345\n";
|
||||
echo " php scripts/facebook-events-import.php --access-token=... --pages-file=/path/to/page-ids.txt\n";
|
||||
echo " php scripts/facebook-events-import.php --access-token=... --area=\"Keyser, WV\" --center=39.4364,-78.9762 --distance=25000 --query=\"Keyser\" --discover-pages\n\n";
|
||||
echo "Required:\n";
|
||||
echo " --access-token=TOKEN Graph API access token, or set FACEBOOK_GRAPH_ACCESS_TOKEN.\n\n";
|
||||
echo "Sources:\n";
|
||||
echo " --page-id=ID[,ID] Facebook Page IDs to read through /{page-id}/events.\n";
|
||||
echo " --pages-file=FILE Plain-text Page ID file, one ID per line.\n";
|
||||
echo " --event-id=ID[,ID] Specific Facebook Event IDs to read through /{event-id}.\n";
|
||||
echo " --events-file=FILE Plain-text Event ID file, one ID per line.\n";
|
||||
echo " --discover-pages Use Graph place search for --center/--distance/--query, then read events for matching Page/place IDs.\n\n";
|
||||
echo "Options:\n";
|
||||
echo " --area=TEXT Label shown in the admin import review queue.\n";
|
||||
echo " --center=LAT,LNG Center point for Graph place search.\n";
|
||||
echo " --distance=METERS Radius for Graph place search. Default: 25000.\n";
|
||||
echo " --query=TEXT Place search query. Defaults to --area.\n";
|
||||
echo " --place-limit=N Maximum places to discover. Default: 50.\n";
|
||||
echo " --limit=N Maximum events to stage total. Default: 100.\n";
|
||||
echo " --since=YYYY-MM-DD Start date for Page event reads. Default: today.\n";
|
||||
echo " --until=YYYY-MM-DD End date for Page event reads. Default: +1 year.\n";
|
||||
echo " --graph-version=vNN.0 Graph API version. Default: ".FB_GRAPH_DEFAULT_VERSION." or FACEBOOK_GRAPH_VERSION.\n";
|
||||
echo " --no-images Stage Graph cover URLs but skip downloading cover photos.\n";
|
||||
echo " --dry-run Fetch and print candidates without writing to the database.\n";
|
||||
echo " --help Show this help.\n\n";
|
||||
echo "Notes:\n";
|
||||
echo " This script uses only official Graph API endpoints. It does not scrape HTML, log in,\n";
|
||||
echo " solve challenges, or bypass Facebook access controls. Your Meta app/token must have\n";
|
||||
echo " permission to read the requested Page or Event data.\n";
|
||||
}
|
||||
|
||||
function fbCliOptions(array $argv): array {
|
||||
$options = [];
|
||||
for ($i = 1; $i < count($argv); $i++) {
|
||||
$arg = $argv[$i];
|
||||
if (!str_starts_with($arg, '--')) continue;
|
||||
$arg = substr($arg, 2);
|
||||
if (str_contains($arg, '=')) {
|
||||
[$key, $value] = explode('=', $arg, 2);
|
||||
} else {
|
||||
$key = $arg;
|
||||
$value = true;
|
||||
if ($i + 1 < count($argv) && !str_starts_with($argv[$i + 1], '--')) $value = $argv[++$i];
|
||||
}
|
||||
if (isset($options[$key])) {
|
||||
if (!is_array($options[$key])) $options[$key] = [$options[$key]];
|
||||
$options[$key][] = $value;
|
||||
} else {
|
||||
$options[$key] = $value;
|
||||
}
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
function fbCliValues(array $options, string $key): array {
|
||||
if (!isset($options[$key])) return [];
|
||||
$value = $options[$key];
|
||||
if (!is_array($value)) $value = [$value];
|
||||
$out = [];
|
||||
foreach ($value as $item) {
|
||||
foreach (explode(',', (string)$item) as $part) {
|
||||
$part = trim($part);
|
||||
if ($part !== '') $out[] = $part;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
function fbCliFileValues(array $options, string $key): array {
|
||||
$out = [];
|
||||
foreach (fbCliValues($options, $key) as $file) {
|
||||
if (!is_file($file) || !is_readable($file)) {
|
||||
throw new RuntimeException("File is not readable: ".$file);
|
||||
}
|
||||
foreach (file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
|
||||
$line = trim(preg_replace('/#.*/', '', $line));
|
||||
if ($line !== '') $out[] = $line;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
function fbCliLine(string $message = ''): void {
|
||||
echo $message."\n";
|
||||
}
|
||||
|
||||
function fbCliGraphVersion(array $options): string {
|
||||
$version = trim((string)($options['graph-version'] ?? getenv('FACEBOOK_GRAPH_VERSION') ?: FB_GRAPH_DEFAULT_VERSION));
|
||||
if ($version === '') $version = FB_GRAPH_DEFAULT_VERSION;
|
||||
return str_starts_with($version, 'v') ? $version : 'v'.$version;
|
||||
}
|
||||
|
||||
function fbCliGraphBase(string $version): string {
|
||||
return 'https://graph.facebook.com/'.rawurlencode($version).'/';
|
||||
}
|
||||
|
||||
function fbCliGraphRequest(string $path, array $params, string $token, string $version): array {
|
||||
$path = ltrim($path, '/');
|
||||
$url = fbCliGraphBase($version).$path;
|
||||
if ($params) $url .= '?'.http_build_query($params);
|
||||
|
||||
if (function_exists('curl_init')) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => 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);
|
||||
}
|
||||
Reference in New Issue
Block a user