@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.
Files changed (204) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +6 -6
  3. package/dist/base/base-social.action.d.ts.map +1 -1
  4. package/dist/base/base-social.action.js +18 -24
  5. package/dist/base/base-social.action.js.map +1 -1
  6. package/dist/providers/buffer/buffer-base.action.d.ts.map +1 -1
  7. package/dist/providers/buffer/buffer-base.action.js +35 -34
  8. package/dist/providers/buffer/buffer-base.action.js.map +1 -1
  9. package/dist/providers/facebook/actions/boost-post.action.d.ts.map +1 -1
  10. package/dist/providers/facebook/actions/boost-post.action.js +33 -33
  11. package/dist/providers/facebook/actions/boost-post.action.js.map +1 -1
  12. package/dist/providers/facebook/actions/create-album.action.d.ts.map +1 -1
  13. package/dist/providers/facebook/actions/create-album.action.js +34 -36
  14. package/dist/providers/facebook/actions/create-album.action.js.map +1 -1
  15. package/dist/providers/facebook/actions/create-post.action.d.ts.map +1 -1
  16. package/dist/providers/facebook/actions/create-post.action.js +20 -20
  17. package/dist/providers/facebook/actions/create-post.action.js.map +1 -1
  18. package/dist/providers/facebook/actions/get-page-insights.action.d.ts.map +1 -1
  19. package/dist/providers/facebook/actions/get-page-insights.action.js +25 -27
  20. package/dist/providers/facebook/actions/get-page-insights.action.js.map +1 -1
  21. package/dist/providers/facebook/actions/get-page-posts.action.d.ts.map +1 -1
  22. package/dist/providers/facebook/actions/get-page-posts.action.js +19 -23
  23. package/dist/providers/facebook/actions/get-page-posts.action.js.map +1 -1
  24. package/dist/providers/facebook/actions/get-post-insights.action.d.ts.map +1 -1
  25. package/dist/providers/facebook/actions/get-post-insights.action.js +28 -32
  26. package/dist/providers/facebook/actions/get-post-insights.action.js.map +1 -1
  27. package/dist/providers/facebook/actions/respond-to-comments.action.d.ts.map +1 -1
  28. package/dist/providers/facebook/actions/respond-to-comments.action.js +42 -44
  29. package/dist/providers/facebook/actions/respond-to-comments.action.js.map +1 -1
  30. package/dist/providers/facebook/actions/schedule-post.action.d.ts.map +1 -1
  31. package/dist/providers/facebook/actions/schedule-post.action.js +29 -29
  32. package/dist/providers/facebook/actions/schedule-post.action.js.map +1 -1
  33. package/dist/providers/facebook/actions/search-posts.action.d.ts.map +1 -1
  34. package/dist/providers/facebook/actions/search-posts.action.js +37 -39
  35. package/dist/providers/facebook/actions/search-posts.action.js.map +1 -1
  36. package/dist/providers/facebook/facebook-base.action.d.ts.map +1 -1
  37. package/dist/providers/facebook/facebook-base.action.js +44 -59
  38. package/dist/providers/facebook/facebook-base.action.js.map +1 -1
  39. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.d.ts.map +1 -1
  40. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js +33 -31
  41. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js.map +1 -1
  42. package/dist/providers/hootsuite/actions/create-scheduled-post.action.d.ts.map +1 -1
  43. package/dist/providers/hootsuite/actions/create-scheduled-post.action.js +28 -32
  44. package/dist/providers/hootsuite/actions/create-scheduled-post.action.js.map +1 -1
  45. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.d.ts.map +1 -1
  46. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js +19 -19
  47. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js.map +1 -1
  48. package/dist/providers/hootsuite/actions/get-analytics.action.d.ts.map +1 -1
  49. package/dist/providers/hootsuite/actions/get-analytics.action.js +24 -26
  50. package/dist/providers/hootsuite/actions/get-analytics.action.js.map +1 -1
  51. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.d.ts.map +1 -1
  52. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js +22 -22
  53. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js.map +1 -1
  54. package/dist/providers/hootsuite/actions/get-social-profiles.action.d.ts.map +1 -1
  55. package/dist/providers/hootsuite/actions/get-social-profiles.action.js +32 -34
  56. package/dist/providers/hootsuite/actions/get-social-profiles.action.js.map +1 -1
  57. package/dist/providers/hootsuite/actions/search-posts.action.d.ts.map +1 -1
  58. package/dist/providers/hootsuite/actions/search-posts.action.js +43 -52
  59. package/dist/providers/hootsuite/actions/search-posts.action.js.map +1 -1
  60. package/dist/providers/hootsuite/actions/update-scheduled-post.action.d.ts.map +1 -1
  61. package/dist/providers/hootsuite/actions/update-scheduled-post.action.js +30 -28
  62. package/dist/providers/hootsuite/actions/update-scheduled-post.action.js.map +1 -1
  63. package/dist/providers/hootsuite/hootsuite-base.action.d.ts.map +1 -1
  64. package/dist/providers/hootsuite/hootsuite-base.action.js +18 -20
  65. package/dist/providers/hootsuite/hootsuite-base.action.js.map +1 -1
  66. package/dist/providers/instagram/actions/create-post.action.d.ts.map +1 -1
  67. package/dist/providers/instagram/actions/create-post.action.js +27 -26
  68. package/dist/providers/instagram/actions/create-post.action.js.map +1 -1
  69. package/dist/providers/instagram/actions/create-story.action.d.ts.map +1 -1
  70. package/dist/providers/instagram/actions/create-story.action.js +35 -35
  71. package/dist/providers/instagram/actions/create-story.action.js.map +1 -1
  72. package/dist/providers/instagram/actions/get-account-insights.action.d.ts.map +1 -1
  73. package/dist/providers/instagram/actions/get-account-insights.action.js +38 -59
  74. package/dist/providers/instagram/actions/get-account-insights.action.js.map +1 -1
  75. package/dist/providers/instagram/actions/get-business-posts.action.d.ts.map +1 -1
  76. package/dist/providers/instagram/actions/get-business-posts.action.js +29 -29
  77. package/dist/providers/instagram/actions/get-business-posts.action.js.map +1 -1
  78. package/dist/providers/instagram/actions/get-comments.action.d.ts.map +1 -1
  79. package/dist/providers/instagram/actions/get-comments.action.js +36 -36
  80. package/dist/providers/instagram/actions/get-comments.action.js.map +1 -1
  81. package/dist/providers/instagram/actions/get-post-insights.action.d.ts.map +1 -1
  82. package/dist/providers/instagram/actions/get-post-insights.action.js +23 -25
  83. package/dist/providers/instagram/actions/get-post-insights.action.js.map +1 -1
  84. package/dist/providers/instagram/actions/schedule-post.action.d.ts.map +1 -1
  85. package/dist/providers/instagram/actions/schedule-post.action.js +25 -25
  86. package/dist/providers/instagram/actions/schedule-post.action.js.map +1 -1
  87. package/dist/providers/instagram/actions/search-posts.action.d.ts.map +1 -1
  88. package/dist/providers/instagram/actions/search-posts.action.js +56 -60
  89. package/dist/providers/instagram/actions/search-posts.action.js.map +1 -1
  90. package/dist/providers/instagram/instagram-base.action.d.ts.map +1 -1
  91. package/dist/providers/instagram/instagram-base.action.js +25 -27
  92. package/dist/providers/instagram/instagram-base.action.js.map +1 -1
  93. package/dist/providers/linkedin/actions/create-article.action.d.ts.map +1 -1
  94. package/dist/providers/linkedin/actions/create-article.action.js +55 -45
  95. package/dist/providers/linkedin/actions/create-article.action.js.map +1 -1
  96. package/dist/providers/linkedin/actions/create-post.action.d.ts.map +1 -1
  97. package/dist/providers/linkedin/actions/create-post.action.js +31 -29
  98. package/dist/providers/linkedin/actions/create-post.action.js.map +1 -1
  99. package/dist/providers/linkedin/actions/get-followers.action.d.ts.map +1 -1
  100. package/dist/providers/linkedin/actions/get-followers.action.js +28 -28
  101. package/dist/providers/linkedin/actions/get-followers.action.js.map +1 -1
  102. package/dist/providers/linkedin/actions/get-organization-posts.action.d.ts.map +1 -1
  103. package/dist/providers/linkedin/actions/get-organization-posts.action.js +20 -20
  104. package/dist/providers/linkedin/actions/get-organization-posts.action.js.map +1 -1
  105. package/dist/providers/linkedin/actions/get-personal-posts.action.d.ts.map +1 -1
  106. package/dist/providers/linkedin/actions/get-personal-posts.action.js +19 -19
  107. package/dist/providers/linkedin/actions/get-personal-posts.action.js.map +1 -1
  108. package/dist/providers/linkedin/actions/get-post-analytics.action.d.ts.map +1 -1
  109. package/dist/providers/linkedin/actions/get-post-analytics.action.js +25 -23
  110. package/dist/providers/linkedin/actions/get-post-analytics.action.js.map +1 -1
  111. package/dist/providers/linkedin/actions/schedule-post.action.d.ts.map +1 -1
  112. package/dist/providers/linkedin/actions/schedule-post.action.js +32 -30
  113. package/dist/providers/linkedin/actions/schedule-post.action.js.map +1 -1
  114. package/dist/providers/linkedin/actions/search-posts.action.d.ts.map +1 -1
  115. package/dist/providers/linkedin/actions/search-posts.action.js +28 -30
  116. package/dist/providers/linkedin/actions/search-posts.action.js.map +1 -1
  117. package/dist/providers/linkedin/linkedin-base.action.d.ts.map +1 -1
  118. package/dist/providers/linkedin/linkedin-base.action.js +33 -38
  119. package/dist/providers/linkedin/linkedin-base.action.js.map +1 -1
  120. package/dist/providers/tiktok/tiktok-base.action.d.ts.map +1 -1
  121. package/dist/providers/tiktok/tiktok-base.action.js +25 -26
  122. package/dist/providers/tiktok/tiktok-base.action.js.map +1 -1
  123. package/dist/providers/twitter/actions/create-thread.action.d.ts.map +1 -1
  124. package/dist/providers/twitter/actions/create-thread.action.js +25 -29
  125. package/dist/providers/twitter/actions/create-thread.action.js.map +1 -1
  126. package/dist/providers/twitter/actions/create-tweet.action.d.ts.map +1 -1
  127. package/dist/providers/twitter/actions/create-tweet.action.js +23 -23
  128. package/dist/providers/twitter/actions/create-tweet.action.js.map +1 -1
  129. package/dist/providers/twitter/actions/delete-tweet.action.d.ts.map +1 -1
  130. package/dist/providers/twitter/actions/delete-tweet.action.js +19 -19
  131. package/dist/providers/twitter/actions/delete-tweet.action.js.map +1 -1
  132. package/dist/providers/twitter/actions/get-analytics.action.d.ts.map +1 -1
  133. package/dist/providers/twitter/actions/get-analytics.action.js +40 -47
  134. package/dist/providers/twitter/actions/get-analytics.action.js.map +1 -1
  135. package/dist/providers/twitter/actions/get-mentions.action.d.ts.map +1 -1
  136. package/dist/providers/twitter/actions/get-mentions.action.js +30 -31
  137. package/dist/providers/twitter/actions/get-mentions.action.js.map +1 -1
  138. package/dist/providers/twitter/actions/get-timeline.action.d.ts.map +1 -1
  139. package/dist/providers/twitter/actions/get-timeline.action.js +29 -29
  140. package/dist/providers/twitter/actions/get-timeline.action.js.map +1 -1
  141. package/dist/providers/twitter/actions/schedule-tweet.action.d.ts.map +1 -1
  142. package/dist/providers/twitter/actions/schedule-tweet.action.js +26 -26
  143. package/dist/providers/twitter/actions/schedule-tweet.action.js.map +1 -1
  144. package/dist/providers/twitter/actions/search-tweets.action.d.ts.map +1 -1
  145. package/dist/providers/twitter/actions/search-tweets.action.js +56 -58
  146. package/dist/providers/twitter/actions/search-tweets.action.js.map +1 -1
  147. package/dist/providers/twitter/twitter-base.action.d.ts.map +1 -1
  148. package/dist/providers/twitter/twitter-base.action.js +58 -68
  149. package/dist/providers/twitter/twitter-base.action.js.map +1 -1
  150. package/dist/providers/youtube/youtube-base.action.d.ts +1 -1
  151. package/dist/providers/youtube/youtube-base.action.d.ts.map +1 -1
  152. package/dist/providers/youtube/youtube-base.action.js +22 -25
  153. package/dist/providers/youtube/youtube-base.action.js.map +1 -1
  154. package/package.json +5 -6
  155. package/src/base/base-social.action.ts +217 -224
  156. package/src/providers/buffer/buffer-base.action.ts +435 -441
  157. package/src/providers/facebook/actions/boost-post.action.ts +350 -386
  158. package/src/providers/facebook/actions/create-album.action.ts +291 -307
  159. package/src/providers/facebook/actions/create-post.action.ts +224 -227
  160. package/src/providers/facebook/actions/get-page-insights.action.ts +383 -403
  161. package/src/providers/facebook/actions/get-page-posts.action.ts +214 -225
  162. package/src/providers/facebook/actions/get-post-insights.action.ts +300 -316
  163. package/src/providers/facebook/actions/respond-to-comments.action.ts +319 -336
  164. package/src/providers/facebook/actions/schedule-post.action.ts +289 -292
  165. package/src/providers/facebook/actions/search-posts.action.ts +399 -413
  166. package/src/providers/facebook/facebook-base.action.ts +653 -670
  167. package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
  168. package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +184 -189
  169. package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +160 -161
  170. package/src/providers/hootsuite/actions/get-analytics.action.ts +249 -254
  171. package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +206 -207
  172. package/src/providers/hootsuite/actions/get-social-profiles.action.ts +206 -205
  173. package/src/providers/hootsuite/actions/search-posts.action.ts +351 -369
  174. package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +211 -209
  175. package/src/providers/hootsuite/hootsuite-base.action.ts +301 -307
  176. package/src/providers/instagram/actions/create-post.action.ts +276 -296
  177. package/src/providers/instagram/actions/create-story.action.ts +378 -394
  178. package/src/providers/instagram/actions/get-account-insights.action.ts +384 -420
  179. package/src/providers/instagram/actions/get-business-posts.action.ts +233 -242
  180. package/src/providers/instagram/actions/get-comments.action.ts +365 -377
  181. package/src/providers/instagram/actions/get-post-insights.action.ts +265 -273
  182. package/src/providers/instagram/actions/schedule-post.action.ts +233 -235
  183. package/src/providers/instagram/actions/search-posts.action.ts +512 -538
  184. package/src/providers/instagram/instagram-base.action.ts +368 -393
  185. package/src/providers/linkedin/actions/create-article.action.ts +275 -266
  186. package/src/providers/linkedin/actions/create-post.action.ts +179 -177
  187. package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
  188. package/src/providers/linkedin/actions/get-organization-posts.action.ts +146 -147
  189. package/src/providers/linkedin/actions/get-personal-posts.action.ts +138 -139
  190. package/src/providers/linkedin/actions/get-post-analytics.action.ts +190 -189
  191. package/src/providers/linkedin/actions/schedule-post.action.ts +191 -189
  192. package/src/providers/linkedin/actions/search-posts.action.ts +275 -283
  193. package/src/providers/linkedin/linkedin-base.action.ts +407 -421
  194. package/src/providers/tiktok/tiktok-base.action.ts +305 -320
  195. package/src/providers/twitter/actions/create-thread.action.ts +203 -207
  196. package/src/providers/twitter/actions/create-tweet.action.ts +187 -188
  197. package/src/providers/twitter/actions/delete-tweet.action.ts +128 -129
  198. package/src/providers/twitter/actions/get-analytics.action.ts +402 -411
  199. package/src/providers/twitter/actions/get-mentions.action.ts +218 -219
  200. package/src/providers/twitter/actions/get-timeline.action.ts +232 -233
  201. package/src/providers/twitter/actions/schedule-tweet.action.ts +221 -222
  202. package/src/providers/twitter/actions/search-tweets.action.ts +540 -543
  203. package/src/providers/twitter/twitter-base.action.ts +541 -560
  204. package/src/providers/youtube/youtube-base.action.ts +320 -333
