@@ -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