@memberjunction/actions-bizapps-social 2.111.1 → 2.112.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,6 +1,6 @@
|
|
|
1
1
|
import { RegisterClass } from '@memberjunction/global';
|
|
2
2
|
import { BaseSocialMediaAction, SocialPost, SocialAnalytics, MediaFile } from '../../base/base-social.action';
|
|
3
|
-
import { UserInfo, LogError, LogStatus } from '@memberjunction/
|
|
3
|
+
import { UserInfo, LogError, LogStatus } from '@memberjunction/global';
|
|
4
4
|
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
5
5
|
import FormData from 'form-data';
|
|
6
6
|
import { BaseAction } from '@memberjunction/actions';
|
|
@@ -11,481 +11,475 @@ import { BaseAction } from '@memberjunction/actions';
|
|
|
11
11
|
*/
|
|
12
12
|
@RegisterClass(BaseAction, 'BufferBaseAction')
|
|
13
13
|
export abstract class BufferBaseAction extends BaseSocialMediaAction {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
protected get platformName(): string {
|
|
15
|
+
return 'Buffer';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
protected get apiBaseUrl(): string {
|
|
19
|
+
return 'https://api.bufferapp.com/1';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private axiosInstance: AxiosInstance | null = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get axios instance with authentication
|
|
26
|
+
*/
|
|
27
|
+
protected getAxiosInstance(): AxiosInstance {
|
|
28
|
+
if (!this.axiosInstance) {
|
|
29
|
+
this.axiosInstance = axios.create({
|
|
30
|
+
baseURL: this.apiBaseUrl,
|
|
31
|
+
timeout: 30000,
|
|
32
|
+
headers: {
|
|
33
|
+
Accept: 'application/json',
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Add request interceptor for auth
|
|
39
|
+
this.axiosInstance.interceptors.request.use(
|
|
40
|
+
(config) => {
|
|
41
|
+
const token = this.getAccessToken();
|
|
42
|
+
if (token) {
|
|
43
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
44
|
+
}
|
|
45
|
+
return config;
|
|
46
|
+
},
|
|
47
|
+
(error) => {
|
|
48
|
+
return Promise.reject(error);
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Add response interceptor for rate limiting
|
|
53
|
+
this.axiosInstance.interceptors.response.use(
|
|
54
|
+
(response) => {
|
|
55
|
+
// Log rate limit headers if present
|
|
56
|
+
const headers = response.headers;
|
|
57
|
+
const rateLimitInfo = this.parseRateLimitHeaders(headers);
|
|
58
|
+
if (rateLimitInfo) {
|
|
59
|
+
LogStatus(`Buffer API - Remaining requests: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`);
|
|
60
|
+
}
|
|
61
|
+
return response;
|
|
62
|
+
},
|
|
63
|
+
async (error: AxiosError) => {
|
|
64
|
+
if (error.response?.status === 429) {
|
|
65
|
+
// Rate limit hit
|
|
66
|
+
const retryAfter = error.response.headers['retry-after'];
|
|
67
|
+
await this.handleRateLimit(retryAfter ? parseInt(retryAfter) : 60);
|
|
68
|
+
// Retry the request
|
|
69
|
+
return this.axiosInstance!.request(error.config!);
|
|
70
|
+
}
|
|
71
|
+
return Promise.reject(error);
|
|
72
|
+
}
|
|
73
|
+
);
|
|
16
74
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
75
|
+
return this.axiosInstance;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Refresh access token using refresh token
|
|
80
|
+
*/
|
|
81
|
+
protected async refreshAccessToken(): Promise<void> {
|
|
82
|
+
const refreshToken = this.getRefreshToken();
|
|
83
|
+
if (!refreshToken) {
|
|
84
|
+
throw new Error('No refresh token available for Buffer');
|
|
20
85
|
}
|
|
21
86
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (token) {
|
|
43
|
-
config.headers.Authorization = `Bearer ${token}`;
|
|
44
|
-
}
|
|
45
|
-
return config;
|
|
46
|
-
},
|
|
47
|
-
(error) => {
|
|
48
|
-
return Promise.reject(error);
|
|
49
|
-
}
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
// Add response interceptor for rate limiting
|
|
53
|
-
this.axiosInstance.interceptors.response.use(
|
|
54
|
-
(response) => {
|
|
55
|
-
// Log rate limit headers if present
|
|
56
|
-
const headers = response.headers;
|
|
57
|
-
const rateLimitInfo = this.parseRateLimitHeaders(headers);
|
|
58
|
-
if (rateLimitInfo) {
|
|
59
|
-
LogStatus(`Buffer API - Remaining requests: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`);
|
|
60
|
-
}
|
|
61
|
-
return response;
|
|
62
|
-
},
|
|
63
|
-
async (error: AxiosError) => {
|
|
64
|
-
if (error.response?.status === 429) {
|
|
65
|
-
// Rate limit hit
|
|
66
|
-
const retryAfter = error.response.headers['retry-after'];
|
|
67
|
-
await this.handleRateLimit(retryAfter ? parseInt(retryAfter) : 60);
|
|
68
|
-
// Retry the request
|
|
69
|
-
return this.axiosInstance!.request(error.config!);
|
|
70
|
-
}
|
|
71
|
-
return Promise.reject(error);
|
|
72
|
-
}
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
return this.axiosInstance;
|
|
87
|
+
try {
|
|
88
|
+
const response = await axios.post(`${this.apiBaseUrl}/oauth2/token`, {
|
|
89
|
+
client_id: this.getCustomAttribute(1), // Store Buffer client ID in CustomAttribute1
|
|
90
|
+
client_secret: this.getCustomAttribute(2), // Store Buffer client secret in CustomAttribute2
|
|
91
|
+
refresh_token: refreshToken,
|
|
92
|
+
grant_type: 'refresh_token',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const { access_token, expires_in } = response.data;
|
|
96
|
+
|
|
97
|
+
await this.updateStoredTokens(
|
|
98
|
+
access_token,
|
|
99
|
+
refreshToken, // Buffer doesn't rotate refresh tokens
|
|
100
|
+
expires_in
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
LogStatus('Buffer access token refreshed successfully');
|
|
104
|
+
} catch (error) {
|
|
105
|
+
LogError('Failed to refresh Buffer access token:', error);
|
|
106
|
+
throw new Error('Failed to refresh Buffer access token');
|
|
76
107
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
client_id: this.getCustomAttribute(1), // Store Buffer client ID in CustomAttribute1
|
|
90
|
-
client_secret: this.getCustomAttribute(2), // Store Buffer client secret in CustomAttribute2
|
|
91
|
-
refresh_token: refreshToken,
|
|
92
|
-
grant_type: 'refresh_token'
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const { access_token, expires_in } = response.data;
|
|
96
|
-
|
|
97
|
-
await this.updateStoredTokens(
|
|
98
|
-
access_token,
|
|
99
|
-
refreshToken, // Buffer doesn't rotate refresh tokens
|
|
100
|
-
expires_in
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
LogStatus('Buffer access token refreshed successfully');
|
|
104
|
-
} catch (error) {
|
|
105
|
-
LogError('Failed to refresh Buffer access token:', error);
|
|
106
|
-
throw new Error('Failed to refresh Buffer access token');
|
|
107
|
-
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get Buffer profiles for the authenticated user
|
|
112
|
+
*/
|
|
113
|
+
protected async getProfiles(): Promise<any[]> {
|
|
114
|
+
try {
|
|
115
|
+
const response = await this.getAxiosInstance().get('/profiles.json');
|
|
116
|
+
return response.data || [];
|
|
117
|
+
} catch (error) {
|
|
118
|
+
LogError('Failed to get Buffer profiles:', error);
|
|
119
|
+
throw error;
|
|
108
120
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Upload media to Buffer
|
|
125
|
+
*/
|
|
126
|
+
protected async uploadSingleMedia(file: MediaFile): Promise<string> {
|
|
127
|
+
try {
|
|
128
|
+
const formData = new FormData();
|
|
129
|
+
|
|
130
|
+
// Convert base64 to buffer if needed
|
|
131
|
+
const buffer = typeof file.data === 'string' ? Buffer.from(file.data, 'base64') : file.data;
|
|
132
|
+
|
|
133
|
+
formData.append('media', buffer, {
|
|
134
|
+
filename: file.filename,
|
|
135
|
+
contentType: file.mimeType,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const response = await this.getAxiosInstance().post('/updates/media/upload.json', formData, {
|
|
139
|
+
headers: {
|
|
140
|
+
...formData.getHeaders(),
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (response.data && response.data.media && response.data.media[0]) {
|
|
145
|
+
return response.data.media[0].picture; // URL of uploaded media
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new Error('Failed to upload media to Buffer');
|
|
149
|
+
} catch (error) {
|
|
150
|
+
LogError('Buffer media upload failed:', error);
|
|
151
|
+
throw error;
|
|
121
152
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
contentType: file.mimeType
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const response = await this.getAxiosInstance().post('/updates/media/upload.json', formData, {
|
|
141
|
-
headers: {
|
|
142
|
-
...formData.getHeaders()
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (response.data && response.data.media && response.data.media[0]) {
|
|
147
|
-
return response.data.media[0].picture; // URL of uploaded media
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
throw new Error('Failed to upload media to Buffer');
|
|
151
|
-
} catch (error) {
|
|
152
|
-
LogError('Buffer media upload failed:', error);
|
|
153
|
-
throw error;
|
|
154
|
-
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Create a Buffer update (post)
|
|
157
|
+
*/
|
|
158
|
+
protected async createUpdate(
|
|
159
|
+
profileIds: string[],
|
|
160
|
+
text: string,
|
|
161
|
+
media?: { link?: string; description?: string; picture?: string }[],
|
|
162
|
+
scheduledAt?: Date,
|
|
163
|
+
options?: {
|
|
164
|
+
shorten?: boolean;
|
|
165
|
+
now?: boolean;
|
|
166
|
+
top?: boolean;
|
|
167
|
+
attachment?: boolean;
|
|
155
168
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
options?: {
|
|
166
|
-
shorten?: boolean;
|
|
167
|
-
now?: boolean;
|
|
168
|
-
top?: boolean;
|
|
169
|
-
attachment?: boolean;
|
|
170
|
-
}
|
|
171
|
-
): Promise<any> {
|
|
172
|
-
const data: any = {
|
|
173
|
-
profile_ids: profileIds,
|
|
174
|
-
text: text,
|
|
175
|
-
shorten: options?.shorten !== false // Default to true
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
if (media && media.length > 0) {
|
|
179
|
-
data.media = media;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (scheduledAt) {
|
|
183
|
-
data.scheduled_at = Math.floor(scheduledAt.getTime() / 1000); // Unix timestamp
|
|
184
|
-
} else if (options?.now) {
|
|
185
|
-
data.now = true;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (options?.top) {
|
|
189
|
-
data.top = true;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (options?.attachment !== undefined) {
|
|
193
|
-
data.attachment = options.attachment;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
const response = await this.getAxiosInstance().post('/updates/create.json', data);
|
|
198
|
-
return response.data;
|
|
199
|
-
} catch (error) {
|
|
200
|
-
LogError('Failed to create Buffer update:', error);
|
|
201
|
-
throw error;
|
|
202
|
-
}
|
|
169
|
+
): Promise<any> {
|
|
170
|
+
const data: any = {
|
|
171
|
+
profile_ids: profileIds,
|
|
172
|
+
text: text,
|
|
173
|
+
shorten: options?.shorten !== false, // Default to true
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (media && media.length > 0) {
|
|
177
|
+
data.media = media;
|
|
203
178
|
}
|
|
204
179
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
profileId: string,
|
|
210
|
-
status: 'pending' | 'sent',
|
|
211
|
-
options?: {
|
|
212
|
-
page?: number;
|
|
213
|
-
count?: number;
|
|
214
|
-
since?: Date;
|
|
215
|
-
utc?: boolean;
|
|
216
|
-
}
|
|
217
|
-
): Promise<any> {
|
|
218
|
-
const params: any = {
|
|
219
|
-
page: options?.page || 1,
|
|
220
|
-
count: options?.count || 10,
|
|
221
|
-
utc: options?.utc !== false // Default to true
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
if (options?.since) {
|
|
225
|
-
params.since = Math.floor(options.since.getTime() / 1000);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
const response = await this.getAxiosInstance().get(
|
|
230
|
-
`/profiles/${profileId}/updates/${status}.json`,
|
|
231
|
-
{ params }
|
|
232
|
-
);
|
|
233
|
-
return response.data;
|
|
234
|
-
} catch (error) {
|
|
235
|
-
LogError(`Failed to get ${status} updates from Buffer:`, error);
|
|
236
|
-
throw error;
|
|
237
|
-
}
|
|
180
|
+
if (scheduledAt) {
|
|
181
|
+
data.scheduled_at = Math.floor(scheduledAt.getTime() / 1000); // Unix timestamp
|
|
182
|
+
} else if (options?.now) {
|
|
183
|
+
data.now = true;
|
|
238
184
|
}
|
|
239
185
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
*/
|
|
243
|
-
protected async deleteUpdate(updateId: string): Promise<boolean> {
|
|
244
|
-
try {
|
|
245
|
-
const response = await this.getAxiosInstance().post(`/updates/${updateId}/destroy.json`);
|
|
246
|
-
return response.data.success === true;
|
|
247
|
-
} catch (error) {
|
|
248
|
-
LogError('Failed to delete Buffer update:', error);
|
|
249
|
-
throw error;
|
|
250
|
-
}
|
|
186
|
+
if (options?.top) {
|
|
187
|
+
data.top = true;
|
|
251
188
|
}
|
|
252
189
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
protected async reorderUpdates(profileId: string, updateIds: string[], offset?: number): Promise<any> {
|
|
257
|
-
const data: any = {
|
|
258
|
-
order: updateIds
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
if (offset !== undefined) {
|
|
262
|
-
data.offset = offset;
|
|
263
|
-
}
|
|
190
|
+
if (options?.attachment !== undefined) {
|
|
191
|
+
data.attachment = options.attachment;
|
|
192
|
+
}
|
|
264
193
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
194
|
+
try {
|
|
195
|
+
const response = await this.getAxiosInstance().post('/updates/create.json', data);
|
|
196
|
+
return response.data;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
LogError('Failed to create Buffer update:', error);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get updates (posts) from Buffer
|
|
205
|
+
*/
|
|
206
|
+
protected async getUpdates(
|
|
207
|
+
profileId: string,
|
|
208
|
+
status: 'pending' | 'sent',
|
|
209
|
+
options?: {
|
|
210
|
+
page?: number;
|
|
211
|
+
count?: number;
|
|
212
|
+
since?: Date;
|
|
213
|
+
utc?: boolean;
|
|
214
|
+
}
|
|
215
|
+
): Promise<any> {
|
|
216
|
+
const params: any = {
|
|
217
|
+
page: options?.page || 1,
|
|
218
|
+
count: options?.count || 10,
|
|
219
|
+
utc: options?.utc !== false, // Default to true
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (options?.since) {
|
|
223
|
+
params.since = Math.floor(options.since.getTime() / 1000);
|
|
275
224
|
}
|
|
276
225
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
226
|
+
try {
|
|
227
|
+
const response = await this.getAxiosInstance().get(`/profiles/${profileId}/updates/${status}.json`, { params });
|
|
228
|
+
return response.data;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
LogError(`Failed to get ${status} updates from Buffer:`, error);
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Delete a Buffer update
|
|
237
|
+
*/
|
|
238
|
+
protected async deleteUpdate(updateId: string): Promise<boolean> {
|
|
239
|
+
try {
|
|
240
|
+
const response = await this.getAxiosInstance().post(`/updates/${updateId}/destroy.json`);
|
|
241
|
+
return response.data.success === true;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
LogError('Failed to delete Buffer update:', error);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Reorder updates in the queue
|
|
250
|
+
*/
|
|
251
|
+
protected async reorderUpdates(profileId: string, updateIds: string[], offset?: number): Promise<any> {
|
|
252
|
+
const data: any = {
|
|
253
|
+
order: updateIds,
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
if (offset !== undefined) {
|
|
257
|
+
data.offset = offset;
|
|
288
258
|
}
|
|
289
259
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
260
|
+
try {
|
|
261
|
+
const response = await this.getAxiosInstance().post(`/profiles/${profileId}/updates/reorder.json`, data);
|
|
262
|
+
return response.data;
|
|
263
|
+
} catch (error) {
|
|
264
|
+
LogError('Failed to reorder Buffer updates:', error);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Get analytics for sent posts
|
|
271
|
+
*/
|
|
272
|
+
protected async getAnalytics(updateId: string): Promise<any> {
|
|
273
|
+
try {
|
|
274
|
+
const response = await this.getAxiosInstance().get(`/updates/${updateId}/interactions.json`);
|
|
275
|
+
return response.data;
|
|
276
|
+
} catch (error) {
|
|
277
|
+
LogError('Failed to get Buffer analytics:', error);
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Search posts implementation for Buffer
|
|
284
|
+
* Buffer doesn't have a native search API, so we'll fetch posts and filter client-side
|
|
285
|
+
*/
|
|
286
|
+
protected async searchPosts(params: {
|
|
287
|
+
query?: string;
|
|
288
|
+
hashtags?: string[];
|
|
289
|
+
startDate?: Date;
|
|
290
|
+
endDate?: Date;
|
|
291
|
+
limit?: number;
|
|
292
|
+
offset?: number;
|
|
293
|
+
profileIds?: string[];
|
|
294
|
+
}): Promise<SocialPost[]> {
|
|
295
|
+
const posts: SocialPost[] = [];
|
|
296
|
+
const profileIds = params.profileIds || [];
|
|
297
|
+
|
|
298
|
+
// If no profile IDs provided, get all profiles
|
|
299
|
+
if (profileIds.length === 0) {
|
|
300
|
+
const profiles = await this.getProfiles();
|
|
301
|
+
profileIds.push(...profiles.map((p) => p.id));
|
|
302
|
+
}
|
|
311
303
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
page++;
|
|
341
|
-
hasMore = result.updates.length === 100;
|
|
342
|
-
} else {
|
|
343
|
-
hasMore = false;
|
|
344
|
-
}
|
|
304
|
+
// Fetch sent posts from each profile
|
|
305
|
+
for (const profileId of profileIds) {
|
|
306
|
+
try {
|
|
307
|
+
let page = 1;
|
|
308
|
+
let hasMore = true;
|
|
309
|
+
|
|
310
|
+
while (hasMore && posts.length < (params.limit || 100)) {
|
|
311
|
+
const result = await this.getUpdates(profileId, 'sent', {
|
|
312
|
+
page: page,
|
|
313
|
+
count: 100,
|
|
314
|
+
since: params.startDate,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (result.updates && result.updates.length > 0) {
|
|
318
|
+
for (const update of result.updates) {
|
|
319
|
+
const post = this.normalizePost(update);
|
|
320
|
+
|
|
321
|
+
// Apply filters
|
|
322
|
+
if (this.matchesSearchCriteria(post, params)) {
|
|
323
|
+
posts.push(post);
|
|
324
|
+
|
|
325
|
+
if (posts.length >= (params.limit || 100)) {
|
|
326
|
+
hasMore = false;
|
|
327
|
+
break;
|
|
345
328
|
}
|
|
346
|
-
|
|
347
|
-
LogError(`Failed to search posts for profile ${profileId}:`, error);
|
|
329
|
+
}
|
|
348
330
|
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Sort by published date descending
|
|
352
|
-
posts.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
|
|
353
331
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
332
|
+
page++;
|
|
333
|
+
hasMore = result.updates.length === 100;
|
|
334
|
+
} else {
|
|
335
|
+
hasMore = false;
|
|
336
|
+
}
|
|
357
337
|
}
|
|
358
|
-
|
|
359
|
-
|
|
338
|
+
} catch (error) {
|
|
339
|
+
LogError(`Failed to search posts for profile ${profileId}:`, error);
|
|
340
|
+
}
|
|
360
341
|
}
|
|
361
342
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
*/
|
|
365
|
-
private matchesSearchCriteria(post: SocialPost, params: any): boolean {
|
|
366
|
-
// Check date range
|
|
367
|
-
if (params.endDate && post.publishedAt > params.endDate) {
|
|
368
|
-
return false;
|
|
369
|
-
}
|
|
343
|
+
// Sort by published date descending
|
|
344
|
+
posts.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
|
|
370
345
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (!content.includes(query)) {
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
346
|
+
// Apply offset if specified
|
|
347
|
+
if (params.offset) {
|
|
348
|
+
return posts.slice(params.offset, params.offset + (params.limit || 100));
|
|
349
|
+
}
|
|
379
350
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const postHashtags = this.extractHashtags(post.content);
|
|
383
|
-
const hasMatchingHashtag = params.hashtags.some(tag =>
|
|
384
|
-
postHashtags.includes(tag.toLowerCase().replace('#', ''))
|
|
385
|
-
);
|
|
386
|
-
if (!hasMatchingHashtag) {
|
|
387
|
-
return false;
|
|
388
|
-
}
|
|
389
|
-
}
|
|
351
|
+
return posts.slice(0, params.limit || 100);
|
|
352
|
+
}
|
|
390
353
|
|
|
391
|
-
|
|
354
|
+
/**
|
|
355
|
+
* Check if a post matches search criteria
|
|
356
|
+
*/
|
|
357
|
+
private matchesSearchCriteria(post: SocialPost, params: any): boolean {
|
|
358
|
+
// Check date range
|
|
359
|
+
if (params.endDate && post.publishedAt > params.endDate) {
|
|
360
|
+
return false;
|
|
392
361
|
}
|
|
393
362
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
while ((match = regex.exec(content)) !== null) {
|
|
403
|
-
hashtags.push(match[1].toLowerCase());
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return hashtags;
|
|
363
|
+
// Check query text
|
|
364
|
+
if (params.query) {
|
|
365
|
+
const query = params.query.toLowerCase();
|
|
366
|
+
const content = post.content.toLowerCase();
|
|
367
|
+
if (!content.includes(query)) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
407
370
|
}
|
|
408
371
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (bufferPost.media.picture) {
|
|
417
|
-
media.push(bufferPost.media.picture);
|
|
418
|
-
}
|
|
419
|
-
if (bufferPost.media.link) {
|
|
420
|
-
media.push(bufferPost.media.link);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return {
|
|
425
|
-
id: bufferPost.id,
|
|
426
|
-
platform: 'Buffer',
|
|
427
|
-
profileId: bufferPost.profile_id,
|
|
428
|
-
content: bufferPost.text || '',
|
|
429
|
-
mediaUrls: media,
|
|
430
|
-
publishedAt: new Date(bufferPost.sent_at * 1000), // Convert Unix timestamp
|
|
431
|
-
scheduledFor: bufferPost.due_at ? new Date(bufferPost.due_at * 1000) : undefined,
|
|
432
|
-
analytics: bufferPost.statistics ? this.normalizeAnalytics(bufferPost.statistics) : undefined,
|
|
433
|
-
platformSpecificData: {
|
|
434
|
-
profileService: bufferPost.profile_service,
|
|
435
|
-
status: bufferPost.status,
|
|
436
|
-
userId: bufferPost.user_id,
|
|
437
|
-
viaName: bufferPost.via,
|
|
438
|
-
sourceUrl: bufferPost.source_url,
|
|
439
|
-
day: bufferPost.day,
|
|
440
|
-
dueTime: bufferPost.due_time,
|
|
441
|
-
mediaDescription: bufferPost.media?.description,
|
|
442
|
-
mediaTitle: bufferPost.media?.title,
|
|
443
|
-
mediaLink: bufferPost.media?.link
|
|
444
|
-
}
|
|
445
|
-
};
|
|
372
|
+
// Check hashtags
|
|
373
|
+
if (params.hashtags && params.hashtags.length > 0) {
|
|
374
|
+
const postHashtags = this.extractHashtags(post.content);
|
|
375
|
+
const hasMatchingHashtag = params.hashtags.some((tag) => postHashtags.includes(tag.toLowerCase().replace('#', '')));
|
|
376
|
+
if (!hasMatchingHashtag) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
446
379
|
}
|
|
447
380
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
reach: bufferStats.reach || 0,
|
|
462
|
-
saves: undefined,
|
|
463
|
-
videoViews: undefined,
|
|
464
|
-
platformMetrics: bufferStats
|
|
465
|
-
};
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Extract hashtags from post content
|
|
386
|
+
*/
|
|
387
|
+
protected extractHashtags(content: string): string[] {
|
|
388
|
+
const regex = /#(\w+)/g;
|
|
389
|
+
const hashtags: string[] = [];
|
|
390
|
+
let match;
|
|
391
|
+
|
|
392
|
+
while ((match = regex.exec(content)) !== null) {
|
|
393
|
+
hashtags.push(match[1].toLowerCase());
|
|
466
394
|
}
|
|
467
395
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return 'INVALID_MEDIA';
|
|
486
|
-
}
|
|
487
|
-
}
|
|
396
|
+
return hashtags;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Normalize Buffer post to common format
|
|
401
|
+
*/
|
|
402
|
+
protected normalizePost(bufferPost: any): SocialPost {
|
|
403
|
+
const media: string[] = [];
|
|
404
|
+
|
|
405
|
+
if (bufferPost.media) {
|
|
406
|
+
if (bufferPost.media.picture) {
|
|
407
|
+
media.push(bufferPost.media.picture);
|
|
408
|
+
}
|
|
409
|
+
if (bufferPost.media.link) {
|
|
410
|
+
media.push(bufferPost.media.link);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
488
413
|
|
|
489
|
-
|
|
414
|
+
return {
|
|
415
|
+
id: bufferPost.id,
|
|
416
|
+
platform: 'Buffer',
|
|
417
|
+
profileId: bufferPost.profile_id,
|
|
418
|
+
content: bufferPost.text || '',
|
|
419
|
+
mediaUrls: media,
|
|
420
|
+
publishedAt: new Date(bufferPost.sent_at * 1000), // Convert Unix timestamp
|
|
421
|
+
scheduledFor: bufferPost.due_at ? new Date(bufferPost.due_at * 1000) : undefined,
|
|
422
|
+
analytics: bufferPost.statistics ? this.normalizeAnalytics(bufferPost.statistics) : undefined,
|
|
423
|
+
platformSpecificData: {
|
|
424
|
+
profileService: bufferPost.profile_service,
|
|
425
|
+
status: bufferPost.status,
|
|
426
|
+
userId: bufferPost.user_id,
|
|
427
|
+
viaName: bufferPost.via,
|
|
428
|
+
sourceUrl: bufferPost.source_url,
|
|
429
|
+
day: bufferPost.day,
|
|
430
|
+
dueTime: bufferPost.due_time,
|
|
431
|
+
mediaDescription: bufferPost.media?.description,
|
|
432
|
+
mediaTitle: bufferPost.media?.title,
|
|
433
|
+
mediaLink: bufferPost.media?.link,
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Normalize Buffer analytics to common format
|
|
440
|
+
*/
|
|
441
|
+
protected normalizeAnalytics(bufferStats: any): SocialAnalytics {
|
|
442
|
+
return {
|
|
443
|
+
impressions: bufferStats.reach || 0,
|
|
444
|
+
engagements:
|
|
445
|
+
(bufferStats.clicks || 0) +
|
|
446
|
+
(bufferStats.favorites || 0) +
|
|
447
|
+
(bufferStats.mentions || 0) +
|
|
448
|
+
(bufferStats.retweets || 0) +
|
|
449
|
+
(bufferStats.shares || 0) +
|
|
450
|
+
(bufferStats.comments || 0),
|
|
451
|
+
clicks: bufferStats.clicks || 0,
|
|
452
|
+
shares: bufferStats.shares || bufferStats.retweets || 0,
|
|
453
|
+
comments: bufferStats.comments || bufferStats.mentions || 0,
|
|
454
|
+
likes: bufferStats.favorites || bufferStats.likes || 0,
|
|
455
|
+
reach: bufferStats.reach || 0,
|
|
456
|
+
saves: undefined,
|
|
457
|
+
videoViews: undefined,
|
|
458
|
+
platformMetrics: bufferStats,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Map Buffer error to our error codes
|
|
464
|
+
*/
|
|
465
|
+
protected mapBufferError(error: any): string {
|
|
466
|
+
if (error.response) {
|
|
467
|
+
const status = error.response.status;
|
|
468
|
+
const data = error.response.data;
|
|
469
|
+
|
|
470
|
+
if (status === 401) {
|
|
471
|
+
return 'INVALID_TOKEN';
|
|
472
|
+
} else if (status === 429) {
|
|
473
|
+
return 'RATE_LIMIT';
|
|
474
|
+
} else if (status === 403) {
|
|
475
|
+
return 'INSUFFICIENT_PERMISSIONS';
|
|
476
|
+
} else if (status === 404) {
|
|
477
|
+
return 'POST_NOT_FOUND';
|
|
478
|
+
} else if (data?.error?.includes('media')) {
|
|
479
|
+
return 'INVALID_MEDIA';
|
|
480
|
+
}
|
|
490
481
|
}
|
|
491
|
-
|
|
482
|
+
|
|
483
|
+
return 'PLATFORM_ERROR';
|
|
484
|
+
}
|
|
485
|
+
}
|