@memberjunction/actions-bizapps-social 2.111.0 → 2.112.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +6 -6
  3. package/dist/base/base-social.action.d.ts.map +1 -1
  4. package/dist/base/base-social.action.js +18 -24
  5. package/dist/base/base-social.action.js.map +1 -1
  6. package/dist/providers/buffer/buffer-base.action.d.ts.map +1 -1
  7. package/dist/providers/buffer/buffer-base.action.js +35 -34
  8. package/dist/providers/buffer/buffer-base.action.js.map +1 -1
  9. package/dist/providers/facebook/actions/boost-post.action.d.ts.map +1 -1
  10. package/dist/providers/facebook/actions/boost-post.action.js +33 -33
  11. package/dist/providers/facebook/actions/boost-post.action.js.map +1 -1
  12. package/dist/providers/facebook/actions/create-album.action.d.ts.map +1 -1
  13. package/dist/providers/facebook/actions/create-album.action.js +34 -36
  14. package/dist/providers/facebook/actions/create-album.action.js.map +1 -1
  15. package/dist/providers/facebook/actions/create-post.action.d.ts.map +1 -1
  16. package/dist/providers/facebook/actions/create-post.action.js +20 -20
  17. package/dist/providers/facebook/actions/create-post.action.js.map +1 -1
  18. package/dist/providers/facebook/actions/get-page-insights.action.d.ts.map +1 -1
  19. package/dist/providers/facebook/actions/get-page-insights.action.js +25 -27
  20. package/dist/providers/facebook/actions/get-page-insights.action.js.map +1 -1
  21. package/dist/providers/facebook/actions/get-page-posts.action.d.ts.map +1 -1
  22. package/dist/providers/facebook/actions/get-page-posts.action.js +19 -23
  23. package/dist/providers/facebook/actions/get-page-posts.action.js.map +1 -1
  24. package/dist/providers/facebook/actions/get-post-insights.action.d.ts.map +1 -1
  25. package/dist/providers/facebook/actions/get-post-insights.action.js +28 -32
  26. package/dist/providers/facebook/actions/get-post-insights.action.js.map +1 -1
  27. package/dist/providers/facebook/actions/respond-to-comments.action.d.ts.map +1 -1
  28. package/dist/providers/facebook/actions/respond-to-comments.action.js +42 -44
  29. package/dist/providers/facebook/actions/respond-to-comments.action.js.map +1 -1
  30. package/dist/providers/facebook/actions/schedule-post.action.d.ts.map +1 -1
  31. package/dist/providers/facebook/actions/schedule-post.action.js +29 -29
  32. package/dist/providers/facebook/actions/schedule-post.action.js.map +1 -1
  33. package/dist/providers/facebook/actions/search-posts.action.d.ts.map +1 -1
  34. package/dist/providers/facebook/actions/search-posts.action.js +37 -39
  35. package/dist/providers/facebook/actions/search-posts.action.js.map +1 -1
  36. package/dist/providers/facebook/facebook-base.action.d.ts.map +1 -1
  37. package/dist/providers/facebook/facebook-base.action.js +44 -59
  38. package/dist/providers/facebook/facebook-base.action.js.map +1 -1
  39. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.d.ts.map +1 -1
  40. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js +33 -31
  41. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js.map +1 -1
  42. package/dist/providers/hootsuite/actions/create-scheduled-post.action.d.ts.map +1 -1
  43. package/dist/providers/hootsuite/actions/create-scheduled-post.action.js +28 -32
  44. package/dist/providers/hootsuite/actions/create-scheduled-post.action.js.map +1 -1
  45. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.d.ts.map +1 -1
  46. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js +19 -19
  47. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js.map +1 -1
  48. package/dist/providers/hootsuite/actions/get-analytics.action.d.ts.map +1 -1
  49. package/dist/providers/hootsuite/actions/get-analytics.action.js +24 -26
  50. package/dist/providers/hootsuite/actions/get-analytics.action.js.map +1 -1
  51. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.d.ts.map +1 -1
  52. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js +22 -22
  53. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js.map +1 -1
  54. package/dist/providers/hootsuite/actions/get-social-profiles.action.d.ts.map +1 -1
  55. package/dist/providers/hootsuite/actions/get-social-profiles.action.js +32 -34
  56. package/dist/providers/hootsuite/actions/get-social-profiles.action.js.map +1 -1
  57. package/dist/providers/hootsuite/actions/search-posts.action.d.ts.map +1 -1
  58. package/dist/providers/hootsuite/actions/search-posts.action.js +43 -52
  59. package/dist/providers/hootsuite/actions/search-posts.action.js.map +1 -1
  60. package/dist/providers/hootsuite/actions/update-scheduled-post.action.d.ts.map +1 -1
  61. package/dist/providers/hootsuite/actions/update-scheduled-post.action.js +30 -28
  62. package/dist/providers/hootsuite/actions/update-scheduled-post.action.js.map +1 -1
  63. package/dist/providers/hootsuite/hootsuite-base.action.d.ts.map +1 -1
  64. package/dist/providers/hootsuite/hootsuite-base.action.js +18 -20
  65. package/dist/providers/hootsuite/hootsuite-base.action.js.map +1 -1
  66. package/dist/providers/instagram/actions/create-post.action.d.ts.map +1 -1
  67. package/dist/providers/instagram/actions/create-post.action.js +27 -26
  68. package/dist/providers/instagram/actions/create-post.action.js.map +1 -1
  69. package/dist/providers/instagram/actions/create-story.action.d.ts.map +1 -1
  70. package/dist/providers/instagram/actions/create-story.action.js +35 -35
  71. package/dist/providers/instagram/actions/create-story.action.js.map +1 -1
  72. package/dist/providers/instagram/actions/get-account-insights.action.d.ts.map +1 -1
  73. package/dist/providers/instagram/actions/get-account-insights.action.js +38 -59
  74. package/dist/providers/instagram/actions/get-account-insights.action.js.map +1 -1
  75. package/dist/providers/instagram/actions/get-business-posts.action.d.ts.map +1 -1
  76. package/dist/providers/instagram/actions/get-business-posts.action.js +29 -29
  77. package/dist/providers/instagram/actions/get-business-posts.action.js.map +1 -1
  78. package/dist/providers/instagram/actions/get-comments.action.d.ts.map +1 -1
  79. package/dist/providers/instagram/actions/get-comments.action.js +36 -36
  80. package/dist/providers/instagram/actions/get-comments.action.js.map +1 -1
  81. package/dist/providers/instagram/actions/get-post-insights.action.d.ts.map +1 -1
  82. package/dist/providers/instagram/actions/get-post-insights.action.js +23 -25
  83. package/dist/providers/instagram/actions/get-post-insights.action.js.map +1 -1
  84. package/dist/providers/instagram/actions/schedule-post.action.d.ts.map +1 -1
  85. package/dist/providers/instagram/actions/schedule-post.action.js +25 -25
  86. package/dist/providers/instagram/actions/schedule-post.action.js.map +1 -1
  87. package/dist/providers/instagram/actions/search-posts.action.d.ts.map +1 -1
  88. package/dist/providers/instagram/actions/search-posts.action.js +56 -60
  89. package/dist/providers/instagram/actions/search-posts.action.js.map +1 -1
  90. package/dist/providers/instagram/instagram-base.action.d.ts.map +1 -1
  91. package/dist/providers/instagram/instagram-base.action.js +25 -27
  92. package/dist/providers/instagram/instagram-base.action.js.map +1 -1
  93. package/dist/providers/linkedin/actions/create-article.action.d.ts.map +1 -1
  94. package/dist/providers/linkedin/actions/create-article.action.js +55 -45
  95. package/dist/providers/linkedin/actions/create-article.action.js.map +1 -1
  96. package/dist/providers/linkedin/actions/create-post.action.d.ts.map +1 -1
  97. package/dist/providers/linkedin/actions/create-post.action.js +31 -29
  98. package/dist/providers/linkedin/actions/create-post.action.js.map +1 -1
  99. package/dist/providers/linkedin/actions/get-followers.action.d.ts.map +1 -1
  100. package/dist/providers/linkedin/actions/get-followers.action.js +28 -28
  101. package/dist/providers/linkedin/actions/get-followers.action.js.map +1 -1
  102. package/dist/providers/linkedin/actions/get-organization-posts.action.d.ts.map +1 -1
  103. package/dist/providers/linkedin/actions/get-organization-posts.action.js +20 -20
  104. package/dist/providers/linkedin/actions/get-organization-posts.action.js.map +1 -1
  105. package/dist/providers/linkedin/actions/get-personal-posts.action.d.ts.map +1 -1
  106. package/dist/providers/linkedin/actions/get-personal-posts.action.js +19 -19
  107. package/dist/providers/linkedin/actions/get-personal-posts.action.js.map +1 -1
  108. package/dist/providers/linkedin/actions/get-post-analytics.action.d.ts.map +1 -1
  109. package/dist/providers/linkedin/actions/get-post-analytics.action.js +25 -23
  110. package/dist/providers/linkedin/actions/get-post-analytics.action.js.map +1 -1
  111. package/dist/providers/linkedin/actions/schedule-post.action.d.ts.map +1 -1
  112. package/dist/providers/linkedin/actions/schedule-post.action.js +32 -30
  113. package/dist/providers/linkedin/actions/schedule-post.action.js.map +1 -1
  114. package/dist/providers/linkedin/actions/search-posts.action.d.ts.map +1 -1
  115. package/dist/providers/linkedin/actions/search-posts.action.js +28 -30
  116. package/dist/providers/linkedin/actions/search-posts.action.js.map +1 -1
  117. package/dist/providers/linkedin/linkedin-base.action.d.ts.map +1 -1
  118. package/dist/providers/linkedin/linkedin-base.action.js +33 -38
  119. package/dist/providers/linkedin/linkedin-base.action.js.map +1 -1
  120. package/dist/providers/tiktok/tiktok-base.action.d.ts.map +1 -1
  121. package/dist/providers/tiktok/tiktok-base.action.js +25 -26
  122. package/dist/providers/tiktok/tiktok-base.action.js.map +1 -1
  123. package/dist/providers/twitter/actions/create-thread.action.d.ts.map +1 -1
  124. package/dist/providers/twitter/actions/create-thread.action.js +25 -29
  125. package/dist/providers/twitter/actions/create-thread.action.js.map +1 -1
  126. package/dist/providers/twitter/actions/create-tweet.action.d.ts.map +1 -1
  127. package/dist/providers/twitter/actions/create-tweet.action.js +23 -23
  128. package/dist/providers/twitter/actions/create-tweet.action.js.map +1 -1
  129. package/dist/providers/twitter/actions/delete-tweet.action.d.ts.map +1 -1
  130. package/dist/providers/twitter/actions/delete-tweet.action.js +19 -19
  131. package/dist/providers/twitter/actions/delete-tweet.action.js.map +1 -1
  132. package/dist/providers/twitter/actions/get-analytics.action.d.ts.map +1 -1
  133. package/dist/providers/twitter/actions/get-analytics.action.js +40 -47
  134. package/dist/providers/twitter/actions/get-analytics.action.js.map +1 -1
  135. package/dist/providers/twitter/actions/get-mentions.action.d.ts.map +1 -1
  136. package/dist/providers/twitter/actions/get-mentions.action.js +30 -31
  137. package/dist/providers/twitter/actions/get-mentions.action.js.map +1 -1
  138. package/dist/providers/twitter/actions/get-timeline.action.d.ts.map +1 -1
  139. package/dist/providers/twitter/actions/get-timeline.action.js +29 -29
  140. package/dist/providers/twitter/actions/get-timeline.action.js.map +1 -1
  141. package/dist/providers/twitter/actions/schedule-tweet.action.d.ts.map +1 -1
  142. package/dist/providers/twitter/actions/schedule-tweet.action.js +26 -26
  143. package/dist/providers/twitter/actions/schedule-tweet.action.js.map +1 -1
  144. package/dist/providers/twitter/actions/search-tweets.action.d.ts.map +1 -1
  145. package/dist/providers/twitter/actions/search-tweets.action.js +56 -58
  146. package/dist/providers/twitter/actions/search-tweets.action.js.map +1 -1
  147. package/dist/providers/twitter/twitter-base.action.d.ts.map +1 -1
  148. package/dist/providers/twitter/twitter-base.action.js +58 -68
  149. package/dist/providers/twitter/twitter-base.action.js.map +1 -1
  150. package/dist/providers/youtube/youtube-base.action.d.ts +1 -1
  151. package/dist/providers/youtube/youtube-base.action.d.ts.map +1 -1
  152. package/dist/providers/youtube/youtube-base.action.js +22 -25
  153. package/dist/providers/youtube/youtube-base.action.js.map +1 -1
  154. package/package.json +5 -6
  155. package/src/base/base-social.action.ts +217 -224
  156. package/src/providers/buffer/buffer-base.action.ts +435 -441
  157. package/src/providers/facebook/actions/boost-post.action.ts +350 -386
  158. package/src/providers/facebook/actions/create-album.action.ts +291 -307
  159. package/src/providers/facebook/actions/create-post.action.ts +224 -227
  160. package/src/providers/facebook/actions/get-page-insights.action.ts +383 -403
  161. package/src/providers/facebook/actions/get-page-posts.action.ts +214 -225
  162. package/src/providers/facebook/actions/get-post-insights.action.ts +300 -316
  163. package/src/providers/facebook/actions/respond-to-comments.action.ts +319 -336
  164. package/src/providers/facebook/actions/schedule-post.action.ts +289 -292
  165. package/src/providers/facebook/actions/search-posts.action.ts +399 -413
  166. package/src/providers/facebook/facebook-base.action.ts +653 -670
  167. package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
  168. package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +184 -189
  169. package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +160 -161
  170. package/src/providers/hootsuite/actions/get-analytics.action.ts +249 -254
  171. package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +206 -207
  172. package/src/providers/hootsuite/actions/get-social-profiles.action.ts +206 -205
  173. package/src/providers/hootsuite/actions/search-posts.action.ts +351 -369
  174. package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +211 -209
  175. package/src/providers/hootsuite/hootsuite-base.action.ts +301 -307
  176. package/src/providers/instagram/actions/create-post.action.ts +276 -296
  177. package/src/providers/instagram/actions/create-story.action.ts +378 -394
  178. package/src/providers/instagram/actions/get-account-insights.action.ts +384 -420
  179. package/src/providers/instagram/actions/get-business-posts.action.ts +233 -242
  180. package/src/providers/instagram/actions/get-comments.action.ts +365 -377
  181. package/src/providers/instagram/actions/get-post-insights.action.ts +265 -273
  182. package/src/providers/instagram/actions/schedule-post.action.ts +233 -235
  183. package/src/providers/instagram/actions/search-posts.action.ts +512 -538
  184. package/src/providers/instagram/instagram-base.action.ts +368 -393
  185. package/src/providers/linkedin/actions/create-article.action.ts +275 -266
  186. package/src/providers/linkedin/actions/create-post.action.ts +179 -177
  187. package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
  188. package/src/providers/linkedin/actions/get-organization-posts.action.ts +146 -147
  189. package/src/providers/linkedin/actions/get-personal-posts.action.ts +138 -139
  190. package/src/providers/linkedin/actions/get-post-analytics.action.ts +190 -189
  191. package/src/providers/linkedin/actions/schedule-post.action.ts +191 -189
  192. package/src/providers/linkedin/actions/search-posts.action.ts +275 -283
  193. package/src/providers/linkedin/linkedin-base.action.ts +407 -421
  194. package/src/providers/tiktok/tiktok-base.action.ts +305 -320
  195. package/src/providers/twitter/actions/create-thread.action.ts +203 -207
  196. package/src/providers/twitter/actions/create-tweet.action.ts +187 -188
  197. package/src/providers/twitter/actions/delete-tweet.action.ts +128 -129
  198. package/src/providers/twitter/actions/get-analytics.action.ts +402 -411
  199. package/src/providers/twitter/actions/get-mentions.action.ts +218 -219
  200. package/src/providers/twitter/actions/get-timeline.action.ts +232 -233
  201. package/src/providers/twitter/actions/schedule-tweet.action.ts +221 -222
  202. package/src/providers/twitter/actions/search-tweets.action.ts +540 -543
  203. package/src/providers/twitter/twitter-base.action.ts +541 -560
  204. package/src/providers/youtube/youtube-base.action.ts +320 -333
