@memberjunction/actions-bizapps-social 2.111.0 → 2.112.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +6 -6
- package/dist/base/base-social.action.d.ts.map +1 -1
- package/dist/base/base-social.action.js +18 -24
- package/dist/base/base-social.action.js.map +1 -1
- package/dist/providers/buffer/buffer-base.action.d.ts.map +1 -1
- package/dist/providers/buffer/buffer-base.action.js +35 -34
- package/dist/providers/buffer/buffer-base.action.js.map +1 -1
- package/dist/providers/facebook/actions/boost-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/boost-post.action.js +33 -33
- package/dist/providers/facebook/actions/boost-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/create-album.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/create-album.action.js +34 -36
- package/dist/providers/facebook/actions/create-album.action.js.map +1 -1
- package/dist/providers/facebook/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/create-post.action.js +20 -20
- package/dist/providers/facebook/actions/create-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-page-insights.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-page-insights.action.js +25 -27
- package/dist/providers/facebook/actions/get-page-insights.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-page-posts.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-page-posts.action.js +19 -23
- package/dist/providers/facebook/actions/get-page-posts.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-post-insights.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-post-insights.action.js +28 -32
- package/dist/providers/facebook/actions/get-post-insights.action.js.map +1 -1
- package/dist/providers/facebook/actions/respond-to-comments.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/respond-to-comments.action.js +42 -44
- package/dist/providers/facebook/actions/respond-to-comments.action.js.map +1 -1
- package/dist/providers/facebook/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/schedule-post.action.js +29 -29
- package/dist/providers/facebook/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/search-posts.action.js +37 -39
- package/dist/providers/facebook/actions/search-posts.action.js.map +1 -1
- package/dist/providers/facebook/facebook-base.action.d.ts.map +1 -1
- package/dist/providers/facebook/facebook-base.action.js +44 -59
- package/dist/providers/facebook/facebook-base.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js +33 -31
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.js +28 -32
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js +19 -19
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-analytics.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-analytics.action.js +24 -26
- package/dist/providers/hootsuite/actions/get-analytics.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js +22 -22
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-social-profiles.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-social-profiles.action.js +32 -34
- package/dist/providers/hootsuite/actions/get-social-profiles.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/search-posts.action.js +43 -52
- package/dist/providers/hootsuite/actions/search-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.js +30 -28
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/hootsuite-base.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/hootsuite-base.action.js +18 -20
- package/dist/providers/hootsuite/hootsuite-base.action.js.map +1 -1
- package/dist/providers/instagram/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/create-post.action.js +27 -26
- package/dist/providers/instagram/actions/create-post.action.js.map +1 -1
- package/dist/providers/instagram/actions/create-story.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/create-story.action.js +35 -35
- package/dist/providers/instagram/actions/create-story.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-account-insights.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-account-insights.action.js +38 -59
- package/dist/providers/instagram/actions/get-account-insights.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-business-posts.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-business-posts.action.js +29 -29
- package/dist/providers/instagram/actions/get-business-posts.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-comments.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-comments.action.js +36 -36
- package/dist/providers/instagram/actions/get-comments.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-post-insights.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-post-insights.action.js +23 -25
- package/dist/providers/instagram/actions/get-post-insights.action.js.map +1 -1
- package/dist/providers/instagram/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/schedule-post.action.js +25 -25
- package/dist/providers/instagram/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/instagram/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/search-posts.action.js +56 -60
- package/dist/providers/instagram/actions/search-posts.action.js.map +1 -1
- package/dist/providers/instagram/instagram-base.action.d.ts.map +1 -1
- package/dist/providers/instagram/instagram-base.action.js +25 -27
- package/dist/providers/instagram/instagram-base.action.js.map +1 -1
- package/dist/providers/linkedin/actions/create-article.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/create-article.action.js +55 -45
- package/dist/providers/linkedin/actions/create-article.action.js.map +1 -1
- package/dist/providers/linkedin/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/create-post.action.js +31 -29
- package/dist/providers/linkedin/actions/create-post.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-followers.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-followers.action.js +28 -28
- package/dist/providers/linkedin/actions/get-followers.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-organization-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-organization-posts.action.js +20 -20
- package/dist/providers/linkedin/actions/get-organization-posts.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-personal-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-personal-posts.action.js +19 -19
- package/dist/providers/linkedin/actions/get-personal-posts.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-post-analytics.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-post-analytics.action.js +25 -23
- package/dist/providers/linkedin/actions/get-post-analytics.action.js.map +1 -1
- package/dist/providers/linkedin/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/schedule-post.action.js +32 -30
- package/dist/providers/linkedin/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/linkedin/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/search-posts.action.js +28 -30
- package/dist/providers/linkedin/actions/search-posts.action.js.map +1 -1
- package/dist/providers/linkedin/linkedin-base.action.d.ts.map +1 -1
- package/dist/providers/linkedin/linkedin-base.action.js +33 -38
- package/dist/providers/linkedin/linkedin-base.action.js.map +1 -1
- package/dist/providers/tiktok/tiktok-base.action.d.ts.map +1 -1
- package/dist/providers/tiktok/tiktok-base.action.js +25 -26
- package/dist/providers/tiktok/tiktok-base.action.js.map +1 -1
- package/dist/providers/twitter/actions/create-thread.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/create-thread.action.js +25 -29
- package/dist/providers/twitter/actions/create-thread.action.js.map +1 -1
- package/dist/providers/twitter/actions/create-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/create-tweet.action.js +23 -23
- package/dist/providers/twitter/actions/create-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/delete-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/delete-tweet.action.js +19 -19
- package/dist/providers/twitter/actions/delete-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-analytics.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-analytics.action.js +40 -47
- package/dist/providers/twitter/actions/get-analytics.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-mentions.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-mentions.action.js +30 -31
- package/dist/providers/twitter/actions/get-mentions.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-timeline.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-timeline.action.js +29 -29
- package/dist/providers/twitter/actions/get-timeline.action.js.map +1 -1
- package/dist/providers/twitter/actions/schedule-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/schedule-tweet.action.js +26 -26
- package/dist/providers/twitter/actions/schedule-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/search-tweets.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/search-tweets.action.js +56 -58
- package/dist/providers/twitter/actions/search-tweets.action.js.map +1 -1
- package/dist/providers/twitter/twitter-base.action.d.ts.map +1 -1
- package/dist/providers/twitter/twitter-base.action.js +58 -68
- package/dist/providers/twitter/twitter-base.action.js.map +1 -1
- package/dist/providers/youtube/youtube-base.action.d.ts +1 -1
- package/dist/providers/youtube/youtube-base.action.d.ts.map +1 -1
- package/dist/providers/youtube/youtube-base.action.js +22 -25
- package/dist/providers/youtube/youtube-base.action.js.map +1 -1
- package/package.json +5 -6
- package/src/base/base-social.action.ts +217 -224
- package/src/providers/buffer/buffer-base.action.ts +435 -441
- package/src/providers/facebook/actions/boost-post.action.ts +350 -386
- package/src/providers/facebook/actions/create-album.action.ts +291 -307
- package/src/providers/facebook/actions/create-post.action.ts +224 -227
- package/src/providers/facebook/actions/get-page-insights.action.ts +383 -403
- package/src/providers/facebook/actions/get-page-posts.action.ts +214 -225
- package/src/providers/facebook/actions/get-post-insights.action.ts +300 -316
- package/src/providers/facebook/actions/respond-to-comments.action.ts +319 -336
- package/src/providers/facebook/actions/schedule-post.action.ts +289 -292
- package/src/providers/facebook/actions/search-posts.action.ts +399 -413
- package/src/providers/facebook/facebook-base.action.ts +653 -670
- package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
- package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +184 -189
- package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +160 -161
- package/src/providers/hootsuite/actions/get-analytics.action.ts +249 -254
- package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +206 -207
- package/src/providers/hootsuite/actions/get-social-profiles.action.ts +206 -205
- package/src/providers/hootsuite/actions/search-posts.action.ts +351 -369
- package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +211 -209
- package/src/providers/hootsuite/hootsuite-base.action.ts +301 -307
- package/src/providers/instagram/actions/create-post.action.ts +276 -296
- package/src/providers/instagram/actions/create-story.action.ts +378 -394
- package/src/providers/instagram/actions/get-account-insights.action.ts +384 -420
- package/src/providers/instagram/actions/get-business-posts.action.ts +233 -242
- package/src/providers/instagram/actions/get-comments.action.ts +365 -377
- package/src/providers/instagram/actions/get-post-insights.action.ts +265 -273
- package/src/providers/instagram/actions/schedule-post.action.ts +233 -235
- package/src/providers/instagram/actions/search-posts.action.ts +512 -538
- package/src/providers/instagram/instagram-base.action.ts +368 -393
- package/src/providers/linkedin/actions/create-article.action.ts +275 -266
- package/src/providers/linkedin/actions/create-post.action.ts +179 -177
- package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
- package/src/providers/linkedin/actions/get-organization-posts.action.ts +146 -147
- package/src/providers/linkedin/actions/get-personal-posts.action.ts +138 -139
- package/src/providers/linkedin/actions/get-post-analytics.action.ts +190 -189
- package/src/providers/linkedin/actions/schedule-post.action.ts +191 -189
- package/src/providers/linkedin/actions/search-posts.action.ts +275 -283
- package/src/providers/linkedin/linkedin-base.action.ts +407 -421
- package/src/providers/tiktok/tiktok-base.action.ts +305 -320
- package/src/providers/twitter/actions/create-thread.action.ts +203 -207
- package/src/providers/twitter/actions/create-tweet.action.ts +187 -188
- package/src/providers/twitter/actions/delete-tweet.action.ts +128 -129
- package/src/providers/twitter/actions/get-analytics.action.ts +402 -411
- package/src/providers/twitter/actions/get-mentions.action.ts +218 -219
- package/src/providers/twitter/actions/get-timeline.action.ts +232 -233
- package/src/providers/twitter/actions/schedule-tweet.action.ts +221 -222
- package/src/providers/twitter/actions/search-tweets.action.ts +540 -543
- package/src/providers/twitter/twitter-base.action.ts +541 -560
- package/src/providers/youtube/youtube-base.action.ts +320 -333
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RegisterClass } from '@memberjunction/global';
|
|
2
2
|
import { TwitterBaseAction, Tweet, TwitterSearchParams } from '../twitter-base.action';
|
|
3
3
|
import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
|
|
4
|
-
import { LogStatus, LogError } from '@memberjunction/
|
|
4
|
+
import { LogStatus, LogError } from '@memberjunction/global';
|
|
5
5
|
import { SocialPost, SearchParams } from '../../../base/base-social.action';
|
|
6
6
|
import { BaseAction } from '@memberjunction/actions';
|
|
7
7
|
|
|
@@ -10,580 +10,577 @@ import { BaseAction } from '@memberjunction/actions';
|
|
|
10
10
|
*/
|
|
11
11
|
@RegisterClass(BaseAction, 'TwitterSearchTweetsAction')
|
|
12
12
|
export class TwitterSearchTweetsAction extends TwitterBaseAction {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Search for tweets on Twitter
|
|
15
|
+
*/
|
|
16
|
+
protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
|
|
17
|
+
const { Params, ContextUser } = params;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Initialize OAuth
|
|
21
|
+
const companyIntegrationId = this.getParamValue(Params, 'CompanyIntegrationID');
|
|
22
|
+
if (!(await this.initializeOAuth(companyIntegrationId))) {
|
|
23
|
+
throw new Error('Failed to initialize OAuth connection');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Extract parameters
|
|
27
|
+
const query = this.getParamValue(Params, 'Query');
|
|
28
|
+
const hashtags = this.getParamValue(Params, 'Hashtags');
|
|
29
|
+
const fromUser = this.getParamValue(Params, 'FromUser');
|
|
30
|
+
const toUser = this.getParamValue(Params, 'ToUser');
|
|
31
|
+
const mentionUser = this.getParamValue(Params, 'MentionUser');
|
|
32
|
+
const startDate = this.getParamValue(Params, 'StartDate');
|
|
33
|
+
const endDate = this.getParamValue(Params, 'EndDate');
|
|
34
|
+
const language = this.getParamValue(Params, 'Language');
|
|
35
|
+
const hasMedia = this.getParamValue(Params, 'HasMedia');
|
|
36
|
+
const hasLinks = this.getParamValue(Params, 'HasLinks');
|
|
37
|
+
const isRetweet = this.getParamValue(Params, 'IsRetweet');
|
|
38
|
+
const isReply = this.getParamValue(Params, 'IsReply');
|
|
39
|
+
const isQuote = this.getParamValue(Params, 'IsQuote');
|
|
40
|
+
const isVerified = this.getParamValue(Params, 'IsVerified');
|
|
41
|
+
const minLikes = this.getParamValue(Params, 'MinLikes');
|
|
42
|
+
const minRetweets = this.getParamValue(Params, 'MinRetweets');
|
|
43
|
+
const minReplies = this.getParamValue(Params, 'MinReplies');
|
|
44
|
+
const place = this.getParamValue(Params, 'Place');
|
|
45
|
+
const maxResults = this.getParamValue(Params, 'MaxResults') || 100;
|
|
46
|
+
const sortOrder = this.getParamValue(Params, 'SortOrder') || 'recency';
|
|
47
|
+
|
|
48
|
+
// Build search query with advanced operators
|
|
49
|
+
const searchQuery = this.buildAdvancedSearchQuery({
|
|
50
|
+
query,
|
|
51
|
+
hashtags,
|
|
52
|
+
fromUser,
|
|
53
|
+
toUser,
|
|
54
|
+
mentionUser,
|
|
55
|
+
language,
|
|
56
|
+
hasMedia,
|
|
57
|
+
hasLinks,
|
|
58
|
+
isRetweet,
|
|
59
|
+
isReply,
|
|
60
|
+
isQuote,
|
|
61
|
+
isVerified,
|
|
62
|
+
minLikes,
|
|
63
|
+
minRetweets,
|
|
64
|
+
minReplies,
|
|
65
|
+
place,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Validate query
|
|
69
|
+
if (!searchQuery.trim()) {
|
|
70
|
+
throw new Error('At least one search parameter must be provided');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Twitter search query length limit
|
|
74
|
+
if (searchQuery.length > 512) {
|
|
75
|
+
throw new Error(`Search query exceeds Twitter's 512 character limit (current: ${searchQuery.length} characters)`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Build search parameters
|
|
79
|
+
const searchParams: TwitterSearchParams = {
|
|
80
|
+
query: searchQuery,
|
|
81
|
+
max_results: Math.min(maxResults, 100), // API limit per request
|
|
82
|
+
sort_order: sortOrder as 'recency' | 'relevancy',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Add date filters
|
|
86
|
+
if (startDate) {
|
|
87
|
+
searchParams.start_time = this.formatTwitterDate(startDate);
|
|
88
|
+
}
|
|
89
|
+
if (endDate) {
|
|
90
|
+
searchParams.end_time = this.formatTwitterDate(endDate);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Perform search
|
|
94
|
+
LogStatus(`Searching tweets with query: ${searchQuery.substring(0, 100)}${searchQuery.length > 100 ? '...' : ''}`);
|
|
95
|
+
const tweets = await this.searchTweetsInternal(searchParams, maxResults);
|
|
96
|
+
|
|
97
|
+
// Convert to normalized format
|
|
98
|
+
const normalizedPosts: SocialPost[] = tweets.map((tweet) => this.normalizePost(tweet));
|
|
99
|
+
|
|
100
|
+
// Analyze search results
|
|
101
|
+
const analysis = this.analyzeSearchResults(tweets, normalizedPosts);
|
|
102
|
+
|
|
103
|
+
// Update output parameters
|
|
104
|
+
const outputParams = [...Params];
|
|
105
|
+
const postsParam = outputParams.find((p) => p.Name === 'Posts');
|
|
106
|
+
if (postsParam) postsParam.Value = normalizedPosts;
|
|
107
|
+
const tweetsParam = outputParams.find((p) => p.Name === 'Tweets');
|
|
108
|
+
if (tweetsParam) tweetsParam.Value = tweets;
|
|
109
|
+
const analysisParam = outputParams.find((p) => p.Name === 'Analysis');
|
|
110
|
+
if (analysisParam) analysisParam.Value = analysis;
|
|
111
|
+
const actualQueryParam = outputParams.find((p) => p.Name === 'ActualQuery');
|
|
112
|
+
if (actualQueryParam) actualQueryParam.Value = searchQuery;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
Success: true,
|
|
116
|
+
ResultCode: 'SUCCESS',
|
|
117
|
+
Message: `Successfully found ${normalizedPosts.length} tweets matching search criteria`,
|
|
118
|
+
Params: outputParams,
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
Success: false,
|
|
125
|
+
ResultCode: this.getErrorCode(error),
|
|
126
|
+
Message: `Failed to search tweets: ${errorMessage}`,
|
|
127
|
+
Params,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
25
131
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const mentionUser = this.getParamValue(Params, 'MentionUser');
|
|
32
|
-
const startDate = this.getParamValue(Params, 'StartDate');
|
|
33
|
-
const endDate = this.getParamValue(Params, 'EndDate');
|
|
34
|
-
const language = this.getParamValue(Params, 'Language');
|
|
35
|
-
const hasMedia = this.getParamValue(Params, 'HasMedia');
|
|
36
|
-
const hasLinks = this.getParamValue(Params, 'HasLinks');
|
|
37
|
-
const isRetweet = this.getParamValue(Params, 'IsRetweet');
|
|
38
|
-
const isReply = this.getParamValue(Params, 'IsReply');
|
|
39
|
-
const isQuote = this.getParamValue(Params, 'IsQuote');
|
|
40
|
-
const isVerified = this.getParamValue(Params, 'IsVerified');
|
|
41
|
-
const minLikes = this.getParamValue(Params, 'MinLikes');
|
|
42
|
-
const minRetweets = this.getParamValue(Params, 'MinRetweets');
|
|
43
|
-
const minReplies = this.getParamValue(Params, 'MinReplies');
|
|
44
|
-
const place = this.getParamValue(Params, 'Place');
|
|
45
|
-
const maxResults = this.getParamValue(Params, 'MaxResults') || 100;
|
|
46
|
-
const sortOrder = this.getParamValue(Params, 'SortOrder') || 'recency';
|
|
47
|
-
|
|
48
|
-
// Build search query with advanced operators
|
|
49
|
-
const searchQuery = this.buildAdvancedSearchQuery({
|
|
50
|
-
query,
|
|
51
|
-
hashtags,
|
|
52
|
-
fromUser,
|
|
53
|
-
toUser,
|
|
54
|
-
mentionUser,
|
|
55
|
-
language,
|
|
56
|
-
hasMedia,
|
|
57
|
-
hasLinks,
|
|
58
|
-
isRetweet,
|
|
59
|
-
isReply,
|
|
60
|
-
isQuote,
|
|
61
|
-
isVerified,
|
|
62
|
-
minLikes,
|
|
63
|
-
minRetweets,
|
|
64
|
-
minReplies,
|
|
65
|
-
place
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Validate query
|
|
69
|
-
if (!searchQuery.trim()) {
|
|
70
|
-
throw new Error('At least one search parameter must be provided');
|
|
71
|
-
}
|
|
132
|
+
/**
|
|
133
|
+
* Build advanced search query with Twitter operators
|
|
134
|
+
*/
|
|
135
|
+
private buildAdvancedSearchQuery(params: any): string {
|
|
136
|
+
const parts: string[] = [];
|
|
72
137
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
138
|
+
// Basic query
|
|
139
|
+
if (params.query) {
|
|
140
|
+
parts.push(params.query);
|
|
141
|
+
}
|
|
77
142
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
143
|
+
// Hashtags
|
|
144
|
+
if (params.hashtags && Array.isArray(params.hashtags) && params.hashtags.length > 0) {
|
|
145
|
+
const hashtagQuery = params.hashtags.map((tag: string) => (tag.startsWith('#') ? tag : `#${tag}`)).join(' OR ');
|
|
146
|
+
parts.push(`(${hashtagQuery})`);
|
|
147
|
+
}
|
|
84
148
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
149
|
+
// User filters
|
|
150
|
+
if (params.fromUser) {
|
|
151
|
+
parts.push(`from:${params.fromUser}`);
|
|
152
|
+
}
|
|
153
|
+
if (params.toUser) {
|
|
154
|
+
parts.push(`to:${params.toUser}`);
|
|
155
|
+
}
|
|
156
|
+
if (params.mentionUser) {
|
|
157
|
+
parts.push(`@${params.mentionUser}`);
|
|
158
|
+
}
|
|
92
159
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
// Convert to normalized format
|
|
98
|
-
const normalizedPosts: SocialPost[] = tweets.map(tweet => this.normalizePost(tweet));
|
|
99
|
-
|
|
100
|
-
// Analyze search results
|
|
101
|
-
const analysis = this.analyzeSearchResults(tweets, normalizedPosts);
|
|
102
|
-
|
|
103
|
-
// Update output parameters
|
|
104
|
-
const outputParams = [...Params];
|
|
105
|
-
const postsParam = outputParams.find(p => p.Name === 'Posts');
|
|
106
|
-
if (postsParam) postsParam.Value = normalizedPosts;
|
|
107
|
-
const tweetsParam = outputParams.find(p => p.Name === 'Tweets');
|
|
108
|
-
if (tweetsParam) tweetsParam.Value = tweets;
|
|
109
|
-
const analysisParam = outputParams.find(p => p.Name === 'Analysis');
|
|
110
|
-
if (analysisParam) analysisParam.Value = analysis;
|
|
111
|
-
const actualQueryParam = outputParams.find(p => p.Name === 'ActualQuery');
|
|
112
|
-
if (actualQueryParam) actualQueryParam.Value = searchQuery;
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
Success: true,
|
|
116
|
-
ResultCode: 'SUCCESS',
|
|
117
|
-
Message: `Successfully found ${normalizedPosts.length} tweets matching search criteria`,
|
|
118
|
-
Params: outputParams
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
} catch (error) {
|
|
122
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
Success: false,
|
|
126
|
-
ResultCode: this.getErrorCode(error),
|
|
127
|
-
Message: `Failed to search tweets: ${errorMessage}`,
|
|
128
|
-
Params
|
|
129
|
-
};
|
|
130
|
-
}
|
|
160
|
+
// Language filter
|
|
161
|
+
if (params.language) {
|
|
162
|
+
parts.push(`lang:${params.language}`);
|
|
131
163
|
}
|
|
132
164
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
165
|
+
// Media and link filters
|
|
166
|
+
if (params.hasMedia === true) {
|
|
167
|
+
parts.push('has:media');
|
|
168
|
+
} else if (params.hasMedia === false) {
|
|
169
|
+
parts.push('-has:media');
|
|
170
|
+
}
|
|
138
171
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
172
|
+
if (params.hasLinks === true) {
|
|
173
|
+
parts.push('has:links');
|
|
174
|
+
} else if (params.hasLinks === false) {
|
|
175
|
+
parts.push('-has:links');
|
|
176
|
+
}
|
|
143
177
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
178
|
+
// Tweet type filters
|
|
179
|
+
if (params.isRetweet === true) {
|
|
180
|
+
parts.push('is:retweet');
|
|
181
|
+
} else if (params.isRetweet === false) {
|
|
182
|
+
parts.push('-is:retweet');
|
|
183
|
+
}
|
|
151
184
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
parts.push(`to:${params.toUser}`);
|
|
158
|
-
}
|
|
159
|
-
if (params.mentionUser) {
|
|
160
|
-
parts.push(`@${params.mentionUser}`);
|
|
161
|
-
}
|
|
185
|
+
if (params.isReply === true) {
|
|
186
|
+
parts.push('is:reply');
|
|
187
|
+
} else if (params.isReply === false) {
|
|
188
|
+
parts.push('-is:reply');
|
|
189
|
+
}
|
|
162
190
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
191
|
+
if (params.isQuote === true) {
|
|
192
|
+
parts.push('is:quote');
|
|
193
|
+
} else if (params.isQuote === false) {
|
|
194
|
+
parts.push('-is:quote');
|
|
195
|
+
}
|
|
167
196
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
197
|
+
// Verified filter
|
|
198
|
+
if (params.isVerified === true) {
|
|
199
|
+
parts.push('is:verified');
|
|
200
|
+
} else if (params.isVerified === false) {
|
|
201
|
+
parts.push('-is:verified');
|
|
202
|
+
}
|
|
174
203
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
204
|
+
// Engagement filters (Note: These require Academic Research access)
|
|
205
|
+
if (params.minLikes && params.minLikes > 0) {
|
|
206
|
+
parts.push(`min_faves:${params.minLikes}`);
|
|
207
|
+
}
|
|
208
|
+
if (params.minRetweets && params.minRetweets > 0) {
|
|
209
|
+
parts.push(`min_retweets:${params.minRetweets}`);
|
|
210
|
+
}
|
|
211
|
+
if (params.minReplies && params.minReplies > 0) {
|
|
212
|
+
parts.push(`min_replies:${params.minReplies}`);
|
|
213
|
+
}
|
|
180
214
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
parts.push('-is:retweet');
|
|
186
|
-
}
|
|
215
|
+
// Place filter
|
|
216
|
+
if (params.place) {
|
|
217
|
+
parts.push(`place:"${params.place}"`);
|
|
218
|
+
}
|
|
187
219
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
220
|
+
return parts.join(' ');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Internal method to search tweets with pagination
|
|
225
|
+
*/
|
|
226
|
+
private async searchTweetsInternal(searchParams: TwitterSearchParams, maxResults: number): Promise<Tweet[]> {
|
|
227
|
+
const tweets: Tweet[] = [];
|
|
228
|
+
let nextToken: string | undefined;
|
|
229
|
+
|
|
230
|
+
const queryParams: Record<string, any> = {
|
|
231
|
+
query: searchParams.query,
|
|
232
|
+
'tweet.fields':
|
|
233
|
+
'id,text,created_at,author_id,conversation_id,public_metrics,attachments,entities,referenced_tweets,lang,possibly_sensitive',
|
|
234
|
+
'user.fields': 'id,name,username,profile_image_url,description,created_at,verified',
|
|
235
|
+
'media.fields': 'url,preview_image_url,type,width,height',
|
|
236
|
+
expansions: 'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id',
|
|
237
|
+
max_results: searchParams.max_results,
|
|
238
|
+
sort_order: searchParams.sort_order,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Add time filters
|
|
242
|
+
if (searchParams.start_time) {
|
|
243
|
+
queryParams['start_time'] = searchParams.start_time;
|
|
244
|
+
}
|
|
245
|
+
if (searchParams.end_time) {
|
|
246
|
+
queryParams['end_time'] = searchParams.end_time;
|
|
247
|
+
}
|
|
193
248
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
249
|
+
while (tweets.length < maxResults) {
|
|
250
|
+
if (nextToken) {
|
|
251
|
+
queryParams['next_token'] = nextToken;
|
|
252
|
+
}
|
|
199
253
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
parts.push('-is:verified');
|
|
205
|
-
}
|
|
254
|
+
try {
|
|
255
|
+
const response = await this.axiosInstance.get('/tweets/search/recent', {
|
|
256
|
+
params: queryParams,
|
|
257
|
+
});
|
|
206
258
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
parts.push(`min_faves:${params.minLikes}`);
|
|
210
|
-
}
|
|
211
|
-
if (params.minRetweets && params.minRetweets > 0) {
|
|
212
|
-
parts.push(`min_retweets:${params.minRetweets}`);
|
|
213
|
-
}
|
|
214
|
-
if (params.minReplies && params.minReplies > 0) {
|
|
215
|
-
parts.push(`min_replies:${params.minReplies}`);
|
|
259
|
+
if (response.data.data && Array.isArray(response.data.data)) {
|
|
260
|
+
tweets.push(...response.data.data);
|
|
216
261
|
}
|
|
217
262
|
|
|
218
|
-
//
|
|
219
|
-
if (
|
|
220
|
-
|
|
263
|
+
// Check if we've reached the desired number of results
|
|
264
|
+
if (tweets.length >= maxResults) {
|
|
265
|
+
return tweets.slice(0, maxResults);
|
|
221
266
|
}
|
|
222
267
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
* Internal method to search tweets with pagination
|
|
228
|
-
*/
|
|
229
|
-
private async searchTweetsInternal(searchParams: TwitterSearchParams, maxResults: number): Promise<Tweet[]> {
|
|
230
|
-
const tweets: Tweet[] = [];
|
|
231
|
-
let nextToken: string | undefined;
|
|
232
|
-
|
|
233
|
-
const queryParams: Record<string, any> = {
|
|
234
|
-
'query': searchParams.query,
|
|
235
|
-
'tweet.fields': 'id,text,created_at,author_id,conversation_id,public_metrics,attachments,entities,referenced_tweets,lang,possibly_sensitive',
|
|
236
|
-
'user.fields': 'id,name,username,profile_image_url,description,created_at,verified',
|
|
237
|
-
'media.fields': 'url,preview_image_url,type,width,height',
|
|
238
|
-
'expansions': 'author_id,attachments.media_keys,referenced_tweets.id,referenced_tweets.id.author_id',
|
|
239
|
-
'max_results': searchParams.max_results,
|
|
240
|
-
'sort_order': searchParams.sort_order
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
// Add time filters
|
|
244
|
-
if (searchParams.start_time) {
|
|
245
|
-
queryParams['start_time'] = searchParams.start_time;
|
|
246
|
-
}
|
|
247
|
-
if (searchParams.end_time) {
|
|
248
|
-
queryParams['end_time'] = searchParams.end_time;
|
|
268
|
+
// Check for more pages
|
|
269
|
+
nextToken = response.data.meta?.next_token;
|
|
270
|
+
if (!nextToken) {
|
|
271
|
+
break;
|
|
249
272
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
273
|
+
} catch (error) {
|
|
274
|
+
// If we get a 400 error, it might be due to unsupported operators
|
|
275
|
+
if ((error as any).response?.status === 400) {
|
|
276
|
+
const errorDetail = (error as any).response?.data?.detail || '';
|
|
277
|
+
if (errorDetail.includes('min_faves') || errorDetail.includes('min_retweets') || errorDetail.includes('min_replies')) {
|
|
278
|
+
LogStatus('Note: Engagement filters (min_likes, min_retweets, min_replies) require Academic Research access');
|
|
279
|
+
// Retry without engagement filters
|
|
280
|
+
const cleanedQuery = searchParams.query
|
|
281
|
+
.replace(/min_faves:\d+\s*/g, '')
|
|
282
|
+
.replace(/min_retweets:\d+\s*/g, '')
|
|
283
|
+
.replace(/min_replies:\d+\s*/g, '')
|
|
284
|
+
.trim();
|
|
285
|
+
|
|
286
|
+
if (cleanedQuery !== searchParams.query) {
|
|
287
|
+
queryParams['query'] = cleanedQuery;
|
|
288
|
+
continue;
|
|
254
289
|
}
|
|
255
|
-
|
|
256
|
-
try {
|
|
257
|
-
const response = await this.axiosInstance.get('/tweets/search/recent', {
|
|
258
|
-
params: queryParams
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
if (response.data.data && Array.isArray(response.data.data)) {
|
|
262
|
-
tweets.push(...response.data.data);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Check if we've reached the desired number of results
|
|
266
|
-
if (tweets.length >= maxResults) {
|
|
267
|
-
return tweets.slice(0, maxResults);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Check for more pages
|
|
271
|
-
nextToken = response.data.meta?.next_token;
|
|
272
|
-
if (!nextToken) {
|
|
273
|
-
break;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
} catch (error) {
|
|
277
|
-
// If we get a 400 error, it might be due to unsupported operators
|
|
278
|
-
if ((error as any).response?.status === 400) {
|
|
279
|
-
const errorDetail = (error as any).response?.data?.detail || '';
|
|
280
|
-
if (errorDetail.includes('min_faves') || errorDetail.includes('min_retweets') || errorDetail.includes('min_replies')) {
|
|
281
|
-
LogStatus('Note: Engagement filters (min_likes, min_retweets, min_replies) require Academic Research access');
|
|
282
|
-
// Retry without engagement filters
|
|
283
|
-
const cleanedQuery = searchParams.query
|
|
284
|
-
.replace(/min_faves:\d+\s*/g, '')
|
|
285
|
-
.replace(/min_retweets:\d+\s*/g, '')
|
|
286
|
-
.replace(/min_replies:\d+\s*/g, '')
|
|
287
|
-
.trim();
|
|
288
|
-
|
|
289
|
-
if (cleanedQuery !== searchParams.query) {
|
|
290
|
-
queryParams['query'] = cleanedQuery;
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
throw error;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return tweets;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Implement searchPosts for base class
|
|
304
|
-
*/
|
|
305
|
-
protected async searchPosts(params: SearchParams): Promise<SocialPost[]> {
|
|
306
|
-
const searchParams: TwitterSearchParams = {
|
|
307
|
-
query: this.buildSearchQuery(params),
|
|
308
|
-
max_results: params.limit || 100
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
if (params.startDate) {
|
|
312
|
-
searchParams.start_time = this.formatTwitterDate(params.startDate);
|
|
313
|
-
}
|
|
314
|
-
if (params.endDate) {
|
|
315
|
-
searchParams.end_time = this.formatTwitterDate(params.endDate);
|
|
290
|
+
}
|
|
316
291
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return tweets.map(tweet => this.normalizePost(tweet));
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
320
294
|
}
|
|
321
295
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
*/
|
|
325
|
-
private analyzeSearchResults(tweets: Tweet[], normalizedPosts: SocialPost[]): any {
|
|
326
|
-
const analysis = {
|
|
327
|
-
totalResults: tweets.length,
|
|
328
|
-
dateRange: {
|
|
329
|
-
earliest: null as string | null,
|
|
330
|
-
latest: null as string | null
|
|
331
|
-
},
|
|
332
|
-
languages: {} as Record<string, number>,
|
|
333
|
-
tweetTypes: {
|
|
334
|
-
original: 0,
|
|
335
|
-
replies: 0,
|
|
336
|
-
retweets: 0,
|
|
337
|
-
quotes: 0
|
|
338
|
-
},
|
|
339
|
-
topHashtags: [] as Array<{ tag: string; count: number }>,
|
|
340
|
-
topMentions: [] as Array<{ username: string; count: number }>,
|
|
341
|
-
engagementStats: {
|
|
342
|
-
totalLikes: 0,
|
|
343
|
-
totalRetweets: 0,
|
|
344
|
-
totalReplies: 0,
|
|
345
|
-
totalQuotes: 0,
|
|
346
|
-
averageEngagement: 0
|
|
347
|
-
},
|
|
348
|
-
topEngagedTweets: [] as any[]
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// Track hashtags and mentions
|
|
352
|
-
const hashtagCounts = new Map<string, number>();
|
|
353
|
-
const mentionCounts = new Map<string, number>();
|
|
354
|
-
|
|
355
|
-
tweets.forEach((tweet, index) => {
|
|
356
|
-
// Date range
|
|
357
|
-
const createdAt = tweet.created_at;
|
|
358
|
-
if (!analysis.dateRange.earliest || createdAt < analysis.dateRange.earliest) {
|
|
359
|
-
analysis.dateRange.earliest = createdAt;
|
|
360
|
-
}
|
|
361
|
-
if (!analysis.dateRange.latest || createdAt > analysis.dateRange.latest) {
|
|
362
|
-
analysis.dateRange.latest = createdAt;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Language
|
|
366
|
-
const lang = (tweet as any).lang || 'unknown';
|
|
367
|
-
analysis.languages[lang] = (analysis.languages[lang] || 0) + 1;
|
|
368
|
-
|
|
369
|
-
// Tweet types
|
|
370
|
-
if (tweet.referenced_tweets) {
|
|
371
|
-
const types = tweet.referenced_tweets.map(ref => ref.type);
|
|
372
|
-
if (types.includes('replied_to')) analysis.tweetTypes.replies++;
|
|
373
|
-
else if (types.includes('retweeted')) analysis.tweetTypes.retweets++;
|
|
374
|
-
else if (types.includes('quoted')) analysis.tweetTypes.quotes++;
|
|
375
|
-
} else {
|
|
376
|
-
analysis.tweetTypes.original++;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Hashtags
|
|
380
|
-
if (tweet.entities?.hashtags) {
|
|
381
|
-
tweet.entities.hashtags.forEach(hashtag => {
|
|
382
|
-
const tag = hashtag.tag.toLowerCase();
|
|
383
|
-
hashtagCounts.set(tag, (hashtagCounts.get(tag) || 0) + 1);
|
|
384
|
-
});
|
|
385
|
-
}
|
|
296
|
+
return tweets;
|
|
297
|
+
}
|
|
386
298
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
// Engagement stats
|
|
396
|
-
if (tweet.public_metrics) {
|
|
397
|
-
analysis.engagementStats.totalLikes += tweet.public_metrics.like_count || 0;
|
|
398
|
-
analysis.engagementStats.totalRetweets += tweet.public_metrics.retweet_count || 0;
|
|
399
|
-
analysis.engagementStats.totalReplies += tweet.public_metrics.reply_count || 0;
|
|
400
|
-
analysis.engagementStats.totalQuotes += tweet.public_metrics.quote_count || 0;
|
|
401
|
-
}
|
|
402
|
-
});
|
|
299
|
+
/**
|
|
300
|
+
* Implement searchPosts for base class
|
|
301
|
+
*/
|
|
302
|
+
protected async searchPosts(params: SearchParams): Promise<SocialPost[]> {
|
|
303
|
+
const searchParams: TwitterSearchParams = {
|
|
304
|
+
query: this.buildSearchQuery(params),
|
|
305
|
+
max_results: params.limit || 100,
|
|
306
|
+
};
|
|
403
307
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const totalEngagement =
|
|
407
|
-
analysis.engagementStats.totalLikes +
|
|
408
|
-
analysis.engagementStats.totalRetweets +
|
|
409
|
-
analysis.engagementStats.totalReplies +
|
|
410
|
-
analysis.engagementStats.totalQuotes;
|
|
411
|
-
analysis.engagementStats.averageEngagement = Math.round(totalEngagement / tweets.length);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Top hashtags
|
|
415
|
-
analysis.topHashtags = Array.from(hashtagCounts.entries())
|
|
416
|
-
.sort((a, b) => b[1] - a[1])
|
|
417
|
-
.slice(0, 10)
|
|
418
|
-
.map(([tag, count]) => ({ tag, count }));
|
|
419
|
-
|
|
420
|
-
// Top mentions
|
|
421
|
-
analysis.topMentions = Array.from(mentionCounts.entries())
|
|
422
|
-
.sort((a, b) => b[1] - a[1])
|
|
423
|
-
.slice(0, 10)
|
|
424
|
-
.map(([username, count]) => ({ username, count }));
|
|
425
|
-
|
|
426
|
-
// Top engaged tweets
|
|
427
|
-
analysis.topEngagedTweets = normalizedPosts
|
|
428
|
-
.filter(post => post.analytics)
|
|
429
|
-
.sort((a, b) => (b.analytics?.engagements || 0) - (a.analytics?.engagements || 0))
|
|
430
|
-
.slice(0, 5)
|
|
431
|
-
.map(post => ({
|
|
432
|
-
id: post.id,
|
|
433
|
-
content: post.content.substring(0, 100) + (post.content.length > 100 ? '...' : ''),
|
|
434
|
-
engagement: post.analytics?.engagements,
|
|
435
|
-
metrics: post.analytics
|
|
436
|
-
}));
|
|
437
|
-
|
|
438
|
-
return analysis;
|
|
308
|
+
if (params.startDate) {
|
|
309
|
+
searchParams.start_time = this.formatTwitterDate(params.startDate);
|
|
439
310
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
* Get error code based on error type
|
|
443
|
-
*/
|
|
444
|
-
private getErrorCode(error: any): string {
|
|
445
|
-
if (error instanceof Error) {
|
|
446
|
-
if (error.message.includes('Rate Limit')) return 'RATE_LIMIT';
|
|
447
|
-
if (error.message.includes('Unauthorized')) return 'INVALID_TOKEN';
|
|
448
|
-
if (error.message.includes('character limit')) return 'QUERY_TOO_LONG';
|
|
449
|
-
if (error.message.includes('Academic Research')) return 'INSUFFICIENT_ACCESS';
|
|
450
|
-
}
|
|
451
|
-
return 'ERROR';
|
|
311
|
+
if (params.endDate) {
|
|
312
|
+
searchParams.end_time = this.formatTwitterDate(params.endDate);
|
|
452
313
|
}
|
|
453
314
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
Value: null
|
|
549
|
-
},
|
|
550
|
-
{
|
|
551
|
-
Name: 'MaxResults',
|
|
552
|
-
Type: 'Input',
|
|
553
|
-
Value: 100
|
|
554
|
-
},
|
|
555
|
-
{
|
|
556
|
-
Name: 'SortOrder',
|
|
557
|
-
Type: 'Input',
|
|
558
|
-
Value: 'recency' // 'recency' or 'relevancy'
|
|
559
|
-
},
|
|
560
|
-
{
|
|
561
|
-
Name: 'Posts',
|
|
562
|
-
Type: 'Output',
|
|
563
|
-
Value: null
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
Name: 'Tweets',
|
|
567
|
-
Type: 'Output',
|
|
568
|
-
Value: null
|
|
569
|
-
},
|
|
570
|
-
{
|
|
571
|
-
Name: 'Analysis',
|
|
572
|
-
Type: 'Output',
|
|
573
|
-
Value: null
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
Name: 'ActualQuery',
|
|
577
|
-
Type: 'Output',
|
|
578
|
-
Value: null
|
|
579
|
-
}
|
|
580
|
-
];
|
|
315
|
+
const tweets = await this.searchTweetsInternal(searchParams, params.limit || 100);
|
|
316
|
+
return tweets.map((tweet) => this.normalizePost(tweet));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Analyze search results
|
|
321
|
+
*/
|
|
322
|
+
private analyzeSearchResults(tweets: Tweet[], normalizedPosts: SocialPost[]): any {
|
|
323
|
+
const analysis = {
|
|
324
|
+
totalResults: tweets.length,
|
|
325
|
+
dateRange: {
|
|
326
|
+
earliest: null as string | null,
|
|
327
|
+
latest: null as string | null,
|
|
328
|
+
},
|
|
329
|
+
languages: {} as Record<string, number>,
|
|
330
|
+
tweetTypes: {
|
|
331
|
+
original: 0,
|
|
332
|
+
replies: 0,
|
|
333
|
+
retweets: 0,
|
|
334
|
+
quotes: 0,
|
|
335
|
+
},
|
|
336
|
+
topHashtags: [] as Array<{ tag: string; count: number }>,
|
|
337
|
+
topMentions: [] as Array<{ username: string; count: number }>,
|
|
338
|
+
engagementStats: {
|
|
339
|
+
totalLikes: 0,
|
|
340
|
+
totalRetweets: 0,
|
|
341
|
+
totalReplies: 0,
|
|
342
|
+
totalQuotes: 0,
|
|
343
|
+
averageEngagement: 0,
|
|
344
|
+
},
|
|
345
|
+
topEngagedTweets: [] as any[],
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Track hashtags and mentions
|
|
349
|
+
const hashtagCounts = new Map<string, number>();
|
|
350
|
+
const mentionCounts = new Map<string, number>();
|
|
351
|
+
|
|
352
|
+
tweets.forEach((tweet, index) => {
|
|
353
|
+
// Date range
|
|
354
|
+
const createdAt = tweet.created_at;
|
|
355
|
+
if (!analysis.dateRange.earliest || createdAt < analysis.dateRange.earliest) {
|
|
356
|
+
analysis.dateRange.earliest = createdAt;
|
|
357
|
+
}
|
|
358
|
+
if (!analysis.dateRange.latest || createdAt > analysis.dateRange.latest) {
|
|
359
|
+
analysis.dateRange.latest = createdAt;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Language
|
|
363
|
+
const lang = (tweet as any).lang || 'unknown';
|
|
364
|
+
analysis.languages[lang] = (analysis.languages[lang] || 0) + 1;
|
|
365
|
+
|
|
366
|
+
// Tweet types
|
|
367
|
+
if (tweet.referenced_tweets) {
|
|
368
|
+
const types = tweet.referenced_tweets.map((ref) => ref.type);
|
|
369
|
+
if (types.includes('replied_to')) analysis.tweetTypes.replies++;
|
|
370
|
+
else if (types.includes('retweeted')) analysis.tweetTypes.retweets++;
|
|
371
|
+
else if (types.includes('quoted')) analysis.tweetTypes.quotes++;
|
|
372
|
+
} else {
|
|
373
|
+
analysis.tweetTypes.original++;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Hashtags
|
|
377
|
+
if (tweet.entities?.hashtags) {
|
|
378
|
+
tweet.entities.hashtags.forEach((hashtag) => {
|
|
379
|
+
const tag = hashtag.tag.toLowerCase();
|
|
380
|
+
hashtagCounts.set(tag, (hashtagCounts.get(tag) || 0) + 1);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Mentions
|
|
385
|
+
if (tweet.entities?.mentions) {
|
|
386
|
+
tweet.entities.mentions.forEach((mention) => {
|
|
387
|
+
const username = mention.username.toLowerCase();
|
|
388
|
+
mentionCounts.set(username, (mentionCounts.get(username) || 0) + 1);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Engagement stats
|
|
393
|
+
if (tweet.public_metrics) {
|
|
394
|
+
analysis.engagementStats.totalLikes += tweet.public_metrics.like_count || 0;
|
|
395
|
+
analysis.engagementStats.totalRetweets += tweet.public_metrics.retweet_count || 0;
|
|
396
|
+
analysis.engagementStats.totalReplies += tweet.public_metrics.reply_count || 0;
|
|
397
|
+
analysis.engagementStats.totalQuotes += tweet.public_metrics.quote_count || 0;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Calculate average engagement
|
|
402
|
+
if (tweets.length > 0) {
|
|
403
|
+
const totalEngagement =
|
|
404
|
+
analysis.engagementStats.totalLikes +
|
|
405
|
+
analysis.engagementStats.totalRetweets +
|
|
406
|
+
analysis.engagementStats.totalReplies +
|
|
407
|
+
analysis.engagementStats.totalQuotes;
|
|
408
|
+
analysis.engagementStats.averageEngagement = Math.round(totalEngagement / tweets.length);
|
|
581
409
|
}
|
|
582
410
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
411
|
+
// Top hashtags
|
|
412
|
+
analysis.topHashtags = Array.from(hashtagCounts.entries())
|
|
413
|
+
.sort((a, b) => b[1] - a[1])
|
|
414
|
+
.slice(0, 10)
|
|
415
|
+
.map(([tag, count]) => ({ tag, count }));
|
|
416
|
+
|
|
417
|
+
// Top mentions
|
|
418
|
+
analysis.topMentions = Array.from(mentionCounts.entries())
|
|
419
|
+
.sort((a, b) => b[1] - a[1])
|
|
420
|
+
.slice(0, 10)
|
|
421
|
+
.map(([username, count]) => ({ username, count }));
|
|
422
|
+
|
|
423
|
+
// Top engaged tweets
|
|
424
|
+
analysis.topEngagedTweets = normalizedPosts
|
|
425
|
+
.filter((post) => post.analytics)
|
|
426
|
+
.sort((a, b) => (b.analytics?.engagements || 0) - (a.analytics?.engagements || 0))
|
|
427
|
+
.slice(0, 5)
|
|
428
|
+
.map((post) => ({
|
|
429
|
+
id: post.id,
|
|
430
|
+
content: post.content.substring(0, 100) + (post.content.length > 100 ? '...' : ''),
|
|
431
|
+
engagement: post.analytics?.engagements,
|
|
432
|
+
metrics: post.analytics,
|
|
433
|
+
}));
|
|
434
|
+
|
|
435
|
+
return analysis;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Get error code based on error type
|
|
440
|
+
*/
|
|
441
|
+
private getErrorCode(error: any): string {
|
|
442
|
+
if (error instanceof Error) {
|
|
443
|
+
if (error.message.includes('Rate Limit')) return 'RATE_LIMIT';
|
|
444
|
+
if (error.message.includes('Unauthorized')) return 'INVALID_TOKEN';
|
|
445
|
+
if (error.message.includes('character limit')) return 'QUERY_TOO_LONG';
|
|
446
|
+
if (error.message.includes('Academic Research')) return 'INSUFFICIENT_ACCESS';
|
|
588
447
|
}
|
|
589
|
-
|
|
448
|
+
return 'ERROR';
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Define the parameters this action expects
|
|
453
|
+
*/
|
|
454
|
+
public get Params(): ActionParam[] {
|
|
455
|
+
return [
|
|
456
|
+
...this.commonSocialParams,
|
|
457
|
+
{
|
|
458
|
+
Name: 'Query',
|
|
459
|
+
Type: 'Input',
|
|
460
|
+
Value: null,
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
Name: 'Hashtags',
|
|
464
|
+
Type: 'Input',
|
|
465
|
+
Value: null,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
Name: 'FromUser',
|
|
469
|
+
Type: 'Input',
|
|
470
|
+
Value: null,
|
|
471
|
+
},
|
|
472
|
+
{
|
|
473
|
+
Name: 'ToUser',
|
|
474
|
+
Type: 'Input',
|
|
475
|
+
Value: null,
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
Name: 'MentionUser',
|
|
479
|
+
Type: 'Input',
|
|
480
|
+
Value: null,
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
Name: 'StartDate',
|
|
484
|
+
Type: 'Input',
|
|
485
|
+
Value: null,
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
Name: 'EndDate',
|
|
489
|
+
Type: 'Input',
|
|
490
|
+
Value: null,
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
Name: 'Language',
|
|
494
|
+
Type: 'Input',
|
|
495
|
+
Value: null,
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
Name: 'HasMedia',
|
|
499
|
+
Type: 'Input',
|
|
500
|
+
Value: null,
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
Name: 'HasLinks',
|
|
504
|
+
Type: 'Input',
|
|
505
|
+
Value: null,
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
Name: 'IsRetweet',
|
|
509
|
+
Type: 'Input',
|
|
510
|
+
Value: null,
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
Name: 'IsReply',
|
|
514
|
+
Type: 'Input',
|
|
515
|
+
Value: null,
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
Name: 'IsQuote',
|
|
519
|
+
Type: 'Input',
|
|
520
|
+
Value: null,
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
Name: 'IsVerified',
|
|
524
|
+
Type: 'Input',
|
|
525
|
+
Value: null,
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
Name: 'MinLikes',
|
|
529
|
+
Type: 'Input',
|
|
530
|
+
Value: null,
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
Name: 'MinRetweets',
|
|
534
|
+
Type: 'Input',
|
|
535
|
+
Value: null,
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
Name: 'MinReplies',
|
|
539
|
+
Type: 'Input',
|
|
540
|
+
Value: null,
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
Name: 'Place',
|
|
544
|
+
Type: 'Input',
|
|
545
|
+
Value: null,
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
Name: 'MaxResults',
|
|
549
|
+
Type: 'Input',
|
|
550
|
+
Value: 100,
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
Name: 'SortOrder',
|
|
554
|
+
Type: 'Input',
|
|
555
|
+
Value: 'recency', // 'recency' or 'relevancy'
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
Name: 'Posts',
|
|
559
|
+
Type: 'Output',
|
|
560
|
+
Value: null,
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
Name: 'Tweets',
|
|
564
|
+
Type: 'Output',
|
|
565
|
+
Value: null,
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
Name: 'Analysis',
|
|
569
|
+
Type: 'Output',
|
|
570
|
+
Value: null,
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
Name: 'ActualQuery',
|
|
574
|
+
Type: 'Output',
|
|
575
|
+
Value: null,
|
|
576
|
+
},
|
|
577
|
+
];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Get action description
|
|
582
|
+
*/
|
|
583
|
+
public get Description(): string {
|
|
584
|
+
return 'Searches for tweets on Twitter/X using advanced operators and filters, with comprehensive analysis of results including historical data';
|
|
585
|
+
}
|
|
586
|
+
}
|