@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.
Files changed (204) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +13 -0
  3. package/dist/base/base-social.action.d.ts.map +1 -1
  4. package/dist/base/base-social.action.js +24 -18
  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 +34 -35
  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 +36 -34
  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 +27 -25
  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 +23 -19
  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 +32 -28
  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 +44 -42
  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 +39 -37
  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 +59 -44
  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 +31 -33
  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 +32 -28
  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 +26 -24
  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 +34 -32
  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 +52 -43
  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 +28 -30
  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 +20 -18
  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 +26 -27
  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 +59 -38
  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 +25 -23
  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 +60 -56
  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 +27 -25
  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 +45 -55
  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 +29 -31
  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 +23 -25
  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 +30 -32
  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 +30 -28
  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 +38 -33
  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 +26 -25
  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 +29 -25
  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 +47 -40
  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 +31 -30
  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 +58 -56
  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 +68 -58
  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 +25 -22
  153. package/dist/providers/youtube/youtube-base.action.js.map +1 -1
  154. package/package.json +6 -5
  155. package/src/base/base-social.action.ts +224 -217
  156. package/src/providers/buffer/buffer-base.action.ts +441 -435
  157. package/src/providers/facebook/actions/boost-post.action.ts +386 -350
  158. package/src/providers/facebook/actions/create-album.action.ts +307 -291
  159. package/src/providers/facebook/actions/create-post.action.ts +227 -224
  160. package/src/providers/facebook/actions/get-page-insights.action.ts +403 -383
  161. package/src/providers/facebook/actions/get-page-posts.action.ts +225 -214
  162. package/src/providers/facebook/actions/get-post-insights.action.ts +316 -300
  163. package/src/providers/facebook/actions/respond-to-comments.action.ts +336 -319
  164. package/src/providers/facebook/actions/schedule-post.action.ts +292 -289
  165. package/src/providers/facebook/actions/search-posts.action.ts +413 -399
  166. package/src/providers/facebook/facebook-base.action.ts +670 -653
  167. package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
  168. package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +189 -184
  169. package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +161 -160
  170. package/src/providers/hootsuite/actions/get-analytics.action.ts +254 -249
  171. package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +207 -206
  172. package/src/providers/hootsuite/actions/get-social-profiles.action.ts +205 -206
  173. package/src/providers/hootsuite/actions/search-posts.action.ts +369 -351
  174. package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +209 -211
  175. package/src/providers/hootsuite/hootsuite-base.action.ts +307 -301
  176. package/src/providers/instagram/actions/create-post.action.ts +296 -276
  177. package/src/providers/instagram/actions/create-story.action.ts +394 -378
  178. package/src/providers/instagram/actions/get-account-insights.action.ts +420 -384
  179. package/src/providers/instagram/actions/get-business-posts.action.ts +242 -233
  180. package/src/providers/instagram/actions/get-comments.action.ts +377 -365
  181. package/src/providers/instagram/actions/get-post-insights.action.ts +273 -265
  182. package/src/providers/instagram/actions/schedule-post.action.ts +235 -233
  183. package/src/providers/instagram/actions/search-posts.action.ts +538 -512
  184. package/src/providers/instagram/instagram-base.action.ts +393 -368
  185. package/src/providers/linkedin/actions/create-article.action.ts +266 -275
  186. package/src/providers/linkedin/actions/create-post.action.ts +177 -179
  187. package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
  188. package/src/providers/linkedin/actions/get-organization-posts.action.ts +147 -146
  189. package/src/providers/linkedin/actions/get-personal-posts.action.ts +139 -138
  190. package/src/providers/linkedin/actions/get-post-analytics.action.ts +189 -190
  191. package/src/providers/linkedin/actions/schedule-post.action.ts +189 -191
  192. package/src/providers/linkedin/actions/search-posts.action.ts +283 -275
  193. package/src/providers/linkedin/linkedin-base.action.ts +421 -407
  194. package/src/providers/tiktok/tiktok-base.action.ts +320 -305
  195. package/src/providers/twitter/actions/create-thread.action.ts +207 -203
  196. package/src/providers/twitter/actions/create-tweet.action.ts +188 -187
  197. package/src/providers/twitter/actions/delete-tweet.action.ts +129 -128
  198. package/src/providers/twitter/actions/get-analytics.action.ts +411 -402
  199. package/src/providers/twitter/actions/get-mentions.action.ts +219 -218
  200. package/src/providers/twitter/actions/get-timeline.action.ts +233 -232
  201. package/src/providers/twitter/actions/schedule-tweet.action.ts +222 -221
  202. package/src/providers/twitter/actions/search-tweets.action.ts +543 -540
  203. package/src/providers/twitter/twitter-base.action.ts +560 -541
  204. 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/global';
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
- * 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
- );
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
- 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
- }
23
+
24
+ protected get apiBaseUrl(): string {
25
+ return 'https://graph.facebook.com/v18.0';
128
26
  }
129
27
 
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
28
+ /**
29
+ * API version for cleaner URL building
30
+ */
31
+ protected get apiVersion(): string {
32
+ return 'v18.0';
134
33
  }
135
34
 
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;
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
- * 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;
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
- * 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);
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
- if (!page) {
204
- throw new Error(`Page ${pageId} not found or user doesn't have access`);
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
- 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;
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
- * 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;
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
- * 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'];
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
- const supportedVideoTypes = ['video/mp4', 'video/quicktime', 'video/x-matroska', 'video/webm'];
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
- const supportedTypes = [...supportedImageTypes, ...supportedVideoTypes];
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
- if (!supportedTypes.includes(file.mimeType)) {
283
- throw new Error(`Unsupported media type: ${file.mimeType}. Supported types: ${supportedTypes.join(', ')}`);
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
- // 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
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
- if (file.size > maxSize) {
297
- throw new Error(`File size exceeds limit. Max: ${maxSize / 1024 / 1024}MB, Got: ${file.size / 1024 / 1024}MB`);
298
- }
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);
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
- * 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;
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
- * 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;
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
- 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);
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
- 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;
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
- 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}`);
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
- // 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}`);
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
- * 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
500
+
501
+ /**
502
+ * Normalize Facebook analytics to common format
503
+ */
504
+ protected normalizeAnalytics(fbMetrics: any): SocialAnalytics {
597
505
  return {
598
- remaining: Math.max(0, 100 - callCount),
599
- reset: new Date(Date.now() + 3600000), // Reset in 1 hour
600
- limit: 100,
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
- return null;
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
- id: string;
616
- name: string;
617
- access_token: string;
618
- category: string;
619
- picture?: {
620
- data: {
621
- url: string;
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
- 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';
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
- 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;
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
- comments?: {
677
- summary: {
678
- total_count: number;
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
- since?: Date;
693
- until?: Date;
694
- limit?: number;
695
- published?: boolean;
709
+ since?: Date;
710
+ until?: Date;
711
+ limit?: number;
712
+ published?: boolean;
696
713
  }
697
714
 
698
715
  export interface FacebookInsight {
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;
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
- count: number;
733
- created_time: string;
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
+ }