@@ -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/core';
3
+ import { UserInfo, LogStatus, LogError } from '@memberjunction/global';
4
4
  import axios, { AxiosInstance, AxiosError } from 'axios';
5
5
  import { BaseAction } from '@memberjunction/actions';
6
6
 
@@ -11,421 +11,396 @@ 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';
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
+ );
16
66
  }
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
+ );
17
98
 
18
- protected get apiBaseUrl(): string {
19
- return 'https://graph.facebook.com/v18.0';
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');
20
105
  }
21
-
22
- /**
23
- * Instagram Business Account ID (stored in CustomAttribute1)
24
- */
25
- protected get instagramBusinessAccountId(): string {
26
- return this.getCustomAttribute(1) || '';
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;
27
134
  }
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) || '';
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');
34
144
  }
35
145
 
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;
68
- }
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;
69
150
 
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
- }
106
- }
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
107
154
 
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
- }
155
+ throw {
156
+ code: 'RATE_LIMIT',
157
+ message: 'Instagram API rate limit exceeded',
158
+ retryAfter,
159
+ };
135
160
  }
136
161
 
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
- };
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
+ };
193
168
  }
194
169
 
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
- }
170
+ // Media errors
171
+ if (errorCode === 100 && errorMessage.toLowerCase().includes('media')) {
172
+ throw {
173
+ code: 'INVALID_MEDIA',
174
+ message: errorMessage,
175
+ };
213
176
  }
214
177
 
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;
258
- }
178
+ // Post not found
179
+ if (errorCode === 100 && errorSubcode === 33) {
180
+ throw {
181
+ code: 'POST_NOT_FOUND',
182
+ message: 'Instagram post not found',
183
+ };
259
184
  }
260
185
 
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.');
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;
268
210
  }
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;
284
- }
285
-
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;
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;
301
233
  }
302
-
303
- const response = await this.makeInstagramRequest<{ data: any[] }>(
304
- `${objectId}/insights`,
305
- 'GET',
306
- null,
307
- params
308
- );
309
-
310
- return response.data;
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;
311
252
  }
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);
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;
365
287
  }
366
288
 
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
- };
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);
396
312
  }
397
-
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';
313
+ if (params.endDate) {
314
+ queryParams.until = Math.floor(new Date(params.endDate).getTime() / 1000);
413
315
  }
414
316
 
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
- }
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
+ }
342
+ }
428
343
 
429
- throw new Error('Media container processing timeout');
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));
430
402
  }
431
- }
403
+
404
+ throw new Error('Media container processing timeout');
405
+ }
406
+ }