@memberjunction/actions-bizapps-social 2.112.0 → 2.113.1
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 +13 -0
- package/dist/base/base-social.action.d.ts.map +1 -1
- package/dist/base/base-social.action.js +24 -18
- 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 +34 -35
- 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 +36 -34
- 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 +27 -25
- 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 +23 -19
- 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 +32 -28
- 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 +44 -42
- 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 +39 -37
- 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 +59 -44
- 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 +31 -33
- 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 +32 -28
- 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 +26 -24
- 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 +34 -32
- 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 +52 -43
- 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 +28 -30
- 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 +20 -18
- 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 +26 -27
- 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 +59 -38
- 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 +25 -23
- 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 +60 -56
- 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 +27 -25
- 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 +45 -55
- 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 +29 -31
- 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 +23 -25
- 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 +30 -32
- 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 +30 -28
- 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 +38 -33
- 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 +26 -25
- 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 +29 -25
- 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 +47 -40
- 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 +31 -30
- 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 +58 -56
- 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 +68 -58
- 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 +25 -22
- package/dist/providers/youtube/youtube-base.action.js.map +1 -1
- package/package.json +6 -5
- package/src/base/base-social.action.ts +224 -217
- package/src/providers/buffer/buffer-base.action.ts +441 -435
- package/src/providers/facebook/actions/boost-post.action.ts +386 -350
- package/src/providers/facebook/actions/create-album.action.ts +307 -291
- package/src/providers/facebook/actions/create-post.action.ts +227 -224
- package/src/providers/facebook/actions/get-page-insights.action.ts +403 -383
- package/src/providers/facebook/actions/get-page-posts.action.ts +225 -214
- package/src/providers/facebook/actions/get-post-insights.action.ts +316 -300
- package/src/providers/facebook/actions/respond-to-comments.action.ts +336 -319
- package/src/providers/facebook/actions/schedule-post.action.ts +292 -289
- package/src/providers/facebook/actions/search-posts.action.ts +413 -399
- package/src/providers/facebook/facebook-base.action.ts +670 -653
- package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
- package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +189 -184
- package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +161 -160
- package/src/providers/hootsuite/actions/get-analytics.action.ts +254 -249
- package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +207 -206
- package/src/providers/hootsuite/actions/get-social-profiles.action.ts +205 -206
- package/src/providers/hootsuite/actions/search-posts.action.ts +369 -351
- package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +209 -211
- package/src/providers/hootsuite/hootsuite-base.action.ts +307 -301
- package/src/providers/instagram/actions/create-post.action.ts +296 -276
- package/src/providers/instagram/actions/create-story.action.ts +394 -378
- package/src/providers/instagram/actions/get-account-insights.action.ts +420 -384
- package/src/providers/instagram/actions/get-business-posts.action.ts +242 -233
- package/src/providers/instagram/actions/get-comments.action.ts +377 -365
- package/src/providers/instagram/actions/get-post-insights.action.ts +273 -265
- package/src/providers/instagram/actions/schedule-post.action.ts +235 -233
- package/src/providers/instagram/actions/search-posts.action.ts +538 -512
- package/src/providers/instagram/instagram-base.action.ts +393 -368
- package/src/providers/linkedin/actions/create-article.action.ts +266 -275
- package/src/providers/linkedin/actions/create-post.action.ts +177 -179
- package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
- package/src/providers/linkedin/actions/get-organization-posts.action.ts +147 -146
- package/src/providers/linkedin/actions/get-personal-posts.action.ts +139 -138
- package/src/providers/linkedin/actions/get-post-analytics.action.ts +189 -190
- package/src/providers/linkedin/actions/schedule-post.action.ts +189 -191
- package/src/providers/linkedin/actions/search-posts.action.ts +283 -275
- package/src/providers/linkedin/linkedin-base.action.ts +421 -407
- package/src/providers/tiktok/tiktok-base.action.ts +320 -305
- package/src/providers/twitter/actions/create-thread.action.ts +207 -203
- package/src/providers/twitter/actions/create-tweet.action.ts +188 -187
- package/src/providers/twitter/actions/delete-tweet.action.ts +129 -128
- package/src/providers/twitter/actions/get-analytics.action.ts +411 -402
- package/src/providers/twitter/actions/get-mentions.action.ts +219 -218
- package/src/providers/twitter/actions/get-timeline.action.ts +233 -232
- package/src/providers/twitter/actions/schedule-tweet.action.ts +222 -221
- package/src/providers/twitter/actions/search-tweets.action.ts +543 -540
- package/src/providers/twitter/twitter-base.action.ts +560 -541
- package/src/providers/youtube/youtube-base.action.ts +333 -320
|
@@ -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/core';
|
|
5
5
|
import { SocialPost, SearchParams } from '../../../base/base-social.action';
|
|
6
6
|
import { BaseAction } from '@memberjunction/actions';
|
|
7
7
|
|
|
@@ -10,577 +10,580 @@ 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
|
-
|
|
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
|
-
}
|
|
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
|
+
}
|
|
131
25
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
|
137
72
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
+
}
|
|
142
77
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
};
|
|
148
84
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (params.mentionUser) {
|
|
157
|
-
parts.push(`@${params.mentionUser}`);
|
|
158
|
-
}
|
|
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
|
+
}
|
|
159
92
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
|
|
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
|
+
}
|
|
163
131
|
}
|
|
164
132
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
133
|
+
/**
|
|
134
|
+
* Build advanced search query with Twitter operators
|
|
135
|
+
*/
|
|
136
|
+
private buildAdvancedSearchQuery(params: any): string {
|
|
137
|
+
const parts: string[] = [];
|
|
171
138
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
139
|
+
// Basic query
|
|
140
|
+
if (params.query) {
|
|
141
|
+
parts.push(params.query);
|
|
142
|
+
}
|
|
177
143
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
144
|
+
// Hashtags
|
|
145
|
+
if (params.hashtags && Array.isArray(params.hashtags) && params.hashtags.length > 0) {
|
|
146
|
+
const hashtagQuery = params.hashtags
|
|
147
|
+
.map((tag: string) => tag.startsWith('#') ? tag : `#${tag}`)
|
|
148
|
+
.join(' OR ');
|
|
149
|
+
parts.push(`(${hashtagQuery})`);
|
|
150
|
+
}
|
|
184
151
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
152
|
+
// User filters
|
|
153
|
+
if (params.fromUser) {
|
|
154
|
+
parts.push(`from:${params.fromUser}`);
|
|
155
|
+
}
|
|
156
|
+
if (params.toUser) {
|
|
157
|
+
parts.push(`to:${params.toUser}`);
|
|
158
|
+
}
|
|
159
|
+
if (params.mentionUser) {
|
|
160
|
+
parts.push(`@${params.mentionUser}`);
|
|
161
|
+
}
|
|
190
162
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
163
|
+
// Language filter
|
|
164
|
+
if (params.language) {
|
|
165
|
+
parts.push(`lang:${params.language}`);
|
|
166
|
+
}
|
|
196
167
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
168
|
+
// Media and link filters
|
|
169
|
+
if (params.hasMedia === true) {
|
|
170
|
+
parts.push('has:media');
|
|
171
|
+
} else if (params.hasMedia === false) {
|
|
172
|
+
parts.push('-has:media');
|
|
173
|
+
}
|
|
203
174
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
parts.push(`min_retweets:${params.minRetweets}`);
|
|
210
|
-
}
|
|
211
|
-
if (params.minReplies && params.minReplies > 0) {
|
|
212
|
-
parts.push(`min_replies:${params.minReplies}`);
|
|
213
|
-
}
|
|
175
|
+
if (params.hasLinks === true) {
|
|
176
|
+
parts.push('has:links');
|
|
177
|
+
} else if (params.hasLinks === false) {
|
|
178
|
+
parts.push('-has:links');
|
|
179
|
+
}
|
|
214
180
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
181
|
+
// Tweet type filters
|
|
182
|
+
if (params.isRetweet === true) {
|
|
183
|
+
parts.push('is:retweet');
|
|
184
|
+
} else if (params.isRetweet === false) {
|
|
185
|
+
parts.push('-is:retweet');
|
|
186
|
+
}
|
|
219
187
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
}
|
|
188
|
+
if (params.isReply === true) {
|
|
189
|
+
parts.push('is:reply');
|
|
190
|
+
} else if (params.isReply === false) {
|
|
191
|
+
parts.push('-is:reply');
|
|
192
|
+
}
|
|
248
193
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
194
|
+
if (params.isQuote === true) {
|
|
195
|
+
parts.push('is:quote');
|
|
196
|
+
} else if (params.isQuote === false) {
|
|
197
|
+
parts.push('-is:quote');
|
|
198
|
+
}
|
|
253
199
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
})
|
|
200
|
+
// Verified filter
|
|
201
|
+
if (params.isVerified === true) {
|
|
202
|
+
parts.push('is:verified');
|
|
203
|
+
} else if (params.isVerified === false) {
|
|
204
|
+
parts.push('-is:verified');
|
|
205
|
+
}
|
|
258
206
|
|
|
259
|
-
|
|
260
|
-
|
|
207
|
+
// Engagement filters (Note: These require Academic Research access)
|
|
208
|
+
if (params.minLikes && params.minLikes > 0) {
|
|
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}`);
|
|
261
216
|
}
|
|
262
217
|
|
|
263
|
-
//
|
|
264
|
-
if (
|
|
265
|
-
|
|
218
|
+
// Place filter
|
|
219
|
+
if (params.place) {
|
|
220
|
+
parts.push(`place:"${params.place}"`);
|
|
266
221
|
}
|
|
267
222
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
223
|
+
return parts.join(' ');
|
|
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;
|
|
272
246
|
}
|
|
273
|
-
|
|
274
|
-
|
|
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;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
247
|
+
if (searchParams.end_time) {
|
|
248
|
+
queryParams['end_time'] = searchParams.end_time;
|
|
291
249
|
}
|
|
292
|
-
throw error;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
250
|
|
|
296
|
-
|
|
297
|
-
|
|
251
|
+
while (tweets.length < maxResults) {
|
|
252
|
+
if (nextToken) {
|
|
253
|
+
queryParams['next_token'] = nextToken;
|
|
254
|
+
}
|
|
298
255
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
+
}
|
|
307
298
|
|
|
308
|
-
|
|
309
|
-
searchParams.start_time = this.formatTwitterDate(params.startDate);
|
|
299
|
+
return tweets;
|
|
310
300
|
}
|
|
311
|
-
|
|
312
|
-
|
|
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);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const tweets = await this.searchTweetsInternal(searchParams, params.limit || 100);
|
|
319
|
+
return tweets.map(tweet => this.normalizePost(tweet));
|
|
313
320
|
}
|
|
314
321
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const tag = hashtag.tag.toLowerCase();
|
|
380
|
-
hashtagCounts.set(tag, (hashtagCounts.get(tag) || 0) + 1);
|
|
381
|
-
});
|
|
382
|
-
}
|
|
322
|
+
/**
|
|
323
|
+
* Analyze search results
|
|
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
|
+
}
|
|
383
386
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
387
|
+
// Mentions
|
|
388
|
+
if (tweet.entities?.mentions) {
|
|
389
|
+
tweet.entities.mentions.forEach(mention => {
|
|
390
|
+
const username = mention.username.toLowerCase();
|
|
391
|
+
mentionCounts.set(username, (mentionCounts.get(username) || 0) + 1);
|
|
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
|
+
}
|
|
389
402
|
});
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
403
|
+
|
|
404
|
+
// Calculate average engagement
|
|
405
|
+
if (tweets.length > 0) {
|
|
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;
|
|
439
|
+
}
|
|
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';
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Define the parameters this action expects
|
|
456
|
+
*/
|
|
457
|
+
public get Params(): ActionParam[] {
|
|
458
|
+
return [
|
|
459
|
+
...this.commonSocialParams,
|
|
460
|
+
{
|
|
461
|
+
Name: 'Query',
|
|
462
|
+
Type: 'Input',
|
|
463
|
+
Value: null
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
Name: 'Hashtags',
|
|
467
|
+
Type: 'Input',
|
|
468
|
+
Value: null
|
|
469
|
+
},
|
|
470
|
+
{
|
|
471
|
+
Name: 'FromUser',
|
|
472
|
+
Type: 'Input',
|
|
473
|
+
Value: null
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
Name: 'ToUser',
|
|
477
|
+
Type: 'Input',
|
|
478
|
+
Value: null
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
Name: 'MentionUser',
|
|
482
|
+
Type: 'Input',
|
|
483
|
+
Value: null
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
Name: 'StartDate',
|
|
487
|
+
Type: 'Input',
|
|
488
|
+
Value: null
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
Name: 'EndDate',
|
|
492
|
+
Type: 'Input',
|
|
493
|
+
Value: null
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
Name: 'Language',
|
|
497
|
+
Type: 'Input',
|
|
498
|
+
Value: null
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
Name: 'HasMedia',
|
|
502
|
+
Type: 'Input',
|
|
503
|
+
Value: null
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
Name: 'HasLinks',
|
|
507
|
+
Type: 'Input',
|
|
508
|
+
Value: null
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
Name: 'IsRetweet',
|
|
512
|
+
Type: 'Input',
|
|
513
|
+
Value: null
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
Name: 'IsReply',
|
|
517
|
+
Type: 'Input',
|
|
518
|
+
Value: null
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
Name: 'IsQuote',
|
|
522
|
+
Type: 'Input',
|
|
523
|
+
Value: null
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
Name: 'IsVerified',
|
|
527
|
+
Type: 'Input',
|
|
528
|
+
Value: null
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
Name: 'MinLikes',
|
|
532
|
+
Type: 'Input',
|
|
533
|
+
Value: null
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
Name: 'MinRetweets',
|
|
537
|
+
Type: 'Input',
|
|
538
|
+
Value: null
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
Name: 'MinReplies',
|
|
542
|
+
Type: 'Input',
|
|
543
|
+
Value: null
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
Name: 'Place',
|
|
547
|
+
Type: 'Input',
|
|
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
|
+
];
|
|
409
581
|
}
|
|
410
582
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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';
|
|
583
|
+
/**
|
|
584
|
+
* Get action description
|
|
585
|
+
*/
|
|
586
|
+
public get Description(): string {
|
|
587
|
+
return 'Searches for tweets on Twitter/X using advanced operators and filters, with comprehensive analysis of results including historical data';
|
|
447
588
|
}
|
|
448
|
-
|
|
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
|
-
}
|
|
589
|
+
}
|