@memberjunction/actions-bizapps-social 2.111.0 → 2.112.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +6 -6
- package/dist/base/base-social.action.d.ts.map +1 -1
- package/dist/base/base-social.action.js +18 -24
- package/dist/base/base-social.action.js.map +1 -1
- package/dist/providers/buffer/buffer-base.action.d.ts.map +1 -1
- package/dist/providers/buffer/buffer-base.action.js +35 -34
- package/dist/providers/buffer/buffer-base.action.js.map +1 -1
- package/dist/providers/facebook/actions/boost-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/boost-post.action.js +33 -33
- package/dist/providers/facebook/actions/boost-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/create-album.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/create-album.action.js +34 -36
- package/dist/providers/facebook/actions/create-album.action.js.map +1 -1
- package/dist/providers/facebook/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/create-post.action.js +20 -20
- package/dist/providers/facebook/actions/create-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-page-insights.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-page-insights.action.js +25 -27
- package/dist/providers/facebook/actions/get-page-insights.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-page-posts.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-page-posts.action.js +19 -23
- package/dist/providers/facebook/actions/get-page-posts.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-post-insights.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-post-insights.action.js +28 -32
- package/dist/providers/facebook/actions/get-post-insights.action.js.map +1 -1
- package/dist/providers/facebook/actions/respond-to-comments.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/respond-to-comments.action.js +42 -44
- package/dist/providers/facebook/actions/respond-to-comments.action.js.map +1 -1
- package/dist/providers/facebook/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/schedule-post.action.js +29 -29
- package/dist/providers/facebook/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/search-posts.action.js +37 -39
- package/dist/providers/facebook/actions/search-posts.action.js.map +1 -1
- package/dist/providers/facebook/facebook-base.action.d.ts.map +1 -1
- package/dist/providers/facebook/facebook-base.action.js +44 -59
- package/dist/providers/facebook/facebook-base.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js +33 -31
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.js +28 -32
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js +19 -19
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-analytics.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-analytics.action.js +24 -26
- package/dist/providers/hootsuite/actions/get-analytics.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js +22 -22
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-social-profiles.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-social-profiles.action.js +32 -34
- package/dist/providers/hootsuite/actions/get-social-profiles.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/search-posts.action.js +43 -52
- package/dist/providers/hootsuite/actions/search-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.js +30 -28
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/hootsuite-base.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/hootsuite-base.action.js +18 -20
- package/dist/providers/hootsuite/hootsuite-base.action.js.map +1 -1
- package/dist/providers/instagram/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/create-post.action.js +27 -26
- package/dist/providers/instagram/actions/create-post.action.js.map +1 -1
- package/dist/providers/instagram/actions/create-story.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/create-story.action.js +35 -35
- package/dist/providers/instagram/actions/create-story.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-account-insights.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-account-insights.action.js +38 -59
- package/dist/providers/instagram/actions/get-account-insights.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-business-posts.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-business-posts.action.js +29 -29
- package/dist/providers/instagram/actions/get-business-posts.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-comments.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-comments.action.js +36 -36
- package/dist/providers/instagram/actions/get-comments.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-post-insights.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-post-insights.action.js +23 -25
- package/dist/providers/instagram/actions/get-post-insights.action.js.map +1 -1
- package/dist/providers/instagram/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/schedule-post.action.js +25 -25
- package/dist/providers/instagram/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/instagram/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/search-posts.action.js +56 -60
- package/dist/providers/instagram/actions/search-posts.action.js.map +1 -1
- package/dist/providers/instagram/instagram-base.action.d.ts.map +1 -1
- package/dist/providers/instagram/instagram-base.action.js +25 -27
- package/dist/providers/instagram/instagram-base.action.js.map +1 -1
- package/dist/providers/linkedin/actions/create-article.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/create-article.action.js +55 -45
- package/dist/providers/linkedin/actions/create-article.action.js.map +1 -1
- package/dist/providers/linkedin/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/create-post.action.js +31 -29
- package/dist/providers/linkedin/actions/create-post.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-followers.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-followers.action.js +28 -28
- package/dist/providers/linkedin/actions/get-followers.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-organization-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-organization-posts.action.js +20 -20
- package/dist/providers/linkedin/actions/get-organization-posts.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-personal-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-personal-posts.action.js +19 -19
- package/dist/providers/linkedin/actions/get-personal-posts.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-post-analytics.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-post-analytics.action.js +25 -23
- package/dist/providers/linkedin/actions/get-post-analytics.action.js.map +1 -1
- package/dist/providers/linkedin/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/schedule-post.action.js +32 -30
- package/dist/providers/linkedin/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/linkedin/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/search-posts.action.js +28 -30
- package/dist/providers/linkedin/actions/search-posts.action.js.map +1 -1
- package/dist/providers/linkedin/linkedin-base.action.d.ts.map +1 -1
- package/dist/providers/linkedin/linkedin-base.action.js +33 -38
- package/dist/providers/linkedin/linkedin-base.action.js.map +1 -1
- package/dist/providers/tiktok/tiktok-base.action.d.ts.map +1 -1
- package/dist/providers/tiktok/tiktok-base.action.js +25 -26
- package/dist/providers/tiktok/tiktok-base.action.js.map +1 -1
- package/dist/providers/twitter/actions/create-thread.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/create-thread.action.js +25 -29
- package/dist/providers/twitter/actions/create-thread.action.js.map +1 -1
- package/dist/providers/twitter/actions/create-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/create-tweet.action.js +23 -23
- package/dist/providers/twitter/actions/create-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/delete-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/delete-tweet.action.js +19 -19
- package/dist/providers/twitter/actions/delete-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-analytics.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-analytics.action.js +40 -47
- package/dist/providers/twitter/actions/get-analytics.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-mentions.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-mentions.action.js +30 -31
- package/dist/providers/twitter/actions/get-mentions.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-timeline.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-timeline.action.js +29 -29
- package/dist/providers/twitter/actions/get-timeline.action.js.map +1 -1
- package/dist/providers/twitter/actions/schedule-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/schedule-tweet.action.js +26 -26
- package/dist/providers/twitter/actions/schedule-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/search-tweets.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/search-tweets.action.js +56 -58
- package/dist/providers/twitter/actions/search-tweets.action.js.map +1 -1
- package/dist/providers/twitter/twitter-base.action.d.ts.map +1 -1
- package/dist/providers/twitter/twitter-base.action.js +58 -68
- package/dist/providers/twitter/twitter-base.action.js.map +1 -1
- package/dist/providers/youtube/youtube-base.action.d.ts +1 -1
- package/dist/providers/youtube/youtube-base.action.d.ts.map +1 -1
- package/dist/providers/youtube/youtube-base.action.js +22 -25
- package/dist/providers/youtube/youtube-base.action.js.map +1 -1
- package/package.json +5 -6
- package/src/base/base-social.action.ts +217 -224
- package/src/providers/buffer/buffer-base.action.ts +435 -441
- package/src/providers/facebook/actions/boost-post.action.ts +350 -386
- package/src/providers/facebook/actions/create-album.action.ts +291 -307
- package/src/providers/facebook/actions/create-post.action.ts +224 -227
- package/src/providers/facebook/actions/get-page-insights.action.ts +383 -403
- package/src/providers/facebook/actions/get-page-posts.action.ts +214 -225
- package/src/providers/facebook/actions/get-post-insights.action.ts +300 -316
- package/src/providers/facebook/actions/respond-to-comments.action.ts +319 -336
- package/src/providers/facebook/actions/schedule-post.action.ts +289 -292
- package/src/providers/facebook/actions/search-posts.action.ts +399 -413
- package/src/providers/facebook/facebook-base.action.ts +653 -670
- package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
- package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +184 -189
- package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +160 -161
- package/src/providers/hootsuite/actions/get-analytics.action.ts +249 -254
- package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +206 -207
- package/src/providers/hootsuite/actions/get-social-profiles.action.ts +206 -205
- package/src/providers/hootsuite/actions/search-posts.action.ts +351 -369
- package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +211 -209
- package/src/providers/hootsuite/hootsuite-base.action.ts +301 -307
- package/src/providers/instagram/actions/create-post.action.ts +276 -296
- package/src/providers/instagram/actions/create-story.action.ts +378 -394
- package/src/providers/instagram/actions/get-account-insights.action.ts +384 -420
- package/src/providers/instagram/actions/get-business-posts.action.ts +233 -242
- package/src/providers/instagram/actions/get-comments.action.ts +365 -377
- package/src/providers/instagram/actions/get-post-insights.action.ts +265 -273
- package/src/providers/instagram/actions/schedule-post.action.ts +233 -235
- package/src/providers/instagram/actions/search-posts.action.ts +512 -538
- package/src/providers/instagram/instagram-base.action.ts +368 -393
- package/src/providers/linkedin/actions/create-article.action.ts +275 -266
- package/src/providers/linkedin/actions/create-post.action.ts +179 -177
- package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
- package/src/providers/linkedin/actions/get-organization-posts.action.ts +146 -147
- package/src/providers/linkedin/actions/get-personal-posts.action.ts +138 -139
- package/src/providers/linkedin/actions/get-post-analytics.action.ts +190 -189
- package/src/providers/linkedin/actions/schedule-post.action.ts +191 -189
- package/src/providers/linkedin/actions/search-posts.action.ts +275 -283
- package/src/providers/linkedin/linkedin-base.action.ts +407 -421
- package/src/providers/tiktok/tiktok-base.action.ts +305 -320
- package/src/providers/twitter/actions/create-thread.action.ts +203 -207
- package/src/providers/twitter/actions/create-tweet.action.ts +187 -188
- package/src/providers/twitter/actions/delete-tweet.action.ts +128 -129
- package/src/providers/twitter/actions/get-analytics.action.ts +402 -411
- package/src/providers/twitter/actions/get-mentions.action.ts +218 -219
- package/src/providers/twitter/actions/get-timeline.action.ts +232 -233
- package/src/providers/twitter/actions/schedule-tweet.action.ts +221 -222
- package/src/providers/twitter/actions/search-tweets.action.ts +540 -543
- package/src/providers/twitter/twitter-base.action.ts +541 -560
- package/src/providers/youtube/youtube-base.action.ts +320 -333
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RegisterClass } from '@memberjunction/global';
|
|
2
2
|
import { InstagramBaseAction } from '../instagram-base.action';
|
|
3
3
|
import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
|
|
4
|
-
import { LogError } from '@memberjunction/
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
309
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
+
}
|