<?php
|
/**
|
* Instagram Integration
|
* File: /inc/integrations/Instagram.php
|
*/
|
namespace JVBase\integrations;
|
|
use JVBase\integrations\Integrations;
|
use WP_Error;
|
use WP_REST_Request;
|
use WP_REST_Response;
|
use Exception;
|
|
if (!defined('ABSPATH')) {
|
exit;
|
}
|
|
class Instagram extends Integrations
|
{
|
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/';
|
|
public function __construct(?int $userID = null)
|
{
|
$this->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.";
|
}
|
}
|