@memberjunction/actions-bizapps-social 2.112.0 → 2.113.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 +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, SocialPost, SocialAnalytics, MediaFile } from '../../base/base-social.action';
3
- import { UserInfo, LogError, LogStatus } from '@memberjunction/global';
3
+ import { UserInfo, LogError, LogStatus } from '@memberjunction/core';
4
4
  import axios, { AxiosInstance, AxiosError } from 'axios';
5
5
  import FormData from 'form-data';
6
6
  import { BaseAction } from '@memberjunction/actions';
@@ -11,475 +11,481 @@ import { BaseAction } from '@memberjunction/actions';
11
11
  */
12
12
  @RegisterClass(BaseAction, 'BufferBaseAction')
13
13
  export abstract class BufferBaseAction extends BaseSocialMediaAction {
14
- protected get platformName(): string {
15
- return 'Buffer';
16
- }
17
-
18
- protected get apiBaseUrl(): string {
19
- return 'https://api.bufferapp.com/1';
20
- }
21
-
22
- private axiosInstance: AxiosInstance | null = null;
23
-
24
- /**
25
- * Get axios instance with authentication
26
- */
27
- protected getAxiosInstance(): AxiosInstance {
28
- if (!this.axiosInstance) {
29
- this.axiosInstance = axios.create({
30
- baseURL: this.apiBaseUrl,
31
- timeout: 30000,
32
- headers: {
33
- Accept: 'application/json',
34
- 'Content-Type': 'application/json',
35
- },
36
- });
37
-
38
- // Add request interceptor for auth
39
- this.axiosInstance.interceptors.request.use(
40
- (config) => {
41
- const token = this.getAccessToken();
42
- if (token) {
43
- config.headers.Authorization = `Bearer ${token}`;
44
- }
45
- return config;
46
- },
47
- (error) => {
48
- return Promise.reject(error);
49
- }
50
- );
51
-
52
- // Add response interceptor for rate limiting
53
- this.axiosInstance.interceptors.response.use(
54
- (response) => {
55
- // Log rate limit headers if present
56
- const headers = response.headers;
57
- const rateLimitInfo = this.parseRateLimitHeaders(headers);
58
- if (rateLimitInfo) {
59
- LogStatus(`Buffer API - Remaining requests: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`);
60
- }
61
- return response;
62
- },
63
- async (error: AxiosError) => {
64
- if (error.response?.status === 429) {
65
- // Rate limit hit
66
- const retryAfter = error.response.headers['retry-after'];
67
- await this.handleRateLimit(retryAfter ? parseInt(retryAfter) : 60);
68
- // Retry the request
69
- return this.axiosInstance!.request(error.config!);
70
- }
71
- return Promise.reject(error);
72
- }
73
- );
74
- }
75
- return this.axiosInstance;
76
- }
77
-
78
- /**
79
- * Refresh access token using refresh token
80
- */
81
- protected async refreshAccessToken(): Promise<void> {
82
- const refreshToken = this.getRefreshToken();
83
- if (!refreshToken) {
84
- throw new Error('No refresh token available for Buffer');
14
+ protected get platformName(): string {
15
+ return 'Buffer';
85
16
  }
86
17
 
87
- try {
88
- const response = await axios.post(`${this.apiBaseUrl}/oauth2/token`, {
89
- client_id: this.getCustomAttribute(1), // Store Buffer client ID in CustomAttribute1
90
- client_secret: this.getCustomAttribute(2), // Store Buffer client secret in CustomAttribute2
91
- refresh_token: refreshToken,
92
- grant_type: 'refresh_token',
93
- });
94
-
95
- const { access_token, expires_in } = response.data;
96
-
97
- await this.updateStoredTokens(
98
- access_token,
99
- refreshToken, // Buffer doesn't rotate refresh tokens
100
- expires_in
101
- );
102
-
103
- LogStatus('Buffer access token refreshed successfully');
104
- } catch (error) {
105
- LogError('Failed to refresh Buffer access token:', error);
106
- throw new Error('Failed to refresh Buffer access token');
107
- }
108
- }
109
-
110
- /**
111
- * Get Buffer profiles for the authenticated user
112
- */
113
- protected async getProfiles(): Promise<any[]> {
114
- try {
115
- const response = await this.getAxiosInstance().get('/profiles.json');
116
- return response.data || [];
117
- } catch (error) {
118
- LogError('Failed to get Buffer profiles:', error);
119
- throw error;
120
- }
121
- }
122
-
123
- /**
124
- * Upload media to Buffer
125
- */
126
- protected async uploadSingleMedia(file: MediaFile): Promise<string> {
127
- try {
128
- const formData = new FormData();
129
-
130
- // Convert base64 to buffer if needed
131
- const buffer = typeof file.data === 'string' ? Buffer.from(file.data, 'base64') : file.data;
132
-
133
- formData.append('media', buffer, {
134
- filename: file.filename,
135
- contentType: file.mimeType,
136
- });
137
-
138
- const response = await this.getAxiosInstance().post('/updates/media/upload.json', formData, {
139
- headers: {
140
- ...formData.getHeaders(),
141
- },
142
- });
143
-
144
- if (response.data && response.data.media && response.data.media[0]) {
145
- return response.data.media[0].picture; // URL of uploaded media
146
- }
147
-
148
- throw new Error('Failed to upload media to Buffer');
149
- } catch (error) {
150
- LogError('Buffer media upload failed:', error);
151
- throw error;
152
- }
153
- }
154
-
155
- /**
156
- * Create a Buffer update (post)
157
- */
158
- protected async createUpdate(
159
- profileIds: string[],
160
- text: string,
161
- media?: { link?: string; description?: string; picture?: string }[],
162
- scheduledAt?: Date,
163
- options?: {
164
- shorten?: boolean;
165
- now?: boolean;
166
- top?: boolean;
167
- attachment?: boolean;
168
- }
169
- ): Promise<any> {
170
- const data: any = {
171
- profile_ids: profileIds,
172
- text: text,
173
- shorten: options?.shorten !== false, // Default to true
174
- };
175
-
176
- if (media && media.length > 0) {
177
- data.media = media;
18
+ protected get apiBaseUrl(): string {
19
+ return 'https://api.bufferapp.com/1';
178
20
  }
179
21
 
180
- if (scheduledAt) {
181
- data.scheduled_at = Math.floor(scheduledAt.getTime() / 1000); // Unix timestamp
182
- } else if (options?.now) {
183
- data.now = true;
22
+ private axiosInstance: AxiosInstance | null = null;
23
+
24
+ /**
25
+ * Get axios instance with authentication
26
+ */
27
+ protected getAxiosInstance(): AxiosInstance {
28
+ if (!this.axiosInstance) {
29
+ this.axiosInstance = axios.create({
30
+ baseURL: this.apiBaseUrl,
31
+ timeout: 30000,
32
+ headers: {
33
+ 'Accept': 'application/json',
34
+ 'Content-Type': 'application/json'
35
+ }
36
+ });
37
+
38
+ // Add request interceptor for auth
39
+ this.axiosInstance.interceptors.request.use(
40
+ (config) => {
41
+ const token = this.getAccessToken();
42
+ if (token) {
43
+ config.headers.Authorization = `Bearer ${token}`;
44
+ }
45
+ return config;
46
+ },
47
+ (error) => {
48
+ return Promise.reject(error);
49
+ }
50
+ );
51
+
52
+ // Add response interceptor for rate limiting
53
+ this.axiosInstance.interceptors.response.use(
54
+ (response) => {
55
+ // Log rate limit headers if present
56
+ const headers = response.headers;
57
+ const rateLimitInfo = this.parseRateLimitHeaders(headers);
58
+ if (rateLimitInfo) {
59
+ LogStatus(`Buffer API - Remaining requests: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`);
60
+ }
61
+ return response;
62
+ },
63
+ async (error: AxiosError) => {
64
+ if (error.response?.status === 429) {
65
+ // Rate limit hit
66
+ const retryAfter = error.response.headers['retry-after'];
67
+ await this.handleRateLimit(retryAfter ? parseInt(retryAfter) : 60);
68
+ // Retry the request
69
+ return this.axiosInstance!.request(error.config!);
70
+ }
71
+ return Promise.reject(error);
72
+ }
73
+ );
74
+ }
75
+ return this.axiosInstance;
184
76
  }
185
77
 
186
- if (options?.top) {
187
- data.top = true;
188
- }
78
+ /**
79
+ * Refresh access token using refresh token
80
+ */
81
+ protected async refreshAccessToken(): Promise<void> {
82
+ const refreshToken = this.getRefreshToken();
83
+ if (!refreshToken) {
84
+ throw new Error('No refresh token available for Buffer');
85
+ }
189
86
 
190
- if (options?.attachment !== undefined) {
191
- data.attachment = options.attachment;
87
+ try {
88
+ const response = await axios.post(`${this.apiBaseUrl}/oauth2/token`, {
89
+ client_id: this.getCustomAttribute(1), // Store Buffer client ID in CustomAttribute1
90
+ client_secret: this.getCustomAttribute(2), // Store Buffer client secret in CustomAttribute2
91
+ refresh_token: refreshToken,
92
+ grant_type: 'refresh_token'
93
+ });
94
+
95
+ const { access_token, expires_in } = response.data;
96
+
97
+ await this.updateStoredTokens(
98
+ access_token,
99
+ refreshToken, // Buffer doesn't rotate refresh tokens
100
+ expires_in
101
+ );
102
+
103
+ LogStatus('Buffer access token refreshed successfully');
104
+ } catch (error) {
105
+ LogError('Failed to refresh Buffer access token:', error);
106
+ throw new Error('Failed to refresh Buffer access token');
107
+ }
192
108
  }
193
109
 
194
- try {
195
- const response = await this.getAxiosInstance().post('/updates/create.json', data);
196
- return response.data;
197
- } catch (error) {
198
- LogError('Failed to create Buffer update:', error);
199
- throw error;
200
- }
201
- }
202
-
203
- /**
204
- * Get updates (posts) from Buffer
205
- */
206
- protected async getUpdates(
207
- profileId: string,
208
- status: 'pending' | 'sent',
209
- options?: {
210
- page?: number;
211
- count?: number;
212
- since?: Date;
213
- utc?: boolean;
214
- }
215
- ): Promise<any> {
216
- const params: any = {
217
- page: options?.page || 1,
218
- count: options?.count || 10,
219
- utc: options?.utc !== false, // Default to true
220
- };
221
-
222
- if (options?.since) {
223
- params.since = Math.floor(options.since.getTime() / 1000);
110
+ /**
111
+ * Get Buffer profiles for the authenticated user
112
+ */
113
+ protected async getProfiles(): Promise<any[]> {
114
+ try {
115
+ const response = await this.getAxiosInstance().get('/profiles.json');
116
+ return response.data || [];
117
+ } catch (error) {
118
+ LogError('Failed to get Buffer profiles:', error);
119
+ throw error;
120
+ }
224
121
  }
225
122
 
226
- try {
227
- const response = await this.getAxiosInstance().get(`/profiles/${profileId}/updates/${status}.json`, { params });
228
- return response.data;
229
- } catch (error) {
230
- LogError(`Failed to get ${status} updates from Buffer:`, error);
231
- throw error;
123
+ /**
124
+ * Upload media to Buffer
125
+ */
126
+ protected async uploadSingleMedia(file: MediaFile): Promise<string> {
127
+ try {
128
+ const formData = new FormData();
129
+
130
+ // Convert base64 to buffer if needed
131
+ const buffer = typeof file.data === 'string'
132
+ ? Buffer.from(file.data, 'base64')
133
+ : file.data;
134
+
135
+ formData.append('media', buffer, {
136
+ filename: file.filename,
137
+ contentType: file.mimeType
138
+ });
139
+
140
+ const response = await this.getAxiosInstance().post('/updates/media/upload.json', formData, {
141
+ headers: {
142
+ ...formData.getHeaders()
143
+ }
144
+ });
145
+
146
+ if (response.data && response.data.media && response.data.media[0]) {
147
+ return response.data.media[0].picture; // URL of uploaded media
148
+ }
149
+
150
+ throw new Error('Failed to upload media to Buffer');
151
+ } catch (error) {
152
+ LogError('Buffer media upload failed:', error);
153
+ throw error;
154
+ }
232
155
  }
233
- }
234
-
235
- /**
236
- * Delete a Buffer update
237
- */
238
- protected async deleteUpdate(updateId: string): Promise<boolean> {
239
- try {
240
- const response = await this.getAxiosInstance().post(`/updates/${updateId}/destroy.json`);
241
- return response.data.success === true;
242
- } catch (error) {
243
- LogError('Failed to delete Buffer update:', error);
244
- throw error;
156
+
157
+ /**
158
+ * Create a Buffer update (post)
159
+ */
160
+ protected async createUpdate(
161
+ profileIds: string[],
162
+ text: string,
163
+ media?: { link?: string; description?: string; picture?: string }[],
164
+ scheduledAt?: Date,
165
+ options?: {
166
+ shorten?: boolean;
167
+ now?: boolean;
168
+ top?: boolean;
169
+ attachment?: boolean;
170
+ }
171
+ ): Promise<any> {
172
+ const data: any = {
173
+ profile_ids: profileIds,
174
+ text: text,
175
+ shorten: options?.shorten !== false // Default to true
176
+ };
177
+
178
+ if (media && media.length > 0) {
179
+ data.media = media;
180
+ }
181
+
182
+ if (scheduledAt) {
183
+ data.scheduled_at = Math.floor(scheduledAt.getTime() / 1000); // Unix timestamp
184
+ } else if (options?.now) {
185
+ data.now = true;
186
+ }
187
+
188
+ if (options?.top) {
189
+ data.top = true;
190
+ }
191
+
192
+ if (options?.attachment !== undefined) {
193
+ data.attachment = options.attachment;
194
+ }
195
+
196
+ try {
197
+ const response = await this.getAxiosInstance().post('/updates/create.json', data);
198
+ return response.data;
199
+ } catch (error) {
200
+ LogError('Failed to create Buffer update:', error);
201
+ throw error;
202
+ }
245
203
  }
246
- }
247
-
248
- /**
249
- * Reorder updates in the queue
250
- */
251
- protected async reorderUpdates(profileId: string, updateIds: string[], offset?: number): Promise<any> {
252
- const data: any = {
253
- order: updateIds,
254
- };
255
-
256
- if (offset !== undefined) {
257
- data.offset = offset;
204
+
205
+ /**
206
+ * Get updates (posts) from Buffer
207
+ */
208
+ protected async getUpdates(
209
+ profileId: string,
210
+ status: 'pending' | 'sent',
211
+ options?: {
212
+ page?: number;
213
+ count?: number;
214
+ since?: Date;
215
+ utc?: boolean;
216
+ }
217
+ ): Promise<any> {
218
+ const params: any = {
219
+ page: options?.page || 1,
220
+ count: options?.count || 10,
221
+ utc: options?.utc !== false // Default to true
222
+ };
223
+
224
+ if (options?.since) {
225
+ params.since = Math.floor(options.since.getTime() / 1000);
226
+ }
227
+
228
+ try {
229
+ const response = await this.getAxiosInstance().get(
230
+ `/profiles/${profileId}/updates/${status}.json`,
231
+ { params }
232
+ );
233
+ return response.data;
234
+ } catch (error) {
235
+ LogError(`Failed to get ${status} updates from Buffer:`, error);
236
+ throw error;
237
+ }
258
238
  }
259
239
 
260
- try {
261
- const response = await this.getAxiosInstance().post(`/profiles/${profileId}/updates/reorder.json`, data);
262
- return response.data;
263
- } catch (error) {
264
- LogError('Failed to reorder Buffer updates:', error);
265
- throw error;
240
+ /**
241
+ * Delete a Buffer update
242
+ */
243
+ protected async deleteUpdate(updateId: string): Promise<boolean> {
244
+ try {
245
+ const response = await this.getAxiosInstance().post(`/updates/${updateId}/destroy.json`);
246
+ return response.data.success === true;
247
+ } catch (error) {
248
+ LogError('Failed to delete Buffer update:', error);
249
+ throw error;
250
+ }
266
251
  }
267
- }
268
-
269
- /**
270
- * Get analytics for sent posts
271
- */
272
- protected async getAnalytics(updateId: string): Promise<any> {
273
- try {
274
- const response = await this.getAxiosInstance().get(`/updates/${updateId}/interactions.json`);
275
- return response.data;
276
- } catch (error) {
277
- LogError('Failed to get Buffer analytics:', error);
278
- throw error;
252
+
253
+ /**
254
+ * Reorder updates in the queue
255
+ */
256
+ protected async reorderUpdates(profileId: string, updateIds: string[], offset?: number): Promise<any> {
257
+ const data: any = {
258
+ order: updateIds
259
+ };
260
+
261
+ if (offset !== undefined) {
262
+ data.offset = offset;
263
+ }
264
+
265
+ try {
266
+ const response = await this.getAxiosInstance().post(
267
+ `/profiles/${profileId}/updates/reorder.json`,
268
+ data
269
+ );
270
+ return response.data;
271
+ } catch (error) {
272
+ LogError('Failed to reorder Buffer updates:', error);
273
+ throw error;
274
+ }
279
275
  }
280
- }
281
-
282
- /**
283
- * Search posts implementation for Buffer
284
- * Buffer doesn't have a native search API, so we'll fetch posts and filter client-side
285
- */
286
- protected async searchPosts(params: {
287
- query?: string;
288
- hashtags?: string[];
289
- startDate?: Date;
290
- endDate?: Date;
291
- limit?: number;
292
- offset?: number;
293
- profileIds?: string[];
294
- }): Promise<SocialPost[]> {
295
- const posts: SocialPost[] = [];
296
- const profileIds = params.profileIds || [];
297
-
298
- // If no profile IDs provided, get all profiles
299
- if (profileIds.length === 0) {
300
- const profiles = await this.getProfiles();
301
- profileIds.push(...profiles.map((p) => p.id));
276
+
277
+ /**
278
+ * Get analytics for sent posts
279
+ */
280
+ protected async getAnalytics(updateId: string): Promise<any> {
281
+ try {
282
+ const response = await this.getAxiosInstance().get(`/updates/${updateId}/interactions.json`);
283
+ return response.data;
284
+ } catch (error) {
285
+ LogError('Failed to get Buffer analytics:', error);
286
+ throw error;
287
+ }
302
288
  }
303
289
 
304
- // Fetch sent posts from each profile
305
- for (const profileId of profileIds) {
306
- try {
307
- let page = 1;
308
- let hasMore = true;
309
-
310
- while (hasMore && posts.length < (params.limit || 100)) {
311
- const result = await this.getUpdates(profileId, 'sent', {
312
- page: page,
313
- count: 100,
314
- since: params.startDate,
315
- });
316
-
317
- if (result.updates && result.updates.length > 0) {
318
- for (const update of result.updates) {
319
- const post = this.normalizePost(update);
320
-
321
- // Apply filters
322
- if (this.matchesSearchCriteria(post, params)) {
323
- posts.push(post);
324
-
325
- if (posts.length >= (params.limit || 100)) {
326
- hasMore = false;
327
- break;
290
+ /**
291
+ * Search posts implementation for Buffer
292
+ * Buffer doesn't have a native search API, so we'll fetch posts and filter client-side
293
+ */
294
+ protected async searchPosts(params: {
295
+ query?: string;
296
+ hashtags?: string[];
297
+ startDate?: Date;
298
+ endDate?: Date;
299
+ limit?: number;
300
+ offset?: number;
301
+ profileIds?: string[];
302
+ }): Promise<SocialPost[]> {
303
+ const posts: SocialPost[] = [];
304
+ const profileIds = params.profileIds || [];
305
+
306
+ // If no profile IDs provided, get all profiles
307
+ if (profileIds.length === 0) {
308
+ const profiles = await this.getProfiles();
309
+ profileIds.push(...profiles.map(p => p.id));
310
+ }
311
+
312
+ // Fetch sent posts from each profile
313
+ for (const profileId of profileIds) {
314
+ try {
315
+ let page = 1;
316
+ let hasMore = true;
317
+
318
+ while (hasMore && posts.length < (params.limit || 100)) {
319
+ const result = await this.getUpdates(profileId, 'sent', {
320
+ page: page,
321
+ count: 100,
322
+ since: params.startDate
323
+ });
324
+
325
+ if (result.updates && result.updates.length > 0) {
326
+ for (const update of result.updates) {
327
+ const post = this.normalizePost(update);
328
+
329
+ // Apply filters
330
+ if (this.matchesSearchCriteria(post, params)) {
331
+ posts.push(post);
332
+
333
+ if (posts.length >= (params.limit || 100)) {
334
+ hasMore = false;
335
+ break;
336
+ }
337
+ }
338
+ }
339
+
340
+ page++;
341
+ hasMore = result.updates.length === 100;
342
+ } else {
343
+ hasMore = false;
344
+ }
328
345
  }
329
- }
346
+ } catch (error) {
347
+ LogError(`Failed to search posts for profile ${profileId}:`, error);
330
348
  }
331
-
332
- page++;
333
- hasMore = result.updates.length === 100;
334
- } else {
335
- hasMore = false;
336
- }
337
349
  }
338
- } catch (error) {
339
- LogError(`Failed to search posts for profile ${profileId}:`, error);
340
- }
341
- }
342
350
 
343
- // Sort by published date descending
344
- posts.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
351
+ // Sort by published date descending
352
+ posts.sort((a, b) => b.publishedAt.getTime() - a.publishedAt.getTime());
353
+
354
+ // Apply offset if specified
355
+ if (params.offset) {
356
+ return posts.slice(params.offset, params.offset + (params.limit || 100));
357
+ }
345
358
 
346
- // Apply offset if specified
347
- if (params.offset) {
348
- return posts.slice(params.offset, params.offset + (params.limit || 100));
359
+ return posts.slice(0, params.limit || 100);
349
360
  }
350
361
 
351
- return posts.slice(0, params.limit || 100);
352
- }
362
+ /**
363
+ * Check if a post matches search criteria
364
+ */
365
+ private matchesSearchCriteria(post: SocialPost, params: any): boolean {
366
+ // Check date range
367
+ if (params.endDate && post.publishedAt > params.endDate) {
368
+ return false;
369
+ }
353
370
 
354
- /**
355
- * Check if a post matches search criteria
356
- */
357
- private matchesSearchCriteria(post: SocialPost, params: any): boolean {
358
- // Check date range
359
- if (params.endDate && post.publishedAt > params.endDate) {
360
- return false;
361
- }
371
+ // Check query text
372
+ if (params.query) {
373
+ const query = params.query.toLowerCase();
374
+ const content = post.content.toLowerCase();
375
+ if (!content.includes(query)) {
376
+ return false;
377
+ }
378
+ }
362
379
 
363
- // Check query text
364
- if (params.query) {
365
- const query = params.query.toLowerCase();
366
- const content = post.content.toLowerCase();
367
- if (!content.includes(query)) {
368
- return false;
369
- }
370
- }
380
+ // Check hashtags
381
+ if (params.hashtags && params.hashtags.length > 0) {
382
+ const postHashtags = this.extractHashtags(post.content);
383
+ const hasMatchingHashtag = params.hashtags.some(tag =>
384
+ postHashtags.includes(tag.toLowerCase().replace('#', ''))
385
+ );
386
+ if (!hasMatchingHashtag) {
387
+ return false;
388
+ }
389
+ }
371
390
 
372
- // Check hashtags
373
- if (params.hashtags && params.hashtags.length > 0) {
374
- const postHashtags = this.extractHashtags(post.content);
375
- const hasMatchingHashtag = params.hashtags.some((tag) => postHashtags.includes(tag.toLowerCase().replace('#', '')));
376
- if (!hasMatchingHashtag) {
377
- return false;
378
- }
391
+ return true;
379
392
  }
380
393
 
381
- return true;
382
- }
394
+ /**
395
+ * Extract hashtags from post content
396
+ */
397
+ protected extractHashtags(content: string): string[] {
398
+ const regex = /#(\w+)/g;
399
+ const hashtags: string[] = [];
400
+ let match;
401
+
402
+ while ((match = regex.exec(content)) !== null) {
403
+ hashtags.push(match[1].toLowerCase());
404
+ }
405
+
406
+ return hashtags;
407
+ }
383
408
 
384
- /**
385
- * Extract hashtags from post content
386
- */
387
- protected extractHashtags(content: string): string[] {
388
- const regex = /#(\w+)/g;
389
- const hashtags: string[] = [];
390
- let match;
409
+ /**
410
+ * Normalize Buffer post to common format
411
+ */
412
+ protected normalizePost(bufferPost: any): SocialPost {
413
+ const media: string[] = [];
414
+
415
+ if (bufferPost.media) {
416
+ if (bufferPost.media.picture) {
417
+ media.push(bufferPost.media.picture);
418
+ }
419
+ if (bufferPost.media.link) {
420
+ media.push(bufferPost.media.link);
421
+ }
422
+ }
391
423
 
392
- while ((match = regex.exec(content)) !== null) {
393
- hashtags.push(match[1].toLowerCase());
424
+ return {
425
+ id: bufferPost.id,
426
+ platform: 'Buffer',
427
+ profileId: bufferPost.profile_id,
428
+ content: bufferPost.text || '',
429
+ mediaUrls: media,
430
+ publishedAt: new Date(bufferPost.sent_at * 1000), // Convert Unix timestamp
431
+ scheduledFor: bufferPost.due_at ? new Date(bufferPost.due_at * 1000) : undefined,
432
+ analytics: bufferPost.statistics ? this.normalizeAnalytics(bufferPost.statistics) : undefined,
433
+ platformSpecificData: {
434
+ profileService: bufferPost.profile_service,
435
+ status: bufferPost.status,
436
+ userId: bufferPost.user_id,
437
+ viaName: bufferPost.via,
438
+ sourceUrl: bufferPost.source_url,
439
+ day: bufferPost.day,
440
+ dueTime: bufferPost.due_time,
441
+ mediaDescription: bufferPost.media?.description,
442
+ mediaTitle: bufferPost.media?.title,
443
+ mediaLink: bufferPost.media?.link
444
+ }
445
+ };
394
446
  }
395
447
 
396
- return hashtags;
397
- }
398
-
399
- /**
400
- * Normalize Buffer post to common format
401
- */
402
- protected normalizePost(bufferPost: any): SocialPost {
403
- const media: string[] = [];
404
-
405
- if (bufferPost.media) {
406
- if (bufferPost.media.picture) {
407
- media.push(bufferPost.media.picture);
408
- }
409
- if (bufferPost.media.link) {
410
- media.push(bufferPost.media.link);
411
- }
448
+ /**
449
+ * Normalize Buffer analytics to common format
450
+ */
451
+ protected normalizeAnalytics(bufferStats: any): SocialAnalytics {
452
+ return {
453
+ impressions: bufferStats.reach || 0,
454
+ engagements: (bufferStats.clicks || 0) + (bufferStats.favorites || 0) +
455
+ (bufferStats.mentions || 0) + (bufferStats.retweets || 0) +
456
+ (bufferStats.shares || 0) + (bufferStats.comments || 0),
457
+ clicks: bufferStats.clicks || 0,
458
+ shares: bufferStats.shares || bufferStats.retweets || 0,
459
+ comments: bufferStats.comments || bufferStats.mentions || 0,
460
+ likes: bufferStats.favorites || bufferStats.likes || 0,
461
+ reach: bufferStats.reach || 0,
462
+ saves: undefined,
463
+ videoViews: undefined,
464
+ platformMetrics: bufferStats
465
+ };
412
466
  }
413
467
 
414
- return {
415
- id: bufferPost.id,
416
- platform: 'Buffer',
417
- profileId: bufferPost.profile_id,
418
- content: bufferPost.text || '',
419
- mediaUrls: media,
420
- publishedAt: new Date(bufferPost.sent_at * 1000), // Convert Unix timestamp
421
- scheduledFor: bufferPost.due_at ? new Date(bufferPost.due_at * 1000) : undefined,
422
- analytics: bufferPost.statistics ? this.normalizeAnalytics(bufferPost.statistics) : undefined,
423
- platformSpecificData: {
424
- profileService: bufferPost.profile_service,
425
- status: bufferPost.status,
426
- userId: bufferPost.user_id,
427
- viaName: bufferPost.via,
428
- sourceUrl: bufferPost.source_url,
429
- day: bufferPost.day,
430
- dueTime: bufferPost.due_time,
431
- mediaDescription: bufferPost.media?.description,
432
- mediaTitle: bufferPost.media?.title,
433
- mediaLink: bufferPost.media?.link,
434
- },
435
- };
436
- }
437
-
438
- /**
439
- * Normalize Buffer analytics to common format
440
- */
441
- protected normalizeAnalytics(bufferStats: any): SocialAnalytics {
442
- return {
443
- impressions: bufferStats.reach || 0,
444
- engagements:
445
- (bufferStats.clicks || 0) +
446
- (bufferStats.favorites || 0) +
447
- (bufferStats.mentions || 0) +
448
- (bufferStats.retweets || 0) +
449
- (bufferStats.shares || 0) +
450
- (bufferStats.comments || 0),
451
- clicks: bufferStats.clicks || 0,
452
- shares: bufferStats.shares || bufferStats.retweets || 0,
453
- comments: bufferStats.comments || bufferStats.mentions || 0,
454
- likes: bufferStats.favorites || bufferStats.likes || 0,
455
- reach: bufferStats.reach || 0,
456
- saves: undefined,
457
- videoViews: undefined,
458
- platformMetrics: bufferStats,
459
- };
460
- }
461
-
462
- /**
463
- * Map Buffer error to our error codes
464
- */
465
- protected mapBufferError(error: any): string {
466
- if (error.response) {
467
- const status = error.response.status;
468
- const data = error.response.data;
469
-
470
- if (status === 401) {
471
- return 'INVALID_TOKEN';
472
- } else if (status === 429) {
473
- return 'RATE_LIMIT';
474
- } else if (status === 403) {
475
- return 'INSUFFICIENT_PERMISSIONS';
476
- } else if (status === 404) {
477
- return 'POST_NOT_FOUND';
478
- } else if (data?.error?.includes('media')) {
479
- return 'INVALID_MEDIA';
480
- }
481
- }
468
+ /**
469
+ * Map Buffer error to our error codes
470
+ */
471
+ protected mapBufferError(error: any): string {
472
+ if (error.response) {
473
+ const status = error.response.status;
474
+ const data = error.response.data;
475
+
476
+ if (status === 401) {
477
+ return 'INVALID_TOKEN';
478
+ } else if (status === 429) {
479
+ return 'RATE_LIMIT';
480
+ } else if (status === 403) {
481
+ return 'INSUFFICIENT_PERMISSIONS';
482
+ } else if (status === 404) {
483
+ return 'POST_NOT_FOUND';
484
+ } else if (data?.error?.includes('media')) {
485
+ return 'INVALID_MEDIA';
486
+ }
487
+ }
482
488
 
483
- return 'PLATFORM_ERROR';
484
- }
485
- }
489
+ return 'PLATFORM_ERROR';
490
+ }
491
+ }