UNPKG

@memberjunction/actions-bizapps-social

Version:

Social Media Actions for MemberJunction - Twitter, LinkedIn, Facebook, Instagram, TikTok, YouTube, HootSuite, Buffer

396 lines (340 loc) 13.7 kB
import { RegisterClass } from '@memberjunction/global'; import { InstagramBaseAction } from '../instagram-base.action'; import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base'; import { LogError } from '@memberjunction/core'; import { BaseAction } from '@memberjunction/actions'; /** * Retrieves comments for an Instagram post, including nested replies. * Supports filtering, pagination, and sentiment analysis. */ @RegisterClass(BaseAction, 'Instagram - Get Comments') export class InstagramGetCommentsAction extends InstagramBaseAction { protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> { try { const companyIntegrationId = this.getParamValue(params.Params, 'CompanyIntegrationID'); const postId = this.getParamValue(params.Params, 'PostID'); const includeReplies = this.getParamValue(params.Params, 'IncludeReplies') !== false; const includeHidden = this.getParamValue(params.Params, 'IncludeHidden') || false; const limit = this.getParamValue(params.Params, 'Limit') || 50; const afterCursor = this.getParamValue(params.Params, 'AfterCursor'); // Initialize OAuth if (!await this.initializeOAuth(companyIntegrationId)) { return { Success: false, Message: 'Failed to initialize Instagram authentication', ResultCode: 'AUTH_FAILED' }; } // Validate inputs if (!postId) { return { Success: false, Message: 'PostID is required', ResultCode: 'MISSING_PARAMS' }; } // Build fields for comment data const fields = 'id,text,username,timestamp,like_count,replies{id,text,username,timestamp,like_count}'; // Build query parameters const queryParams: any = { fields, access_token: this.getAccessToken(), limit: Math.min(limit, 100) }; if (afterCursor) { queryParams.after = afterCursor; } // Get comments const response = await this.makeInstagramRequest<{ data: any[]; paging?: { cursors: { before: string; after: string; }; next?: string; }; }>( `${postId}/comments`, 'GET', null, queryParams ); const comments = response.data || []; // Process comments const processedComments = await this.processComments(comments, includeReplies); // Get hidden comments if requested let hiddenComments: any[] = []; if (includeHidden) { hiddenComments = await this.getHiddenComments(postId); } // Calculate engagement metrics const metrics = this.calculateCommentMetrics(processedComments); // Analyze sentiment patterns const sentimentAnalysis = this.analyzeSentiment(processedComments); // Store result in output params const outputParams = [...params.Params]; outputParams.push({ Name: 'ResultData', Type: 'Output', Value: JSON.stringify({ postId, comments: processedComments, hiddenComments, metrics, sentimentAnalysis, paging: { hasNext: !!response.paging?.next, afterCursor: response.paging?.cursors?.after } }) }); return { Success: true, Message: `Retrieved ${processedComments.length} comments`, ResultCode: 'SUCCESS', Params: outputParams }; } catch (error: any) { LogError('Failed to retrieve Instagram comments', error); if (error.code === 'RATE_LIMIT') { return { Success: false, Message: 'Instagram API rate limit exceeded. Please try again later.', ResultCode: 'RATE_LIMIT' }; } if (error.code === 'POST_NOT_FOUND') { return { Success: false, Message: 'Instagram post not found or access denied', ResultCode: 'POST_NOT_FOUND' }; } return { Success: false, Message: `Failed to retrieve comments: ${error.message}`, ResultCode: 'ERROR' }; } } /** * Process comments and fetch replies if needed */ private async processComments(comments: any[], includeReplies: boolean): Promise<any[]> { const processed: any[] = []; for (const comment of comments) { const processedComment: any = { id: comment.id, text: comment.text, username: comment.username, timestamp: comment.timestamp, likeCount: comment.like_count || 0, replies: [], metrics: { wordCount: this.countWords(comment.text), hasEmojis: this.containsEmojis(comment.text), hasMentions: this.containsMentions(comment.text), hasHashtags: this.containsHashtags(comment.text) } }; // Process replies if they exist and are requested if (includeReplies && comment.replies?.data) { processedComment.replies = comment.replies.data.map((reply: any) => ({ id: reply.id, text: reply.text, username: reply.username, timestamp: reply.timestamp, likeCount: reply.like_count || 0, metrics: { wordCount: this.countWords(reply.text), hasEmojis: this.containsEmojis(reply.text), hasMentions: this.containsMentions(reply.text), hasHashtags: this.containsHashtags(reply.text) } })); } processed.push(processedComment); } return processed; } /** * Get hidden comments (comments hidden by the account) */ private async getHiddenComments(postId: string): Promise<any[]> { try { const response = await this.makeInstagramRequest<{ data: any[] }>( `${postId}/comments`, 'GET', null, { fields: 'id,text,username,timestamp,hidden', access_token: this.getAccessToken(), filter: 'hidden' } ); return response.data || []; } catch (error) { LogError('Failed to get hidden comments', error); return []; } } /** * Calculate comment metrics */ private calculateCommentMetrics(comments: any[]): any { const metrics = { totalComments: comments.length, totalReplies: 0, avgCommentLength: 0, avgLikesPerComment: 0, topCommenters: [] as any[], engagementRate: 0, responseRate: 0 }; if (comments.length === 0) return metrics; let totalLength = 0; let totalLikes = 0; const commenterCounts: Record<string, number> = {}; comments.forEach(comment => { totalLength += comment.metrics.wordCount; totalLikes += comment.likeCount; // Count commenters commenterCounts[comment.username] = (commenterCounts[comment.username] || 0) + 1; // Count replies metrics.totalReplies += comment.replies.length; // Add reply stats comment.replies.forEach((reply: any) => { totalLength += reply.metrics.wordCount; totalLikes += reply.likeCount; commenterCounts[reply.username] = (commenterCounts[reply.username] || 0) + 1; }); }); const totalInteractions = comments.length + metrics.totalReplies; metrics.avgCommentLength = Math.round(totalLength / totalInteractions); metrics.avgLikesPerComment = Math.round(totalLikes / totalInteractions); // Get top commenters metrics.topCommenters = Object.entries(commenterCounts) .sort(([, a], [, b]) => b - a) .slice(0, 10) .map(([username, count]) => ({ username, count })); // Calculate response rate (comments with replies) const commentsWithReplies = comments.filter(c => c.replies.length > 0).length; metrics.responseRate = (commentsWithReplies / comments.length) * 100; return metrics; } /** * Analyze sentiment patterns in comments */ private analyzeSentiment(comments: any[]): any { const analysis = { positive: 0, negative: 0, neutral: 0, questions: 0, keywords: [] as any[], emojis: [] as any[] }; const keywordCounts: Record<string, number> = {}; const emojiCounts: Record<string, number> = {}; // Simple sentiment analysis based on keywords and patterns const positiveWords = ['love', 'amazing', 'beautiful', 'great', 'awesome', 'perfect', 'excellent', 'wonderful', '❤️', '😍', '🔥', '💯']; const negativeWords = ['hate', 'awful', 'terrible', 'bad', 'worst', 'ugly', 'disgusting', '😠', '😡', '👎']; const questionWords = ['?', 'what', 'where', 'when', 'how', 'why', 'who']; comments.forEach(comment => { const text = comment.text.toLowerCase(); // Check sentiment const hasPositive = positiveWords.some(word => text.includes(word)); const hasNegative = negativeWords.some(word => text.includes(word)); const hasQuestion = questionWords.some(word => text.includes(word)); if (hasQuestion) { analysis.questions++; } if (hasPositive && !hasNegative) { analysis.positive++; } else if (hasNegative && !hasPositive) { analysis.negative++; } else { analysis.neutral++; } // Extract keywords (simple word frequency) const words = text.split(/\s+/).filter(word => word.length > 3); words.forEach(word => { keywordCounts[word] = (keywordCounts[word] || 0) + 1; }); // Extract emojis const emojis = text.match(/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]/gu) || []; emojis.forEach(emoji => { emojiCounts[emoji] = (emojiCounts[emoji] || 0) + 1; }); // Analyze replies too comment.replies.forEach((reply: any) => { // Similar analysis for replies... }); }); // Get top keywords and emojis analysis.keywords = Object.entries(keywordCounts) .sort(([, a], [, b]) => b - a) .slice(0, 20) .map(([keyword, count]) => ({ keyword, count })); analysis.emojis = Object.entries(emojiCounts) .sort(([, a], [, b]) => b - a) .slice(0, 10) .map(([emoji, count]) => ({ emoji, count })); return analysis; } /** * Helper methods for text analysis */ private countWords(text: string): number { return text.trim().split(/\s+/).length; } private containsEmojis(text: string): boolean { return /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]/u.test(text); } private containsMentions(text: string): boolean { return /@\w+/.test(text); } private containsHashtags(text: string): boolean { return /#\w+/.test(text); } /** * Define the parameters for this action */ public get Params(): ActionParam[] { return [ ...this.commonSocialParams, { Name: 'PostID', Type: 'Input', Value: null }, { Name: 'IncludeReplies', Type: 'Input', Value: true }, { Name: 'IncludeHidden', Type: 'Input', Value: false }, { Name: 'Limit', Type: 'Input', Value: 50 }, { Name: 'AfterCursor', Type: 'Input', Value: null } ]; } /** * Get the description for this action */ public get Description(): string { return 'Retrieves comments for an Instagram post including replies, metrics, and sentiment analysis.'; } }