@@ -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/core';
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
- * 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';
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
- protected get apiBaseUrl(): string {
25
- return 'https://graph.facebook.com/v18.0';
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
- * API version for cleaner URL building
30
- */
31
- protected get apiVersion(): string {
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
- * 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;
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
- * 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
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
- * 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
- }
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
- * 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
- }
203
+ if (!page) {
204
+ throw new Error(`Page ${pageId} not found or user doesn't have access`);
192
205
  }
193
206
 
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;
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
- * 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
+ }
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
- * 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
- }
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
- // 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
- }
280
+ const supportedTypes = [...supportedImageTypes, ...supportedVideoTypes];
308
281
 
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
- }
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
- * 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
- }
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
- * 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
- }
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
- * 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
- }
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
- * 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;
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
- * 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)';
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
- * 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
- }
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
- * 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
- }
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
- * Normalize Facebook analytics to common format
503
- */
504
- protected normalizeAnalytics(fbMetrics: any): SocialAnalytics {
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
- 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
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
- * 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
- }
607
+ return null;
608
+ }
626
609
  }
627
610
 
628
611
  /**
629
612
  * Facebook-specific interfaces
630
613
  */
631
614
  export interface FacebookPage {
632
- id: string;
633
- name: string;
634
- access_token: string;
635
- category: string;
636
- picture?: {
637
- data: {
638
- url: string;
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
- 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';
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
- 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;
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
- insights?: {
699
- data: Array<{
700
- name: string;
701
- values: Array<{
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
- since?: Date;
710
- until?: Date;
711
- limit?: number;
712
- published?: boolean;
692
+ since?: Date;
693
+ until?: Date;
694
+ limit?: number;
695
+ published?: boolean;
713
696
  }
714
697
 
715
698
  export interface FacebookInsight {
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;
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
- 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
- };
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
- name: string;
744
- description?: string;
745
- link: string;
746
- cover_photo?: {
747
- id: string;
748
- };
749
- count: number;
750
- created_time: string;
751
- }
731
+ };
732
+ count: number;
733
+ created_time: string;
734
+ }