service_name = 'instagram'; $this->title = 'Instagram'; $this->icon = 'instagram'; $this->apiBase = [ 'graph' => self::GRAPH_API_BASE . self::API_VERSION, 'instagram' => self::INSTAGRAM_BASE . self::API_VERSION ]; $this->apiEndpoints = [ 'me/accounts', 'media', 'media_publish', 'content_publishing_limit', 'insights' ]; $this->isOAuthService = true; // OAuth configuration for Facebook/Instagram $this->oauth = [ 'authorize' => 'https://www.facebook.com/' . self::API_VERSION . '/dialog/oauth', 'token' => self::GRAPH_API_BASE . self::API_VERSION . '/oauth/access_token', 'redirect_uri' => rest_url('jvb/v1/oauth/instagram'), 'scopes' => [ 'instagram_basic', 'instagram_content_publish', 'instagram_manage_insights', 'pages_show_list', 'pages_read_engagement', 'business_management' ] ]; // Define sync capabilities $this->canSync = [ 'create' => true, 'update' => false, // Instagram doesn't allow editing posts 'delete' => true ]; $this->fields = [ ]; $this->advanced = [ ]; $this->instructions = [ 'Setup Facebook in order to use Instagram.' ]; $this->defaults = [ ]; $this->handleWebhooks = true; parent::__construct($userID); $this->actions = array_merge( $this->actions, [ 'switch_account' => 'switchInstagramAccount' ] ); } protected function initialize(): void { $this->page_access_token = $this->credentials['page_access_token'] ?? ''; $this->ig_user_id = $this->credentials['ig_user_id'] ?? ''; $this->page_id = $this->credentials['page_id'] ?? ''; $this->business_account = $this->credentials['business_account'] ?? []; $this->auto_post = $this->credentials['auto_post'] ?? false; $this->post_settings = $this->credentials['post_settings'] ?? [ 'include_caption' => true, 'include_hashtags' => true, 'include_location' => false, 'default_hashtags' => '' ]; // Set rate limits $this->rate_limits = [ 'min_interval' => 30, 'daily_limit' => 25, // Instagram content publishing limit 'hourly_limit' => 5 ]; } /** * Get request headers for Instagram API */ protected function getRequestHeaders(): array { return [ 'Content-Type' => 'application/json', 'Accept' => 'application/json' ]; } /** * Build Instagram API URL */ protected function getApiUrl(string $endpoint, ?string $baseKey = null): string|false { $base = ($baseKey === 'instagram') ? $this->apiBase['instagram'] : $this->apiBase['graph']; // Handle Instagram-specific endpoints if (in_array($endpoint, ['media', 'media_publish'])) { return "{$base}/{$this->ig_user_id}/{$endpoint}"; } return "{$base}/{$endpoint}"; } /** * Add OAuth parameters specific to Facebook/Instagram */ protected function addOAuthParams(array $params): array { $params['display'] = 'popup'; $params['auth_type'] = 'rerequest'; return $params; } public function isSetUp():bool { $fb = JVB()->connect('facebook'); return $fb->isSetUp(); } protected function getOAuthCredentials(): array { // Get Facebook integration $facebook = JVB()->connect('facebook'); if (!$facebook || !$facebook->isSetUp()) { return []; } $fbCredentials = $facebook->getCredentials(); return [ 'client_id' => $fbCredentials['app_id'] ?? $fbCredentials['client_id'] ?? '', 'client_secret' => $fbCredentials['app_secret'] ?? $fbCredentials['client_secret'] ?? '', 'access_token' => $fbCredentials['access_token'] ?? '', ]; } public function getCredentials():array { parent::getCredentials(); if (empty($this->credentials)) { $facebook = JVB()->connect('facebook'); if (!$facebook || !$facebook->isSetUp()) { return []; } $fbCredentials = $facebook->getCredentials(); $this->credentials = [ 'client_id' => $fbCredentials['client_id'], 'client_secret' => $fbCredentials['client_secret'], ]; } return $this->credentials; } private function switchInstagramAccount(array $data): WP_Error|array { $page_id = $data['page_id'] ?? ''; if (empty($page_id)) { return [ 'success' => false, 'message' => 'Page ID is required' ]; } // Find the selected page in available pages $selected_page = null; foreach ($this->credentials['available_pages'] ?? [] as $page) { if ($page['id'] === $page_id) { $selected_page = $page; break; } } if (!$selected_page || empty($selected_page['instagram_business_account'])) { return [ 'success' => false, 'message' => 'Invalid page or no Instagram account' ]; } // Update credentials with new page $this->credentials['page_id'] = $selected_page['id']; $this->credentials['page_access_token'] = $selected_page['access_token']; $this->credentials['ig_user_id'] = $selected_page['instagram_business_account']['id']; $this->credentials['business_account'] = $this->getBusinessProfile( $selected_page['instagram_business_account']['id'], $selected_page['access_token'] ); $this->saveCredentials($this->credentials); // Reinitialize with new credentials $this->initialize(); return [ 'success' => true, 'message' => 'Switched to @' . ($this->credentials['business_account']['username'] ?? 'Instagram account'), 'account' => $this->credentials['business_account'] ]; } /** * Handle post save - queue Instagram post creation */ public function handleTheSavePost(int $postID, \WP_Post $post, bool $update, array $settings): void { // Check if post has featured image (Instagram requirement) if (!has_post_thumbnail($postID)) { $this->logError('Cannot post to Instagram without featured image', [ 'post_id' => $postID ]); return; } // Queue the Instagram post creation $this->queueOperation('create_post', [ 'post_id' => $postID, 'type' => get_post_type($postID) ], [ 'priority' => 'normal', 'delay' => 60 // Wait 1 minute to ensure all metadata is saved ]); update_post_meta($postID, BASE . '_instagram_sync_status', 'queued'); } /** * Process queued operations */ public function processOperation(\WP_Error|array $result, object $operation, array $data): \WP_Error|array { $base = strtolower($this->service_name) . '_'; $insta = (array_key_exists('user', $data)) ? new self((int)$data['user']) : $this; switch ($operation->type) { case $base . 'create_post': return $insta->processCreatePost($data); case $base . 'create_story': return $insta->processCreateStory($data); case $base . 'delete_post': return $insta->processDeletePost($data); default: return $result; } } private function processDeletePost(array $data): array { return [ 'success' => true, 'message' => 'Instagram sync data removed (post remains on Instagram)', 'note' => 'Instagram API does not support deleting posts programmatically' ]; } /** * Process post creation for Instagram */ private function processCreatePost(array $data): array { $post_id = $data['post_id'] ?? 0; $post = get_post($post_id); if (!$post) { return ['success' => false, 'message' => 'Post not found']; } try { // Get image URL $image_url = get_the_post_thumbnail_url($post_id, 'full'); if (!$image_url) { throw new Exception('No featured image found'); } // Build caption $caption = $this->buildCaption($post); // Create media container $container = $this->createMediaContainer($image_url, $caption); if (!$container || empty($container['id'])) { throw new Exception('Failed to create media container'); } // Wait for processing (Instagram needs time to process the image) sleep(10); // Publish the media $published = $this->publishMedia($container['id']); if ($published && !empty($published['id'])) { // Store Instagram media ID update_post_meta($post_id, BASE . '_instagram_media_id', $published['id']); update_post_meta($post_id, BASE . '_instagram_sync_status', 'synced'); update_post_meta($post_id, BASE . '_instagram_sync_date', current_time('mysql')); // Get permalink if available if (!empty($published['permalink'])) { update_post_meta($post_id, BASE . '_instagram_permalink', $published['permalink']); } return [ 'success' => true, 'message' => 'Posted to Instagram successfully', 'instagram_id' => $published['id'] ]; } throw new Exception('Failed to publish media'); } catch (Exception $e) { $this->logError('Instagram post creation failed', [ 'post_id' => $post_id, 'error' => $e->getMessage() ]); update_post_meta($post_id, BASE . '_instagram_sync_status', 'failed'); update_post_meta($post_id, BASE . '_instagram_sync_error', $e->getMessage()); return [ 'success' => false, 'message' => $e->getMessage() ]; } } /** * Build caption for Instagram post */ private function buildCaption(\WP_Post $post): string { $caption_parts = []; // Add title and description if enabled if ($this->post_settings['include_caption'] ?? true) { $caption_parts[] = $post->post_title; if (!empty($post->post_excerpt)) { $caption_parts[] = "\n\n" . $post->post_excerpt; } } // Add hashtags if ($this->post_settings['include_hashtags'] ?? true) { $hashtags = []; // Get tags from post $tags = wp_get_post_tags($post->ID, ['fields' => 'names']); foreach ($tags as $tag) { $hashtags[] = '#' . str_replace(' ', '', $tag); } // Add default hashtags if (!empty($this->post_settings['default_hashtags'])) { $default = preg_split('/[\s,]+/', $this->post_settings['default_hashtags']); foreach ($default as $tag) { $tag = trim($tag); if (strpos($tag, '#') !== 0) { $tag = '#' . $tag; } $hashtags[] = $tag; } } if (!empty($hashtags)) { $caption_parts[] = "\n\n" . implode(' ', array_unique($hashtags)); } } // Add website link $caption_parts[] = "\n\nšŸ”— " . get_permalink($post); return implode('', $caption_parts); } /** * Webhook validation */ protected function validateWebhook(array $payload): bool { // Facebook webhook verification if (isset($payload['hub_mode']) && $payload['hub_mode'] === 'subscribe') { $verify_token = $this->credentials['webhook_verify_token'] ?? 'jvb_instagram_webhook'; if ($payload['hub_verify_token'] === $verify_token) { // Echo the challenge for verification echo $payload['hub_challenge']; exit; } return false; } // Validate webhook signature for actual events $signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? ''; if (!$signature) { return false; } $app_secret = $this->credentials['app_secret'] ?? ''; if (!$app_secret) { return true; // Allow if no secret configured } $expected = 'sha256=' . hash_hmac('sha256', file_get_contents('php://input'), $app_secret); return hash_equals($expected, $signature); } /** * Process webhook */ protected function processWebhook(array $payload): bool { $entry = $payload['entry'][0] ?? []; $changes = $entry['changes'] ?? []; foreach ($changes as $change) { $field = $change['field'] ?? ''; $value = $change['value'] ?? []; switch ($field) { case 'comments': $this->handleCommentWebhook($value); break; case 'mentions': $this->handleMentionWebhook($value); break; default: $this->logDebug('Unhandled webhook field', ['field' => $field]); } } return true; } /** * API Helper Methods */ private function getFacebookPages(string $access_token): array { $response = wp_remote_get( $this->apiBase['graph'] . '/me/accounts?' . http_build_query([ 'access_token' => $access_token, 'fields' => 'id,name,access_token,instagram_business_account' ]) ); if (is_wp_error($response)) { $this->logError('Failed to get Facebook pages', ['error' => $response->get_error_message()]); return []; } $data = json_decode(wp_remote_retrieve_body($response), true); return $data['data'] ?? []; } private function getBusinessProfile(string $ig_user_id, string $access_token): array { $response = wp_remote_get( $this->apiBase['graph'] . '/' . $ig_user_id . '?' . http_build_query([ 'access_token' => $access_token, 'fields' => 'id,username,name,profile_picture_url,followers_count,media_count,biography,website' ]) ); if (is_wp_error($response)) { return []; } return json_decode(wp_remote_retrieve_body($response), true) ?: []; } private function createMediaContainer(string $image_url, string $caption): ?array { $response = wp_remote_post( $this->getApiUrl('media', 'instagram'), [ 'body' => [ 'image_url' => $image_url, 'caption' => $caption, 'access_token' => $this->page_access_token ] ] ); if (is_wp_error($response)) { $this->logError('Failed to create media container', ['error' => $response->get_error_message()]); return null; } return json_decode(wp_remote_retrieve_body($response), true); } private function publishMedia(string $creation_id): ?array { $response = wp_remote_post( $this->getApiUrl('media_publish', 'instagram'), [ 'body' => [ 'creation_id' => $creation_id, 'access_token' => $this->page_access_token ] ] ); if (is_wp_error($response)) { $this->logError('Failed to publish media', ['error' => $response->get_error_message()]); return null; } return json_decode(wp_remote_retrieve_body($response), true); } private function getPublishingLimit(): array { $response = wp_remote_get( $this->getApiUrl('content_publishing_limit', 'instagram') . '?' . http_build_query([ 'access_token' => $this->page_access_token ]) ); if (is_wp_error($response)) { return new WP_Error('api_error', $response->get_error_message()); } $data = json_decode(wp_remote_retrieve_body($response), true); return [ 'success' => true, 'data' => [ 'quota_usage' => $data['data'][0]['quota_usage'] ?? 0, 'quota_total' => $data['data'][0]['config']['quota_total'] ?? 25, 'quota_duration' => $data['data'][0]['config']['quota_duration'] ?? 86400 ] ]; } protected function performConnectionTest(): bool { if (empty($this->ig_user_id) || empty($this->page_access_token)) { return false; } // Test by getting account info $profile = $this->getBusinessProfile($this->ig_user_id, $this->page_access_token); if (!empty($profile['id'])) { return true; } return false; } private function createStory(array $data): WP_Error|array { $image_url = $data['image_url'] ?? ''; if (empty($image_url)) { return new WP_Error('missing_image', 'Image URL required for story'); } // Queue story creation $this->queueOperation('create_story', [ 'image_url' => $image_url, 'sticker_url' => $data['sticker_url'] ?? '' ], [ 'priority' => 'high' ]); return [ 'success' => true, 'message' => 'Story creation queued' ]; } private function processCreateStory(array $data): array { $params = [ 'image_url' => $data['image_url'], 'media_type' => 'STORIES', 'access_token' => $this->page_access_token ]; if (!empty($data['sticker_url'])) { $params['sticker_url'] = $data['sticker_url']; } // Create story container $response = wp_remote_post( $this->getApiUrl('media', 'instagram'), ['body' => $params] ); if (is_wp_error($response)) { return [ 'success' => false, 'message' => $response->get_error_message() ]; } $container = json_decode(wp_remote_retrieve_body($response), true); if (empty($container['id'])) { return [ 'success' => false, 'message' => 'Failed to create story container' ]; } // Wait for processing sleep(5); // Publish story $published = $this->publishMedia($container['id']); return [ 'success' => !empty($published['id']), 'message' => !empty($published['id']) ? 'Story published' : 'Failed to publish story', 'story_id' => $published['id'] ?? null ]; } private function handleCommentWebhook(array $value): void { // Log new comments for moderation $this->logDebug('New Instagram comment', $value); // Could trigger notifications or store for moderation do_action(BASE . 'instagram_comment_received', $value); } private function handleMentionWebhook(array $value): void { // Log mentions $this->logDebug('New Instagram mention', $value); // Could trigger notifications do_action(BASE . 'instagram_mention_received', $value); } /** * Get service description */ public function getServiceDescription(): string { return "Connect your Instagram Business account to automatically share your content and manage your Instagram presence."; } }