@memberjunction/actions-bizapps-social 2.111.1 → 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.
Files changed (204) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +6 -6
  3. package/dist/base/base-social.action.d.ts.map +1 -1
  4. package/dist/base/base-social.action.js +18 -24
  5. package/dist/base/base-social.action.js.map +1 -1
  6. package/dist/providers/buffer/buffer-base.action.d.ts.map +1 -1
  7. package/dist/providers/buffer/buffer-base.action.js +35 -34
  8. package/dist/providers/buffer/buffer-base.action.js.map +1 -1
  9. package/dist/providers/facebook/actions/boost-post.action.d.ts.map +1 -1
  10. package/dist/providers/facebook/actions/boost-post.action.js +33 -33
  11. package/dist/providers/facebook/actions/boost-post.action.js.map +1 -1
  12. package/dist/providers/facebook/actions/create-album.action.d.ts.map +1 -1
  13. package/dist/providers/facebook/actions/create-album.action.js +34 -36
  14. package/dist/providers/facebook/actions/create-album.action.js.map +1 -1
  15. package/dist/providers/facebook/actions/create-post.action.d.ts.map +1 -1
  16. package/dist/providers/facebook/actions/create-post.action.js +20 -20
  17. package/dist/providers/facebook/actions/create-post.action.js.map +1 -1
  18. package/dist/providers/facebook/actions/get-page-insights.action.d.ts.map +1 -1
  19. package/dist/providers/facebook/actions/get-page-insights.action.js +25 -27
  20. package/dist/providers/facebook/actions/get-page-insights.action.js.map +1 -1
  21. package/dist/providers/facebook/actions/get-page-posts.action.d.ts.map +1 -1
  22. package/dist/providers/facebook/actions/get-page-posts.action.js +19 -23
  23. package/dist/providers/facebook/actions/get-page-posts.action.js.map +1 -1
  24. package/dist/providers/facebook/actions/get-post-insights.action.d.ts.map +1 -1
  25. package/dist/providers/facebook/actions/get-post-insights.action.js +28 -32
  26. package/dist/providers/facebook/actions/get-post-insights.action.js.map +1 -1
  27. package/dist/providers/facebook/actions/respond-to-comments.action.d.ts.map +1 -1
  28. package/dist/providers/facebook/actions/respond-to-comments.action.js +42 -44
  29. package/dist/providers/facebook/actions/respond-to-comments.action.js.map +1 -1
  30. package/dist/providers/facebook/actions/schedule-post.action.d.ts.map +1 -1
  31. package/dist/providers/facebook/actions/schedule-post.action.js +29 -29
  32. package/dist/providers/facebook/actions/schedule-post.action.js.map +1 -1
  33. package/dist/providers/facebook/actions/search-posts.action.d.ts.map +1 -1
  34. package/dist/providers/facebook/actions/search-posts.action.js +37 -39
  35. package/dist/providers/facebook/actions/search-posts.action.js.map +1 -1
  36. package/dist/providers/facebook/facebook-base.action.d.ts.map +1 -1
  37. package/dist/providers/facebook/facebook-base.action.js +44 -59
  38. package/dist/providers/facebook/facebook-base.action.js.map +1 -1
  39. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.d.ts.map +1 -1
  40. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js +33 -31
  41. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js.map +1 -1
  42. package/dist/providers/hootsuite/actions/create-scheduled-post.action.d.ts.map +1 -1
  43. package/dist/providers/hootsuite/actions/create-scheduled-post.action.js +28 -32
  44. package/dist/providers/hootsuite/actions/create-scheduled-post.action.js.map +1 -1
  45. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.d.ts.map +1 -1
  46. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js +19 -19
  47. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js.map +1 -1
  48. package/dist/providers/hootsuite/actions/get-analytics.action.d.ts.map +1 -1
  49. package/dist/providers/hootsuite/actions/get-analytics.action.js +24 -26
  50. package/dist/providers/hootsuite/actions/get-analytics.action.js.map +1 -1
  51. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.d.ts.map +1 -1
  52. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js +22 -22
  53. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js.map +1 -1
  54. package/dist/providers/hootsuite/actions/get-social-profiles.action.d.ts.map +1 -1
  55. package/dist/providers/hootsuite/actions/get-social-profiles.action.js +32 -34
  56. package/dist/providers/hootsuite/actions/get-social-profiles.action.js.map +1 -1
  57. package/dist/providers/hootsuite/actions/search-posts.action.d.ts.map +1 -1
  58. package/dist/providers/hootsuite/actions/search-posts.action.js +43 -52
  59. package/dist/providers/hootsuite/actions/search-posts.action.js.map +1 -1
  60. package/dist/providers/hootsuite/actions/update-scheduled-post.action.d.ts.map +1 -1
  61. package/dist/providers/hootsuite/actions/update-scheduled-post.action.js +30 -28
  62. package/dist/providers/hootsuite/actions/update-scheduled-post.action.js.map +1 -1
  63. package/dist/providers/hootsuite/hootsuite-base.action.d.ts.map +1 -1
  64. package/dist/providers/hootsuite/hootsuite-base.action.js +18 -20
  65. package/dist/providers/hootsuite/hootsuite-base.action.js.map +1 -1
  66. package/dist/providers/instagram/actions/create-post.action.d.ts.map +1 -1
  67. package/dist/providers/instagram/actions/create-post.action.js +27 -26
  68. package/dist/providers/instagram/actions/create-post.action.js.map +1 -1
  69. package/dist/providers/instagram/actions/create-story.action.d.ts.map +1 -1
  70. package/dist/providers/instagram/actions/create-story.action.js +35 -35
  71. package/dist/providers/instagram/actions/create-story.action.js.map +1 -1
  72. package/dist/providers/instagram/actions/get-account-insights.action.d.ts.map +1 -1
  73. package/dist/providers/instagram/actions/get-account-insights.action.js +38 -59
  74. package/dist/providers/instagram/actions/get-account-insights.action.js.map +1 -1
  75. package/dist/providers/instagram/actions/get-business-posts.action.d.ts.map +1 -1
  76. package/dist/providers/instagram/actions/get-business-posts.action.js +29 -29
  77. package/dist/providers/instagram/actions/get-business-posts.action.js.map +1 -1
  78. package/dist/providers/instagram/actions/get-comments.action.d.ts.map +1 -1
  79. package/dist/providers/instagram/actions/get-comments.action.js +36 -36
  80. package/dist/providers/instagram/actions/get-comments.action.js.map +1 -1
  81. package/dist/providers/instagram/actions/get-post-insights.action.d.ts.map +1 -1
  82. package/dist/providers/instagram/actions/get-post-insights.action.js +23 -25
  83. package/dist/providers/instagram/actions/get-post-insights.action.js.map +1 -1
  84. package/dist/providers/instagram/actions/schedule-post.action.d.ts.map +1 -1
  85. package/dist/providers/instagram/actions/schedule-post.action.js +25 -25
  86. package/dist/providers/instagram/actions/schedule-post.action.js.map +1 -1
  87. package/dist/providers/instagram/actions/search-posts.action.d.ts.map +1 -1
  88. package/dist/providers/instagram/actions/search-posts.action.js +56 -60
  89. package/dist/providers/instagram/actions/search-posts.action.js.map +1 -1
  90. package/dist/providers/instagram/instagram-base.action.d.ts.map +1 -1
  91. package/dist/providers/instagram/instagram-base.action.js +25 -27
  92. package/dist/providers/instagram/instagram-base.action.js.map +1 -1
  93. package/dist/providers/linkedin/actions/create-article.action.d.ts.map +1 -1
  94. package/dist/providers/linkedin/actions/create-article.action.js +55 -45
  95. package/dist/providers/linkedin/actions/create-article.action.js.map +1 -1
  96. package/dist/providers/linkedin/actions/create-post.action.d.ts.map +1 -1
  97. package/dist/providers/linkedin/actions/create-post.action.js +31 -29
  98. package/dist/providers/linkedin/actions/create-post.action.js.map +1 -1
  99. package/dist/providers/linkedin/actions/get-followers.action.d.ts.map +1 -1
  100. package/dist/providers/linkedin/actions/get-followers.action.js +28 -28
  101. package/dist/providers/linkedin/actions/get-followers.action.js.map +1 -1
  102. package/dist/providers/linkedin/actions/get-organization-posts.action.d.ts.map +1 -1
  103. package/dist/providers/linkedin/actions/get-organization-posts.action.js +20 -20
  104. package/dist/providers/linkedin/actions/get-organization-posts.action.js.map +1 -1
  105. package/dist/providers/linkedin/actions/get-personal-posts.action.d.ts.map +1 -1
  106. package/dist/providers/linkedin/actions/get-personal-posts.action.js +19 -19
  107. package/dist/providers/linkedin/actions/get-personal-posts.action.js.map +1 -1
  108. package/dist/providers/linkedin/actions/get-post-analytics.action.d.ts.map +1 -1
  109. package/dist/providers/linkedin/actions/get-post-analytics.action.js +25 -23
  110. package/dist/providers/linkedin/actions/get-post-analytics.action.js.map +1 -1
  111. package/dist/providers/linkedin/actions/schedule-post.action.d.ts.map +1 -1
  112. package/dist/providers/linkedin/actions/schedule-post.action.js +32 -30
  113. package/dist/providers/linkedin/actions/schedule-post.action.js.map +1 -1
  114. package/dist/providers/linkedin/actions/search-posts.action.d.ts.map +1 -1
  115. package/dist/providers/linkedin/actions/search-posts.action.js +28 -30
  116. package/dist/providers/linkedin/actions/search-posts.action.js.map +1 -1
  117. package/dist/providers/linkedin/linkedin-base.action.d.ts.map +1 -1
  118. package/dist/providers/linkedin/linkedin-base.action.js +33 -38
  119. package/dist/providers/linkedin/linkedin-base.action.js.map +1 -1
  120. package/dist/providers/tiktok/tiktok-base.action.d.ts.map +1 -1
  121. package/dist/providers/tiktok/tiktok-base.action.js +25 -26
  122. package/dist/providers/tiktok/tiktok-base.action.js.map +1 -1
  123. package/dist/providers/twitter/actions/create-thread.action.d.ts.map +1 -1
  124. package/dist/providers/twitter/actions/create-thread.action.js +25 -29
  125. package/dist/providers/twitter/actions/create-thread.action.js.map +1 -1
  126. package/dist/providers/twitter/actions/create-tweet.action.d.ts.map +1 -1
  127. package/dist/providers/twitter/actions/create-tweet.action.js +23 -23
  128. package/dist/providers/twitter/actions/create-tweet.action.js.map +1 -1
  129. package/dist/providers/twitter/actions/delete-tweet.action.d.ts.map +1 -1
  130. package/dist/providers/twitter/actions/delete-tweet.action.js +19 -19
  131. package/dist/providers/twitter/actions/delete-tweet.action.js.map +1 -1
  132. package/dist/providers/twitter/actions/get-analytics.action.d.ts.map +1 -1
  133. package/dist/providers/twitter/actions/get-analytics.action.js +40 -47
  134. package/dist/providers/twitter/actions/get-analytics.action.js.map +1 -1
  135. package/dist/providers/twitter/actions/get-mentions.action.d.ts.map +1 -1
  136. package/dist/providers/twitter/actions/get-mentions.action.js +30 -31
  137. package/dist/providers/twitter/actions/get-mentions.action.js.map +1 -1
  138. package/dist/providers/twitter/actions/get-timeline.action.d.ts.map +1 -1
  139. package/dist/providers/twitter/actions/get-timeline.action.js +29 -29
  140. package/dist/providers/twitter/actions/get-timeline.action.js.map +1 -1
  141. package/dist/providers/twitter/actions/schedule-tweet.action.d.ts.map +1 -1
  142. package/dist/providers/twitter/actions/schedule-tweet.action.js +26 -26
  143. package/dist/providers/twitter/actions/schedule-tweet.action.js.map +1 -1
  144. package/dist/providers/twitter/actions/search-tweets.action.d.ts.map +1 -1
  145. package/dist/providers/twitter/actions/search-tweets.action.js +56 -58
  146. package/dist/providers/twitter/actions/search-tweets.action.js.map +1 -1
  147. package/dist/providers/twitter/twitter-base.action.d.ts.map +1 -1
  148. package/dist/providers/twitter/twitter-base.action.js +58 -68
  149. package/dist/providers/twitter/twitter-base.action.js.map +1 -1
  150. package/dist/providers/youtube/youtube-base.action.d.ts +1 -1
  151. package/dist/providers/youtube/youtube-base.action.d.ts.map +1 -1
  152. package/dist/providers/youtube/youtube-base.action.js +22 -25
  153. package/dist/providers/youtube/youtube-base.action.js.map +1 -1
  154. package/package.json +5 -6
  155. package/src/base/base-social.action.ts +217 -224
  156. package/src/providers/buffer/buffer-base.action.ts +435 -441
  157. package/src/providers/facebook/actions/boost-post.action.ts +350 -386
  158. package/src/providers/facebook/actions/create-album.action.ts +291 -307
  159. package/src/providers/facebook/actions/create-post.action.ts +224 -227
  160. package/src/providers/facebook/actions/get-page-insights.action.ts +383 -403
  161. package/src/providers/facebook/actions/get-page-posts.action.ts +214 -225
  162. package/src/providers/facebook/actions/get-post-insights.action.ts +300 -316
  163. package/src/providers/facebook/actions/respond-to-comments.action.ts +319 -336
  164. package/src/providers/facebook/actions/schedule-post.action.ts +289 -292
  165. package/src/providers/facebook/actions/search-posts.action.ts +399 -413
  166. package/src/providers/facebook/facebook-base.action.ts +653 -670
  167. package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
  168. package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +184 -189
  169. package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +160 -161
  170. package/src/providers/hootsuite/actions/get-analytics.action.ts +249 -254
  171. package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +206 -207
  172. package/src/providers/hootsuite/actions/get-social-profiles.action.ts +206 -205
  173. package/src/providers/hootsuite/actions/search-posts.action.ts +351 -369
  174. package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +211 -209
  175. package/src/providers/hootsuite/hootsuite-base.action.ts +301 -307
  176. package/src/providers/instagram/actions/create-post.action.ts +276 -296
  177. package/src/providers/instagram/actions/create-story.action.ts +378 -394
  178. package/src/providers/instagram/actions/get-account-insights.action.ts +384 -420
  179. package/src/providers/instagram/actions/get-business-posts.action.ts +233 -242
  180. package/src/providers/instagram/actions/get-comments.action.ts +365 -377
  181. package/src/providers/instagram/actions/get-post-insights.action.ts +265 -273
  182. package/src/providers/instagram/actions/schedule-post.action.ts +233 -235
  183. package/src/providers/instagram/actions/search-posts.action.ts +512 -538
  184. package/src/providers/instagram/instagram-base.action.ts +368 -393
  185. package/src/providers/linkedin/actions/create-article.action.ts +275 -266
  186. package/src/providers/linkedin/actions/create-post.action.ts +179 -177
  187. package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
  188. package/src/providers/linkedin/actions/get-organization-posts.action.ts +146 -147
  189. package/src/providers/linkedin/actions/get-personal-posts.action.ts +138 -139
  190. package/src/providers/linkedin/actions/get-post-analytics.action.ts +190 -189
  191. package/src/providers/linkedin/actions/schedule-post.action.ts +191 -189
  192. package/src/providers/linkedin/actions/search-posts.action.ts +275 -283
  193. package/src/providers/linkedin/linkedin-base.action.ts +407 -421
  194. package/src/providers/tiktok/tiktok-base.action.ts +305 -320
  195. package/src/providers/twitter/actions/create-thread.action.ts +203 -207
  196. package/src/providers/twitter/actions/create-tweet.action.ts +187 -188
  197. package/src/providers/twitter/actions/delete-tweet.action.ts +128 -129
  198. package/src/providers/twitter/actions/get-analytics.action.ts +402 -411
  199. package/src/providers/twitter/actions/get-mentions.action.ts +218 -219
  200. package/src/providers/twitter/actions/get-timeline.action.ts +232 -233
  201. package/src/providers/twitter/actions/schedule-tweet.action.ts +221 -222
  202. package/src/providers/twitter/actions/search-tweets.action.ts +540 -543
  203. package/src/providers/twitter/twitter-base.action.ts +541 -560
  204. 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/core';
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
- * 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
- }
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
- // 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
- }
132
+ /**
133
+ * Build advanced search query with Twitter operators
134
+ */
135
+ private buildAdvancedSearchQuery(params: any): string {
136
+ const parts: string[] = [];
72
137
 
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
- }
138
+ // Basic query
139
+ if (params.query) {
140
+ parts.push(params.query);
141
+ }
77
142
 
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
- };
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
- // 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
- }
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
- // 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
- }
160
+ // Language filter
161
+ if (params.language) {
162
+ parts.push(`lang:${params.language}`);
131
163
  }
