@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
@@ -1,6 +1,6 @@
1
1
  import { RegisterClass } from '@memberjunction/global';
2
2
  import { BaseSocialMediaAction, MediaFile } from '../../base/base-social.action';
3
- import { UserInfo, LogStatus, LogError } from '@memberjunction/global';
3
+ import { UserInfo, LogStatus, LogError } from '@memberjunction/core';
4
4
  import axios, { AxiosInstance, AxiosError } from 'axios';
5
5
  import { BaseAction } from '@memberjunction/actions';
6
6
 
@@ -11,396 +11,421 @@ import { BaseAction } from '@memberjunction/actions';
11
11
  */
12
12
  @RegisterClass(BaseAction, 'InstagramBaseAction')
13
13
  export abstract class InstagramBaseAction extends BaseSocialMediaAction {
14
- protected get platformName(): string {
15
- return 'Instagram';
16
- }
17
-
18
- protected get apiBaseUrl(): string {
19
- return 'https://graph.facebook.com/v18.0';
20
- }
21
-
22
- /**
23
- * Instagram Business Account ID (stored in CustomAttribute1)
24
- */
25
- protected get instagramBusinessAccountId(): string {
26
- return this.getCustomAttribute(1) || '';
27
- }
28
-
29
- /**
30
- * Facebook Page ID (stored in CustomAttribute2) - required for Instagram Business API
31
- */
32
- protected get facebookPageId(): string {
33
- return this.getCustomAttribute(2) || '';
34
- }
35
-
36
- /**
37
- * Axios instance for API calls
38
- */
39
- private _axiosInstance: AxiosInstance | null = null;
40
-
41
- /**
42
- * Get or create axios instance with authentication
43
- */
44
- protected get axios(): AxiosInstance {
45
- if (!this._axiosInstance) {
46
- this._axiosInstance = axios.create({
47
- baseURL: this.apiBaseUrl,
48
- headers: this.buildHeaders(),
49
- });
50
-
51
- // Add response interceptor for error handling
52
- this._axiosInstance.interceptors.response.use(
53
- (response) => response,
54
- async (error) => {
55
- if (error.response?.status === 401 && this.isAuthError(error)) {
56
- LogStatus('Instagram token appears invalid, attempting refresh...');
57
- await this.refreshAccessToken();
58
-
59
- // Retry the request with new token
60
- error.config.headers.Authorization = `Bearer ${this.getAccessToken()}`;
61
- return axios.request(error.config);
62
- }
63
- throw error;
64
- }
65
- );
14
+ protected get platformName(): string {
15
+ return 'Instagram';
66
16
  }
67
- return this._axiosInstance;
68
- }
69
-
70
- /**
71
- * Refresh the Instagram/Facebook access token
72
- */
73
- protected async refreshAccessToken(): Promise<void> {
74
- try {
75
- // Instagram uses Facebook's OAuth system
76
- // Long-lived tokens need to be exchanged periodically
77
- const currentToken = this.getAccessToken();
78
- if (!currentToken) {
79
- throw new Error('No access token available to refresh');
80
- }
81
-
82
- // Exchange for a new long-lived token
83
- const response = await axios.get(`${this.apiBaseUrl}/oauth/access_token`, {
84
- params: {
85
- grant_type: 'fb_exchange_token',
86
- client_id: this.getCustomAttribute(3), // App ID stored in CustomAttribute3
87
- client_secret: this.getCustomAttribute(4), // App Secret stored in CustomAttribute4
88
- fb_exchange_token: currentToken,
89
- },
90
- });
91
-
92
- if (response.data.access_token) {
93
- await this.updateStoredTokens(
94
- response.data.access_token,
95
- undefined, // Instagram doesn't use refresh tokens
96
- response.data.expires_in || 5184000 // Default to 60 days
97
- );
98
17
 
99
- // Reset axios instance to use new token
100
- this._axiosInstance = null;
101
- }
102
- } catch (error) {
103
- LogError('Failed to refresh Instagram access token', error);
104
- throw new Error('Failed to refresh Instagram access token');
105
- }
106
- }
107
-
108
- /**
109
- * Make an Instagram Graph API request
110
- */
111
- protected async makeInstagramRequest<T = any>(
112
- endpoint: string,
113
- method: 'GET' | 'POST' | 'DELETE' = 'GET',
114
- data?: any,
115
- params?: any
116
- ): Promise<T> {
117
- try {
118
- this.logApiRequest(method, `${this.apiBaseUrl}/${endpoint}`, data || params);
119
-
120
- const response = await this.axios.request<T>({
121
- url: endpoint,
122
- method,
123
- data,
124
- params,
125
- });
126
-
127
- this.logApiResponse(response.data);
128
- return response.data;
129
- } catch (error) {
130
- if (axios.isAxiosError(error)) {
131
- this.handleInstagramError(error);
132
- }
133
- throw error;
134
- }
135
- }
136
-
137
- /**
138
- * Handle Instagram-specific errors
139
- */
140
- protected handleInstagramError(error: AxiosError): void {
141
- const response = error.response;
142
- if (!response) {
143
- throw new Error('Network error occurred while calling Instagram API');
18
+ protected get apiBaseUrl(): string {
19
+ return 'https://graph.facebook.com/v18.0';
144
20
  }
145
21
 
146
- const errorData = response.data as any;
147
- const errorMessage = errorData?.error?.message || 'Unknown Instagram API error';
148
- const errorCode = errorData?.error?.code;
149
- const errorSubcode = errorData?.error?.error_subcode;
22
+ /**
23
+ * Instagram Business Account ID (stored in CustomAttribute1)
24
+ */
25
+ protected get instagramBusinessAccountId(): string {
26
+ return this.getCustomAttribute(1) || '';
27
+ }
150
28
 
151
- // Check for rate limiting
152
- if (errorCode === 32 || errorCode === 4 || response.status === 429) {
153
- const retryAfter = response.headers['x-app-usage'] ? this.parseAppUsage(response.headers['x-app-usage']) : 3600; // Default to 1 hour
29
+ /**
30
+ * Facebook Page ID (stored in CustomAttribute2) - required for Instagram Business API
31
+ */
32
+ protected get facebookPageId(): string {
33
+ return this.getCustomAttribute(2) || '';
34
+ }
154
35
 
155
- throw {
156
- code: 'RATE_LIMIT',
157
- message: 'Instagram API rate limit exceeded',
158
- retryAfter,
159
- };
36
+ /**
37
+ * Axios instance for API calls
38
+ */
39
+ private _axiosInstance: AxiosInstance | null = null;
40
+
41
+ /**
42
+ * Get or create axios instance with authentication
43
+ */
44
+ protected get axios(): AxiosInstance {
45
+ if (!this._axiosInstance) {
46
+ this._axiosInstance = axios.create({
47
+ baseURL: this.apiBaseUrl,
48
+ headers: this.buildHeaders()
49
+ });
50
+
51
+ // Add response interceptor for error handling
52
+ this._axiosInstance.interceptors.response.use(
53
+ response => response,
54
+ async error => {
55
+ if (error.response?.status === 401 && this.isAuthError(error)) {
56
+ LogStatus('Instagram token appears invalid, attempting refresh...');
57
+ await this.refreshAccessToken();
58
+
59
+ // Retry the request with new token
60
+ error.config.headers.Authorization = `Bearer ${this.getAccessToken()}`;
61
+ return axios.request(error.config);
62
+ }
63
+ throw error;
64
+ }
65
+ );
66
+ }
67
+ return this._axiosInstance;
160
68
  }
161
69
 
162
- // Check for permission errors
163
- if (errorCode === 10 || errorSubcode === 460) {
164
- throw {
165
- code: 'INSUFFICIENT_PERMISSIONS',
166
- message: 'Insufficient permissions for this Instagram operation',
167
- };
70
+ /**
71
+ * Refresh the Instagram/Facebook access token
72
+ */
73
+ protected async refreshAccessToken(): Promise<void> {
74
+ try {
75
+ // Instagram uses Facebook's OAuth system
76
+ // Long-lived tokens need to be exchanged periodically
77
+ const currentToken = this.getAccessToken();
78
+ if (!currentToken) {
79
+ throw new Error('No access token available to refresh');
80
+ }
81
+
82
+ // Exchange for a new long-lived token
83
+ const response = await axios.get(`${this.apiBaseUrl}/oauth/access_token`, {
84
+ params: {
85
+ grant_type: 'fb_exchange_token',
86
+ client_id: this.getCustomAttribute(3), // App ID stored in CustomAttribute3
87
+ client_secret: this.getCustomAttribute(4), // App Secret stored in CustomAttribute4
88
+ fb_exchange_token: currentToken
89
+ }
90
+ });
91
+
92
+ if (response.data.access_token) {
93
+ await this.updateStoredTokens(
94
+ response.data.access_token,
95
+ undefined, // Instagram doesn't use refresh tokens
96
+ response.data.expires_in || 5184000 // Default to 60 days
97
+ );
98
+
99
+ // Reset axios instance to use new token
100
+ this._axiosInstance = null;
101
+ }
102
+ } catch (error) {
103
+ LogError('Failed to refresh Instagram access token', error);
104
+ throw new Error('Failed to refresh Instagram access token');
105
+ }
168
106
  }
169
107
 
170
- // Media errors
171
- if (errorCode === 100 && errorMessage.toLowerCase().includes('media')) {
172
- throw {
173
- code: 'INVALID_MEDIA',
174
- message: errorMessage,
175
- };
108
+ /**
109
+ * Make an Instagram Graph API request
110
+ */
111
+ protected async makeInstagramRequest<T = any>(
112
+ endpoint: string,
113
+ method: 'GET' | 'POST' | 'DELETE' = 'GET',
114
+ data?: any,
115
+ params?: any
116
+ ): Promise<T> {
117
+ try {
118
+ this.logApiRequest(method, `${this.apiBaseUrl}/${endpoint}`, data || params);
119
+
120
+ const response = await this.axios.request<T>({
121
+ url: endpoint,
122
+ method,
123
+ data,
124
+ params
125
+ });
126
+
127
+ this.logApiResponse(response.data);
128
+ return response.data;
129
+ } catch (error) {
130
+ if (axios.isAxiosError(error)) {
131
+ this.handleInstagramError(error);
132
+ }
133
+ throw error;
134
+ }
176
135
  }
177
136
 
178
- // Post not found
179
- if (errorCode === 100 && errorSubcode === 33) {
180
- throw {
181
- code: 'POST_NOT_FOUND',
182
- message: 'Instagram post not found',
183
- };
137
+ /**
138
+ * Handle Instagram-specific errors
139
+ */
140
+ protected handleInstagramError(error: AxiosError): void {
141
+ const response = error.response;
142
+ if (!response) {
143
+ throw new Error('Network error occurred while calling Instagram API');
144
+ }
145
+
146
+ const errorData = response.data as any;
147
+ const errorMessage = errorData?.error?.message || 'Unknown Instagram API error';
148
+ const errorCode = errorData?.error?.code;
149
+ const errorSubcode = errorData?.error?.error_subcode;
150
+
151
+ // Check for rate limiting
152
+ if (errorCode === 32 || errorCode === 4 || response.status === 429) {
153
+ const retryAfter = response.headers['x-app-usage']
154
+ ? this.parseAppUsage(response.headers['x-app-usage'])
155
+ : 3600; // Default to 1 hour
156
+
157
+ throw {
158
+ code: 'RATE_LIMIT',
159
+ message: 'Instagram API rate limit exceeded',
160
+ retryAfter
161
+ };
162
+ }
163
+
164
+ // Check for permission errors
165
+ if (errorCode === 10 || errorSubcode === 460) {
166
+ throw {
167
+ code: 'INSUFFICIENT_PERMISSIONS',
168
+ message: 'Insufficient permissions for this Instagram operation'
169
+ };
170
+ }
171
+
172
+ // Media errors
173
+ if (errorCode === 100 && errorMessage.toLowerCase().includes('media')) {
174
+ throw {
175
+ code: 'INVALID_MEDIA',
176
+ message: errorMessage
177
+ };
178
+ }
179
+
180
+ // Post not found
181
+ if (errorCode === 100 && errorSubcode === 33) {
182
+ throw {
183
+ code: 'POST_NOT_FOUND',
184
+ message: 'Instagram post not found'
185
+ };
186
+ }
187
+
188
+ throw {
189
+ code: 'PLATFORM_ERROR',
190
+ message: errorMessage,
191
+ details: errorData
192
+ };
184
193
  }
185
194
 
186
- throw {
187
- code: 'PLATFORM_ERROR',
188
- message: errorMessage,
189
- details: errorData,
190
- };
191
- }
192
-
193
- /**
194
- * Parse Facebook's app usage header to determine rate limit status
195
- */
196
- private parseAppUsage(appUsage: string): number {
197
- try {
198
- const usage = JSON.parse(appUsage);
199
- const callCount = usage.call_count || 0;
200
- const totalTime = usage.total_time || 0;
201
- const totalCputime = usage.total_cputime || 0;
202
-
203
- // If any metric is above 90%, implement backoff
204
- if (callCount > 90 || totalTime > 90 || totalCputime > 90) {
205
- return 3600; // Wait 1 hour
206
- }
207
- return 0;
208
- } catch {
209
- return 0;
195
+ /**
196
+ * Parse Facebook's app usage header to determine rate limit status
197
+ */
198
+ private parseAppUsage(appUsage: string): number {
199
+ try {
200
+ const usage = JSON.parse(appUsage);
201
+ const callCount = usage.call_count || 0;
202
+ const totalTime = usage.total_time || 0;
203
+ const totalCputime = usage.total_cputime || 0;
204
+
205
+ // If any metric is above 90%, implement backoff
206
+ if (callCount > 90 || totalTime > 90 || totalCputime > 90) {
207
+ return 3600; // Wait 1 hour
208
+ }
209
+ return 0;
210
+ } catch {
211
+ return 0;
212
+ }
210
213
  }
211
- }
212
-
213
- /**
214
- * Upload media to Instagram (returns container ID)
215
- */
216
- protected async uploadSingleMedia(file: MediaFile): Promise<string> {
217
- try {
218
- // For Instagram, media must be hosted at a public URL
219
- // This is a simplified version - in production, you'd upload to a CDN first
220
- const mediaUrl = await this.uploadMediaToCDN(file);
221
-
222
- let containerParams: any = {
223
- access_token: this.getAccessToken(),
224
- };
225
-
226
- // Determine media type and set appropriate parameters
227
- if (file.mimeType.startsWith('image/')) {
228
- containerParams.image_url = mediaUrl;
229
-
230
- // Check if it's a carousel
231
- if (file.filename.includes('carousel')) {
232
- containerParams.is_carousel_item = true;
214
+
215
+ /**
216
+ * Upload media to Instagram (returns container ID)
217
+ */
218
+ protected async uploadSingleMedia(file: MediaFile): Promise<string> {
219
+ try {
220
+ // For Instagram, media must be hosted at a public URL
221
+ // This is a simplified version - in production, you'd upload to a CDN first
222
+ const mediaUrl = await this.uploadMediaToCDN(file);
223
+
224
+ let containerParams: any = {
225
+ access_token: this.getAccessToken()
226
+ };
227
+
228
+ // Determine media type and set appropriate parameters
229
+ if (file.mimeType.startsWith('image/')) {
230
+ containerParams.image_url = mediaUrl;
231
+
232
+ // Check if it's a carousel
233
+ if (file.filename.includes('carousel')) {
234
+ containerParams.is_carousel_item = true;
235
+ }
236
+ } else if (file.mimeType.startsWith('video/')) {
237
+ containerParams.video_url = mediaUrl;
238
+ containerParams.media_type = 'REELS'; // or 'VIDEO' for feed videos
239
+ }
240
+
241
+ // Add caption if provided in metadata
242
+ const metadata = (file as any).metadata;
243
+ if (metadata?.caption) {
244
+ containerParams.caption = metadata.caption;
245
+ }
246
+
247
+ // Create media container
248
+ const response = await this.makeInstagramRequest<{ id: string }>(
249
+ `${this.instagramBusinessAccountId}/media`,
250
+ 'POST',
251
+ containerParams
252
+ );
253
+
254
+ return response.id;
255
+ } catch (error) {
256
+ LogError('Failed to upload media to Instagram', error);
257
+ throw error;
233
258
  }
234
- } else if (file.mimeType.startsWith('video/')) {
235
- containerParams.video_url = mediaUrl;
236
- containerParams.media_type = 'REELS'; // or 'VIDEO' for feed videos
237
- }
238
-
239
- // Add caption if provided in metadata
240
- const metadata = (file as any).metadata;
241
- if (metadata?.caption) {
242
- containerParams.caption = metadata.caption;
243
- }
244
-
245
- // Create media container
246
- const response = await this.makeInstagramRequest<{ id: string }>(`${this.instagramBusinessAccountId}/media`, 'POST', containerParams);
247
-
248
- return response.id;
249
- } catch (error) {
250
- LogError('Failed to upload media to Instagram', error);
251
- throw error;
252
259
  }
253
- }
254
-
255
- /**
256
- * Upload media to a CDN (placeholder - implement based on your CDN)
257
- */
258
- private async uploadMediaToCDN(file: MediaFile): Promise<string> {
259
- // In a real implementation, this would upload to S3, Cloudinary, etc.
260
- // For now, throw an error indicating this needs implementation
261
- throw new Error('Media CDN upload not implemented. Instagram requires media to be hosted at a public URL.');
262
- }
263
-
264
- /**
265
- * Publish a media container
266
- */
267
- protected async publishMediaContainer(containerId: string): Promise<string> {
268
- const response = await this.makeInstagramRequest<{ id: string }>(`${this.instagramBusinessAccountId}/media_publish`, 'POST', {
269
- creation_id: containerId,
270
- access_token: this.getAccessToken(),
271
- });
272
-
273
- return response.id;
274
- }
275
-
276
- /**
277
- * Get insights for a media object or account
278
- */
279
- protected async getInsights(objectId: string, metrics: string[], period?: 'lifetime' | 'day' | 'week' | 'days_28'): Promise<any> {
280
- const params: any = {
281
- metric: metrics.join(','),
282
- access_token: this.getAccessToken(),
283
- };
284
-
285
- if (period) {
286
- params.period = period;
260
+
261
+ /**
262
+ * Upload media to a CDN (placeholder - implement based on your CDN)
263
+ */
264
+ private async uploadMediaToCDN(file: MediaFile): Promise<string> {
265
+ // In a real implementation, this would upload to S3, Cloudinary, etc.
266
+ // For now, throw an error indicating this needs implementation
267
+ throw new Error('Media CDN upload not implemented. Instagram requires media to be hosted at a public URL.');
268
+ }
269
+
270
+ /**
271
+ * Publish a media container
272
+ */
273
+ protected async publishMediaContainer(containerId: string): Promise<string> {
274
+ const response = await this.makeInstagramRequest<{ id: string }>(
275
+ `${this.instagramBusinessAccountId}/media_publish`,
276
+ 'POST',
277
+ {
278
+ creation_id: containerId,
279
+ access_token: this.getAccessToken()
280
+ }
281
+ );
282
+
283
+ return response.id;
287
284
  }
288
285
 
289
- const response = await this.makeInstagramRequest<{ data: any[] }>(`${objectId}/insights`, 'GET', null, params);
290
-
291
- return response.data;
292
- }
293
-
294
- /**
295
- * Search for posts (limited to business account's own posts)
296
- */
297
- protected async searchPosts(params: any): Promise<any[]> {
298
- // Instagram doesn't have a general search API
299
- // We can only search within the business account's own posts
300
- const fields = 'id,caption,media_type,media_url,permalink,timestamp,like_count,comments_count';
301
-
302
- let endpoint = `${this.instagramBusinessAccountId}/media`;
303
- const queryParams: any = {
304
- fields,
305
- access_token: this.getAccessToken(),
306
- limit: params.limit || 25,
307
- };
308
-
309
- // Add date filtering if provided
310
- if (params.startDate) {
311
- queryParams.since = Math.floor(new Date(params.startDate).getTime() / 1000);
286
+ /**
287
+ * Get insights for a media object or account
288
+ */
289
+ protected async getInsights(
290
+ objectId: string,
291
+ metrics: string[],
292
+ period?: 'lifetime' | 'day' | 'week' | 'days_28'
293
+ ): Promise<any> {
294
+ const params: any = {
295
+ metric: metrics.join(','),
296
+ access_token: this.getAccessToken()
297
+ };
298
+
299
+ if (period) {
300
+ params.period = period;
301
+ }
302
+
303
+ const response = await this.makeInstagramRequest<{ data: any[] }>(
304
+ `${objectId}/insights`,
305
+ 'GET',
306
+ null,
307
+ params
308
+ );
309
+
310
+ return response.data;
312
311
  }
313
- if (params.endDate) {
314
- queryParams.until = Math.floor(new Date(params.endDate).getTime() / 1000);
312
+
313
+ /**
314
+ * Search for posts (limited to business account's own posts)
315
+ */
316
+ protected async searchPosts(params: any): Promise<any[]> {
317
+ // Instagram doesn't have a general search API
318
+ // We can only search within the business account's own posts
319
+ const fields = 'id,caption,media_type,media_url,permalink,timestamp,like_count,comments_count';
320
+
321
+ let endpoint = `${this.instagramBusinessAccountId}/media`;
322
+ const queryParams: any = {
323
+ fields,
324
+ access_token: this.getAccessToken(),
325
+ limit: params.limit || 25
326
+ };
327
+
328
+ // Add date filtering if provided
329
+ if (params.startDate) {
330
+ queryParams.since = Math.floor(new Date(params.startDate).getTime() / 1000);
331
+ }
332
+ if (params.endDate) {
333
+ queryParams.until = Math.floor(new Date(params.endDate).getTime() / 1000);
334
+ }
335
+
336
+ const posts: any[] = [];
337
+ let hasNext = true;
338
+
339
+ while (hasNext && posts.length < (params.limit || 100)) {
340
+ const response = await this.makeInstagramRequest<{
341
+ data: any[];
342
+ paging?: { next: string };
343
+ }>(endpoint, 'GET', null, queryParams);
344
+
345
+ if (response.data) {
346
+ // Filter by caption if query is provided
347
+ const filtered = params.query
348
+ ? response.data.filter(post =>
349
+ post.caption?.toLowerCase().includes(params.query.toLowerCase()))
350
+ : response.data;
351
+
352
+ posts.push(...filtered);
353
+ }
354
+
355
+ if (response.paging?.next) {
356
+ // Parse next URL for pagination
357
+ const nextUrl = new URL(response.paging.next);
358
+ queryParams.after = nextUrl.searchParams.get('after');
359
+ } else {
360
+ hasNext = false;
361
+ }
362
+ }
363
+
364
+ return posts.slice(0, params.limit || 100);
315
365
  }
316
366
 
317
- const posts: any[] = [];
318
- let hasNext = true;
319
-
320
- while (hasNext && posts.length < (params.limit || 100)) {
321
- const response = await this.makeInstagramRequest<{
322
- data: any[];
323
- paging?: { next: string };
324
- }>(endpoint, 'GET', null, queryParams);
325
-
326
- if (response.data) {
327
- // Filter by caption if query is provided
328
- const filtered = params.query
329
- ? response.data.filter((post) => post.caption?.toLowerCase().includes(params.query.toLowerCase()))
330
- : response.data;
331
-
332
- posts.push(...filtered);
333
- }
334
-
335
- if (response.paging?.next) {
336
- // Parse next URL for pagination
337
- const nextUrl = new URL(response.paging.next);
338
- queryParams.after = nextUrl.searchParams.get('after');
339
- } else {
340
- hasNext = false;
341
- }
367
+ /**
368
+ * Normalize Instagram post to common format
369
+ */
370
+ protected normalizePost(instagramPost: any): any {
371
+ return {
372
+ id: instagramPost.id,
373
+ platform: 'Instagram',
374
+ profileId: this.instagramBusinessAccountId,
375
+ content: instagramPost.caption || '',
376
+ mediaUrls: instagramPost.media_url ? [instagramPost.media_url] : [],
377
+ publishedAt: new Date(instagramPost.timestamp),
378
+ analytics: {
379
+ impressions: instagramPost.impressions_count || 0,
380
+ engagements: (instagramPost.like_count || 0) + (instagramPost.comments_count || 0),
381
+ clicks: 0, // Not available in basic metrics
382
+ shares: 0, // Instagram doesn't track shares
383
+ comments: instagramPost.comments_count || 0,
384
+ likes: instagramPost.like_count || 0,
385
+ reach: instagramPost.reach || 0,
386
+ saves: instagramPost.saved || 0,
387
+ videoViews: instagramPost.video_views || 0,
388
+ platformMetrics: instagramPost
389
+ },
390
+ platformSpecificData: {
391
+ mediaType: instagramPost.media_type,
392
+ permalink: instagramPost.permalink,
393
+ isCarousel: instagramPost.media_type === 'CAROUSEL_ALBUM'
394
+ }
395
+ };
342
396
  }
343
397
 
344
- return posts.slice(0, params.limit || 100);
345
- }
346
-
347
- /**
348
- * Normalize Instagram post to common format
349
- */
350
- protected normalizePost(instagramPost: any): any {
351
- return {
352
- id: instagramPost.id,
353
- platform: 'Instagram',
354
- profileId: this.instagramBusinessAccountId,
355
- content: instagramPost.caption || '',
356
- mediaUrls: instagramPost.media_url ? [instagramPost.media_url] : [],
357
- publishedAt: new Date(instagramPost.timestamp),
358
- analytics: {
359
- impressions: instagramPost.impressions_count || 0,
360
- engagements: (instagramPost.like_count || 0) + (instagramPost.comments_count || 0),
361
- clicks: 0, // Not available in basic metrics
362
- shares: 0, // Instagram doesn't track shares
363
- comments: instagramPost.comments_count || 0,
364
- likes: instagramPost.like_count || 0,
365
- reach: instagramPost.reach || 0,
366
- saves: instagramPost.saved || 0,
367
- videoViews: instagramPost.video_views || 0,
368
- platformMetrics: instagramPost,
369
- },
370
- platformSpecificData: {
371
- mediaType: instagramPost.media_type,
372
- permalink: instagramPost.permalink,
373
- isCarousel: instagramPost.media_type === 'CAROUSEL_ALBUM',
374
- },
375
- };
376
- }
377
-
378
- /**
379
- * Check if media container is ready for publishing
380
- */
381
- protected async isMediaContainerReady(containerId: string): Promise<boolean> {
382
- const response = await this.makeInstagramRequest<{ status_code: string }>(containerId, 'GET', null, {
383
- fields: 'status_code',
384
- access_token: this.getAccessToken(),
385
- });
386
-
387
- return response.status_code === 'FINISHED';
388
- }
389
-
390
- /**
391
- * Wait for media container to be ready
392
- */
393
- protected async waitForMediaContainer(containerId: string, maxWaitTime: number = 60000): Promise<void> {
394
- const startTime = Date.now();
395
- const pollInterval = 2000; // 2 seconds
396
-
397
- while (Date.now() - startTime < maxWaitTime) {
398
- if (await this.isMediaContainerReady(containerId)) {
399
- return;
400
- }
401
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
398
+ /**
399
+ * Check if media container is ready for publishing
400
+ */
401
+ protected async isMediaContainerReady(containerId: string): Promise<boolean> {
402
+ const response = await this.makeInstagramRequest<{ status_code: string }>(
403
+ containerId,
404
+ 'GET',
405
+ null,
406
+ {
407
+ fields: 'status_code',
408
+ access_token: this.getAccessToken()
409
+ }
410
+ );
411
+
412
+ return response.status_code === 'FINISHED';
402
413
  }
403
414
 
404
- throw new Error('Media container processing timeout');
405
- }
406
- }
415
+ /**
416
+ * Wait for media container to be ready
417
+ */
418
+ protected async waitForMediaContainer(containerId: string, maxWaitTime: number = 60000): Promise<void> {
419
+ const startTime = Date.now();
420
+ const pollInterval = 2000; // 2 seconds
421
+
422
+ while (Date.now() - startTime < maxWaitTime) {
423
+ if (await this.isMediaContainerReady(containerId)) {
424
+ return;
425
+ }
426
+ await new Promise(resolve => setTimeout(resolve, pollInterval));
427
+ }
428
+
429
+ throw new Error('Media container processing timeout');
430
+ }
431
+ }