UNPKG

@memberjunction/actions-bizapps-social

Version:

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

441 lines (389 loc) 16.9 kB
import { RegisterClass } from '@memberjunction/global'; import { TwitterBaseAction, Tweet, TwitterMetrics } from '../twitter-base.action'; import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base'; import { LogStatus, LogError } from '@memberjunction/core'; import { SocialAnalytics } from '../../../base/base-social.action'; import { BaseAction } from '@memberjunction/actions'; /** * Action to get analytics for tweets or user account from Twitter/X */ @RegisterClass(BaseAction, 'TwitterGetAnalyticsAction') export class TwitterGetAnalyticsAction extends TwitterBaseAction { /** * Get analytics from Twitter */ protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> { const { Params, ContextUser } = params; try { // Initialize OAuth const companyIntegrationId = this.getParamValue(Params, 'CompanyIntegrationID'); if (!await this.initializeOAuth(companyIntegrationId)) { throw new Error('Failed to initialize OAuth connection'); } // Extract parameters const analyticsType = this.getParamValue(Params, 'AnalyticsType') || 'account'; // 'account' or 'tweets' const tweetIds = this.getParamValue(Params, 'TweetIDs'); const startDate = this.getParamValue(Params, 'StartDate'); const endDate = this.getParamValue(Params, 'EndDate'); const granularity = this.getParamValue(Params, 'Granularity') || 'day'; // 'hour', 'day', 'total' if (analyticsType === 'tweets') { // Get analytics for specific tweets if (!tweetIds || !Array.isArray(tweetIds) || tweetIds.length === 0) { throw new Error('TweetIDs array is required for tweet analytics'); } return await this.getTweetAnalytics(Params, tweetIds); } else { // Get account-level analytics return await this.getAccountAnalytics(Params, startDate, endDate, granularity); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return { Success: false, ResultCode: this.getErrorCode(error), Message: `Failed to get analytics: ${errorMessage}`, Params }; } } /** * Get analytics for specific tweets */ private async getTweetAnalytics(params: ActionParam[], tweetIds: string[]): Promise<ActionResultSimple> { try { LogStatus(`Getting analytics for ${tweetIds.length} tweets...`); // Twitter API v2 requires organic metrics scope for detailed analytics // We'll get public metrics for each tweet const tweetAnalytics: any[] = []; // Process in batches of 100 (API limit) const batchSize = 100; for (let i = 0; i < tweetIds.length; i += batchSize) { const batch = tweetIds.slice(i, i + batchSize); const ids = batch.join(','); const response = await this.axiosInstance.get('/tweets', { params: { 'ids': ids, 'tweet.fields': 'id,text,created_at,public_metrics,organic_metrics,promoted_metrics', 'expansions': 'author_id', 'user.fields': 'id,username' } }); if (response.data.data) { for (const tweet of response.data.data) { const metrics: TwitterMetrics = { impression_count: 0, engagement_count: 0, retweet_count: 0, reply_count: 0, like_count: 0, quote_count: 0, bookmark_count: 0, url_link_clicks: 0, user_profile_clicks: 0 }; // Combine public and organic metrics if available if (tweet.public_metrics) { Object.assign(metrics, tweet.public_metrics); } if (tweet.organic_metrics) { Object.assign(metrics, tweet.organic_metrics); } // Calculate engagement count metrics.engagement_count = metrics.retweet_count + metrics.reply_count + metrics.like_count + metrics.quote_count + metrics.url_link_clicks + metrics.user_profile_clicks; const normalizedAnalytics = this.normalizeAnalytics(metrics); tweetAnalytics.push({ tweetId: tweet.id, text: tweet.text.substring(0, 100) + (tweet.text.length > 100 ? '...' : ''), createdAt: tweet.created_at, metrics: normalizedAnalytics, engagementRate: metrics.impression_count > 0 ? ((metrics.engagement_count / metrics.impression_count) * 100).toFixed(2) + '%' : '0%' }); } } } // Calculate aggregate metrics const aggregateMetrics = this.calculateAggregateMetrics(tweetAnalytics); // Update output parameters const outputParams = [...params]; const analyticsParam = outputParams.find(p => p.Name === 'Analytics'); if (analyticsParam) analyticsParam.Value = tweetAnalytics; const aggregateParam = outputParams.find(p => p.Name === 'AggregateMetrics'); if (aggregateParam) aggregateParam.Value = aggregateMetrics; return { Success: true, ResultCode: 'SUCCESS', Message: `Successfully retrieved analytics for ${tweetAnalytics.length} tweets`, Params: outputParams }; } catch (error) { throw error; } } /** * Get account-level analytics */ private async getAccountAnalytics( params: ActionParam[], startDate?: string, endDate?: string, granularity?: string ): Promise<ActionResultSimple> { try { // Get current user const currentUser = await this.getCurrentUser(); LogStatus(`Getting account analytics for @${currentUser.username}...`); // For account analytics, we'll analyze recent tweets performance const queryParams: Record<string, any> = { 'tweet.fields': 'id,text,created_at,public_metrics,organic_metrics', 'max_results': 100 }; if (startDate) { queryParams['start_time'] = this.formatTwitterDate(startDate); } if (endDate) { queryParams['end_time'] = this.formatTwitterDate(endDate); } // Get user's tweets const tweets = await this.getPaginatedTweets( `/users/${currentUser.id}/tweets`, queryParams, 200 // Get up to 200 tweets for analysis ); // Calculate time-based analytics const timeBasedAnalytics = this.calculateTimeBasedAnalytics(tweets, granularity || 'day'); // Calculate overall metrics const overallMetrics = { totalTweets: tweets.length, totalImpressions: 0, totalEngagements: 0, totalLikes: 0, totalRetweets: 0, totalReplies: 0, totalQuotes: 0, averageEngagementRate: 0, topPerformingTweets: [] as any[] }; tweets.forEach(tweet => { if (tweet.public_metrics) { overallMetrics.totalImpressions += tweet.public_metrics.impression_count || 0; overallMetrics.totalLikes += tweet.public_metrics.like_count || 0; overallMetrics.totalRetweets += tweet.public_metrics.retweet_count || 0; overallMetrics.totalReplies += tweet.public_metrics.reply_count || 0; overallMetrics.totalQuotes += tweet.public_metrics.quote_count || 0; const engagement = (tweet.public_metrics.like_count || 0) + (tweet.public_metrics.retweet_count || 0) + (tweet.public_metrics.reply_count || 0) + (tweet.public_metrics.quote_count || 0); overallMetrics.totalEngagements += engagement; } }); // Calculate average engagement rate if (overallMetrics.totalImpressions > 0) { overallMetrics.averageEngagementRate = parseFloat(((overallMetrics.totalEngagements / overallMetrics.totalImpressions) * 100).toFixed(2)); } // Get top performing tweets overallMetrics.topPerformingTweets = tweets .filter(t => t.public_metrics) .sort((a, b) => { const aEngagement = this.calculateTweetEngagement(a.public_metrics!); const bEngagement = this.calculateTweetEngagement(b.public_metrics!); return bEngagement - aEngagement; }) .slice(0, 5) .map(tweet => ({ id: tweet.id, text: tweet.text.substring(0, 100) + (tweet.text.length > 100 ? '...' : ''), createdAt: tweet.created_at, metrics: tweet.public_metrics, engagement: this.calculateTweetEngagement(tweet.public_metrics!) })); // Update output parameters const outputParams = [...params]; const overallParam = outputParams.find(p => p.Name === 'OverallMetrics'); if (overallParam) overallParam.Value = overallMetrics; const timeBasedParam = outputParams.find(p => p.Name === 'TimeBasedAnalytics'); if (timeBasedParam) timeBasedParam.Value = timeBasedAnalytics; return { Success: true, ResultCode: 'SUCCESS', Message: `Successfully retrieved account analytics for ${tweets.length} tweets`, Params: outputParams }; } catch (error) { throw error; } } /** * Calculate aggregate metrics from tweet analytics */ private calculateAggregateMetrics(tweetAnalytics: any[]): any { const aggregate = { totalImpressions: 0, totalEngagements: 0, totalLikes: 0, totalRetweets: 0, totalReplies: 0, averageEngagementRate: 0, bestPerformingTweet: null as any, worstPerformingTweet: null as any }; let bestEngagement = -1; let worstEngagement = Infinity; tweetAnalytics.forEach(analytics => { const metrics = analytics.metrics; aggregate.totalImpressions += metrics.impressions; aggregate.totalEngagements += metrics.engagements; aggregate.totalLikes += metrics.likes; aggregate.totalRetweets += metrics.shares; aggregate.totalReplies += metrics.comments; if (metrics.engagements > bestEngagement) { bestEngagement = metrics.engagements; aggregate.bestPerformingTweet = analytics; } if (metrics.engagements < worstEngagement) { worstEngagement = metrics.engagements; aggregate.worstPerformingTweet = analytics; } }); if (aggregate.totalImpressions > 0) { aggregate.averageEngagementRate = parseFloat(((aggregate.totalEngagements / aggregate.totalImpressions) * 100).toFixed(2)); } return aggregate; } /** * Calculate time-based analytics */ private calculateTimeBasedAnalytics(tweets: Tweet[], granularity: string): any[] { const buckets: Map<string, any> = new Map(); tweets.forEach(tweet => { const date = new Date(tweet.created_at); let bucketKey: string; switch (granularity) { case 'hour': bucketKey = `${date.toISOString().slice(0, 13)}:00:00Z`; break; case 'day': bucketKey = date.toISOString().slice(0, 10); break; case 'total': bucketKey = 'total'; break; default: bucketKey = date.toISOString().slice(0, 10); } if (!buckets.has(bucketKey)) { buckets.set(bucketKey, { period: bucketKey, tweets: 0, impressions: 0, engagements: 0, likes: 0, retweets: 0, replies: 0 }); } const bucket = buckets.get(bucketKey)!; bucket.tweets++; if (tweet.public_metrics) { bucket.impressions += tweet.public_metrics.impression_count || 0; bucket.likes += tweet.public_metrics.like_count || 0; bucket.retweets += tweet.public_metrics.retweet_count || 0; bucket.replies += tweet.public_metrics.reply_count || 0; bucket.engagements += this.calculateTweetEngagement(tweet.public_metrics); } }); return Array.from(buckets.values()).sort((a, b) => a.period.localeCompare(b.period) ); } /** * Calculate tweet engagement from public metrics */ private calculateTweetEngagement(metrics: any): number { return (metrics.like_count || 0) + (metrics.retweet_count || 0) + (metrics.reply_count || 0) + (metrics.quote_count || 0); } /** * Get error code based on error type */ private getErrorCode(error: any): string { if (error instanceof Error) { if (error.message.includes('Rate Limit')) return 'RATE_LIMIT'; if (error.message.includes('Unauthorized')) return 'INVALID_TOKEN'; if (error.message.includes('Forbidden')) return 'INSUFFICIENT_PERMISSIONS'; } return 'ERROR'; } /** * Define the parameters this action expects */ public get Params(): ActionParam[] { return [ ...this.commonSocialParams, { Name: 'AnalyticsType', Type: 'Input', Value: 'account' // 'account' or 'tweets' }, { Name: 'TweetIDs', Type: 'Input', Value: null }, { Name: 'StartDate', Type: 'Input', Value: null }, { Name: 'EndDate', Type: 'Input', Value: null }, { Name: 'Granularity', Type: 'Input', Value: 'day' // 'hour', 'day', 'total' }, { Name: 'Analytics', Type: 'Output', Value: null }, { Name: 'AggregateMetrics', Type: 'Output', Value: null }, { Name: 'OverallMetrics', Type: 'Output', Value: null }, { Name: 'TimeBasedAnalytics', Type: 'Output', Value: null } ]; } /** * Get action description */ public get Description(): string { return 'Gets analytics data from Twitter/X for specific tweets or account-level metrics with time-based analysis'; } }