132
164
 
133
- /**
134
- * Build advanced search query with Twitter operators
135
- */
136
- private buildAdvancedSearchQuery(params: any): string {
137
- const parts: string[] = [];
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
- // Basic query
140
- if (params.query) {
141
- parts.push(params.query);
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
- // 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
- }
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
- // 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
- }
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
- // Language filter
164
- if (params.language) {
165
- parts.push(`lang:${params.language}`);
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
- // 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
- }
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
- if (params.hasLinks === true) {
176
- parts.push('has:links');
177
- } else if (params.hasLinks === false) {
178
- parts.push('-has:links');
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
- // 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
- }
215
+ // Place filter
216
+ if (params.place) {
217
+ parts.push(`place:"${params.place}"`);
218
+ }
187
219
 
188
- if (params.isReply === true) {
189
- parts.push('is:reply');
190
- } else if (params.isReply === false) {
191
- parts.push('-is:reply');
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
- if (params.isQuote === true) {
195
- parts.push('is:quote');
196
- } else if (params.isQuote === false) {
197
- parts.push('-is:quote');
198
- }
249
+ while (tweets.length < maxResults) {
250
+ if (nextToken) {
251
+ queryParams['next_token'] = nextToken;
252
+ }
199
253
 
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
- }
254
+ try {
255
+ const response = await this.axiosInstance.get('/tweets/search/recent', {
256
+ params: queryParams,
257
+ });
206
258
 
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}`);
259
+ if (response.data.data && Array.isArray(response.data.data)) {
260
+ tweets.push(...response.data.data);
216
261
  }
217
262
 
218
- // Place filter
219
- if (params.place) {
220
- parts.push(`place:"${params.place}"`);
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
- 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;
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
- while (tweets.length < maxResults) {
252
- if (nextToken) {
253
- queryParams['next_token'] = nextToken;
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
- const tweets = await this.searchTweetsInternal(searchParams, params.limit || 100);
319
- return tweets.map(tweet => this.normalizePost(tweet));
292
+ throw error;
293
+ }
320
294
  }
321
295
 
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
- }
296
+ return tweets;
297
+ }
386
298
 
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
- }
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
- // 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;
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
- * 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
- ];
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
- * 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';
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
+ }