From a8ff85042ffbfe94b678fd818c02deb613bfd116 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Fri, 07 Aug 2026 20:42:54 +0000
Subject: [PATCH] =Continuing on with Integrations setup. Working on PostMark now
---
inc/integrations/services/Instagram.php | 712 +++-------------------------------------------------------
1 files changed, 42 insertions(+), 670 deletions(-)
diff --git a/inc/integrations/services/Instagram.php b/inc/integrations/services/Instagram.php
index 838246f..183679f 100644
--- a/inc/integrations/services/Instagram.php
+++ b/inc/integrations/services/Instagram.php
@@ -2,11 +2,19 @@
/**
* Instagram Integration
* File: /inc/integrations/Instagram.php
+ *
+ * Limits:
+ * - 50 uploads (including carousels) per user per 24-hour period
*/
namespace JVBase\integrations\services;
+use JVBase\integrations\Admin;
+use JVBase\integrations\Instance;
use JVBase\integrations\Integrations;
-use JVBase\meta\Meta;
+use JVBase\integrations\OAuth;
+use JVBase\integrations\OAuthCredentials;
+use JVBase\integrations\OAuthURLs;
+use JVBase\integrations\Requests;
if (!defined('ABSPATH')) {
exit;
@@ -14,692 +22,56 @@
class Instagram extends Integrations
{
- protected array $allowedContent = [
- 'post'
- ];
- private string $ig_user_id = '';
- private string $page_id = '';
- private string $page_access_token = '';
- private array $business_account = [];
- private bool $auto_post = false;
- private array $post_settings = [];
-
- // Instagram API endpoints
- private const API_VERSION = 'v18.0';
- private const GRAPH_API_BASE = 'https://graph.facebook.com/';
- private const INSTAGRAM_BASE = 'https://graph.instagram.com/';
-
- protected static array $instances = [];
- public static function getInstance(?int $userID = null):self
- {
- $key = is_null($userID) ? 'base' : $userID;
- if (!array_key_exists($key, self::$instances)) {
- self::$instances[$key] = new self($userID);
- }
-
- return self::$instances[$key];
- }
-
- protected function __construct(?int $userID = null)
+ use Admin, Instance, OAuth, Requests;
+ protected function __construct()
{
$this->service_name = 'instagram';
$this->title = 'Instagram';
$this->icon = 'instagram-logo';
- $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'
- ]
- ];
+ $this->apiVersion = 'v25.0';
+ $this->supportsWebp = false;
- // Define sync capabilities
- $this->canSync = [
- 'create' => true,
- 'update' => false, // Instagram doesn't allow editing posts
- 'delete' => true
- ];
+// $this->defineFields();
+ $this->defineOAuth();
+// $this->defineActions();
- $this->fields = [
-
- ];
-
- $this->advanced = [
-
- ];
-
- $this->instructions = [
- 'Setup Facebook in order to use Instagram.'
- ];
-
- $this->defaults = [
-
- ];
- $this->hasWebhooks = true;
- parent::__construct($userID);
-
- $this->actions = array_merge(
- $this->actions,
- [
- 'switch_account' => 'switchInstagramAccount'
- ]
- );
+ $this->baseUrl = 'https://graph.instagram.com/'.$this->apiVersion;
+ parent::__construct();
}
+ public function defineOAuth():void
+ {
+ $this->oauth = new OAuthURLs(
+ 'https://www.facebook.com/'.$this->apiVersion.'/dialog/oauth',
+ 'https://graph.facebook.com/'.$this->apiVersion.'/oauth/access_token',
+ 'https://graph.facebook.com/'.$this->apiVersion.'/me/permissions'
+ );
+ $this->oathScopes = [
+ 'instagram_basic',
+ 'instagram_content_publish',
+ ];
+ }
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
- ];
+ // TODO: Implement initialize() method.
}
- /**
- * Get request headers for Instagram API
- */
+ protected function refreshAccessToken(OAuthCredentials $credentials): array
+ {
+ // TODO: Implement refreshAccessToken() method.
+ return [];
+ }
+
protected function getRequestHeaders(): array
{
- return [
- 'Content-Type' => 'application/json',
- 'Accept' => 'application/json'
- ];
+ // TODO: Implement getRequestHeaders() method.
+ return [];
}
- /**
- * Build Instagram API URL
- */
- protected function getApiUrl(string $endpoint, ?string $baseKey = null): string|false
+ protected function handleResponse(\WP_Error|array $response, string $method, string $endpoint): array
{
- $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 {
- $credentials = parent::getCredentials();
- if (empty($credentials)) {
- $facebook = JVB()->connect('facebook');
-
- if (!$facebook || !$facebook->isSetUp()) {
- return [];
- }
-
- $fbCredentials = $facebook->getCredentials();
-
- return [
- 'client_id' => $fbCredentials['client_id'],
- 'client_secret' => $fbCredentials['client_secret'],
- ];
- }
- return $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
- {
-
- if (!$this->hasOAuthCredentials()) {
- error_log('OAuth Not set up for '.$this->service_name);
- return;
- }
- if (!has_post_thumbnail($postID)) {
- return;
- }
-
- $this->queueOperation(self::$syncTo, [
- 'items' => [$postID],
- 'user' => user_can($post->post_author, 'manage_options') ? null : $post->post_author
- ], [
- 'delay' => 60
- ]);
- Meta::forPost($postID)->set('_'.$this->service_name.'_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.";
+ // TODO: Implement handleResponse() method.
+ return [];
}
}
--
Gitblit v1.10.0