@memberjunction/actions-bizapps-social 2.111.0 → 2.112.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 { InstagramBaseAction } from '../instagram-base.action';
3
3
  import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
4
- import { LogError } from '@memberjunction/core';
4
+ import { LogError } from '@memberjunction/global';
5
5
  import { SocialPost, SearchParams } from '../../../base/base-social.action';
6
6
  import { BaseAction } from '@memberjunction/actions';
7
7
 
@@ -12,559 +12,533 @@ import { BaseAction } from '@memberjunction/actions';
12
12
  */
13
13
  @RegisterClass(BaseAction, 'Instagram - Search Posts')
14
14
  export class InstagramSearchPostsAction extends InstagramBaseAction {
15
-
16
- protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
17
- try {
18
- const companyIntegrationId = this.getParamValue(params.Params, 'CompanyIntegrationID');
19
- const query = this.getParamValue(params.Params, 'Query');
20
- const hashtags = this.getParamValue(params.Params, 'Hashtags') as string[];
21
- const startDate = this.getParamValue(params.Params, 'StartDate');
22
- const endDate = this.getParamValue(params.Params, 'EndDate');
23
- const mediaType = this.getParamValue(params.Params, 'MediaType');
24
- const minEngagement = this.getParamValue(params.Params, 'MinEngagement') || 0;
25
- const limit = this.getParamValue(params.Params, 'Limit') || 100;
26
- const includeArchived = this.getParamValue(params.Params, 'IncludeArchived') || false;
27
-
28
- // Initialize OAuth
29
- if (!await this.initializeOAuth(companyIntegrationId)) {
30
- return {
31
- Success: false,
32
- Message: 'Failed to initialize Instagram authentication',
33
- ResultCode: 'AUTH_FAILED'
34
- };
35
- }
36
-
37
- // Build search parameters
38
- const searchParams: SearchParams = {
39
- query,
40
- hashtags,
41
- startDate: startDate ? new Date(startDate) : undefined,
42
- endDate: endDate ? new Date(endDate) : undefined,
43
- limit
44
- };
45
-
46
- // Perform search
47
- const posts = await this.searchPosts(searchParams);
48
-
49
- // Filter by media type if specified
50
- let filteredPosts = posts;
51
- if (mediaType) {
52
- filteredPosts = posts.filter(post =>
53
- post.platformSpecificData.mediaType === mediaType
54
- );
55
- }
56
-
57
- // Filter by minimum engagement
58
- if (minEngagement > 0) {
59
- filteredPosts = filteredPosts.filter(post => {
60
- const totalEngagement =
61
- (post.analytics?.likes || 0) +
62
- (post.analytics?.comments || 0);
63
- return totalEngagement >= minEngagement;
64
- });
65
- }
66
-
67
- // Include archived posts if requested
68
- if (includeArchived) {
69
- const archivedPosts = await this.getArchivedPosts(searchParams);
70
- filteredPosts = [...filteredPosts, ...archivedPosts];
71
- }
72
-
73
- // Sort posts by relevance
74
- const sortedPosts = this.sortByRelevance(filteredPosts, query, hashtags);
75
-
76
- // Analyze search results
77
- const analysis = this.analyzeSearchResults(sortedPosts, searchParams);
78
-
79
- // Store result in output params
80
- const outputParams = [...params.Params];
81
- outputParams.push({
82
- Name: 'ResultData',
83
- Type: 'Output',
84
- Value: JSON.stringify({
85
- posts: sortedPosts.slice(0, limit),
86
- totalFound: sortedPosts.length,
87
- searchCriteria: {
88
- query,
89
- hashtags,
90
- dateRange: {
91
- start: startDate,
92
- end: endDate
93
- },
94
- mediaType,
95
- minEngagement
96
- },
97
- analysis,
98
- suggestions: this.generateSearchSuggestions(sortedPosts, searchParams)
99
- })
100
- });
101
-
102
- return {
103
- Success: true,
104
- Message: `Found ${sortedPosts.length} matching posts`,
105
- ResultCode: 'SUCCESS',
106
- Params: outputParams
107
- };
108
-
109
- } catch (error: any) {
110
- LogError('Failed to search Instagram posts', error);
111
-
112
- if (error.code === 'RATE_LIMIT') {
113
- return {
114
- Success: false,
115
- Message: 'Instagram API rate limit exceeded. Please try again later.',
116
- ResultCode: 'RATE_LIMIT'
117
- };
118
- }
119
-
120
- return {
121
- Success: false,
122
- Message: `Failed to search posts: ${error.message}`,
123
- ResultCode: 'ERROR'
124
- };
125
- }
126
- }
127
-
128
- /**
129
- * Search posts implementation (Instagram only allows searching own posts)
130
- */
131
- protected async searchPosts(params: SearchParams): Promise<SocialPost[]> {
132
- // Instagram doesn't have a public search API, so we fetch all posts
133
- // and filter them client-side
134
- const allPosts = await this.fetchAllAccountPosts(params.startDate, params.endDate);
135
-
136
- // Filter posts based on search criteria
137
- let filtered = allPosts;
138
-
139
- // Filter by query in caption
140
- if (params.query) {
141
- const queryLower = params.query.toLowerCase();
142
- filtered = filtered.filter(post =>
143
- post.content.toLowerCase().includes(queryLower)
144
- );
145
- }
146
-
147
- // Filter by hashtags
148
- if (params.hashtags && params.hashtags.length > 0) {
149
- const searchHashtags = params.hashtags.map(tag =>
150
- tag.startsWith('#') ? tag.toLowerCase() : `#${tag}`.toLowerCase()
151
- );
152
-
153
- filtered = filtered.filter(post => {
154
- const postHashtags = this.extractHashtags(post.content);
155
- return searchHashtags.some(searchTag =>
156
- postHashtags.includes(searchTag)
157
- );
158
- });
159
- }
160
-
161
- return filtered.slice(0, params.limit || 100);
162
- }
163
-
164
- /**
165
- * Fetch all posts from the account within date range
166
- */
167
- private async fetchAllAccountPosts(startDate?: Date, endDate?: Date): Promise<SocialPost[]> {
168
- const posts: SocialPost[] = [];
169
- let hasNext = true;
170
- let afterCursor: string | undefined;
171
-
172
- while (hasNext) {
173
- const queryParams: any = {
174
- fields: 'id,caption,media_type,media_url,permalink,timestamp,like_count,comments_count',
175
- access_token: this.getAccessToken(),
176
- limit: 100
177
- };
178
-
179
- if (afterCursor) {
180
- queryParams.after = afterCursor;
181
- }
182
-
183
- if (startDate) {
184
- queryParams.since = Math.floor(startDate.getTime() / 1000);
185
- }
186
- if (endDate) {
187
- queryParams.until = Math.floor(endDate.getTime() / 1000);
188
- }
189
-
190
- const response = await this.makeInstagramRequest<{
191
- data: any[];
192
- paging?: {
193
- cursors: { after: string };
194
- next?: string;
195
- };
196
- }>(
197
- `${this.instagramBusinessAccountId}/media`,
198
- 'GET',
199
- null,
200
- queryParams
201
- );
202
-
203
- if (response.data) {
204
- const normalizedPosts = response.data.map(post => this.normalizePost(post));
205
- posts.push(...normalizedPosts);
206
- }
207
-
208
- if (response.paging?.next && response.paging?.cursors?.after) {
209
- afterCursor = response.paging.cursors.after;
210
- } else {
211
- hasNext = false;
212
- }
213
-
214
- // Stop if we've reached the date range limit
215
- if (posts.length > 0 && startDate) {
216
- const oldestPost = posts[posts.length - 1];
217
- if (oldestPost.publishedAt < startDate) {
218
- hasNext = false;
219
- }
220
- }
221
- }
222
-
223
- return posts;
224
- }
225
-
226
- /**
227
- * Get archived posts (if any)
228
- */
229
- private async getArchivedPosts(params: SearchParams): Promise<SocialPost[]> {
230
- try {
231
- // Instagram API doesn't have a specific endpoint for archived posts
232
- // This would require additional implementation or different API access
233
- return [];
234
- } catch (error) {
235
- LogError('Failed to get archived posts', error);
236
- return [];
237
- }
238
- }
239
-
240
- /**
241
- * Extract hashtags from content
242
- */
243
- private extractHashtags(content: string): string[] {
244
- const hashtagRegex = /#[\w\u0590-\u05ff]+/g;
245
- const matches = content.match(hashtagRegex) || [];
246
- return matches.map(tag => tag.toLowerCase());
247
- }
248
-
249
- /**
250
- * Sort posts by relevance
251
- */
252
- private sortByRelevance(
253
- posts: SocialPost[],
254
- query?: string,
255
- hashtags?: string[]
256
- ): SocialPost[] {
257
- return posts.sort((a, b) => {
258
- let scoreA = 0;
259
- let scoreB = 0;
260
-
261
- // Score based on query match position
262
- if (query) {
263
- const queryLower = query.toLowerCase();
264
- const posA = a.content.toLowerCase().indexOf(queryLower);
265
- const posB = b.content.toLowerCase().indexOf(queryLower);
266
-
267
- if (posA === 0) scoreA += 10; // Starts with query
268
- else if (posA > 0) scoreA += 5; // Contains query
269
-
270
- if (posB === 0) scoreB += 10;
271
- else if (posB > 0) scoreB += 5;
272
- }
273
-
274
- // Score based on hashtag matches
275
- if (hashtags && hashtags.length > 0) {
276
- const hashtagsA = this.extractHashtags(a.content);
277
- const hashtagsB = this.extractHashtags(b.content);
278
-
279
- hashtags.forEach(tag => {
280
- const searchTag = tag.startsWith('#') ? tag.toLowerCase() : `#${tag}`.toLowerCase();
281
- if (hashtagsA.includes(searchTag)) scoreA += 3;
282
- if (hashtagsB.includes(searchTag)) scoreB += 3;
283
- });
284
- }
285
-
286
- // Score based on engagement
287
- const engagementA = (a.analytics?.likes || 0) + (a.analytics?.comments || 0);
288
- const engagementB = (b.analytics?.likes || 0) + (b.analytics?.comments || 0);
289
-
290
- scoreA += Math.log10(engagementA + 1);
291
- scoreB += Math.log10(engagementB + 1);
292
-
293
- // Sort by score (descending) then by date (descending)
294
- if (scoreA !== scoreB) {
295
- return scoreB - scoreA;
296
- }
297
- return b.publishedAt.getTime() - a.publishedAt.getTime();
15
+ protected async InternalRunAction(params: RunActionParams): Promise<ActionResultSimple> {
16
+ try {
17
+ const companyIntegrationId = this.getParamValue(params.Params, 'CompanyIntegrationID');
18
+ const query = this.getParamValue(params.Params, 'Query');
19
+ const hashtags = this.getParamValue(params.Params, 'Hashtags') as string[];
20
+ const startDate = this.getParamValue(params.Params, 'StartDate');
21
+ const endDate = this.getParamValue(params.Params, 'EndDate');
22
+ const mediaType = this.getParamValue(params.Params, 'MediaType');
23
+ const minEngagement = this.getParamValue(params.Params, 'MinEngagement') || 0;
24
+ const limit = this.getParamValue(params.Params, 'Limit') || 100;
25
+ const includeArchived = this.getParamValue(params.Params, 'IncludeArchived') || false;
26
+
27
+ // Initialize OAuth
28
+ if (!(await this.initializeOAuth(companyIntegrationId))) {
29
+ return {
30
+ Success: false,
31
+ Message: 'Failed to initialize Instagram authentication',
32
+ ResultCode: 'AUTH_FAILED',
33
+ };
34
+ }
35
+
36
+ // Build search parameters
37
+ const searchParams: SearchParams = {
38
+ query,
39
+ hashtags,
40
+ startDate: startDate ? new Date(startDate) : undefined,
41
+ endDate: endDate ? new Date(endDate) : undefined,
42
+ limit,
43
+ };
44
+
45
+ // Perform search
46
+ const posts = await this.searchPosts(searchParams);
47
+
48
+ // Filter by media type if specified
49
+ let filteredPosts = posts;
50
+ if (mediaType) {
51
+ filteredPosts = posts.filter((post) => post.platformSpecificData.mediaType === mediaType);
52
+ }
53
+
54
+ // Filter by minimum engagement
55
+ if (minEngagement > 0) {
56
+ filteredPosts = filteredPosts.filter((post) => {
57
+ const totalEngagement = (post.analytics?.likes || 0) + (post.analytics?.comments || 0);
58
+ return totalEngagement >= minEngagement;
298
59
  });
299
- }
300
-
301
- /**
302
- * Analyze search results
303
- */
304
- private analyzeSearchResults(posts: SocialPost[], params: SearchParams): any {
305
- const analysis = {
306
- totalPosts: posts.length,
60
+ }
61
+
62
+ // Include archived posts if requested
63
+ if (includeArchived) {
64
+ const archivedPosts = await this.getArchivedPosts(searchParams);
65
+ filteredPosts = [...filteredPosts, ...archivedPosts];
66
+ }
67
+
68
+ // Sort posts by relevance
69
+ const sortedPosts = this.sortByRelevance(filteredPosts, query, hashtags);
70
+
71
+ // Analyze search results
72
+ const analysis = this.analyzeSearchResults(sortedPosts, searchParams);
73
+
74
+ // Store result in output params
75
+ const outputParams = [...params.Params];
76
+ outputParams.push({
77
+ Name: 'ResultData',
78
+ Type: 'Output',
79
+ Value: JSON.stringify({
80
+ posts: sortedPosts.slice(0, limit),
81
+ totalFound: sortedPosts.length,
82
+ searchCriteria: {
83
+ query,
84
+ hashtags,
307
85
  dateRange: {
308
- earliest: null as Date | null,
309
- latest: null as Date | null
310
- },
311
- mediaTypes: {
312
- IMAGE: 0,
313
- VIDEO: 0,
314
- CAROUSEL_ALBUM: 0,
315
- REELS: 0
316
- },
317
- engagement: {
318
- totalLikes: 0,
319
- totalComments: 0,
320
- avgLikesPerPost: 0,
321
- avgCommentsPerPost: 0,
322
- topPost: null as any
86
+ start: startDate,
87
+ end: endDate,
323
88
  },
324
- hashtagFrequency: {} as Record<string, number>,
325
- postingPatterns: {
326
- byDayOfWeek: {} as Record<string, number>,
327
- byHour: {} as Record<number, number>
328
- }
89
+ mediaType,
90
+ minEngagement,
91
+ },
92
+ analysis,
93
+ suggestions: this.generateSearchSuggestions(sortedPosts, searchParams),
94
+ }),
95
+ });
96
+
97
+ return {
98
+ Success: true,
99
+ Message: `Found ${sortedPosts.length} matching posts`,
100
+ ResultCode: 'SUCCESS',
101
+ Params: outputParams,
102
+ };
103
+ } catch (error: any) {
104
+ LogError('Failed to search Instagram posts', error);
105
+
106
+ if (error.code === 'RATE_LIMIT') {
107
+ return {
108
+ Success: false,
109
+ Message: 'Instagram API rate limit exceeded. Please try again later.',
110
+ ResultCode: 'RATE_LIMIT',
329
111
  };
112
+ }
330
113
 
331
- if (posts.length === 0) return analysis;
332
-
333
- // Find date range
334
- const dates = posts.map(p => p.publishedAt.getTime());
335
- analysis.dateRange.earliest = new Date(Math.min(...dates));
336
- analysis.dateRange.latest = new Date(Math.max(...dates));
337
-
338
- // Analyze posts
339
- let topEngagement = 0;
340
- posts.forEach(post => {
341
- // Media types
342
- const mediaType = post.platformSpecificData.mediaType;
343
- if (analysis.mediaTypes[mediaType] !== undefined) {
344
- analysis.mediaTypes[mediaType]++;
345
- }
346
-
347
- // Engagement
348
- const likes = post.analytics?.likes || 0;
349
- const comments = post.analytics?.comments || 0;
350
- const totalEngagement = likes + comments;
351
-
352
- analysis.engagement.totalLikes += likes;
353
- analysis.engagement.totalComments += comments;
354
-
355
- if (totalEngagement > topEngagement) {
356
- topEngagement = totalEngagement;
357
- analysis.engagement.topPost = {
358
- id: post.id,
359
- content: post.content.substring(0, 100) + '...',
360
- engagement: totalEngagement,
361
- publishedAt: post.publishedAt
362
- };
363
- }
364
-
365
- // Hashtags
366
- const hashtags = this.extractHashtags(post.content);
367
- hashtags.forEach(tag => {
368
- analysis.hashtagFrequency[tag] = (analysis.hashtagFrequency[tag] || 0) + 1;
369
- });
370
-
371
- // Posting patterns
372
- const dayOfWeek = post.publishedAt.toLocaleDateString('en-US', { weekday: 'long' });
373
- const hour = post.publishedAt.getHours();
374
-
375
- analysis.postingPatterns.byDayOfWeek[dayOfWeek] =
376
- (analysis.postingPatterns.byDayOfWeek[dayOfWeek] || 0) + 1;
377
- analysis.postingPatterns.byHour[hour] =
378
- (analysis.postingPatterns.byHour[hour] || 0) + 1;
379
- });
114
+ return {
115
+ Success: false,
116
+ Message: `Failed to search posts: ${error.message}`,
117
+ ResultCode: 'ERROR',
118
+ };
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Search posts implementation (Instagram only allows searching own posts)
124
+ */
125
+ protected async searchPosts(params: SearchParams): Promise<SocialPost[]> {
126
+ // Instagram doesn't have a public search API, so we fetch all posts
127
+ // and filter them client-side
128
+ const allPosts = await this.fetchAllAccountPosts(params.startDate, params.endDate);
129
+
130
+ // Filter posts based on search criteria
131
+ let filtered = allPosts;
132
+
133
+ // Filter by query in caption
134
+ if (params.query) {
135
+ const queryLower = params.query.toLowerCase();
136
+ filtered = filtered.filter((post) => post.content.toLowerCase().includes(queryLower));
137
+ }
380
138
 
381
- // Calculate averages
382
- analysis.engagement.avgLikesPerPost =
383
- Math.round(analysis.engagement.totalLikes / posts.length);
384
- analysis.engagement.avgCommentsPerPost =
385
- Math.round(analysis.engagement.totalComments / posts.length);
139
+ // Filter by hashtags
140
+ if (params.hashtags && params.hashtags.length > 0) {
141
+ const searchHashtags = params.hashtags.map((tag) => (tag.startsWith('#') ? tag.toLowerCase() : `#${tag}`.toLowerCase()));
386
142
 
387
- return analysis;
143
+ filtered = filtered.filter((post) => {
144
+ const postHashtags = this.extractHashtags(post.content);
145
+ return searchHashtags.some((searchTag) => postHashtags.includes(searchTag));
146
+ });
388
147
  }
389
148
 
390
- /**
391
- * Generate search suggestions based on results
392
- */
393
- private generateSearchSuggestions(posts: SocialPost[], params: SearchParams): any {
394
- const suggestions = {
395
- relatedHashtags: [] as string[],
396
- optimalPostingTimes: [] as any[],
397
- contentThemes: [] as string[],
398
- performanceInsights: [] as string[]
149
+ return filtered.slice(0, params.limit || 100);
150
+ }
151
+
152
+ /**
153
+ * Fetch all posts from the account within date range
154
+ */
155
+ private async fetchAllAccountPosts(startDate?: Date, endDate?: Date): Promise<SocialPost[]> {
156
+ const posts: SocialPost[] = [];
157
+ let hasNext = true;
158
+ let afterCursor: string | undefined;
159
+
160
+ while (hasNext) {
161
+ const queryParams: any = {
162
+ fields: 'id,caption,media_type,media_url,permalink,timestamp,like_count,comments_count',
163
+ access_token: this.getAccessToken(),
164
+ limit: 100,
165
+ };
166
+
167
+ if (afterCursor) {
168
+ queryParams.after = afterCursor;
169
+ }
170
+
171
+ if (startDate) {
172
+ queryParams.since = Math.floor(startDate.getTime() / 1000);
173
+ }
174
+ if (endDate) {
175
+ queryParams.until = Math.floor(endDate.getTime() / 1000);
176
+ }
177
+
178
+ const response = await this.makeInstagramRequest<{
179
+ data: any[];
180
+ paging?: {
181
+ cursors: { after: string };
182
+ next?: string;
399
183
  };
400
-
401
- if (posts.length === 0) {
402
- suggestions.performanceInsights.push('No posts found matching your criteria. Try broadening your search.');
403
- return suggestions;
184
+ }>(`${this.instagramBusinessAccountId}/media`, 'GET', null, queryParams);
185
+
186
+ if (response.data) {
187
+ const normalizedPosts = response.data.map((post) => this.normalizePost(post));
188
+ posts.push(...normalizedPosts);
189
+ }
190
+
191
+ if (response.paging?.next && response.paging?.cursors?.after) {
192
+ afterCursor = response.paging.cursors.after;
193
+ } else {
194
+ hasNext = false;
195
+ }
196
+
197
+ // Stop if we've reached the date range limit
198
+ if (posts.length > 0 && startDate) {
199
+ const oldestPost = posts[posts.length - 1];
200
+ if (oldestPost.publishedAt < startDate) {
201
+ hasNext = false;
404
202
  }
405
-
406
- // Related hashtags (most frequently used)
407
- const hashtagCounts: Record<string, number> = {};
408
- posts.forEach(post => {
409
- const hashtags = this.extractHashtags(post.content);
410
- hashtags.forEach(tag => {
411
- if (!params.hashtags?.includes(tag)) {
412
- hashtagCounts[tag] = (hashtagCounts[tag] || 0) + 1;
413
- }
414
- });
415
- });
416
-
417
- suggestions.relatedHashtags = Object.entries(hashtagCounts)
418
- .sort(([, a], [, b]) => b - a)
419
- .slice(0, 10)
420
- .map(([tag]) => tag);
421
-
422
- // Optimal posting times (based on engagement)
423
- const timeEngagement: Record<string, { total: number; count: number }> = {};
424
- posts.forEach(post => {
425
- const hour = post.publishedAt.getHours();
426
- const day = post.publishedAt.toLocaleDateString('en-US', { weekday: 'long' });
427
- const key = `${day} ${hour}:00`;
428
-
429
- if (!timeEngagement[key]) {
430
- timeEngagement[key] = { total: 0, count: 0 };
431
- }
432
-
433
- const engagement = (post.analytics?.likes || 0) + (post.analytics?.comments || 0);
434
- timeEngagement[key].total += engagement;
435
- timeEngagement[key].count++;
436
- });
437
-
438
- suggestions.optimalPostingTimes = Object.entries(timeEngagement)
439
- .map(([time, data]) => ({
440
- time,
441
- avgEngagement: Math.round(data.total / data.count)
442
- }))
443
- .sort((a, b) => b.avgEngagement - a.avgEngagement)
444
- .slice(0, 5);
445
-
446
- // Performance insights
447
- const avgEngagement = posts.reduce((sum, post) =>
448
- sum + (post.analytics?.likes || 0) + (post.analytics?.comments || 0), 0
449
- ) / posts.length;
450
-
451
- suggestions.performanceInsights.push(
452
- `Average engagement per post: ${Math.round(avgEngagement)}`,
453
- `Most successful media type: ${this.getMostSuccessfulMediaType(posts)}`,
454
- `Posts with questions get ${this.getQuestionEngagementBoost(posts)}% more engagement`
455
- );
456
-
457
- return suggestions;
203
+ }
458
204
  }
459
205
 
460
- /**
461
- * Get most successful media type
462
- */
463
- private getMostSuccessfulMediaType(posts: SocialPost[]): string {
464
- const typeEngagement: Record<string, { total: number; count: number }> = {};
465
-
466
- posts.forEach(post => {
467
- const type = post.platformSpecificData.mediaType;
468
- if (!typeEngagement[type]) {
469
- typeEngagement[type] = { total: 0, count: 0 };
470
- }
471
-
472
- const engagement = (post.analytics?.likes || 0) + (post.analytics?.comments || 0);
473
- typeEngagement[type].total += engagement;
474
- typeEngagement[type].count++;
475
- });
476
-
477
- let bestType = 'IMAGE';
478
- let bestAvg = 0;
479
-
480
- Object.entries(typeEngagement).forEach(([type, data]) => {
481
- const avg = data.total / data.count;
482
- if (avg > bestAvg) {
483
- bestAvg = avg;
484
- bestType = type;
485
- }
206
+ return posts;
207
+ }
208
+
209
+ /**
210
+ * Get archived posts (if any)
211
+ */
212
+ private async getArchivedPosts(params: SearchParams): Promise<SocialPost[]> {
213
+ try {
214
+ // Instagram API doesn't have a specific endpoint for archived posts
215
+ // This would require additional implementation or different API access
216
+ return [];
217
+ } catch (error) {
218
+ LogError('Failed to get archived posts', error);
219
+ return [];
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Extract hashtags from content
225
+ */
226
+ private extractHashtags(content: string): string[] {
227
+ const hashtagRegex = /#[\w\u0590-\u05ff]+/g;
228
+ const matches = content.match(hashtagRegex) || [];
229
+ return matches.map((tag) => tag.toLowerCase());
230
+ }
231
+
232
+ /**
233
+ * Sort posts by relevance
234
+ */
235
+ private sortByRelevance(posts: SocialPost[], query?: string, hashtags?: string[]): SocialPost[] {
236
+ return posts.sort((a, b) => {
237
+ let scoreA = 0;
238
+ let scoreB = 0;
239
+
240
+ // Score based on query match position
241
+ if (query) {
242
+ const queryLower = query.toLowerCase();
243
+ const posA = a.content.toLowerCase().indexOf(queryLower);
244
+ const posB = b.content.toLowerCase().indexOf(queryLower);
245
+
246
+ if (posA === 0)
247
+ scoreA += 10; // Starts with query
248
+ else if (posA > 0) scoreA += 5; // Contains query
249
+
250
+ if (posB === 0) scoreB += 10;
251
+ else if (posB > 0) scoreB += 5;
252
+ }
253
+
254
+ // Score based on hashtag matches
255
+ if (hashtags && hashtags.length > 0) {
256
+ const hashtagsA = this.extractHashtags(a.content);
257
+ const hashtagsB = this.extractHashtags(b.content);
258
+
259
+ hashtags.forEach((tag) => {
260
+ const searchTag = tag.startsWith('#') ? tag.toLowerCase() : `#${tag}`.toLowerCase();
261
+ if (hashtagsA.includes(searchTag)) scoreA += 3;
262
+ if (hashtagsB.includes(searchTag)) scoreB += 3;
486
263
  });
487
-
488
- return bestType;
264
+ }
265
+
266
+ // Score based on engagement
267
+ const engagementA = (a.analytics?.likes || 0) + (a.analytics?.comments || 0);
268
+ const engagementB = (b.analytics?.likes || 0) + (b.analytics?.comments || 0);
269
+
270
+ scoreA += Math.log10(engagementA + 1);
271
+ scoreB += Math.log10(engagementB + 1);
272
+
273
+ // Sort by score (descending) then by date (descending)
274
+ if (scoreA !== scoreB) {
275
+ return scoreB - scoreA;
276
+ }
277
+ return b.publishedAt.getTime() - a.publishedAt.getTime();
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Analyze search results
283
+ */
284
+ private analyzeSearchResults(posts: SocialPost[], params: SearchParams): any {
285
+ const analysis = {
286
+ totalPosts: posts.length,
287
+ dateRange: {
288
+ earliest: null as Date | null,
289
+ latest: null as Date | null,
290
+ },
291
+ mediaTypes: {
292
+ IMAGE: 0,
293
+ VIDEO: 0,
294
+ CAROUSEL_ALBUM: 0,
295
+ REELS: 0,
296
+ },
297
+ engagement: {
298
+ totalLikes: 0,
299
+ totalComments: 0,
300
+ avgLikesPerPost: 0,
301
+ avgCommentsPerPost: 0,
302
+ topPost: null as any,
303
+ },
304
+ hashtagFrequency: {} as Record<string, number>,
305
+ postingPatterns: {
306
+ byDayOfWeek: {} as Record<string, number>,
307
+ byHour: {} as Record<number, number>,
308
+ },
309
+ };
310
+
311
+ if (posts.length === 0) return analysis;
312
+
313
+ // Find date range
314
+ const dates = posts.map((p) => p.publishedAt.getTime());
315
+ analysis.dateRange.earliest = new Date(Math.min(...dates));
316
+ analysis.dateRange.latest = new Date(Math.max(...dates));
317
+
318
+ // Analyze posts
319
+ let topEngagement = 0;
320
+ posts.forEach((post) => {
321
+ // Media types
322
+ const mediaType = post.platformSpecificData.mediaType;
323
+ if (analysis.mediaTypes[mediaType] !== undefined) {
324
+ analysis.mediaTypes[mediaType]++;
325
+ }
326
+
327
+ // Engagement
328
+ const likes = post.analytics?.likes || 0;
329
+ const comments = post.analytics?.comments || 0;
330
+ const totalEngagement = likes + comments;
331
+
332
+ analysis.engagement.totalLikes += likes;
333
+ analysis.engagement.totalComments += comments;
334
+
335
+ if (totalEngagement > topEngagement) {
336
+ topEngagement = totalEngagement;
337
+ analysis.engagement.topPost = {
338
+ id: post.id,
339
+ content: post.content.substring(0, 100) + '...',
340
+ engagement: totalEngagement,
341
+ publishedAt: post.publishedAt,
342
+ };
343
+ }
344
+
345
+ // Hashtags
346
+ const hashtags = this.extractHashtags(post.content);
347
+ hashtags.forEach((tag) => {
348
+ analysis.hashtagFrequency[tag] = (analysis.hashtagFrequency[tag] || 0) + 1;
349
+ });
350
+
351
+ // Posting patterns
352
+ const dayOfWeek = post.publishedAt.toLocaleDateString('en-US', { weekday: 'long' });
353
+ const hour = post.publishedAt.getHours();
354
+
355
+ analysis.postingPatterns.byDayOfWeek[dayOfWeek] = (analysis.postingPatterns.byDayOfWeek[dayOfWeek] || 0) + 1;
356
+ analysis.postingPatterns.byHour[hour] = (analysis.postingPatterns.byHour[hour] || 0) + 1;
357
+ });
358
+
359
+ // Calculate averages
360
+ analysis.engagement.avgLikesPerPost = Math.round(analysis.engagement.totalLikes / posts.length);
361
+ analysis.engagement.avgCommentsPerPost = Math.round(analysis.engagement.totalComments / posts.length);
362
+
363
+ return analysis;
364
+ }
365
+
366
+ /**
367
+ * Generate search suggestions based on results
368
+ */
369
+ private generateSearchSuggestions(posts: SocialPost[], params: SearchParams): any {
370
+ const suggestions = {
371
+ relatedHashtags: [] as string[],
372
+ optimalPostingTimes: [] as any[],
373
+ contentThemes: [] as string[],
374
+ performanceInsights: [] as string[],
375
+ };
376
+
377
+ if (posts.length === 0) {
378
+ suggestions.performanceInsights.push('No posts found matching your criteria. Try broadening your search.');
379
+ return suggestions;
489
380
  }
490
381
 
491
- /**
492
- * Calculate engagement boost for posts with questions
493
- */
494
- private getQuestionEngagementBoost(posts: SocialPost[]): number {
495
- const withQuestions = posts.filter(p => p.content.includes('?'));
496
- const withoutQuestions = posts.filter(p => !p.content.includes('?'));
497
-
498
- if (withQuestions.length === 0 || withoutQuestions.length === 0) {
499
- return 0;
382
+ // Related hashtags (most frequently used)
383
+ const hashtagCounts: Record<string, number> = {};
384
+ posts.forEach((post) => {
385
+ const hashtags = this.extractHashtags(post.content);
386
+ hashtags.forEach((tag) => {
387
+ if (!params.hashtags?.includes(tag)) {
388
+ hashtagCounts[tag] = (hashtagCounts[tag] || 0) + 1;
500
389
  }
501
-
502
- const avgWithQuestions = withQuestions.reduce((sum, post) =>
503
- sum + (post.analytics?.likes || 0) + (post.analytics?.comments || 0), 0
504
- ) / withQuestions.length;
505
-
506
- const avgWithoutQuestions = withoutQuestions.reduce((sum, post) =>
507
- sum + (post.analytics?.likes || 0) + (post.analytics?.comments || 0), 0
508
- ) / withoutQuestions.length;
509
-
510
- if (avgWithoutQuestions === 0) return 0;
511
-
512
- return Math.round(((avgWithQuestions - avgWithoutQuestions) / avgWithoutQuestions) * 100);
390
+ });
391
+ });
392
+
393
+ suggestions.relatedHashtags = Object.entries(hashtagCounts)
394
+ .sort(([, a], [, b]) => b - a)
395
+ .slice(0, 10)
396
+ .map(([tag]) => tag);
397
+
398
+ // Optimal posting times (based on engagement)
399
+ const timeEngagement: Record<string, { total: number; count: number }> = {};
400
+ posts.forEach((post) => {
401
+ const hour = post.publishedAt.getHours();
402
+ const day = post.publishedAt.toLocaleDateString('en-US', { weekday: 'long' });
403
+ const key = `${day} ${hour}:00`;
404
+
405
+ if (!timeEngagement[key]) {
406
+ timeEngagement[key] = { total: 0, count: 0 };
407
+ }
408
+
409
+ const engagement = (post.analytics?.likes || 0) + (post.analytics?.comments || 0);
410
+ timeEngagement[key].total += engagement;
411
+ timeEngagement[key].count++;
412
+ });
413
+
414
+ suggestions.optimalPostingTimes = Object.entries(timeEngagement)
415
+ .map(([time, data]) => ({
416
+ time,
417
+ avgEngagement: Math.round(data.total / data.count),
418
+ }))
419
+ .sort((a, b) => b.avgEngagement - a.avgEngagement)
420
+ .slice(0, 5);
421
+
422
+ // Performance insights
423
+ const avgEngagement =
424
+ posts.reduce((sum, post) => sum + (post.analytics?.likes || 0) + (post.analytics?.comments || 0), 0) / posts.length;
425
+
426
+ suggestions.performanceInsights.push(
427
+ `Average engagement per post: ${Math.round(avgEngagement)}`,
428
+ `Most successful media type: ${this.getMostSuccessfulMediaType(posts)}`,
429
+ `Posts with questions get ${this.getQuestionEngagementBoost(posts)}% more engagement`
430
+ );
431
+
432
+ return suggestions;
433
+ }
434
+
435
+ /**
436
+ * Get most successful media type
437
+ */
438
+ private getMostSuccessfulMediaType(posts: SocialPost[]): string {
439
+ const typeEngagement: Record<string, { total: number; count: number }> = {};
440
+
441
+ posts.forEach((post) => {
442
+ const type = post.platformSpecificData.mediaType;
443
+ if (!typeEngagement[type]) {
444
+ typeEngagement[type] = { total: 0, count: 0 };
445
+ }
446
+
447
+ const engagement = (post.analytics?.likes || 0) + (post.analytics?.comments || 0);
448
+ typeEngagement[type].total += engagement;
449
+ typeEngagement[type].count++;
450
+ });
451
+
452
+ let bestType = 'IMAGE';
453
+ let bestAvg = 0;
454
+
455
+ Object.entries(typeEngagement).forEach(([type, data]) => {
456
+ const avg = data.total / data.count;
457
+ if (avg > bestAvg) {
458
+ bestAvg = avg;
459
+ bestType = type;
460
+ }
461
+ });
462
+
463
+ return bestType;
464
+ }
465
+
466
+ /**
467
+ * Calculate engagement boost for posts with questions
468
+ */
469
+ private getQuestionEngagementBoost(posts: SocialPost[]): number {
470
+ const withQuestions = posts.filter((p) => p.content.includes('?'));
471
+ const withoutQuestions = posts.filter((p) => !p.content.includes('?'));
472
+
473
+ if (withQuestions.length === 0 || withoutQuestions.length === 0) {
474
+ return 0;
513
475
  }
514
476
 
515
- /**
516
- * Define the parameters for this action
517
- */
518
- public get Params(): ActionParam[] {
519
- return [
520
- ...this.commonSocialParams,
521
- {
522
- Name: 'Query',
523
- Type: 'Input',
524
- Value: null
525
- },
526
- {
527
- Name: 'Hashtags',
528
- Type: 'Input',
529
- Value: null
530
- },
531
- {
532
- Name: 'StartDate',
533
- Type: 'Input',
534
- Value: null
535
- },
536
- {
537
- Name: 'EndDate',
538
- Type: 'Input',
539
- Value: null
540
- },
541
- {
542
- Name: 'MediaType',
543
- Type: 'Input',
544
- Value: null
545
- },
546
- {
547
- Name: 'MinEngagement',
548
- Type: 'Input',
549
- Value: 0
550
- },
551
- {
552
- Name: 'Limit',
553
- Type: 'Input',
554
- Value: 100
555
- },
556
- {
557
- Name: 'IncludeArchived',
558
- Type: 'Input',
559
- Value: false
560
- }
561
- ];
562
- }
563
-
564
- /**
565
- * Get the description for this action
566
- */
567
- public get Description(): string {
568
- return 'Searches historical Instagram posts from your business account with filters for date range, hashtags, content, and engagement metrics.';
569
- }
570
- }
477
+ const avgWithQuestions =
478
+ withQuestions.reduce((sum, post) => sum + (post.analytics?.likes || 0) + (post.analytics?.comments || 0), 0) / withQuestions.length;
479
+
480
+ const avgWithoutQuestions =
481
+ withoutQuestions.reduce((sum, post) => sum + (post.analytics?.likes || 0) + (post.analytics?.comments || 0), 0) /
482
+ withoutQuestions.length;
483
+
484
+ if (avgWithoutQuestions === 0) return 0;
485
+
486
+ return Math.round(((avgWithQuestions - avgWithoutQuestions) / avgWithoutQuestions) * 100);
487
+ }
488
+
489
+ /**
490
+ * Define the parameters for this action
491
+ */
492
+ public get Params(): ActionParam[] {
493
+ return [
494
+ ...this.commonSocialParams,
495
+ {
496
+ Name: 'Query',
497
+ Type: 'Input',
498
+ Value: null,
499
+ },
500
+ {
501
+ Name: 'Hashtags',
502
+ Type: 'Input',
503
+ Value: null,
504
+ },
505
+ {
506
+ Name: 'StartDate',
507
+ Type: 'Input',
508
+ Value: null,
509
+ },
510
+ {
511
+ Name: 'EndDate',
512
+ Type: 'Input',
513
+ Value: null,
514
+ },
515
+ {
516
+ Name: 'MediaType',
517
+ Type: 'Input',
518
+ Value: null,
519
+ },
520
+ {
521
+ Name: 'MinEngagement',
522
+ Type: 'Input',
523
+ Value: 0,
524
+ },
525
+ {
526
+ Name: 'Limit',
527
+ Type: 'Input',
528
+ Value: 100,
529
+ },
530
+ {
531
+ Name: 'IncludeArchived',
532
+ Type: 'Input',
533
+ Value: false,
534
+ },
535
+ ];
536
+ }
537
+
538
+ /**
539
+ * Get the description for this action
540
+ */
541
+ public get Description(): string {
542
+ return 'Searches historical Instagram posts from your business account with filters for date range, hashtags, content, and engagement metrics.';
543
+ }
544
+ }