Per-gym Graph API /tags fetch with file cache and local thumbnails, section after the gallery, navbar link and admin page (Gestão > Site).
189 lines
7.9 KiB
PHP
189 lines
7.9 KiB
PHP
<?php
|
|
// «Comunidade»: the latest Instagram posts where the gym was tagged, via the
|
|
// Instagram Graph API (GET /{ig-user-id}/tags). Per gym, no migration: the
|
|
// account id and token live in system_settings as instagram_user_id_<gid> /
|
|
// instagram_token_<gid> (token encrypted, or INSTAGRAM_TOKEN_<gid> in .env).
|
|
// Same cache pattern as functions_reviews.php: config/cache/instagram_<gid>.cache.php,
|
|
// lazy refresh under a non-blocking lock, old copy kept on error. Thumbnails
|
|
// are downloaded at refresh time to uploads/instagram/<gid>/ (Instagram CDN
|
|
// URLs expire), so visitors never wait on Instagram.
|
|
|
|
const INSTAGRAM_GRAPH = 'https://graph.facebook.com/v21.0/';
|
|
|
|
function instagram_use_gym(array $gym): void {
|
|
$GLOBALS['_instagram_gym'] = $gym;
|
|
}
|
|
|
|
function instagram_gym(): array {
|
|
return $GLOBALS['_instagram_gym'] ?? current_gym();
|
|
}
|
|
|
|
function _instagram_gid(): int {
|
|
return (int) (instagram_gym()['id'] ?? 1);
|
|
}
|
|
|
|
function instagram_config(): array {
|
|
$gid = _instagram_gid();
|
|
return [
|
|
'user_id' => preg_replace('/\D+/', '', (string) setting('instagram_user_id_' . $gid, '')),
|
|
'token' => setting_secret('instagram_token_' . $gid, 'INSTAGRAM_TOKEN_' . $gid),
|
|
'max' => max(3, min(24, (int) setting('instagram_max', 8))),
|
|
'cache_hours' => max(1, (int) setting('instagram_cache_hours', 6)),
|
|
];
|
|
}
|
|
|
|
function instagram_configured(): bool {
|
|
$c = instagram_config();
|
|
return $c['user_id'] !== '' && $c['token'] !== '';
|
|
}
|
|
|
|
function _instagram_cache_path(): string {
|
|
return __DIR__ . '/cache/instagram_' . _instagram_gid() . '.cache.php';
|
|
}
|
|
|
|
function _instagram_media_dir(): string {
|
|
return dirname(__DIR__) . '/uploads/instagram/' . _instagram_gid() . '/';
|
|
}
|
|
|
|
function instagram_cache_read(): ?array {
|
|
$path = _instagram_cache_path();
|
|
if (!is_file($path)) return null;
|
|
$data = @include $path;
|
|
return is_array($data) ? $data : null;
|
|
}
|
|
|
|
function _instagram_cache_write(array $data): void {
|
|
$path = _instagram_cache_path();
|
|
if (!is_dir(dirname($path))) @mkdir(dirname($path), 0775, true);
|
|
$tmp = $path . '.' . bin2hex(random_bytes(4)) . '.tmp';
|
|
if (@file_put_contents($tmp, "<?php\nreturn " . var_export($data, true) . ";\n", LOCK_EX) === false) return;
|
|
@rename($tmp, $path);
|
|
if (function_exists('opcache_invalidate')) @opcache_invalidate($path, true);
|
|
}
|
|
|
|
function instagram_cache_clear(): void {
|
|
$path = _instagram_cache_path();
|
|
if (is_file($path)) @unlink($path);
|
|
}
|
|
|
|
// GET against the Graph API. Returns [decoded|null, error].
|
|
function _instagram_request(string $url): array {
|
|
if (!function_exists('curl_init')) return [null, 'cURL indisponível nesta instalação.'];
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_CONNECTTIMEOUT => 3,
|
|
CURLOPT_TIMEOUT => 6,
|
|
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
|
]);
|
|
$raw = curl_exec($ch);
|
|
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$cerr = curl_error($ch);
|
|
curl_close($ch);
|
|
if ($raw === false) return [null, 'Sem ligação ao Instagram: ' . $cerr];
|
|
$json = json_decode((string) $raw, true);
|
|
if ($status !== 200) {
|
|
$msg = is_array($json) ? (string) ($json['error']['message'] ?? '') : '';
|
|
return [$json, 'Instagram respondeu ' . $status . ($msg !== '' ? ': ' . $msg : '')];
|
|
}
|
|
return [is_array($json) ? $json : null, is_array($json) ? '' : 'Resposta inválida do Instagram.'];
|
|
}
|
|
|
|
// Saves one thumbnail locally; returns the uploads-relative path or ''.
|
|
function _instagram_save_thumb(string $src, string $id): string {
|
|
if (!preg_match('#^https://[a-z0-9.-]+\.(cdninstagram\.com|fbcdn\.net)/#i', $src)) return '';
|
|
$dir = _instagram_media_dir();
|
|
$rel = 'uploads/instagram/' . _instagram_gid() . '/' . $id . '.jpg';
|
|
if (is_file($dir . $id . '.jpg')) return $rel;
|
|
$ch = curl_init($src);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 2,
|
|
CURLOPT_TIMEOUT => 5, CURLOPT_CONNECTTIMEOUT => 3, CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
|
|
]);
|
|
$bin = curl_exec($ch);
|
|
curl_close($ch);
|
|
$info = (is_string($bin) && $bin !== '' && strlen($bin) < 4 * 1024 * 1024) ? @getimagesizefromstring($bin) : false;
|
|
if (!$info || $info['mime'] !== 'image/jpeg') return '';
|
|
if (!is_dir($dir)) @mkdir($dir, 0775, true);
|
|
return @file_put_contents($dir . $id . '.jpg', $bin) ? $rel : '';
|
|
}
|
|
|
|
// Fetch the tagged media now and rewrite the cache. Returns the cache array.
|
|
function instagram_refresh(): array {
|
|
$c = instagram_config();
|
|
$old = instagram_cache_read() ?? [];
|
|
if ($c['user_id'] === '' || $c['token'] === '') {
|
|
return $old + ['error' => 'Falta o ID da conta ou o token.'];
|
|
}
|
|
// Ask for a few extra: private posts are never returned, and some come without a picture.
|
|
$url = INSTAGRAM_GRAPH . $c['user_id'] . '/tags?' . http_build_query([
|
|
'fields' => 'id,caption,media_type,media_url,thumbnail_url,permalink,timestamp,username',
|
|
'limit' => min(50, $c['max'] + 6),
|
|
'access_token' => $c['token'],
|
|
]);
|
|
[$json, $err] = _instagram_request($url);
|
|
|
|
if ($err !== '') {
|
|
// Keep serving the previous copy; retry after an hour, not a full window.
|
|
$data = $old;
|
|
$data['error'] = $err;
|
|
$data['error_at'] = time();
|
|
$data['fetched_at'] = time() - max(0, $c['cache_hours'] * 3600 - 3600);
|
|
_instagram_cache_write($data);
|
|
error_log('[instagram] ' . $err);
|
|
return $data;
|
|
}
|
|
|
|
$posts = [];
|
|
foreach ((array) ($json['data'] ?? []) as $m) {
|
|
if (count($posts) >= $c['max']) break;
|
|
$id = preg_replace('/\D+/', '', (string) ($m['id'] ?? ''));
|
|
if ($id === '' || empty($m['permalink'])) continue;
|
|
// Videos/reels: the cover (thumbnail_url); photos and albums: media_url.
|
|
$src = (string) (($m['media_type'] ?? '') === 'VIDEO' ? ($m['thumbnail_url'] ?? '') : ($m['media_url'] ?? ''));
|
|
$img = $src !== '' ? _instagram_save_thumb($src, $id) : '';
|
|
if ($img === '') continue;
|
|
$posts[] = [
|
|
'id' => $id,
|
|
'image' => $img,
|
|
'type' => (string) ($m['media_type'] ?? ''),
|
|
'permalink' => (string) $m['permalink'],
|
|
'username' => (string) ($m['username'] ?? ''),
|
|
'caption' => (string) ($m['caption'] ?? ''),
|
|
'timestamp' => (string) ($m['timestamp'] ?? ''),
|
|
];
|
|
}
|
|
// Drop thumbnails of posts that are no longer shown.
|
|
$keep = array_flip(array_column($posts, 'id'));
|
|
foreach (glob(_instagram_media_dir() . '*.jpg') ?: [] as $f) {
|
|
if (!isset($keep[basename($f, '.jpg')])) @unlink($f);
|
|
}
|
|
$data = ['fetched_at' => time(), 'posts' => $posts, 'error' => ''];
|
|
_instagram_cache_write($data);
|
|
return $data;
|
|
}
|
|
|
|
// Cached data, refreshed when older than the cache window (one refresh at a time).
|
|
function instagram_get(): array {
|
|
$c = instagram_config();
|
|
$cache = instagram_cache_read();
|
|
$fresh = $cache && (time() - (int) ($cache['fetched_at'] ?? 0)) < $c['cache_hours'] * 3600;
|
|
if ($fresh || !instagram_configured()) return $cache ?? [];
|
|
|
|
$lock = @fopen(_instagram_cache_path() . '.lock', 'c');
|
|
if ($lock && flock($lock, LOCK_EX | LOCK_NB)) {
|
|
$cache = instagram_refresh();
|
|
flock($lock, LOCK_UN);
|
|
}
|
|
if ($lock) fclose($lock);
|
|
return $cache ?? [];
|
|
}
|
|
|
|
// Posts the homepage shows (only those whose thumbnail is on disk), or [] to hide the section.
|
|
function site_instagram(?array $gym = null): array {
|
|
if ($gym !== null) instagram_use_gym($gym);
|
|
try { $data = instagram_get(); } catch (Throwable $e) { error_log('[instagram] ' . $e->getMessage()); return []; }
|
|
$root = dirname(__DIR__) . '/';
|
|
return array_values(array_filter((array) ($data['posts'] ?? []), fn($p) => is_file($root . $p['image'])));
|
|
}
|