@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
|
@@ -2,7 +2,7 @@ import { RegisterClass } from '@memberjunction/global';
|
|
|
2
2
|
import { BaseSocialMediaAction, MediaFile, SocialPost, SearchParams, SocialAnalytics } from '../../base/base-social.action';
|
|
3
3
|
import axios, { AxiosInstance, AxiosError } from 'axios';
|
|
4
4
|
import { ActionParam, ActionResultSimple, RunActionParams } from '@memberjunction/actions-base';
|
|
5
|
-
import { LogStatus, LogError } from '@memberjunction/
|
|
5
|
+
import { LogStatus, LogError } from '@memberjunction/global';
|
|
6
6
|
import FormData from 'form-data';
|
|
7
7
|
import { BaseAction } from '@memberjunction/actions';
|
|
8
8
|
|
|
@@ -13,739 +13,722 @@ import { BaseAction } from '@memberjunction/actions';
|
|
|
13
13
|
*/
|
|
14
14
|
@RegisterClass(BaseAction, 'FacebookBaseAction')
|
|
15
15
|
export abstract class FacebookBaseAction extends BaseSocialMediaAction {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Internal method that must be implemented by derived action classes
|
|
18
|
+
*/
|
|
19
|
+
protected abstract InternalRunAction(params: RunActionParams): Promise<ActionResultSimple>;
|
|
20
|
+
protected get platformName(): string {
|
|
21
|
+
return 'Facebook';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
protected get apiBaseUrl(): string {
|
|
25
|
+
return 'https://graph.facebook.com/v18.0';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* API version for cleaner URL building
|
|
30
|
+
*/
|
|
31
|
+
protected get apiVersion(): string {
|
|
32
|
+
return 'v18.0';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Axios instance for making HTTP requests
|
|
37
|
+
*/
|
|
38
|
+
private _axiosInstance: AxiosInstance | null = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get or create axios instance with interceptors
|
|
42
|
+
*/
|
|
43
|
+
protected get axiosInstance(): AxiosInstance {
|
|
44
|
+
if (!this._axiosInstance) {
|
|
45
|
+
this._axiosInstance = axios.create({
|
|
46
|
+
baseURL: this.apiBaseUrl,
|
|
47
|
+
timeout: 30000,
|
|
48
|
+
headers: {
|
|
49
|
+
'Content-Type': 'application/json',
|
|
50
|
+
Accept: 'application/json',
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Add request interceptor for auth
|
|
55
|
+
this._axiosInstance.interceptors.request.use(
|
|
56
|
+
(config) => {
|
|
57
|
+
const token = this.getAccessToken();
|
|
58
|
+
if (token) {
|
|
59
|
+
// Facebook uses access_token as query parameter
|
|
60
|
+
config.params = {
|
|
61
|
+
...config.params,
|
|
62
|
+
access_token: token,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return config;
|
|
66
|
+
},
|
|
67
|
+
(error) => Promise.reject(error)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Add response interceptor for rate limit handling
|
|
71
|
+
this._axiosInstance.interceptors.response.use(
|
|
72
|
+
(response) => {
|
|
73
|
+
// Log rate limit info if available
|
|
74
|
+
const rateLimitInfo = this.parseRateLimitHeaders(response.headers);
|
|
75
|
+
if (rateLimitInfo) {
|
|
76
|
+
LogStatus(`Facebook Rate Limit - Remaining: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}`);
|
|
77
|
+
}
|
|
78
|
+
return response;
|
|
79
|
+
},
|
|
80
|
+
async (error: AxiosError) => {
|
|
81
|
+
if (error.response?.status === 429 || this.isFacebookRateLimitError(error)) {
|
|
82
|
+
// Rate limit exceeded - Facebook returns various codes
|
|
83
|
+
const waitTime = this.extractRateLimitWaitTime(error) || 60;
|
|
84
|
+
await this.handleRateLimit(waitTime);
|
|
85
|
+
|
|
86
|
+
// Retry the request
|
|
87
|
+
return this._axiosInstance!.request(error.config!);
|
|
88
|
+
}
|
|
89
|
+
return Promise.reject(error);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
22
92
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
93
|
+
return this._axiosInstance;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if error is a Facebook rate limit error
|
|
98
|
+
*/
|
|
99
|
+
private isFacebookRateLimitError(error: AxiosError): boolean {
|
|
100
|
+
const errorData = error.response?.data as any;
|
|
101
|
+
const errorCode = errorData?.error?.code;
|
|
102
|
+
const errorSubcode = errorData?.error?.error_subcode;
|
|
103
|
+
|
|
104
|
+
// Facebook rate limit error codes
|
|
105
|
+
return (
|
|
106
|
+
errorCode === 4 || // Application request limit reached
|
|
107
|
+
errorCode === 17 || // User request limit reached
|
|
108
|
+
errorCode === 32 || // Page request limit reached
|
|
109
|
+
errorCode === 613 || // Calls to stream have exceeded the rate limit
|
|
110
|
+
errorSubcode === 2446079
|
|
111
|
+
); // Reduce the amount of data you're asking for
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Extract wait time from Facebook rate limit error
|
|
116
|
+
*/
|
|
117
|
+
private extractRateLimitWaitTime(error: AxiosError): number | null {
|
|
118
|
+
const errorData = error.response?.data as any;
|
|
119
|
+
const headers = error.response?.headers;
|
|
120
|
+
|
|
121
|
+
// Check headers first
|
|
122
|
+
if (headers?.['x-app-usage'] || headers?.['x-page-usage'] || headers?.['x-ad-account-usage']) {
|
|
123
|
+
// Parse usage headers to determine wait time
|
|
124
|
+
const usage = JSON.parse(headers['x-app-usage'] || headers['x-page-usage'] || headers['x-ad-account-usage']);
|
|
125
|
+
if (usage.call_count > 90) {
|
|
126
|
+
return 300; // Wait 5 minutes if over 90% usage
|
|
127
|
+
}
|
|
26
128
|
}
|
|
27
129
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return 'v18.0';
|
|
130
|
+
// Check error message for wait time
|
|
131
|
+
const message = errorData?.error?.message;
|
|
132
|
+
if (message && message.includes('Please retry your request later')) {
|
|
133
|
+
return 300; // Default to 5 minutes
|
|
33
134
|
}
|
|
34
135
|
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (rateLimitInfo) {
|
|
76
|
-
LogStatus(`Facebook Rate Limit - Remaining: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}`);
|
|
77
|
-
}
|
|
78
|
-
return response;
|
|
79
|
-
},
|
|
80
|
-
async (error: AxiosError) => {
|
|
81
|
-
if (error.response?.status === 429 || this.isFacebookRateLimitError(error)) {
|
|
82
|
-
// Rate limit exceeded - Facebook returns various codes
|
|
83
|
-
const waitTime = this.extractRateLimitWaitTime(error) || 60;
|
|
84
|
-
await this.handleRateLimit(waitTime);
|
|
85
|
-
|
|
86
|
-
// Retry the request
|
|
87
|
-
return this._axiosInstance!.request(error.config!);
|
|
88
|
-
}
|
|
89
|
-
return Promise.reject(error);
|
|
90
|
-
}
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
return this._axiosInstance;
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Refresh the access token using the refresh token
|
|
141
|
+
* Note: Facebook uses long-lived tokens that last 60 days
|
|
142
|
+
*/
|
|
143
|
+
protected async refreshAccessToken(): Promise<void> {
|
|
144
|
+
try {
|
|
145
|
+
const currentToken = this.getAccessToken();
|
|
146
|
+
if (!currentToken) {
|
|
147
|
+
throw new Error('No access token available for Facebook');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const clientId = this.getCustomAttribute(2) || ''; // App ID stored in CustomAttribute2
|
|
151
|
+
const clientSecret = this.getCustomAttribute(3) || ''; // App Secret stored in CustomAttribute3
|
|
152
|
+
|
|
153
|
+
// Exchange short-lived token for long-lived token
|
|
154
|
+
const response = await axios.get(`${this.apiBaseUrl}/oauth/access_token`, {
|
|
155
|
+
params: {
|
|
156
|
+
grant_type: 'fb_exchange_token',
|
|
157
|
+
client_id: clientId,
|
|
158
|
+
client_secret: clientSecret,
|
|
159
|
+
fb_exchange_token: currentToken,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const { access_token, expires_in } = response.data;
|
|
164
|
+
|
|
165
|
+
// Update stored tokens
|
|
166
|
+
await this.updateStoredTokens(
|
|
167
|
+
access_token,
|
|
168
|
+
undefined, // Facebook doesn't use refresh tokens
|
|
169
|
+
expires_in || 5184000 // Default to 60 days if not specified
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
LogStatus('Facebook access token refreshed successfully');
|
|
173
|
+
} catch (error) {
|
|
174
|
+
LogError(`Failed to refresh Facebook access token: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
175
|
+
throw error;
|
|
94
176
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Extract wait time from Facebook rate limit error
|
|
114
|
-
*/
|
|
115
|
-
private extractRateLimitWaitTime(error: AxiosError): number | null {
|
|
116
|
-
const errorData = error.response?.data as any;
|
|
117
|
-
const headers = error.response?.headers;
|
|
118
|
-
|
|
119
|
-
// Check headers first
|
|
120
|
-
if (headers?.['x-app-usage'] || headers?.['x-page-usage'] || headers?.['x-ad-account-usage']) {
|
|
121
|
-
// Parse usage headers to determine wait time
|
|
122
|
-
const usage = JSON.parse(headers['x-app-usage'] || headers['x-page-usage'] || headers['x-ad-account-usage']);
|
|
123
|
-
if (usage.call_count > 90) {
|
|
124
|
-
return 300; // Wait 5 minutes if over 90% usage
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Check error message for wait time
|
|
129
|
-
const message = errorData?.error?.message;
|
|
130
|
-
if (message && message.includes('Please retry your request later')) {
|
|
131
|
-
return 300; // Default to 5 minutes
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get the authenticated user's pages
|
|
181
|
+
*/
|
|
182
|
+
protected async getUserPages(): Promise<FacebookPage[]> {
|
|
183
|
+
try {
|
|
184
|
+
const response = await this.axiosInstance.get('/me/accounts', {
|
|
185
|
+
params: {
|
|
186
|
+
fields: 'id,name,access_token,category,picture',
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
return response.data.data || [];
|
|
190
|
+
} catch (error) {
|
|
191
|
+
LogError(`Failed to get user pages: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
192
|
+
throw error;
|
|
135
193
|
}
|
|
194
|
+
}
|
|
136
195
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const currentToken = this.getAccessToken();
|
|
144
|
-
if (!currentToken) {
|
|
145
|
-
throw new Error('No access token available for Facebook');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const clientId = this.getCustomAttribute(2) || ''; // App ID stored in CustomAttribute2
|
|
149
|
-
const clientSecret = this.getCustomAttribute(3) || ''; // App Secret stored in CustomAttribute3
|
|
150
|
-
|
|
151
|
-
// Exchange short-lived token for long-lived token
|
|
152
|
-
const response = await axios.get(`${this.apiBaseUrl}/oauth/access_token`, {
|
|
153
|
-
params: {
|
|
154
|
-
grant_type: 'fb_exchange_token',
|
|
155
|
-
client_id: clientId,
|
|
156
|
-
client_secret: clientSecret,
|
|
157
|
-
fb_exchange_token: currentToken
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
const { access_token, expires_in } = response.data;
|
|
162
|
-
|
|
163
|
-
// Update stored tokens
|
|
164
|
-
await this.updateStoredTokens(
|
|
165
|
-
access_token,
|
|
166
|
-
undefined, // Facebook doesn't use refresh tokens
|
|
167
|
-
expires_in || 5184000 // Default to 60 days if not specified
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
LogStatus('Facebook access token refreshed successfully');
|
|
171
|
-
} catch (error) {
|
|
172
|
-
LogError(`Failed to refresh Facebook access token: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
196
|
+
/**
|
|
197
|
+
* Get page access token for a specific page
|
|
198
|
+
*/
|
|
199
|
+
protected async getPageAccessToken(pageId: string): Promise<string> {
|
|
200
|
+
const pages = await this.getUserPages();
|
|
201
|
+
const page = pages.find((p) => p.id === pageId);
|
|
176
202
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
*/
|
|
180
|
-
protected async getUserPages(): Promise<FacebookPage[]> {
|
|
181
|
-
try {
|
|
182
|
-
const response = await this.axiosInstance.get('/me/accounts', {
|
|
183
|
-
params: {
|
|
184
|
-
fields: 'id,name,access_token,category,picture'
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
return response.data.data || [];
|
|
188
|
-
} catch (error) {
|
|
189
|
-
LogError(`Failed to get user pages: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
203
|
+
if (!page) {
|
|
204
|
+
throw new Error(`Page ${pageId} not found or user doesn't have access`);
|
|
192
205
|
}
|
|
193
206
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
return page.access_token;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Upload media to Facebook
|
|
212
|
+
*/
|
|
213
|
+
protected async uploadSingleMedia(file: MediaFile): Promise<string> {
|
|
214
|
+
try {
|
|
215
|
+
const fileData = typeof file.data === 'string' ? Buffer.from(file.data, 'base64') : file.data;
|
|
216
|
+
|
|
217
|
+
const formData = new FormData();
|
|
218
|
+
formData.append('source', fileData, {
|
|
219
|
+
filename: file.filename,
|
|
220
|
+
contentType: file.mimeType,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const isVideo = file.mimeType.startsWith('video/');
|
|
224
|
+
const endpoint = isVideo ? '/me/videos' : '/me/photos';
|
|
225
|
+
|
|
226
|
+
const response = await this.axiosInstance.post(endpoint, formData, {
|
|
227
|
+
headers: formData.getHeaders(),
|
|
228
|
+
params: {
|
|
229
|
+
published: 'false', // Upload as unpublished for later use
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return response.data.id;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
LogError(`Failed to upload media to Facebook: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
236
|
+
throw error;
|
|
206
237
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Upload media to a specific page
|
|
242
|
+
*/
|
|
243
|
+
protected async uploadMediaToPage(pageId: string, file: MediaFile): Promise<string> {
|
|
244
|
+
try {
|
|
245
|
+
const fileData = typeof file.data === 'string' ? Buffer.from(file.data, 'base64') : file.data;
|
|
246
|
+
|
|
247
|
+
const pageToken = await this.getPageAccessToken(pageId);
|
|
248
|
+
const formData = new FormData();
|
|
249
|
+
formData.append('source', fileData, {
|
|
250
|
+
filename: file.filename,
|
|
251
|
+
contentType: file.mimeType,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const isVideo = file.mimeType.startsWith('video/');
|
|
255
|
+
const endpoint = `/${pageId}/${isVideo ? 'videos' : 'photos'}`;
|
|
256
|
+
|
|
257
|
+
const response = await axios.post(`${this.apiBaseUrl}${endpoint}`, formData, {
|
|
258
|
+
headers: formData.getHeaders(),
|
|
259
|
+
params: {
|
|
260
|
+
access_token: pageToken,
|
|
261
|
+
published: 'false', // Upload as unpublished for later use
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return response.data.id;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
LogError(`Failed to upload media to Facebook page: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
268
|
+
throw error;
|
|
238
269
|
}
|
|
270
|
+
}
|
|
239
271
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const fileData = typeof file.data === 'string'
|
|
246
|
-
? Buffer.from(file.data, 'base64')
|
|
247
|
-
: file.data;
|
|
248
|
-
|
|
249
|
-
const pageToken = await this.getPageAccessToken(pageId);
|
|
250
|
-
const formData = new FormData();
|
|
251
|
-
formData.append('source', fileData, {
|
|
252
|
-
filename: file.filename,
|
|
253
|
-
contentType: file.mimeType
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
const isVideo = file.mimeType.startsWith('video/');
|
|
257
|
-
const endpoint = `/${pageId}/${isVideo ? 'videos' : 'photos'}`;
|
|
258
|
-
|
|
259
|
-
const response = await axios.post(`${this.apiBaseUrl}${endpoint}`, formData, {
|
|
260
|
-
headers: formData.getHeaders(),
|
|
261
|
-
params: {
|
|
262
|
-
access_token: pageToken,
|
|
263
|
-
published: 'false' // Upload as unpublished for later use
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
return response.data.id;
|
|
268
|
-
} catch (error) {
|
|
269
|
-
LogError(`Failed to upload media to Facebook page: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
270
|
-
throw error;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
272
|
+
/**
|
|
273
|
+
* Validate media file meets Facebook requirements
|
|
274
|
+
*/
|
|
275
|
+
protected validateMediaFile(file: MediaFile): void {
|
|
276
|
+
const supportedImageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff'];
|
|
273
277
|
|
|
274
|
-
|
|
275
|
-
* Validate media file meets Facebook requirements
|
|
276
|
-
*/
|
|
277
|
-
protected validateMediaFile(file: MediaFile): void {
|
|
278
|
-
const supportedImageTypes = [
|
|
279
|
-
'image/jpeg',
|
|
280
|
-
'image/png',
|
|
281
|
-
'image/gif',
|
|
282
|
-
'image/bmp',
|
|
283
|
-
'image/tiff'
|
|
284
|
-
];
|
|
285
|
-
|
|
286
|
-
const supportedVideoTypes = [
|
|
287
|
-
'video/mp4',
|
|
288
|
-
'video/quicktime',
|
|
289
|
-
'video/x-matroska',
|
|
290
|
-
'video/webm'
|
|
291
|
-
];
|
|
292
|
-
|
|
293
|
-
const supportedTypes = [...supportedImageTypes, ...supportedVideoTypes];
|
|
294
|
-
|
|
295
|
-
if (!supportedTypes.includes(file.mimeType)) {
|
|
296
|
-
throw new Error(`Unsupported media type: ${file.mimeType}. Supported types: ${supportedTypes.join(', ')}`);
|
|
297
|
-
}
|
|
278
|
+
const supportedVideoTypes = ['video/mp4', 'video/quicktime', 'video/x-matroska', 'video/webm'];
|
|
298
279
|
|
|
299
|
-
|
|
300
|
-
let maxSize: number;
|
|
301
|
-
if (supportedImageTypes.includes(file.mimeType)) {
|
|
302
|
-
maxSize = 4 * 1024 * 1024; // 4MB for images
|
|
303
|
-
} else if (supportedVideoTypes.includes(file.mimeType)) {
|
|
304
|
-
maxSize = 10 * 1024 * 1024 * 1024; // 10GB for videos
|
|
305
|
-
} else {
|
|
306
|
-
maxSize = 4 * 1024 * 1024; // Default 4MB
|
|
307
|
-
}
|
|
280
|
+
const supportedTypes = [...supportedImageTypes, ...supportedVideoTypes];
|
|
308
281
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
282
|
+
if (!supportedTypes.includes(file.mimeType)) {
|
|
283
|
+
throw new Error(`Unsupported media type: ${file.mimeType}. Supported types: ${supportedTypes.join(', ')}`);
|
|
312
284
|
}
|
|
313
285
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
`${this.apiBaseUrl}/${pageId}/feed`,
|
|
323
|
-
postData,
|
|
324
|
-
{
|
|
325
|
-
params: {
|
|
326
|
-
access_token: pageToken
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
);
|
|
330
|
-
|
|
331
|
-
// Get the full post data
|
|
332
|
-
return await this.getPost(response.data.id);
|
|
333
|
-
} catch (error) {
|
|
334
|
-
this.handleFacebookError(error as AxiosError);
|
|
335
|
-
}
|
|
286
|
+
// Facebook media size limits
|
|
287
|
+
let maxSize: number;
|
|
288
|
+
if (supportedImageTypes.includes(file.mimeType)) {
|
|
289
|
+
maxSize = 4 * 1024 * 1024; // 4MB for images
|
|
290
|
+
} else if (supportedVideoTypes.includes(file.mimeType)) {
|
|
291
|
+
maxSize = 10 * 1024 * 1024 * 1024; // 10GB for videos
|
|
292
|
+
} else {
|
|
293
|
+
maxSize = 4 * 1024 * 1024; // Default 4MB
|
|
336
294
|
}
|
|
337
295
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
*/
|
|
341
|
-
protected async getPost(postId: string): Promise<FacebookPost> {
|
|
342
|
-
try {
|
|
343
|
-
const response = await this.axiosInstance.get(`/${postId}`, {
|
|
344
|
-
params: {
|
|
345
|
-
fields: this.getPostFields()
|
|
346
|
-
}
|
|
347
|
-
});
|
|
348
|
-
return response.data;
|
|
349
|
-
} catch (error) {
|
|
350
|
-
LogError(`Failed to get post: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
351
|
-
throw error;
|
|
352
|
-
}
|
|
296
|
+
if (file.size > maxSize) {
|
|
297
|
+
throw new Error(`File size exceeds limit. Max: ${maxSize / 1024 / 1024}MB, Got: ${file.size / 1024 / 1024}MB`);
|
|
353
298
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return response.data.data || [];
|
|
374
|
-
} catch (error) {
|
|
375
|
-
LogError(`Failed to get page posts: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
376
|
-
throw error;
|
|
377
|
-
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Create a post on Facebook
|
|
303
|
+
*/
|
|
304
|
+
protected async createPost(pageId: string, postData: CreatePostData): Promise<FacebookPost> {
|
|
305
|
+
try {
|
|
306
|
+
const pageToken = await this.getPageAccessToken(pageId);
|
|
307
|
+
|
|
308
|
+
const response = await axios.post(`${this.apiBaseUrl}/${pageId}/feed`, postData, {
|
|
309
|
+
params: {
|
|
310
|
+
access_token: pageToken,
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Get the full post data
|
|
315
|
+
return await this.getPost(response.data.id);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
this.handleFacebookError(error as AxiosError);
|
|
378
318
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Check if we've reached max results
|
|
398
|
-
if (maxResults && results.length >= maxResults) {
|
|
399
|
-
return results.slice(0, maxResults);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Get next page URL
|
|
403
|
-
nextUrl = response.data.paging?.next || null;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return results;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get a specific post
|
|
323
|
+
*/
|
|
324
|
+
protected async getPost(postId: string): Promise<FacebookPost> {
|
|
325
|
+
try {
|
|
326
|
+
const response = await this.axiosInstance.get(`/${postId}`, {
|
|
327
|
+
params: {
|
|
328
|
+
fields: this.getPostFields(),
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
return response.data;
|
|
332
|
+
} catch (error) {
|
|
333
|
+
LogError(`Failed to get post: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
334
|
+
throw error;
|
|
407
335
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get posts from a page
|
|
340
|
+
*/
|
|
341
|
+
protected async getPagePosts(pageId: string, params: GetPagePostsParams = {}): Promise<FacebookPost[]> {
|
|
342
|
+
try {
|
|
343
|
+
const pageToken = await this.getPageAccessToken(pageId);
|
|
344
|
+
|
|
345
|
+
const response = await axios.get(`${this.apiBaseUrl}/${pageId}/posts`, {
|
|
346
|
+
params: {
|
|
347
|
+
access_token: pageToken,
|
|
348
|
+
fields: this.getPostFields(),
|
|
349
|
+
limit: params.limit || 100,
|
|
350
|
+
since: params.since ? Math.floor(params.since.getTime() / 1000) : undefined,
|
|
351
|
+
until: params.until ? Math.floor(params.until.getTime() / 1000) : undefined,
|
|
352
|
+
...params,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
return response.data.data || [];
|
|
357
|
+
} catch (error) {
|
|
358
|
+
LogError(`Failed to get page posts: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get paginated results from Facebook
|
|
365
|
+
*/
|
|
366
|
+
protected async getPaginatedResults<T>(url: string, params: Record<string, any> = {}, maxResults?: number): Promise<T[]> {
|
|
367
|
+
const results: T[] = [];
|
|
368
|
+
let nextUrl: string | null = url;
|
|
369
|
+
const limit = params.limit || 100;
|
|
370
|
+
|
|
371
|
+
while (nextUrl) {
|
|
372
|
+
const response = await axios.get(nextUrl, {
|
|
373
|
+
params: nextUrl === url ? { ...params, limit } : {},
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (response.data.data && Array.isArray(response.data.data)) {
|
|
377
|
+
results.push(...response.data.data);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check if we've reached max results
|
|
381
|
+
if (maxResults && results.length >= maxResults) {
|
|
382
|
+
return results.slice(0, maxResults);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Get next page URL
|
|
386
|
+
nextUrl = response.data.paging?.next || null;
|
|
414
387
|
}
|
|
415
388
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
389
|
+
return results;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Get default fields for post queries
|
|
394
|
+
*/
|
|
395
|
+
protected getPostFields(): string {
|
|
396
|
+
return 'id,message,created_time,updated_time,from,story,permalink_url,attachments,shares,reactions.summary(true),comments.summary(true),insights.metric(post_impressions,post_impressions_unique,post_engaged_users,post_clicks,post_reactions_by_type_total)';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Convert Facebook post to common format
|
|
401
|
+
*/
|
|
402
|
+
protected normalizePost(fbPost: FacebookPost): SocialPost {
|
|
403
|
+
const mediaUrls: string[] = [];
|
|
404
|
+
|
|
405
|
+
// Extract media URLs from attachments
|
|
406
|
+
if (fbPost.attachments?.data) {
|
|
407
|
+
for (const attachment of fbPost.attachments.data) {
|
|
408
|
+
if (attachment.media?.image?.src) {
|
|
409
|
+
mediaUrls.push(attachment.media.image.src);
|
|
410
|
+
} else if (attachment.media?.source) {
|
|
411
|
+
mediaUrls.push(attachment.media.source);
|
|
431
412
|
}
|
|
432
|
-
|
|
433
|
-
return {
|
|
434
|
-
id: fbPost.id,
|
|
435
|
-
platform: 'Facebook',
|
|
436
|
-
profileId: fbPost.from?.id || '',
|
|
437
|
-
content: fbPost.message || fbPost.story || '',
|
|
438
|
-
mediaUrls,
|
|
439
|
-
publishedAt: new Date(fbPost.created_time),
|
|
440
|
-
analytics: this.extractPostAnalytics(fbPost),
|
|
441
|
-
platformSpecificData: {
|
|
442
|
-
permalinkUrl: fbPost.permalink_url,
|
|
443
|
-
story: fbPost.story,
|
|
444
|
-
attachments: fbPost.attachments,
|
|
445
|
-
postType: fbPost.attachments?.data?.[0]?.type || 'status'
|
|
446
|
-
}
|
|
447
|
-
};
|
|
413
|
+
}
|
|
448
414
|
}
|
|
449
415
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
416
|
+
return {
|
|
417
|
+
id: fbPost.id,
|
|
418
|
+
platform: 'Facebook',
|
|
419
|
+
profileId: fbPost.from?.id || '',
|
|
420
|
+
content: fbPost.message || fbPost.story || '',
|
|
421
|
+
mediaUrls,
|
|
422
|
+
publishedAt: new Date(fbPost.created_time),
|
|
423
|
+
analytics: this.extractPostAnalytics(fbPost),
|
|
424
|
+
platformSpecificData: {
|
|
425
|
+
permalinkUrl: fbPost.permalink_url,
|
|
426
|
+
story: fbPost.story,
|
|
427
|
+
attachments: fbPost.attachments,
|
|
428
|
+
postType: fbPost.attachments?.data?.[0]?.type || 'status',
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Extract analytics from Facebook post
|
|
435
|
+
*/
|
|
436
|
+
private extractPostAnalytics(fbPost: FacebookPost): SocialAnalytics | undefined {
|
|
437
|
+
const insights = fbPost.insights?.data;
|
|
438
|
+
const reactions = fbPost.reactions?.summary?.total_count || 0;
|
|
439
|
+
const comments = fbPost.comments?.summary?.total_count || 0;
|
|
440
|
+
const shares = fbPost.shares?.count || 0;
|
|
441
|
+
|
|
442
|
+
// Extract metrics from insights
|
|
443
|
+
let impressions = 0;
|
|
444
|
+
let reach = 0;
|
|
445
|
+
let engagements = 0;
|
|
446
|
+
let clicks = 0;
|
|
447
|
+
|
|
448
|
+
if (insights) {
|
|
449
|
+
for (const insight of insights) {
|
|
450
|
+
switch (insight.name) {
|
|
451
|
+
case 'post_impressions':
|
|
452
|
+
impressions = insight.values?.[0]?.value || 0;
|
|
453
|
+
break;
|
|
454
|
+
case 'post_impressions_unique':
|
|
455
|
+
reach = insight.values?.[0]?.value || 0;
|
|
456
|
+
break;
|
|
457
|
+
case 'post_engaged_users':
|
|
458
|
+
engagements = insight.values?.[0]?.value || 0;
|
|
459
|
+
break;
|
|
460
|
+
case 'post_clicks':
|
|
461
|
+
clicks = insight.values?.[0]?.value || 0;
|
|
462
|
+
break;
|
|
482
463
|
}
|
|
483
|
-
|
|
484
|
-
return {
|
|
485
|
-
impressions,
|
|
486
|
-
reach,
|
|
487
|
-
engagements: engagements || (reactions + comments + shares),
|
|
488
|
-
clicks,
|
|
489
|
-
shares,
|
|
490
|
-
comments,
|
|
491
|
-
likes: reactions,
|
|
492
|
-
platformMetrics: {
|
|
493
|
-
reactions,
|
|
494
|
-
comments,
|
|
495
|
-
shares,
|
|
496
|
-
insights
|
|
497
|
-
}
|
|
498
|
-
};
|
|
464
|
+
}
|
|
499
465
|
}
|
|
500
466
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
467
|
+
return {
|
|
468
|
+
impressions,
|
|
469
|
+
reach,
|
|
470
|
+
engagements: engagements || reactions + comments + shares,
|
|
471
|
+
clicks,
|
|
472
|
+
shares,
|
|
473
|
+
comments,
|
|
474
|
+
likes: reactions,
|
|
475
|
+
platformMetrics: {
|
|
476
|
+
reactions,
|
|
477
|
+
comments,
|
|
478
|
+
shares,
|
|
479
|
+
insights,
|
|
480
|
+
},
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Normalize Facebook analytics to common format
|
|
486
|
+
*/
|
|
487
|
+
protected normalizeAnalytics(fbMetrics: any): SocialAnalytics {
|
|
488
|
+
return {
|
|
489
|
+
impressions: fbMetrics.impressions || 0,
|
|
490
|
+
engagements: fbMetrics.engaged_users || 0,
|
|
491
|
+
clicks: fbMetrics.clicks || 0,
|
|
492
|
+
shares: fbMetrics.shares || 0,
|
|
493
|
+
comments: fbMetrics.comments || 0,
|
|
494
|
+
likes: fbMetrics.reactions || fbMetrics.likes || 0,
|
|
495
|
+
reach: fbMetrics.reach || 0,
|
|
496
|
+
saves: fbMetrics.saves,
|
|
497
|
+
videoViews: fbMetrics.video_views,
|
|
498
|
+
platformMetrics: fbMetrics,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Search for posts - implemented in search action
|
|
504
|
+
*/
|
|
505
|
+
protected async searchPosts(params: SearchParams): Promise<SocialPost[]> {
|
|
506
|
+
// This is implemented in the search-posts.action.ts
|
|
507
|
+
throw new Error('Search posts is implemented in FacebookSearchPostsAction');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Handle Facebook-specific errors
|
|
512
|
+
*/
|
|
513
|
+
protected handleFacebookError(error: AxiosError): never {
|
|
514
|
+
if (error.response) {
|
|
515
|
+
const { status, data } = error.response;
|
|
516
|
+
const errorData = data as any;
|
|
517
|
+
const fbError = errorData?.error;
|
|
518
|
+
|
|
519
|
+
if (fbError) {
|
|
520
|
+
const code = fbError.code;
|
|
521
|
+
const message = fbError.message;
|
|
522
|
+
const type = fbError.type;
|
|
523
|
+
const subcode = fbError.error_subcode;
|
|
524
|
+
|
|
525
|
+
// Map Facebook error codes to user-friendly messages
|
|
526
|
+
switch (code) {
|
|
527
|
+
case 1:
|
|
528
|
+
throw new Error(`API Unknown Error: ${message}`);
|
|
529
|
+
case 2:
|
|
530
|
+
throw new Error(`API Service Error: ${message}`);
|
|
531
|
+
case 4:
|
|
532
|
+
throw new Error('Application request limit reached. Please try again later.');
|
|
533
|
+
case 10:
|
|
534
|
+
throw new Error('Permission denied. Ensure the app has required Facebook permissions.');
|
|
535
|
+
case 17:
|
|
536
|
+
throw new Error('User request limit reached. Please try again later.');
|
|
537
|
+
case 32:
|
|
538
|
+
throw new Error('Page request limit reached. Please try again later.');
|
|
539
|
+
case 100:
|
|
540
|
+
throw new Error(`Invalid Parameter: ${message}`);
|
|
541
|
+
case 190:
|
|
542
|
+
throw new Error('Access token has expired. Please re-authenticate.');
|
|
543
|
+
case 200:
|
|
544
|
+
case 210:
|
|
545
|
+
case 220:
|
|
546
|
+
throw new Error(`Permission Error: ${message}`);
|
|
547
|
+
case 368:
|
|
548
|
+
throw new Error('Temporarily blocked from posting. Please try again later.');
|
|
549
|
+
case 506:
|
|
550
|
+
throw new Error('Duplicate post. This content has already been posted.');
|
|
551
|
+
default:
|
|
552
|
+
throw new Error(`Facebook API Error (${code}): ${message}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Generic HTTP errors
|
|
557
|
+
switch (status) {
|
|
558
|
+
case 400:
|
|
559
|
+
throw new Error(`Bad Request: ${errorData.message || 'Invalid request parameters'}`);
|
|
560
|
+
case 401:
|
|
561
|
+
throw new Error('Unauthorized: Invalid or expired access token');
|
|
562
|
+
case 403:
|
|
563
|
+
throw new Error('Forbidden: Insufficient permissions');
|
|
564
|
+
case 404:
|
|
565
|
+
throw new Error('Not Found: Resource does not exist');
|
|
566
|
+
case 500:
|
|
567
|
+
throw new Error('Internal Server Error: Facebook service error');
|
|
568
|
+
case 503:
|
|
569
|
+
throw new Error('Service Unavailable: Facebook service temporarily unavailable');
|
|
570
|
+
default:
|
|
571
|
+
throw new Error(`Facebook API Error (${status}): ${errorData.message || 'Unknown error'}`);
|
|
572
|
+
}
|
|
573
|
+
} else if (error.request) {
|
|
574
|
+
throw new Error('Network Error: No response from Facebook');
|
|
575
|
+
} else {
|
|
576
|
+
throw new Error(`Request Error: ${error.message}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Parse Facebook-specific rate limit headers
|
|
582
|
+
*/
|
|
583
|
+
protected parseRateLimitHeaders(headers: any): { remaining: number; reset: Date; limit: number } | null {
|
|
584
|
+
// Facebook uses different headers for rate limiting
|
|
585
|
+
const appUsage = headers['x-app-usage'];
|
|
586
|
+
const pageUsage = headers['x-page-usage'];
|
|
587
|
+
const adAccountUsage = headers['x-ad-account-usage'];
|
|
588
|
+
|
|
589
|
+
if (appUsage || pageUsage || adAccountUsage) {
|
|
590
|
+
try {
|
|
591
|
+
const usage = JSON.parse(appUsage || pageUsage || adAccountUsage);
|
|
592
|
+
const callCount = usage.call_count || 0;
|
|
593
|
+
const totalTime = usage.total_time || 0;
|
|
594
|
+
const totalCPUTime = usage.total_cputime || 0;
|
|
595
|
+
|
|
596
|
+
// Facebook rate limits are percentage-based
|
|
505
597
|
return {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
shares: fbMetrics.shares || 0,
|
|
510
|
-
comments: fbMetrics.comments || 0,
|
|
511
|
-
likes: fbMetrics.reactions || fbMetrics.likes || 0,
|
|
512
|
-
reach: fbMetrics.reach || 0,
|
|
513
|
-
saves: fbMetrics.saves,
|
|
514
|
-
videoViews: fbMetrics.video_views,
|
|
515
|
-
platformMetrics: fbMetrics
|
|
598
|
+
remaining: Math.max(0, 100 - callCount),
|
|
599
|
+
reset: new Date(Date.now() + 3600000), // Reset in 1 hour
|
|
600
|
+
limit: 100,
|
|
516
601
|
};
|
|
602
|
+
} catch (error) {
|
|
603
|
+
LogError(`Failed to parse Facebook rate limit headers: ${error}`);
|
|
604
|
+
}
|
|
517
605
|
}
|
|
518
606
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
*/
|
|
522
|
-
protected async searchPosts(params: SearchParams): Promise<SocialPost[]> {
|
|
523
|
-
// This is implemented in the search-posts.action.ts
|
|
524
|
-
throw new Error('Search posts is implemented in FacebookSearchPostsAction');
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Handle Facebook-specific errors
|
|
529
|
-
*/
|
|
530
|
-
protected handleFacebookError(error: AxiosError): never {
|
|
531
|
-
if (error.response) {
|
|
532
|
-
const { status, data } = error.response;
|
|
533
|
-
const errorData = data as any;
|
|
534
|
-
const fbError = errorData?.error;
|
|
535
|
-
|
|
536
|
-
if (fbError) {
|
|
537
|
-
const code = fbError.code;
|
|
538
|
-
const message = fbError.message;
|
|
539
|
-
const type = fbError.type;
|
|
540
|
-
const subcode = fbError.error_subcode;
|
|
541
|
-
|
|
542
|
-
// Map Facebook error codes to user-friendly messages
|
|
543
|
-
switch (code) {
|
|
544
|
-
case 1:
|
|
545
|
-
throw new Error(`API Unknown Error: ${message}`);
|
|
546
|
-
case 2:
|
|
547
|
-
throw new Error(`API Service Error: ${message}`);
|
|
548
|
-
case 4:
|
|
549
|
-
throw new Error('Application request limit reached. Please try again later.');
|
|
550
|
-
case 10:
|
|
551
|
-
throw new Error('Permission denied. Ensure the app has required Facebook permissions.');
|
|
552
|
-
case 17:
|
|
553
|
-
throw new Error('User request limit reached. Please try again later.');
|
|
554
|
-
case 32:
|
|
555
|
-
throw new Error('Page request limit reached. Please try again later.');
|
|
556
|
-
case 100:
|
|
557
|
-
throw new Error(`Invalid Parameter: ${message}`);
|
|
558
|
-
case 190:
|
|
559
|
-
throw new Error('Access token has expired. Please re-authenticate.');
|
|
560
|
-
case 200:
|
|
561
|
-
case 210:
|
|
562
|
-
case 220:
|
|
563
|
-
throw new Error(`Permission Error: ${message}`);
|
|
564
|
-
case 368:
|
|
565
|
-
throw new Error('Temporarily blocked from posting. Please try again later.');
|
|
566
|
-
case 506:
|
|
567
|
-
throw new Error('Duplicate post. This content has already been posted.');
|
|
568
|
-
default:
|
|
569
|
-
throw new Error(`Facebook API Error (${code}): ${message}`);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Generic HTTP errors
|
|
574
|
-
switch (status) {
|
|
575
|
-
case 400:
|
|
576
|
-
throw new Error(`Bad Request: ${errorData.message || 'Invalid request parameters'}`);
|
|
577
|
-
case 401:
|
|
578
|
-
throw new Error('Unauthorized: Invalid or expired access token');
|
|
579
|
-
case 403:
|
|
580
|
-
throw new Error('Forbidden: Insufficient permissions');
|
|
581
|
-
case 404:
|
|
582
|
-
throw new Error('Not Found: Resource does not exist');
|
|
583
|
-
case 500:
|
|
584
|
-
throw new Error('Internal Server Error: Facebook service error');
|
|
585
|
-
case 503:
|
|
586
|
-
throw new Error('Service Unavailable: Facebook service temporarily unavailable');
|
|
587
|
-
default:
|
|
588
|
-
throw new Error(`Facebook API Error (${status}): ${errorData.message || 'Unknown error'}`);
|
|
589
|
-
}
|
|
590
|
-
} else if (error.request) {
|
|
591
|
-
throw new Error('Network Error: No response from Facebook');
|
|
592
|
-
} else {
|
|
593
|
-
throw new Error(`Request Error: ${error.message}`);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
/**
|
|
598
|
-
* Parse Facebook-specific rate limit headers
|
|
599
|
-
*/
|
|
600
|
-
protected parseRateLimitHeaders(headers: any): { remaining: number; reset: Date; limit: number; } | null {
|
|
601
|
-
// Facebook uses different headers for rate limiting
|
|
602
|
-
const appUsage = headers['x-app-usage'];
|
|
603
|
-
const pageUsage = headers['x-page-usage'];
|
|
604
|
-
const adAccountUsage = headers['x-ad-account-usage'];
|
|
605
|
-
|
|
606
|
-
if (appUsage || pageUsage || adAccountUsage) {
|
|
607
|
-
try {
|
|
608
|
-
const usage = JSON.parse(appUsage || pageUsage || adAccountUsage);
|
|
609
|
-
const callCount = usage.call_count || 0;
|
|
610
|
-
const totalTime = usage.total_time || 0;
|
|
611
|
-
const totalCPUTime = usage.total_cputime || 0;
|
|
612
|
-
|
|
613
|
-
// Facebook rate limits are percentage-based
|
|
614
|
-
return {
|
|
615
|
-
remaining: Math.max(0, 100 - callCount),
|
|
616
|
-
reset: new Date(Date.now() + 3600000), // Reset in 1 hour
|
|
617
|
-
limit: 100
|
|
618
|
-
};
|
|
619
|
-
} catch (error) {
|
|
620
|
-
LogError(`Failed to parse Facebook rate limit headers: ${error}`);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
return null;
|
|
625
|
-
}
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
626
609
|
}
|
|
627
610
|
|
|
628
611
|
/**
|
|
629
612
|
* Facebook-specific interfaces
|
|
630
613
|
*/
|
|
631
614
|
export interface FacebookPage {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
};
|
|
615
|
+
id: string;
|
|
616
|
+
name: string;
|
|
617
|
+
access_token: string;
|
|
618
|
+
category: string;
|
|
619
|
+
picture?: {
|
|
620
|
+
data: {
|
|
621
|
+
url: string;
|
|
640
622
|
};
|
|
623
|
+
};
|
|
641
624
|
}
|
|
642
625
|
|
|
643
626
|
export interface CreatePostData {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
627
|
+
message?: string;
|
|
628
|
+
link?: string;
|
|
629
|
+
place?: string;
|
|
630
|
+
tags?: string[];
|
|
631
|
+
privacy?: {
|
|
632
|
+
value: 'EVERYONE' | 'ALL_FRIENDS' | 'FRIENDS_OF_FRIENDS' | 'SELF';
|
|
633
|
+
};
|
|
634
|
+
attached_media?: Array<{ media_fbid: string }>;
|
|
635
|
+
scheduled_publish_time?: number; // Unix timestamp
|
|
636
|
+
published?: boolean;
|
|
637
|
+
backdated_time?: number;
|
|
638
|
+
backdated_time_granularity?: 'year' | 'month' | 'day' | 'hour' | 'minute';
|
|
656
639
|
}
|
|
657
640
|
|
|
658
641
|
export interface FacebookPost {
|
|
642
|
+
id: string;
|
|
643
|
+
message?: string;
|
|
644
|
+
story?: string;
|
|
645
|
+
created_time: string;
|
|
646
|
+
updated_time: string;
|
|
647
|
+
from?: {
|
|
659
648
|
id: string;
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
url?: string;
|
|
675
|
-
media?: {
|
|
676
|
-
image?: {
|
|
677
|
-
src: string;
|
|
678
|
-
width: number;
|
|
679
|
-
height: number;
|
|
680
|
-
};
|
|
681
|
-
source?: string;
|
|
682
|
-
};
|
|
683
|
-
}>;
|
|
684
|
-
};
|
|
685
|
-
shares?: {
|
|
686
|
-
count: number;
|
|
687
|
-
};
|
|
688
|
-
reactions?: {
|
|
689
|
-
summary: {
|
|
690
|
-
total_count: number;
|
|
691
|
-
};
|
|
692
|
-
};
|
|
693
|
-
comments?: {
|
|
694
|
-
summary: {
|
|
695
|
-
total_count: number;
|
|
649
|
+
name: string;
|
|
650
|
+
};
|
|
651
|
+
permalink_url?: string;
|
|
652
|
+
attachments?: {
|
|
653
|
+
data: Array<{
|
|
654
|
+
type: string;
|
|
655
|
+
title?: string;
|
|
656
|
+
description?: string;
|
|
657
|
+
url?: string;
|
|
658
|
+
media?: {
|
|
659
|
+
image?: {
|
|
660
|
+
src: string;
|
|
661
|
+
width: number;
|
|
662
|
+
height: number;
|
|
696
663
|
};
|
|
664
|
+
source?: string;
|
|
665
|
+
};
|
|
666
|
+
}>;
|
|
667
|
+
};
|
|
668
|
+
shares?: {
|
|
669
|
+
count: number;
|
|
670
|
+
};
|
|
671
|
+
reactions?: {
|
|
672
|
+
summary: {
|
|
673
|
+
total_count: number;
|
|
697
674
|
};
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
value: number;
|
|
703
|
-
}>;
|
|
704
|
-
}>;
|
|
675
|
+
};
|
|
676
|
+
comments?: {
|
|
677
|
+
summary: {
|
|
678
|
+
total_count: number;
|
|
705
679
|
};
|
|
680
|
+
};
|
|
681
|
+
insights?: {
|
|
682
|
+
data: Array<{
|
|
683
|
+
name: string;
|
|
684
|
+
values: Array<{
|
|
685
|
+
value: number;
|
|
686
|
+
}>;
|
|
687
|
+
}>;
|
|
688
|
+
};
|
|
706
689
|
}
|
|
707
690
|
|
|
708
691
|
export interface GetPagePostsParams {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
692
|
+
since?: Date;
|
|
693
|
+
until?: Date;
|
|
694
|
+
limit?: number;
|
|
695
|
+
published?: boolean;
|
|
713
696
|
}
|
|
714
697
|
|
|
715
698
|
export interface FacebookInsight {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
699
|
+
name: string;
|
|
700
|
+
period: string;
|
|
701
|
+
values: Array<{
|
|
702
|
+
value: number | Record<string, number>;
|
|
703
|
+
end_time?: string;
|
|
704
|
+
}>;
|
|
705
|
+
title?: string;
|
|
706
|
+
description?: string;
|
|
724
707
|
}
|
|
725
708
|
|
|
726
709
|
export interface FacebookComment {
|
|
710
|
+
id: string;
|
|
711
|
+
message: string;
|
|
712
|
+
created_time: string;
|
|
713
|
+
from: {
|
|
727
714
|
id: string;
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
comment_count?: number;
|
|
736
|
-
parent?: {
|
|
737
|
-
id: string;
|
|
738
|
-
};
|
|
715
|
+
name: string;
|
|
716
|
+
};
|
|
717
|
+
like_count: number;
|
|
718
|
+
comment_count?: number;
|
|
719
|
+
parent?: {
|
|
720
|
+
id: string;
|
|
721
|
+
};
|
|
739
722
|
}
|
|
740
723
|
|
|
741
724
|
export interface FacebookAlbum {
|
|
725
|
+
id: string;
|
|
726
|
+
name: string;
|
|
727
|
+
description?: string;
|
|
728
|
+
link: string;
|
|
729
|
+
cover_photo?: {
|
|
742
730
|
id: string;
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
id: string;
|
|
748
|
-
};
|
|
749
|
-
count: number;
|
|
750
|
-
created_time: string;
|
|
751
|
-
}
|
|
731
|
+
};
|
|
732
|
+
count: number;
|
|
733
|
+
created_time: string;
|
|
734
|
+
}
|