From 8314cc58a2fed9c8a59fcaded811db9342a99968 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Fri, 30 May 2025 00:43:48 +0700 Subject: [PATCH] feat: analytics --- .../platform-analytics/platform.analytics.tsx | 5 +- .../src/integrations/social/x.provider.ts | 132 +++++++++++++++++- 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/apps/frontend/src/components/platform-analytics/platform.analytics.tsx b/apps/frontend/src/components/platform-analytics/platform.analytics.tsx index fcb23a8a..369f03b2 100644 --- a/apps/frontend/src/components/platform-analytics/platform.analytics.tsx +++ b/apps/frontend/src/components/platform-analytics/platform.analytics.tsx @@ -22,6 +22,7 @@ const allowedIntegrations = [ 'youtube', 'pinterest', 'threads', + 'x', ]; export const PlatformAnalytics = () => { @@ -66,6 +67,7 @@ export const PlatformAnalytics = () => { 'pinterest', 'youtube', 'threads', + 'x', ].indexOf(currentIntegration.identifier) !== -1 ) { arr.push({ @@ -83,6 +85,7 @@ export const PlatformAnalytics = () => { 'pinterest', 'youtube', 'threads', + 'x', ].indexOf(currentIntegration.identifier) !== -1 ) { arr.push({ @@ -92,7 +95,7 @@ export const PlatformAnalytics = () => { } if ( - ['facebook', 'linkedin-page', 'pinterest', 'youtube'].indexOf( + ['facebook', 'linkedin-page', 'pinterest', 'youtube', 'x'].indexOf( currentIntegration.identifier ) !== -1 ) { diff --git a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts index 7ac3ab2a..71407caa 100644 --- a/libraries/nestjs-libraries/src/integrations/social/x.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/x.provider.ts @@ -1,5 +1,6 @@ -import { TwitterApi } from 'twitter-api-v2'; +import { TweetV2, TwitterApi } from 'twitter-api-v2'; import { + AnalyticsData, AuthTokenDetails, PostDetails, PostResponse, @@ -14,6 +15,9 @@ import { Plug } from '@gitroom/helpers/decorators/plug.decorator'; import { Integration } from '@prisma/client'; import { timer } from '@gitroom/helpers/utils/timer'; import { PostPlug } from '@gitroom/helpers/decorators/post.plug'; +import { number, string } from 'yup'; +import dayjs from 'dayjs'; +import { uniqBy } from 'lodash'; export class XProvider extends SocialAbstract implements SocialProvider { identifier = 'x'; @@ -314,7 +318,7 @@ export class XProvider extends SocialAbstract implements SocialProvider { })); } - communities(accessToken: string, data: {search: string}) { + communities(accessToken: string, data: { search: string }) { const [accessTokenSplit, accessSecretSplit] = accessToken.split(':'); const client = new TwitterApi({ appKey: process.env.X_API_KEY!, @@ -333,4 +337,128 @@ export class XProvider extends SocialAbstract implements SocialProvider { // } // }) } + + private loadAllTweets = async ( + client: TwitterApi, + id: string, + until: string, + since: string, + token = '' + ): Promise => { + const tweets = await client.v2.userTimeline(id, { + 'tweet.fields': ['id'], + 'user.fields': [], + 'poll.fields': [], + 'place.fields': [], + 'media.fields': [], + exclude: ['replies', 'retweets'], + start_time: since, + end_time: until, + max_results: 100, + ...(token ? { pagination_token: token } : {}), + }); + + return [ + ...tweets.data.data, + ...(tweets.data.data.length === 100 + ? await this.loadAllTweets( + client, + id, + until, + since, + tweets.meta.next_token + ) + : []), + ]; + }; + + async analytics( + id: string, + accessToken: string, + date: number + ): Promise { + const until = dayjs().endOf('day'); + const since = dayjs().subtract(date, 'day'); + + const [accessTokenSplit, accessSecretSplit] = accessToken.split(':'); + const client = new TwitterApi({ + appKey: process.env.X_API_KEY!, + appSecret: process.env.X_API_SECRET!, + accessToken: accessTokenSplit, + accessSecret: accessSecretSplit, + }); + + try { + const tweets = uniqBy( + await this.loadAllTweets( + client, + id, + until.format('YYYY-MM-DDTHH:mm:ssZ'), + since.format('YYYY-MM-DDTHH:mm:ssZ') + ), + (p) => p.id + ); + + if (tweets.length === 0) { + return []; + } + + console.log(tweets.map((p) => p.id)); + const data = await client.v2.tweets( + tweets.map((p) => p.id), + { + 'tweet.fields': ['public_metrics'], + }, + ); + + const metrics = data.data.reduce( + (all, current) => { + all.impression_count = + (all.impression_count || 0) + + +current.public_metrics.impression_count; + all.bookmark_count = + (all.bookmark_count || 0) + +current.public_metrics.bookmark_count; + all.like_count = + (all.like_count || 0) + +current.public_metrics.like_count; + all.quote_count = + (all.quote_count || 0) + +current.public_metrics.quote_count; + all.reply_count = + (all.reply_count || 0) + +current.public_metrics.reply_count; + all.retweet_count = + (all.retweet_count || 0) + +current.public_metrics.retweet_count; + + return all; + }, + { + impression_count: 0, + bookmark_count: 0, + like_count: 0, + quote_count: 0, + reply_count: 0, + retweet_count: 0, + } + ); + + console.log(metrics); + console.log(JSON.stringify(data, null, 2)); + + return Object.entries(metrics).map(([key, value]) => ({ + label: key.replace('_count', '').replace('_', ' ').toUpperCase(), + percentageChange: 5, + data: [ + { + total: String(0), + date: since.format('YYYY-MM-DD'), + }, + { + total: String(value), + date: until.format('YYYY-MM-DD'), + }, + ], + })); + } catch (err) { + console.log(err); + } + return []; + } }