/ // instagram_token_ (token encrypted, or INSTAGRAM_TOKEN_ in .env). // Same cache pattern as functions_reviews.php: config/cache/instagram_.cache.php, // lazy refresh under a non-blocking lock, old copy kept on error. Thumbnails // are downloaded at refresh time to uploads/instagram// (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, " 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']))); }