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