Skip to content

Commit cb2b3b4

Browse files
authored
Merge pull request #106 from Extra-Chill/feat/generic-comments-api
Add generic comments API with normalized SocialComment shape
2 parents e06e65e + 71b9e73 commit cb2b3b4

4 files changed

Lines changed: 612 additions & 6 deletions

File tree

data-machine-socials.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ function datamachine_socials_enqueue_assets( $hook ) {
199199
require_once DATAMACHINE_SOCIALS_PATH . 'inc/Cli/Commands/BlueskyCommand.php';
200200
require_once DATAMACHINE_SOCIALS_PATH . 'inc/Cli/Commands/LinkedInCommand.php';
201201
require_once DATAMACHINE_SOCIALS_PATH . 'inc/Cli/Commands/SharesCommand.php';
202+
require_once DATAMACHINE_SOCIALS_PATH . 'inc/Cli/Commands/CommentsCommand.php';
203+
WP_CLI::add_command( 'datamachine-socials comments', \DataMachineSocials\Cli\Commands\CommentsCommand::class );
202204
WP_CLI::add_command( 'datamachine-socials linkedin', \DataMachineSocials\Cli\Commands\LinkedInCommand::class );
203205
WP_CLI::add_command( 'datamachine-socials pinterest', \DataMachineSocials\Cli\Commands\PinterestCommand::class );
204206
WP_CLI::add_command( 'datamachine-socials reddit', \DataMachineSocials\Cli\Commands\RedditCommand::class );

inc/Abilities/Instagram/InstagramReadAbility.php

Lines changed: 153 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ class InstagramReadAbility {
2424

2525
const GRAPH_API_URL = 'https://graph.instagram.com';
2626

27+
/**
28+
* Regex for extracting @mentions from comment text.
29+
*/
30+
const MENTION_REGEX = '/@([a-zA-Z0-9._]{1,30})/';
31+
2732
/**
2833
* Fields to request when listing media.
2934
*/
@@ -59,12 +64,12 @@ private function registerAbilities(): void {
5964
'input_schema' => array(
6065
'type' => 'object',
6166
'properties' => array(
62-
'action' => array(
63-
'type' => 'string',
64-
'enum' => array( 'list', 'get', 'comments' ),
65-
'default' => 'list',
66-
'description' => __( 'Action: list (recent posts), get (single post), comments (post comments)', 'data-machine-socials' ),
67-
),
67+
'action' => array(
68+
'type' => 'string',
69+
'enum' => array( 'list', 'get', 'comments', 'comments_all' ),
70+
'default' => 'list',
71+
'description' => __( 'Action: list (recent posts), get (single post), comments (one page), comments_all (all pages, normalized)', 'data-machine-socials' ),
72+
),
6873
'media_id' => array(
6974
'type' => 'string',
7075
'description' => __( 'Instagram media ID (required for get and comments actions)', 'data-machine-socials' ),
@@ -159,6 +164,15 @@ public function execute( array $input ): array {
159164
}
160165
return $this->getComments( $access_token, $input['media_id'], $input );
161166

167+
case 'comments_all':
168+
if ( empty( $input['media_id'] ) ) {
169+
return array(
170+
'success' => false,
171+
'error' => 'media_id is required for the comments_all action',
172+
);
173+
}
174+
return $this->getAllComments( $access_token, $input['media_id'] );
175+
162176
default:
163177
return array(
164178
'success' => false,
@@ -325,6 +339,139 @@ private function getComments( string $access_token, string $media_id, array $inp
325339
);
326340
}
327341

342+
/**
343+
* Fetch ALL comments for a media item, auto-paginating through every page.
344+
*
345+
* Returns comments in the normalized SocialComment shape used across all
346+
* platforms. This enables generic consumers (giveaway picker, CLI, pipelines)
347+
* to work without platform-specific knowledge.
348+
*
349+
* @param string $access_token Valid access token.
350+
* @param string $media_id Instagram media ID.
351+
* @return array Result with normalized comments.
352+
*/
353+
private function getAllComments( string $access_token, string $media_id ): array {
354+
$all_comments = array();
355+
$after = '';
356+
$page = 0;
357+
$max_pages = 200; // Safety limit: 200 pages × 50 comments = 10,000 comments max.
358+
359+
do {
360+
$page++;
361+
$params = array(
362+
'fields' => 'id,text,timestamp,username,like_count',
363+
'limit' => 50, // Max per page for Instagram API.
364+
'access_token' => $access_token,
365+
);
366+
367+
if ( ! empty( $after ) ) {
368+
$params['after'] = $after;
369+
}
370+
371+
$url = self::GRAPH_API_URL . "/{$media_id}/comments?" . http_build_query( $params );
372+
$result = HttpClient::get( $url, array( 'context' => 'Instagram Comments All' ) );
373+
374+
if ( ! $result['success'] ) {
375+
// If we already have some comments, return them with a warning.
376+
if ( ! empty( $all_comments ) ) {
377+
return array(
378+
'success' => true,
379+
'data' => array(
380+
'comments' => $all_comments,
381+
'count' => count( $all_comments ),
382+
'platform' => 'instagram',
383+
'partial' => true,
384+
'error' => 'Pagination interrupted: ' . ( $result['error'] ?? 'unknown' ),
385+
),
386+
);
387+
}
388+
389+
return array(
390+
'success' => false,
391+
'error' => 'Instagram API request failed: ' . ( $result['error'] ?? 'unknown' ),
392+
);
393+
}
394+
395+
$data = json_decode( $result['data'], true );
396+
$http_code = $result['status_code'];
397+
398+
if ( 200 !== $http_code || isset( $data['error'] ) ) {
399+
if ( ! empty( $all_comments ) ) {
400+
return array(
401+
'success' => true,
402+
'data' => array(
403+
'comments' => $all_comments,
404+
'count' => count( $all_comments ),
405+
'platform' => 'instagram',
406+
'partial' => true,
407+
'error' => $data['error']['message'] ?? 'Pagination error',
408+
),
409+
);
410+
}
411+
412+
return array(
413+
'success' => false,
414+
'error' => $data['error']['message'] ?? 'Failed to fetch comments',
415+
);
416+
}
417+
418+
$page_comments = $data['data'] ?? array();
419+
420+
foreach ( $page_comments as $comment ) {
421+
$all_comments[] = self::normalizeComment( $comment );
422+
}
423+
424+
// Check for next page.
425+
$paging = $data['paging'] ?? array();
426+
$after = $paging['cursors']['after'] ?? '';
427+
428+
$has_next = ! empty( $paging['next'] ) && ! empty( $after );
429+
430+
} while ( $has_next && $page < $max_pages );
431+
432+
return array(
433+
'success' => true,
434+
'data' => array(
435+
'comments' => $all_comments,
436+
'count' => count( $all_comments ),
437+
'platform' => 'instagram',
438+
'partial' => false,
439+
'pages' => $page,
440+
),
441+
);
442+
}
443+
444+
/**
445+
* Normalize an Instagram comment into the generic SocialComment shape.
446+
*
447+
* Shape: { id, platform, author_username, text, timestamp, like_count,
448+
* reply_count, mentions, parent_id, raw }
449+
*
450+
* @param array $comment Raw Instagram API comment data.
451+
* @return array Normalized comment.
452+
*/
453+
public static function normalizeComment( array $comment ): array {
454+
$text = $comment['text'] ?? '';
455+
$mentions = array();
456+
457+
if ( preg_match_all( self::MENTION_REGEX, $text, $matches ) ) {
458+
$mentions = array_values( array_unique( $matches[1] ) );
459+
}
460+
461+
return array(
462+
'id' => $comment['id'] ?? '',
463+
'platform' => 'instagram',
464+
'author_username' => $comment['username'] ?? '',
465+
'text' => $text,
466+
'timestamp' => $comment['timestamp'] ?? '',
467+
'like_count' => (int) ( $comment['like_count'] ?? 0 ),
468+
'reply_count' => 0, // Instagram top-level comments endpoint doesn't include reply count.
469+
'mentions' => $mentions,
470+
'parent_id' => null, // Top-level comments; replies would be fetched separately.
471+
'raw' => $comment,
472+
);
473+
}
474+
328475
/**
329476
* Get the Instagram auth provider.
330477
*

0 commit comments

Comments
 (0)