@memberjunction/actions-bizapps-social 2.112.0 → 2.113.1
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 +13 -0
- package/dist/base/base-social.action.d.ts.map +1 -1
- package/dist/base/base-social.action.js +24 -18
- 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 +34 -35
- 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 +36 -34
- 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 +27 -25
- 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 +23 -19
- 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 +32 -28
- 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 +44 -42
- 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 +39 -37
- 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 +59 -44
- 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 +31 -33
- 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 +32 -28
- 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 +26 -24
- 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 +34 -32
- 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 +52 -43
- 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 +28 -30
- 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 +20 -18
- 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 +26 -27
- 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 +59 -38
- 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 +25 -23
- 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 +60 -56
- 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 +27 -25
- 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 +45 -55
- 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 +29 -31
- 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 +23 -25
- 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 +30 -32
- 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 +30 -28
- 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 +38 -33
- 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 +26 -25
- 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 +29 -25
- 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 +47 -40
- 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 +31 -30
- 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 +58 -56
- 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 +68 -58
- 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 +25 -22
- package/dist/providers/youtube/youtube-base.action.js.map +1 -1
- package/package.json +6 -5
- package/src/base/base-social.action.ts +224 -217
- package/src/providers/buffer/buffer-base.action.ts +441 -435
- package/src/providers/facebook/actions/boost-post.action.ts +386 -350
- package/src/providers/facebook/actions/create-album.action.ts +307 -291
- package/src/providers/facebook/actions/create-post.action.ts +227 -224
- package/src/providers/facebook/actions/get-page-insights.action.ts +403 -383
- package/src/providers/facebook/actions/get-page-posts.action.ts +225 -214
- package/src/providers/facebook/actions/get-post-insights.action.ts +316 -300
- package/src/providers/facebook/actions/respond-to-comments.action.ts +336 -319
- package/src/providers/facebook/actions/schedule-post.action.ts +292 -289
- package/src/providers/facebook/actions/search-posts.action.ts +413 -399
- package/src/providers/facebook/facebook-base.action.ts +670 -653
- package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
- package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +189 -184
- package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +161 -160
- package/src/providers/hootsuite/actions/get-analytics.action.ts +254 -249
- package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +207 -206
- package/src/providers/hootsuite/actions/get-social-profiles.action.ts +205 -206
- package/src/providers/hootsuite/actions/search-posts.action.ts +369 -351
- package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +209 -211
- package/src/providers/hootsuite/hootsuite-base.action.ts +307 -301
- package/src/providers/instagram/actions/create-post.action.ts +296 -276
- package/src/providers/instagram/actions/create-story.action.ts +394 -378
- package/src/providers/instagram/actions/get-account-insights.action.ts +420 -384
- package/src/providers/instagram/actions/get-business-posts.action.ts +242 -233
- package/src/providers/instagram/actions/get-comments.action.ts +377 -365
- package/src/providers/instagram/actions/get-post-insights.action.ts +273 -265
- package/src/providers/instagram/actions/schedule-post.action.ts +235 -233
- package/src/providers/instagram/actions/search-posts.action.ts +538 -512
- package/src/providers/instagram/instagram-base.action.ts +393 -368
- package/src/providers/linkedin/actions/create-article.action.ts +266 -275
- package/src/providers/linkedin/actions/create-post.action.ts +177 -179
- package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
- package/src/providers/linkedin/actions/get-organization-posts.action.ts +147 -146
- package/src/providers/linkedin/actions/get-personal-posts.action.ts +139 -138
- package/src/providers/linkedin/actions/get-post-analytics.action.ts +189 -190
- package/src/providers/linkedin/actions/schedule-post.action.ts +189 -191
- package/src/providers/linkedin/actions/search-posts.action.ts +283 -275
- package/src/providers/linkedin/linkedin-base.action.ts +421 -407
- package/src/providers/tiktok/tiktok-base.action.ts +320 -305
- package/src/providers/twitter/actions/create-thread.action.ts +207 -203
- package/src/providers/twitter/actions/create-tweet.action.ts +188 -187
- package/src/providers/twitter/actions/delete-tweet.action.ts +129 -128
- package/src/providers/twitter/actions/get-analytics.action.ts +411 -402
- package/src/providers/twitter/actions/get-mentions.action.ts +219 -218
- package/src/providers/twitter/actions/get-timeline.action.ts +233 -232
- package/src/providers/twitter/actions/schedule-tweet.action.ts +222 -221
- package/src/providers/twitter/actions/search-tweets.action.ts +543 -540
- package/src/providers/twitter/twitter-base.action.ts +560 -541
- package/src/providers/youtube/youtube-base.action.ts +333 -320
|
@@ -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/core';
|
|
6
6
|
import FormData from 'form-data';
|
|
7
7
|
import { BaseAction } from '@memberjunction/actions';
|
|
8
8
|
|
|
@@ -13,722 +13,739 @@ 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
|
-
|
|
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
|
-
);
|
|
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';
|
|
92
22
|
}
|
|
93
|
-
|
|
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
|
-
}
|
|
23
|
+
|
|
24
|
+
protected get apiBaseUrl(): string {
|
|
25
|
+
return 'https://graph.facebook.com/v18.0';
|
|
128
26
|
}
|
|
129
27
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
28
|
+
/**
|
|
29
|
+
* API version for cleaner URL building
|
|
30
|
+
*/
|
|
31
|
+
protected get apiVersion(): string {
|
|
32
|
+
return 'v18.0';
|
|
134
33
|
}
|
|
135
34
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return this._axiosInstance;
|
|
176
94
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
throw error;
|
|
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 errorCode === 4 || // Application request limit reached
|
|
106
|
+
errorCode === 17 || // User request limit reached
|
|
107
|
+
errorCode === 32 || // Page request limit reached
|
|
108
|
+
errorCode === 613 || // Calls to stream have exceeded the rate limit
|
|
109
|
+
errorSubcode === 2446079; // Reduce the amount of data you're asking for
|
|
193
110
|
}
|
|
194
|
-
}
|
|
195
111
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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;
|
|
135
|
+
}
|
|
202
136
|
|
|
203
|
-
|
|
204
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Refresh the access token using the refresh token
|
|
139
|
+
* Note: Facebook uses long-lived tokens that last 60 days
|
|
140
|
+
*/
|
|
141
|
+
protected async refreshAccessToken(): Promise<void> {
|
|
142
|
+
try {
|
|
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
|
+
}
|
|
205
175
|
}
|
|
206
176
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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;
|
|
177
|
+
/**
|
|
178
|
+
* Get the authenticated user's pages
|
|
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
|
+
}
|
|
237
192
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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;
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get page access token for a specific page
|
|
196
|
+
*/
|
|
197
|
+
protected async getPageAccessToken(pageId: string): Promise<string> {
|
|
198
|
+
const pages = await this.getUserPages();
|
|
199
|
+
const page = pages.find(p => p.id === pageId);
|
|
200
|
+
|
|
201
|
+
if (!page) {
|
|
202
|
+
throw new Error(`Page ${pageId} not found or user doesn't have access`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return page.access_token;
|
|
269
206
|
}
|
|
270
|
-
}
|
|
271
207
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Upload media to Facebook
|
|
210
|
+
*/
|
|
211
|
+
protected async uploadSingleMedia(file: MediaFile): Promise<string> {
|
|
212
|
+
try {
|
|
213
|
+
const fileData = typeof file.data === 'string'
|
|
214
|
+
? Buffer.from(file.data, 'base64')
|
|
215
|
+
: 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;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
277
239
|
|
|
278
|
-
|
|
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'
|
|
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
|
+
}
|
|
279
273
|
|
|
280
|
-
|
|
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
|
+
}
|
|
281
298
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
299
|
+
// Facebook media size limits
|
|
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
|
+
}
|
|
285
308
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
309
|
+
if (file.size > maxSize) {
|
|
310
|
+
throw new Error(`File size exceeds limit. Max: ${maxSize / 1024 / 1024}MB, Got: ${file.size / 1024 / 1024}MB`);
|
|
311
|
+
}
|
|
294
312
|
}
|
|
295
313
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
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;
|
|
314
|
+
/**
|
|
315
|
+
* Create a post on Facebook
|
|
316
|
+
*/
|
|
317
|
+
protected async createPost(pageId: string, postData: CreatePostData): Promise<FacebookPost> {
|
|
318
|
+
try {
|
|
319
|
+
const pageToken = await this.getPageAccessToken(pageId);
|
|
320
|
+
|
|
321
|
+
const response = await axios.post(
|
|
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
|
+
}
|
|
335
336
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get a specific post
|
|
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
|
+
}
|
|
360
353
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
// Get next page URL
|
|
386
|
-
nextUrl = response.data.paging?.next || null;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Get posts from a page
|
|
357
|
+
*/
|
|
358
|
+
protected async getPagePosts(pageId: string, params: GetPagePostsParams = {}): Promise<FacebookPost[]> {
|
|
359
|
+
try {
|
|
360
|
+
const pageToken = await this.getPageAccessToken(pageId);
|
|
361
|
+
|
|
362
|
+
const response = await axios.get(`${this.apiBaseUrl}/${pageId}/posts`, {
|
|
363
|
+
params: {
|
|
364
|
+
access_token: pageToken,
|
|
365
|
+
fields: this.getPostFields(),
|
|
366
|
+
limit: params.limit || 100,
|
|
367
|
+
since: params.since ? Math.floor(params.since.getTime() / 1000) : undefined,
|
|
368
|
+
until: params.until ? Math.floor(params.until.getTime() / 1000) : undefined,
|
|
369
|
+
...params
|
|
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
|
+
}
|
|
387
378
|
}
|
|
388
379
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
380
|
+
/**
|
|
381
|
+
* Get paginated results from Facebook
|
|
382
|
+
*/
|
|
383
|
+
protected async getPaginatedResults<T>(url: string, params: Record<string, any> = {}, maxResults?: number): Promise<T[]> {
|
|
384
|
+
const results: T[] = [];
|
|
385
|
+
let nextUrl: string | null = url;
|
|
386
|
+
const limit = params.limit || 100;
|
|
387
|
+
|
|
388
|
+
while (nextUrl) {
|
|
389
|
+
const response = await axios.get(nextUrl, {
|
|
390
|
+
params: nextUrl === url ? { ...params, limit } : {}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
if (response.data.data && Array.isArray(response.data.data)) {
|
|
394
|
+
results.push(...response.data.data);
|
|
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;
|
|
412
404
|
}
|
|
413
|
-
|
|
405
|
+
|
|
406
|
+
return results;
|
|
414
407
|
}
|
|
415
408
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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;
|
|
409
|
+
/**
|
|
410
|
+
* Get default fields for post queries
|
|
411
|
+
*/
|
|
412
|
+
protected getPostFields(): string {
|
|
413
|
+
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)';
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Convert Facebook post to common format
|
|
418
|
+
*/
|
|
419
|
+
protected normalizePost(fbPost: FacebookPost): SocialPost {
|
|
420
|
+
const mediaUrls: string[] = [];
|
|
421
|
+
|
|
422
|
+
// Extract media URLs from attachments
|
|
423
|
+
if (fbPost.attachments?.data) {
|
|
424
|
+
for (const attachment of fbPost.attachments.data) {
|
|
425
|
+
if (attachment.media?.image?.src) {
|
|
426
|
+
mediaUrls.push(attachment.media.image.src);
|
|
427
|
+
} else if (attachment.media?.source) {
|
|
428
|
+
mediaUrls.push(attachment.media.source);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
463
431
|
}
|
|
464
|
-
|
|
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
|
+
};
|
|
465
448
|
}
|
|
466
449
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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}`);
|
|
450
|
+
/**
|
|
451
|
+
* Extract analytics from Facebook post
|
|
452
|
+
*/
|
|
453
|
+
private extractPostAnalytics(fbPost: FacebookPost): SocialAnalytics | undefined {
|
|
454
|
+
const insights = fbPost.insights?.data;
|
|
455
|
+
const reactions = fbPost.reactions?.summary?.total_count || 0;
|
|
456
|
+
const comments = fbPost.comments?.summary?.total_count || 0;
|
|
457
|
+
const shares = fbPost.shares?.count || 0;
|
|
458
|
+
|
|
459
|
+
// Extract metrics from insights
|
|
460
|
+
let impressions = 0;
|
|
461
|
+
let reach = 0;
|
|
462
|
+
let engagements = 0;
|
|
463
|
+
let clicks = 0;
|
|
464
|
+
|
|
465
|
+
if (insights) {
|
|
466
|
+
for (const insight of insights) {
|
|
467
|
+
switch (insight.name) {
|
|
468
|
+
case 'post_impressions':
|
|
469
|
+
impressions = insight.values?.[0]?.value || 0;
|
|
470
|
+
break;
|
|
471
|
+
case 'post_impressions_unique':
|
|
472
|
+
reach = insight.values?.[0]?.value || 0;
|
|
473
|
+
break;
|
|
474
|
+
case 'post_engaged_users':
|
|
475
|
+
engagements = insight.values?.[0]?.value || 0;
|
|
476
|
+
break;
|
|
477
|
+
case 'post_clicks':
|
|
478
|
+
clicks = insight.values?.[0]?.value || 0;
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
553
482
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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}`);
|
|
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
|
+
};
|
|
577
499
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Normalize Facebook analytics to common format
|
|
503
|
+
*/
|
|
504
|
+
protected normalizeAnalytics(fbMetrics: any): SocialAnalytics {
|
|
597
505
|
return {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
506
|
+
impressions: fbMetrics.impressions || 0,
|
|
507
|
+
engagements: fbMetrics.engaged_users || 0,
|
|
508
|
+
clicks: fbMetrics.clicks || 0,
|
|
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
|
|
601
516
|
};
|
|
602
|
-
} catch (error) {
|
|
603
|
-
LogError(`Failed to parse Facebook rate limit headers: ${error}`);
|
|
604
|
-
}
|
|
605
517
|
}
|
|
606
518
|
|
|
607
|
-
|
|
608
|
-
|
|
519
|
+
/**
|
|
520
|
+
* Search for posts - implemented in search action
|
|
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
|
+
}
|
|
609
626
|
}
|
|
610
627
|
|
|
611
628
|
/**
|
|
612
629
|
* Facebook-specific interfaces
|
|
613
630
|
*/
|
|
614
631
|
export interface FacebookPage {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
632
|
+
id: string;
|
|
633
|
+
name: string;
|
|
634
|
+
access_token: string;
|
|
635
|
+
category: string;
|
|
636
|
+
picture?: {
|
|
637
|
+
data: {
|
|
638
|
+
url: string;
|
|
639
|
+
};
|
|
622
640
|
};
|
|
623
|
-
};
|
|
624
641
|
}
|
|
625
642
|
|
|
626
643
|
export interface CreatePostData {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
644
|
+
message?: string;
|
|
645
|
+
link?: string;
|
|
646
|
+
place?: string;
|
|
647
|
+
tags?: string[];
|
|
648
|
+
privacy?: {
|
|
649
|
+
value: 'EVERYONE' | 'ALL_FRIENDS' | 'FRIENDS_OF_FRIENDS' | 'SELF';
|
|
650
|
+
};
|
|
651
|
+
attached_media?: Array<{ media_fbid: string }>;
|
|
652
|
+
scheduled_publish_time?: number; // Unix timestamp
|
|
653
|
+
published?: boolean;
|
|
654
|
+
backdated_time?: number;
|
|
655
|
+
backdated_time_granularity?: 'year' | 'month' | 'day' | 'hour' | 'minute';
|
|
639
656
|
}
|
|
640
657
|
|
|
641
658
|
export interface FacebookPost {
|
|
642
|
-
id: string;
|
|
643
|
-
message?: string;
|
|
644
|
-
story?: string;
|
|
645
|
-
created_time: string;
|
|
646
|
-
updated_time: string;
|
|
647
|
-
from?: {
|
|
648
659
|
id: string;
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
660
|
+
message?: string;
|
|
661
|
+
story?: string;
|
|
662
|
+
created_time: string;
|
|
663
|
+
updated_time: string;
|
|
664
|
+
from?: {
|
|
665
|
+
id: string;
|
|
666
|
+
name: string;
|
|
667
|
+
};
|
|
668
|
+
permalink_url?: string;
|
|
669
|
+
attachments?: {
|
|
670
|
+
data: Array<{
|
|
671
|
+
type: string;
|
|
672
|
+
title?: string;
|
|
673
|
+
description?: string;
|
|
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;
|
|
663
691
|
};
|
|
664
|
-
source?: string;
|
|
665
|
-
};
|
|
666
|
-
}>;
|
|
667
|
-
};
|
|
668
|
-
shares?: {
|
|
669
|
-
count: number;
|
|
670
|
-
};
|
|
671
|
-
reactions?: {
|
|
672
|
-
summary: {
|
|
673
|
-
total_count: number;
|
|
674
692
|
};
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
693
|
+
comments?: {
|
|
694
|
+
summary: {
|
|
695
|
+
total_count: number;
|
|
696
|
+
};
|
|
697
|
+
};
|
|
698
|
+
insights?: {
|
|
699
|
+
data: Array<{
|
|
700
|
+
name: string;
|
|
701
|
+
values: Array<{
|
|
702
|
+
value: number;
|
|
703
|
+
}>;
|
|
704
|
+
}>;
|
|
679
705
|
};
|
|
680
|
-
};
|
|
681
|
-
insights?: {
|
|
682
|
-
data: Array<{
|
|
683
|
-
name: string;
|
|
684
|
-
values: Array<{
|
|
685
|
-
value: number;
|
|
686
|
-
}>;
|
|
687
|
-
}>;
|
|
688
|
-
};
|
|
689
706
|
}
|
|
690
707
|
|
|
691
708
|
export interface GetPagePostsParams {
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
709
|
+
since?: Date;
|
|
710
|
+
until?: Date;
|
|
711
|
+
limit?: number;
|
|
712
|
+
published?: boolean;
|
|
696
713
|
}
|
|
697
714
|
|
|
698
715
|
export interface FacebookInsight {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
716
|
+
name: string;
|
|
717
|
+
period: string;
|
|
718
|
+
values: Array<{
|
|
719
|
+
value: number | Record<string, number>;
|
|
720
|
+
end_time?: string;
|
|
721
|
+
}>;
|
|
722
|
+
title?: string;
|
|
723
|
+
description?: string;
|
|
707
724
|
}
|
|
708
725
|
|
|
709
726
|
export interface FacebookComment {
|
|
710
|
-
id: string;
|
|
711
|
-
message: string;
|
|
712
|
-
created_time: string;
|
|
713
|
-
from: {
|
|
714
|
-
id: string;
|
|
715
|
-
name: string;
|
|
716
|
-
};
|
|
717
|
-
like_count: number;
|
|
718
|
-
comment_count?: number;
|
|
719
|
-
parent?: {
|
|
720
727
|
id: string;
|
|
721
|
-
|
|
728
|
+
message: string;
|
|
729
|
+
created_time: string;
|
|
730
|
+
from: {
|
|
731
|
+
id: string;
|
|
732
|
+
name: string;
|
|
733
|
+
};
|
|
734
|
+
like_count: number;
|
|
735
|
+
comment_count?: number;
|
|
736
|
+
parent?: {
|
|
737
|
+
id: string;
|
|
738
|
+
};
|
|
722
739
|
}
|
|
723
740
|
|
|
724
741
|
export interface FacebookAlbum {
|
|
725
|
-
id: string;
|
|
726
|
-
name: string;
|
|
727
|
-
description?: string;
|
|
728
|
-
link: string;
|
|
729
|
-
cover_photo?: {
|
|
730
742
|
id: string;
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
743
|
+
name: string;
|
|
744
|
+
description?: string;
|
|
745
|
+
link: string;
|
|
746
|
+
cover_photo?: {
|
|
747
|
+
id: string;
|
|
748
|
+
};
|
|
749
|
+
count: number;
|
|
750
|
+
created_time: string;
|
|
751
|
+
}
|