@memberjunction/actions-bizapps-social 2.111.1 → 2.112.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +6 -6
  3. package/dist/base/base-social.action.d.ts.map +1 -1
  4. package/dist/base/base-social.action.js +18 -24
  5. package/dist/base/base-social.action.js.map +1 -1
  6. package/dist/providers/buffer/buffer-base.action.d.ts.map +1 -1
  7. package/dist/providers/buffer/buffer-base.action.js +35 -34
  8. package/dist/providers/buffer/buffer-base.action.js.map +1 -1
  9. package/dist/providers/facebook/actions/boost-post.action.d.ts.map +1 -1
  10. package/dist/providers/facebook/actions/boost-post.action.js +33 -33
  11. package/dist/providers/facebook/actions/boost-post.action.js.map +1 -1
  12. package/dist/providers/facebook/actions/create-album.action.d.ts.map +1 -1
  13. package/dist/providers/facebook/actions/create-album.action.js +34 -36
  14. package/dist/providers/facebook/actions/create-album.action.js.map +1 -1
  15. package/dist/providers/facebook/actions/create-post.action.d.ts.map +1 -1
  16. package/dist/providers/facebook/actions/create-post.action.js +20 -20
  17. package/dist/providers/facebook/actions/create-post.action.js.map +1 -1
  18. package/dist/providers/facebook/actions/get-page-insights.action.d.ts.map +1 -1
  19. package/dist/providers/facebook/actions/get-page-insights.action.js +25 -27
  20. package/dist/providers/facebook/actions/get-page-insights.action.js.map +1 -1
  21. package/dist/providers/facebook/actions/get-page-posts.action.d.ts.map +1 -1
  22. package/dist/providers/facebook/actions/get-page-posts.action.js +19 -23
  23. package/dist/providers/facebook/actions/get-page-posts.action.js.map +1 -1
  24. package/dist/providers/facebook/actions/get-post-insights.action.d.ts.map +1 -1
  25. package/dist/providers/facebook/actions/get-post-insights.action.js +28 -32
  26. package/dist/providers/facebook/actions/get-post-insights.action.js.map +1 -1
  27. package/dist/providers/facebook/actions/respond-to-comments.action.d.ts.map +1 -1
  28. package/dist/providers/facebook/actions/respond-to-comments.action.js +42 -44
  29. package/dist/providers/facebook/actions/respond-to-comments.action.js.map +1 -1
  30. package/dist/providers/facebook/actions/schedule-post.action.d.ts.map +1 -1
  31. package/dist/providers/facebook/actions/schedule-post.action.js +29 -29
  32. package/dist/providers/facebook/actions/schedule-post.action.js.map +1 -1
  33. package/dist/providers/facebook/actions/search-posts.action.d.ts.map +1 -1
  34. package/dist/providers/facebook/actions/search-posts.action.js +37 -39
  35. package/dist/providers/facebook/actions/search-posts.action.js.map +1 -1
  36. package/dist/providers/facebook/facebook-base.action.d.ts.map +1 -1
  37. package/dist/providers/facebook/facebook-base.action.js +44 -59
  38. package/dist/providers/facebook/facebook-base.action.js.map +1 -1
  39. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.d.ts.map +1 -1
  40. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js +33 -31
  41. package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js.map +1 -1
  42. package/dist/providers/hootsuite/actions/create-scheduled-post.action.d.ts.map +1 -1
  43. package/dist/providers/hootsuite/actions/create-scheduled-post.action.js +28 -32
  44. package/dist/providers/hootsuite/actions/create-scheduled-post.action.js.map +1 -1
  45. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.d.ts.map +1 -1
  46. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js +19 -19
  47. package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js.map +1 -1
  48. package/dist/providers/hootsuite/actions/get-analytics.action.d.ts.map +1 -1
  49. package/dist/providers/hootsuite/actions/get-analytics.action.js +24 -26
  50. package/dist/providers/hootsuite/actions/get-analytics.action.js.map +1 -1
  51. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.d.ts.map +1 -1
  52. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js +22 -22
  53. package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js.map +1 -1
  54. package/dist/providers/hootsuite/actions/get-social-profiles.action.d.ts.map +1 -1
  55. package/dist/providers/hootsuite/actions/get-social-profiles.action.js +32 -34
  56. package/dist/providers/hootsuite/actions/get-social-profiles.action.js.map +1 -1
  57. package/dist/providers/hootsuite/actions/search-posts.action.d.ts.map +1 -1
  58. package/dist/providers/hootsuite/actions/search-posts.action.js +43 -52
  59. package/dist/providers/hootsuite/actions/search-posts.action.js.map +1 -1
  60. package/dist/providers/hootsuite/actions/update-scheduled-post.action.d.ts.map +1 -1
  61. package/dist/providers/hootsuite/actions/update-scheduled-post.action.js +30 -28
  62. package/dist/providers/hootsuite/actions/update-scheduled-post.action.js.map +1 -1
  63. package/dist/providers/hootsuite/hootsuite-base.action.d.ts.map +1 -1
  64. package/dist/providers/hootsuite/hootsuite-base.action.js +18 -20
  65. package/dist/providers/hootsuite/hootsuite-base.action.js.map +1 -1
  66. package/dist/providers/instagram/actions/create-post.action.d.ts.map +1 -1
  67. package/dist/providers/instagram/actions/create-post.action.js +27 -26
  68. package/dist/providers/instagram/actions/create-post.action.js.map +1 -1
  69. package/dist/providers/instagram/actions/create-story.action.d.ts.map +1 -1
  70. package/dist/providers/instagram/actions/create-story.action.js +35 -35
  71. package/dist/providers/instagram/actions/create-story.action.js.map +1 -1
  72. package/dist/providers/instagram/actions/get-account-insights.action.d.ts.map +1 -1
  73. package/dist/providers/instagram/actions/get-account-insights.action.js +38 -59
  74. package/dist/providers/instagram/actions/get-account-insights.action.js.map +1 -1
  75. package/dist/providers/instagram/actions/get-business-posts.action.d.ts.map +1 -1
  76. package/dist/providers/instagram/actions/get-business-posts.action.js +29 -29
  77. package/dist/providers/instagram/actions/get-business-posts.action.js.map +1 -1
  78. package/dist/providers/instagram/actions/get-comments.action.d.ts.map +1 -1
  79. package/dist/providers/instagram/actions/get-comments.action.js +36 -36
  80. package/dist/providers/instagram/actions/get-comments.action.js.map +1 -1
  81. package/dist/providers/instagram/actions/get-post-insights.action.d.ts.map +1 -1
  82. package/dist/providers/instagram/actions/get-post-insights.action.js +23 -25
  83. package/dist/providers/instagram/actions/get-post-insights.action.js.map +1 -1
  84. package/dist/providers/instagram/actions/schedule-post.action.d.ts.map +1 -1
  85. package/dist/providers/instagram/actions/schedule-post.action.js +25 -25
  86. package/dist/providers/instagram/actions/schedule-post.action.js.map +1 -1
  87. package/dist/providers/instagram/actions/search-posts.action.d.ts.map +1 -1
  88. package/dist/providers/instagram/actions/search-posts.action.js +56 -60
  89. package/dist/providers/instagram/actions/search-posts.action.js.map +1 -1
  90. package/dist/providers/instagram/instagram-base.action.d.ts.map +1 -1
  91. package/dist/providers/instagram/instagram-base.action.js +25 -27
  92. package/dist/providers/instagram/instagram-base.action.js.map +1 -1
  93. package/dist/providers/linkedin/actions/create-article.action.d.ts.map +1 -1
  94. package/dist/providers/linkedin/actions/create-article.action.js +55 -45
  95. package/dist/providers/linkedin/actions/create-article.action.js.map +1 -1
  96. package/dist/providers/linkedin/actions/create-post.action.d.ts.map +1 -1
  97. package/dist/providers/linkedin/actions/create-post.action.js +31 -29
  98. package/dist/providers/linkedin/actions/create-post.action.js.map +1 -1
  99. package/dist/providers/linkedin/actions/get-followers.action.d.ts.map +1 -1
  100. package/dist/providers/linkedin/actions/get-followers.action.js +28 -28
  101. package/dist/providers/linkedin/actions/get-followers.action.js.map +1 -1
  102. package/dist/providers/linkedin/actions/get-organization-posts.action.d.ts.map +1 -1
  103. package/dist/providers/linkedin/actions/get-organization-posts.action.js +20 -20
  104. package/dist/providers/linkedin/actions/get-organization-posts.action.js.map +1 -1
  105. package/dist/providers/linkedin/actions/get-personal-posts.action.d.ts.map +1 -1
  106. package/dist/providers/linkedin/actions/get-personal-posts.action.js +19 -19
  107. package/dist/providers/linkedin/actions/get-personal-posts.action.js.map +1 -1
  108. package/dist/providers/linkedin/actions/get-post-analytics.action.d.ts.map +1 -1
  109. package/dist/providers/linkedin/actions/get-post-analytics.action.js +25 -23
  110. package/dist/providers/linkedin/actions/get-post-analytics.action.js.map +1 -1
  111. package/dist/providers/linkedin/actions/schedule-post.action.d.ts.map +1 -1
  112. package/dist/providers/linkedin/actions/schedule-post.action.js +32 -30
  113. package/dist/providers/linkedin/actions/schedule-post.action.js.map +1 -1
  114. package/dist/providers/linkedin/actions/search-posts.action.d.ts.map +1 -1
  115. package/dist/providers/linkedin/actions/search-posts.action.js +28 -30
  116. package/dist/providers/linkedin/actions/search-posts.action.js.map +1 -1
  117. package/dist/providers/linkedin/linkedin-base.action.d.ts.map +1 -1
  118. package/dist/providers/linkedin/linkedin-base.action.js +33 -38
  119. package/dist/providers/linkedin/linkedin-base.action.js.map +1 -1
  120. package/dist/providers/tiktok/tiktok-base.action.d.ts.map +1 -1
  121. package/dist/providers/tiktok/tiktok-base.action.js +25 -26
  122. package/dist/providers/tiktok/tiktok-base.action.js.map +1 -1
  123. package/dist/providers/twitter/actions/create-thread.action.d.ts.map +1 -1
  124. package/dist/providers/twitter/actions/create-thread.action.js +25 -29
  125. package/dist/providers/twitter/actions/create-thread.action.js.map +1 -1
  126. package/dist/providers/twitter/actions/create-tweet.action.d.ts.map +1 -1
  127. package/dist/providers/twitter/actions/create-tweet.action.js +23 -23
  128. package/dist/providers/twitter/actions/create-tweet.action.js.map +1 -1
  129. package/dist/providers/twitter/actions/delete-tweet.action.d.ts.map +1 -1
  130. package/dist/providers/twitter/actions/delete-tweet.action.js +19 -19
  131. package/dist/providers/twitter/actions/delete-tweet.action.js.map +1 -1
  132. package/dist/providers/twitter/actions/get-analytics.action.d.ts.map +1 -1
  133. package/dist/providers/twitter/actions/get-analytics.action.js +40 -47
  134. package/dist/providers/twitter/actions/get-analytics.action.js.map +1 -1
  135. package/dist/providers/twitter/actions/get-mentions.action.d.ts.map +1 -1
  136. package/dist/providers/twitter/actions/get-mentions.action.js +30 -31
  137. package/dist/providers/twitter/actions/get-mentions.action.js.map +1 -1
  138. package/dist/providers/twitter/actions/get-timeline.action.d.ts.map +1 -1
  139. package/dist/providers/twitter/actions/get-timeline.action.js +29 -29
  140. package/dist/providers/twitter/actions/get-timeline.action.js.map +1 -1
  141. package/dist/providers/twitter/actions/schedule-tweet.action.d.ts.map +1 -1
  142. package/dist/providers/twitter/actions/schedule-tweet.action.js +26 -26
  143. package/dist/providers/twitter/actions/schedule-tweet.action.js.map +1 -1
  144. package/dist/providers/twitter/actions/search-tweets.action.d.ts.map +1 -1
  145. package/dist/providers/twitter/actions/search-tweets.action.js +56 -58
  146. package/dist/providers/twitter/actions/search-tweets.action.js.map +1 -1
  147. package/dist/providers/twitter/twitter-base.action.d.ts.map +1 -1
  148. package/dist/providers/twitter/twitter-base.action.js +58 -68
  149. package/dist/providers/twitter/twitter-base.action.js.map +1 -1
  150. package/dist/providers/youtube/youtube-base.action.d.ts +1 -1
  151. package/dist/providers/youtube/youtube-base.action.d.ts.map +1 -1
  152. package/dist/providers/youtube/youtube-base.action.js +22 -25
  153. package/dist/providers/youtube/youtube-base.action.js.map +1 -1
  154. package/package.json +5 -6
  155. package/src/base/base-social.action.ts +217 -224
  156. package/src/providers/buffer/buffer-base.action.ts +435 -441
  157. package/src/providers/facebook/actions/boost-post.action.ts +350 -386
  158. package/src/providers/facebook/actions/create-album.action.ts +291 -307
  159. package/src/providers/facebook/actions/create-post.action.ts +224 -227
  160. package/src/providers/facebook/actions/get-page-insights.action.ts +383 -403
  161. package/src/providers/facebook/actions/get-page-posts.action.ts +214 -225
  162. package/src/providers/facebook/actions/get-post-insights.action.ts +300 -316
  163. package/src/providers/facebook/actions/respond-to-comments.action.ts +319 -336
  164. package/src/providers/facebook/actions/schedule-post.action.ts +289 -292
  165. package/src/providers/facebook/actions/search-posts.action.ts +399 -413
  166. package/src/providers/facebook/facebook-base.action.ts +653 -670
  167. package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
  168. package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +184 -189
  169. package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +160 -161
  170. package/src/providers/hootsuite/actions/get-analytics.action.ts +249 -254
  171. package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +206 -207
  172. package/src/providers/hootsuite/actions/get-social-profiles.action.ts +206 -205
  173. package/src/providers/hootsuite/actions/search-posts.action.ts +351 -369
  174. package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +211 -209
  175. package/src/providers/hootsuite/hootsuite-base.action.ts +301 -307
  176. package/src/providers/instagram/actions/create-post.action.ts +276 -296
  177. package/src/providers/instagram/actions/create-story.action.ts +378 -394
  178. package/src/providers/instagram/actions/get-account-insights.action.ts +384 -420
  179. package/src/providers/instagram/actions/get-business-posts.action.ts +233 -242
  180. package/src/providers/instagram/actions/get-comments.action.ts +365 -377
  181. package/src/providers/instagram/actions/get-post-insights.action.ts +265 -273
  182. package/src/providers/instagram/actions/schedule-post.action.ts +233 -235
  183. package/src/providers/instagram/actions/search-posts.action.ts +512 -538
  184. package/src/providers/instagram/instagram-base.action.ts +368 -393
  185. package/src/providers/linkedin/actions/create-article.action.ts +275 -266
  186. package/src/providers/linkedin/actions/create-post.action.ts +179 -177
  187. package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
  188. package/src/providers/linkedin/actions/get-organization-posts.action.ts +146 -147
  189. package/src/providers/linkedin/actions/get-personal-posts.action.ts +138 -139
  190. package/src/providers/linkedin/actions/get-post-analytics.action.ts +190 -189
  191. package/src/providers/linkedin/actions/schedule-post.action.ts +191 -189
  192. package/src/providers/linkedin/actions/search-posts.action.ts +275 -283
  193. package/src/providers/linkedin/linkedin-base.action.ts +407 -421
  194. package/src/providers/tiktok/tiktok-base.action.ts +305 -320
  195. package/src/providers/twitter/actions/create-thread.action.ts +203 -207
  196. package/src/providers/twitter/actions/create-tweet.action.ts +187 -188
  197. package/src/providers/twitter/actions/delete-tweet.action.ts +128 -129
  198. package/src/providers/twitter/actions/get-analytics.action.ts +402 -411
  199. package/src/providers/twitter/actions/get-mentions.action.ts +218 -219
  200. package/src/providers/twitter/actions/get-timeline.action.ts +232 -233
  201. package/src/providers/twitter/actions/schedule-tweet.action.ts +221 -222
  202. package/src/providers/twitter/actions/search-tweets.action.ts +540 -543
  203. package/src/providers/twitter/twitter-base.action.ts +541 -560
  204. package/src/providers/youtube/youtube-base.action.ts +320 -333
@@ -2,7 +2,7 @@ import { RegisterClass } from '@memberjunction/global';
2
2
  import { BaseSocialMediaAction, MediaFile, SocialPost, SearchParams, SocialAnalytics } from '../../base/base-social.action';
3
3
  import axios, { AxiosInstance, AxiosError } from 'axios';
4
4
  import { ActionParam } from '@memberjunction/actions-base';
5
- import { LogStatus, LogError } from '@memberjunction/core';
5
+ import { LogStatus, LogError } from '@memberjunction/global';
6
6
  import FormData from 'form-data';
7
7
  import { BaseAction } from '@memberjunction/actions';
8
8
 
@@ -13,618 +13,599 @@ import { BaseAction } from '@memberjunction/actions';
13
13
  */
14
14
  @RegisterClass(BaseAction, 'TwitterBaseAction')
15
15
  export abstract class TwitterBaseAction extends BaseSocialMediaAction {
16
- protected get platformName(): string {
17
- return 'Twitter';
18
- }
19
-
20
- protected get apiBaseUrl(): string {
21
- return 'https://api.twitter.com/2';
22
- }
23
-
24
- /**
25
- * Upload endpoint for media
26
- */
27
- protected get uploadApiUrl(): string {
28
- return 'https://upload.twitter.com/1.1';
29
- }
30
-
31
- /**
32
- * Axios instance for making HTTP requests
33
- */
34
- private _axiosInstance: AxiosInstance | null = null;
35
-
36
- /**
37
- * Get or create axios instance with interceptors
38
- */
39
- protected get axiosInstance(): AxiosInstance {
40
- if (!this._axiosInstance) {
41
- this._axiosInstance = axios.create({
42
- baseURL: this.apiBaseUrl,
43
- timeout: 30000,
44
- headers: {
45
- 'Content-Type': 'application/json',
46
- 'Accept': 'application/json'
47
- }
48
- });
49
-
50
- // Add request interceptor for auth
51
- this._axiosInstance.interceptors.request.use(
52
- (config) => {
53
- const token = this.getAccessToken();
54
- if (token) {
55
- config.headers.Authorization = `Bearer ${token}`;
56
- }
57
- return config;
58
- },
59
- (error) => Promise.reject(error)
60
- );
61
-
62
- // Add response interceptor for rate limit handling
63
- this._axiosInstance.interceptors.response.use(
64
- (response) => {
65
- // Log rate limit info
66
- const rateLimitInfo = this.parseRateLimitHeaders(response.headers);
67
- if (rateLimitInfo) {
68
- LogStatus(`Twitter Rate Limit - Remaining: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`);
69
- }
70
- return response;
71
- },
72
- async (error: AxiosError) => {
73
- if (error.response?.status === 429) {
74
- // Rate limit exceeded
75
- const resetTime = error.response.headers['x-rate-limit-reset'];
76
- const waitTime = resetTime
77
- ? Math.max(0, parseInt(resetTime) - Math.floor(Date.now() / 1000))
78
- : 60;
79
- await this.handleRateLimit(waitTime);
80
-
81
- // Retry the request
82
- return this._axiosInstance!.request(error.config!);
83
- }
84
- return Promise.reject(error);
85
- }
86
- );
16
+ protected get platformName(): string {
17
+ return 'Twitter';
18
+ }
19
+
20
+ protected get apiBaseUrl(): string {
21
+ return 'https://api.twitter.com/2';
22
+ }
23
+
24
+ /**
25
+ * Upload endpoint for media
26
+ */
27
+ protected get uploadApiUrl(): string {
28
+ return 'https://upload.twitter.com/1.1';
29
+ }
30
+
31
+ /**
32
+ * Axios instance for making HTTP requests
33
+ */
34
+ private _axiosInstance: AxiosInstance | null = null;
35
+
36
+ /**
37
+ * Get or create axios instance with interceptors
38
+ */
39
+ protected get axiosInstance(): AxiosInstance {
40
+ if (!this._axiosInstance) {
41
+ this._axiosInstance = axios.create({
42
+ baseURL: this.apiBaseUrl,
43
+ timeout: 30000,
44
+ headers: {
45
+ 'Content-Type': 'application/json',
46
+ Accept: 'application/json',
47
+ },
48
+ });
49
+
50
+ // Add request interceptor for auth
51
+ this._axiosInstance.interceptors.request.use(
52
+ (config) => {
53
+ const token = this.getAccessToken();
54
+ if (token) {
55
+ config.headers.Authorization = `Bearer ${token}`;
56
+ }
57
+ return config;
58
+ },
59
+ (error) => Promise.reject(error)
60
+ );
61
+
62
+ // Add response interceptor for rate limit handling
63
+ this._axiosInstance.interceptors.response.use(
64
+ (response) => {
65
+ // Log rate limit info
66
+ const rateLimitInfo = this.parseRateLimitHeaders(response.headers);
67
+ if (rateLimitInfo) {
68
+ LogStatus(`Twitter Rate Limit - Remaining: ${rateLimitInfo.remaining}/${rateLimitInfo.limit}, Reset: ${rateLimitInfo.reset}`);
69
+ }
70
+ return response;
71
+ },
72
+ async (error: AxiosError) => {
73
+ if (error.response?.status === 429) {
74
+ // Rate limit exceeded
75
+ const resetTime = error.response.headers['x-rate-limit-reset'];
76
+ const waitTime = resetTime ? Math.max(0, parseInt(resetTime) - Math.floor(Date.now() / 1000)) : 60;
77
+ await this.handleRateLimit(waitTime);
78
+
79
+ // Retry the request
80
+ return this._axiosInstance!.request(error.config!);
81
+ }
82
+ return Promise.reject(error);
87
83
  }
88
- return this._axiosInstance;
84
+ );
89
85
  }
90
-
91
- /**
92
- * Refresh the access token using the refresh token
93
- */
94
- protected async refreshAccessToken(): Promise<void> {
95
- const refreshToken = this.getRefreshToken();
96
- if (!refreshToken) {
97
- throw new Error('No refresh token available for Twitter');
98
- }
99
-
100
- try {
101
- const clientId = this.getCustomAttribute(2) || ''; // Client ID stored in CustomAttribute2
102
- const clientSecret = this.getCustomAttribute(3) || ''; // Client Secret stored in CustomAttribute3
103
-
104
- const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
105
-
106
- const response = await axios.post('https://api.twitter.com/2/oauth2/token',
107
- new URLSearchParams({
108
- grant_type: 'refresh_token',
109
- refresh_token: refreshToken,
110
- client_id: clientId
111
- }).toString(),
112
- {
113
- headers: {
114
- 'Content-Type': 'application/x-www-form-urlencoded',
115
- 'Authorization': `Basic ${basicAuth}`
116
- }
117
- }
118
- );
119
-
120
- const { access_token, refresh_token: newRefreshToken, expires_in } = response.data;
121
-
122
- // Update stored tokens
123
- await this.updateStoredTokens(
124
- access_token,
125
- newRefreshToken || refreshToken,
126
- expires_in
127
- );
128
-
129
- LogStatus('Twitter access token refreshed successfully');
130
- } catch (error) {
131
- LogError(`Failed to refresh Twitter access token: ${error instanceof Error ? error.message : 'Unknown error'}`);
132
- throw error;
133
- }
86
+ return this._axiosInstance;
87
+ }
88
+
89
+ /**
90
+ * Refresh the access token using the refresh token
91
+ */
92
+ protected async refreshAccessToken(): Promise<void> {
93
+ const refreshToken = this.getRefreshToken();
94
+ if (!refreshToken) {
95
+ throw new Error('No refresh token available for Twitter');
134
96
  }
135
97
 
136
- /**
137
- * Get the authenticated user's info
138
- */
139
- protected async getCurrentUser(): Promise<TwitterUser> {
140
- try {
141
- const response = await this.axiosInstance.get('/users/me', {
142
- params: {
143
- 'user.fields': 'id,name,username,profile_image_url,description,created_at,verified'
144
- }
145
- });
146
- return response.data.data;
147
- } catch (error) {
148
- LogError(`Failed to get current user: ${error instanceof Error ? error.message : 'Unknown error'}`);
149
- throw error;
98
+ try {
99
+ const clientId = this.getCustomAttribute(2) || ''; // Client ID stored in CustomAttribute2
100
+ const clientSecret = this.getCustomAttribute(3) || ''; // Client Secret stored in CustomAttribute3
101
+
102
+ const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
103
+
104
+ const response = await axios.post(
105
+ 'https://api.twitter.com/2/oauth2/token',
106
+ new URLSearchParams({
107
+ grant_type: 'refresh_token',
108
+ refresh_token: refreshToken,
109
+ client_id: clientId,
110
+ }).toString(),
111
+ {
112
+ headers: {
113
+ 'Content-Type': 'application/x-www-form-urlencoded',
114
+ Authorization: `Basic ${basicAuth}`,
115
+ },
150
116
  }
151
- }
117
+ );
152
118
 
153
- /**
154
- * Upload media to Twitter
155
- */
156
- protected async uploadSingleMedia(file: MediaFile): Promise<string> {
157
- try {
158
- const fileData = typeof file.data === 'string'
159
- ? Buffer.from(file.data, 'base64')
160
- : file.data;
161
-
162
- // Step 1: Initialize upload
163
- const initResponse = await axios.post(
164
- `${this.uploadApiUrl}/media/upload.json`,
165
- new URLSearchParams({
166
- command: 'INIT',
167
- total_bytes: fileData.length.toString(),
168
- media_type: file.mimeType,
169
- media_category: this.getMediaCategory(file.mimeType)
170
- }).toString(),
171
- {
172
- headers: {
173
- 'Authorization': `Bearer ${this.getAccessToken()}`,
174
- 'Content-Type': 'application/x-www-form-urlencoded'
175
- }
176
- }
177
- );
178
-
179
- const mediaId = initResponse.data.media_id_string;
180
-
181
- // Step 2: Upload chunks (for large files, Twitter requires chunking)
182
- const chunkSize = 5 * 1024 * 1024; // 5MB chunks
183
- let segmentIndex = 0;
184
-
185
- for (let offset = 0; offset < fileData.length; offset += chunkSize) {
186
- const chunk = fileData.slice(offset, Math.min(offset + chunkSize, fileData.length));
187
-
188
- const formData = new FormData();
189
- formData.append('command', 'APPEND');
190
- formData.append('media_id', mediaId);
191
- formData.append('segment_index', segmentIndex.toString());
192
- formData.append('media', chunk, {
193
- filename: file.filename,
194
- contentType: file.mimeType
195
- });
196
-
197
- await axios.post(
198
- `${this.uploadApiUrl}/media/upload.json`,
199
- formData,
200
- {
201
- headers: {
202
- 'Authorization': `Bearer ${this.getAccessToken()}`,
203
- ...formData.getHeaders()
204
- }
205
- }
206
- );
207
-
208
- segmentIndex++;
209
- }
210
-
211
- // Step 3: Finalize upload
212
- await axios.post(
213
- `${this.uploadApiUrl}/media/upload.json`,
214
- new URLSearchParams({
215
- command: 'FINALIZE',
216
- media_id: mediaId
217
- }).toString(),
218
- {
219
- headers: {
220
- 'Authorization': `Bearer ${this.getAccessToken()}`,
221
- 'Content-Type': 'application/x-www-form-urlencoded'
222
- }
223
- }
224
- );
225
-
226
- // Step 4: Check processing status (for videos)
227
- if (file.mimeType.startsWith('video/')) {
228
- await this.waitForMediaProcessing(mediaId);
229
- }
230
-
231
- return mediaId;
232
- } catch (error) {
233
- LogError(`Failed to upload media to Twitter: ${error instanceof Error ? error.message : 'Unknown error'}`);
234
- throw error;
235
- }
236
- }
119
+ const { access_token, refresh_token: newRefreshToken, expires_in } = response.data;
237
120
 
238
- /**
239
- * Wait for media processing to complete (for videos)
240
- */
241
- private async waitForMediaProcessing(mediaId: string, maxWaitTime: number = 60000): Promise<void> {
242
- const startTime = Date.now();
243
-
244
- while (Date.now() - startTime < maxWaitTime) {
245
- const response = await axios.get(
246
- `${this.uploadApiUrl}/media/upload.json`,
247
- {
248
- params: {
249
- command: 'STATUS',
250
- media_id: mediaId
251
- },
252
- headers: {
253
- 'Authorization': `Bearer ${this.getAccessToken()}`
254
- }
255
- }
256
- );
257
-
258
- const { processing_info } = response.data;
259
-
260
- if (!processing_info) {
261
- // Processing complete
262
- return;
263
- }
264
-
265
- if (processing_info.state === 'succeeded') {
266
- return;
267
- }
268
-
269
- if (processing_info.state === 'failed') {
270
- throw new Error(`Media processing failed: ${processing_info.error?.message || 'Unknown error'}`);
271
- }
272
-
273
- // Wait before checking again
274
- const checkAfterSecs = processing_info.check_after_secs || 1;
275
- await new Promise(resolve => setTimeout(resolve, checkAfterSecs * 1000));
276
- }
121
+ // Update stored tokens
122
+ await this.updateStoredTokens(access_token, newRefreshToken || refreshToken, expires_in);
277
123
 
278
- throw new Error('Media processing timeout');
124
+ LogStatus('Twitter access token refreshed successfully');
125
+ } catch (error) {
126
+ LogError(`Failed to refresh Twitter access token: ${error instanceof Error ? error.message : 'Unknown error'}`);
127
+ throw error;
279
128
  }
280
-
281
- /**
282
- * Get media category based on MIME type
283
- */
284
- private getMediaCategory(mimeType: string): string {
285
- if (mimeType.startsWith('image/gif')) {
286
- return 'tweet_gif';
287
- } else if (mimeType.startsWith('image/')) {
288
- return 'tweet_image';
289
- } else if (mimeType.startsWith('video/')) {
290
- return 'tweet_video';
291
- }
292
- return 'tweet_image';
129
+ }
130
+
131
+ /**
132
+ * Get the authenticated user's info
133
+ */
134
+ protected async getCurrentUser(): Promise<TwitterUser> {
135
+ try {
136
+ const response = await this.axiosInstance.get('/users/me', {
137
+ params: {
138
+ 'user.fields': 'id,name,username,profile_image_url,description,created_at,verified',
139
+ },
140
+ });
141
+ return response.data.data;
142
+ } catch (error) {
143
+ LogError(`Failed to get current user: ${error instanceof Error ? error.message : 'Unknown error'}`);
144
+ throw error;
293
145
  }
294
-
295
- /**
296
- * Validate media file meets Twitter requirements
297
- */
298
- protected validateMediaFile(file: MediaFile): void {
299
- const supportedTypes = [
300
- 'image/jpeg',
301
- 'image/png',
302
- 'image/gif',
303
- 'image/webp',
304
- 'video/mp4'
305
- ];
306
-
307
- if (!supportedTypes.includes(file.mimeType)) {
308
- throw new Error(`Unsupported media type: ${file.mimeType}. Supported types: ${supportedTypes.join(', ')}`);
146
+ }
147
+
148
+ /**
149
+ * Upload media to Twitter
150
+ */
151
+ protected async uploadSingleMedia(file: MediaFile): Promise<string> {
152
+ try {
153
+ const fileData = typeof file.data === 'string' ? Buffer.from(file.data, 'base64') : file.data;
154
+
155
+ // Step 1: Initialize upload
156
+ const initResponse = await axios.post(
157
+ `${this.uploadApiUrl}/media/upload.json`,
158
+ new URLSearchParams({
159
+ command: 'INIT',
160
+ total_bytes: fileData.length.toString(),
161
+ media_type: file.mimeType,
162
+ media_category: this.getMediaCategory(file.mimeType),
163
+ }).toString(),
164
+ {
165
+ headers: {
166
+ Authorization: `Bearer ${this.getAccessToken()}`,
167
+ 'Content-Type': 'application/x-www-form-urlencoded',
168
+ },
309
169
  }
310
-
311
- // Twitter media size limits
312
- let maxSize: number;
313
- if (file.mimeType === 'image/gif') {
314
- maxSize = 15 * 1024 * 1024; // 15MB for GIFs
315
- } else if (file.mimeType.startsWith('image/')) {
316
- maxSize = 5 * 1024 * 1024; // 5MB for images
317
- } else if (file.mimeType.startsWith('video/')) {
318
- maxSize = 512 * 1024 * 1024; // 512MB for videos
319
- } else {
320
- maxSize = 5 * 1024 * 1024; // Default 5MB
170
+ );
171
+
172
+ const mediaId = initResponse.data.media_id_string;
173
+
174
+ // Step 2: Upload chunks (for large files, Twitter requires chunking)
175
+ const chunkSize = 5 * 1024 * 1024; // 5MB chunks
176
+ let segmentIndex = 0;
177
+
178
+ for (let offset = 0; offset < fileData.length; offset += chunkSize) {
179
+ const chunk = fileData.slice(offset, Math.min(offset + chunkSize, fileData.length));
180
+
181
+ const formData = new FormData();
182
+ formData.append('command', 'APPEND');
183
+ formData.append('media_id', mediaId);
184
+ formData.append('segment_index', segmentIndex.toString());
185
+ formData.append('media', chunk, {
186
+ filename: file.filename,
187
+ contentType: file.mimeType,
188
+ });
189
+
190
+ await axios.post(`${this.uploadApiUrl}/media/upload.json`, formData, {
191
+ headers: {
192
+ Authorization: `Bearer ${this.getAccessToken()}`,
193
+ ...formData.getHeaders(),
194
+ },
195
+ });
196
+
197
+ segmentIndex++;
198
+ }
199
+
200
+ // Step 3: Finalize upload
201
+ await axios.post(
202
+ `${this.uploadApiUrl}/media/upload.json`,
203
+ new URLSearchParams({
204
+ command: 'FINALIZE',
205
+ media_id: mediaId,
206
+ }).toString(),
207
+ {
208
+ headers: {
209
+ Authorization: `Bearer ${this.getAccessToken()}`,
210
+ 'Content-Type': 'application/x-www-form-urlencoded',
211
+ },
321
212
  }
213
+ );
322
214
 
323
- if (file.size > maxSize) {
324
- throw new Error(`File size exceeds limit. Max: ${maxSize / 1024 / 1024}MB, Got: ${file.size / 1024 / 1024}MB`);
325
- }
326
- }
215
+ // Step 4: Check processing status (for videos)
216
+ if (file.mimeType.startsWith('video/')) {
217
+ await this.waitForMediaProcessing(mediaId);
218
+ }
327
219
 
328
- /**
329
- * Create a tweet
330
- */
331
- protected async createTweet(tweetData: CreateTweetData): Promise<Tweet> {
332
- try {
333
- const response = await this.axiosInstance.post('/tweets', tweetData);
334
- return response.data.data;
335
- } catch (error) {
336
- this.handleTwitterError(error as AxiosError);
337
- }
220
+ return mediaId;
221
+ } catch (error) {
222
+ LogError(`Failed to upload media to Twitter: ${error instanceof Error ? error.message : 'Unknown error'}`);
223
+ throw error;
338
224
  }
339
-
340
- /**
341
- * Delete a tweet
342
- */
343
- protected async deleteTweet(tweetId: string): Promise<void> {
344
- try {
345
- await this.axiosInstance.delete(`/tweets/${tweetId}`);
346
- } catch (error) {
347
- this.handleTwitterError(error as AxiosError);
348
- }
225
+ }
226
+
227
+ /**
228
+ * Wait for media processing to complete (for videos)
229
+ */
230
+ private async waitForMediaProcessing(mediaId: string, maxWaitTime: number = 60000): Promise<void> {
231
+ const startTime = Date.now();
232
+
233
+ while (Date.now() - startTime < maxWaitTime) {
234
+ const response = await axios.get(`${this.uploadApiUrl}/media/upload.json`, {
235
+ params: {
236
+ command: 'STATUS',
237
+ media_id: mediaId,
238
+ },
239
+ headers: {
240
+ Authorization: `Bearer ${this.getAccessToken()}`,
241
+ },
242
+ });
243
+
244
+ const { processing_info } = response.data;
245
+
246
+ if (!processing_info) {
247
+ // Processing complete
248
+ return;
249
+ }
250
+
251
+ if (processing_info.state === 'succeeded') {
252
+ return;
253
+ }
254
+
255
+ if (processing_info.state === 'failed') {
256
+ throw new Error(`Media processing failed: ${processing_info.error?.message || 'Unknown error'}`);
257
+ }
258
+
259
+ // Wait before checking again
260
+ const checkAfterSecs = processing_info.check_after_secs || 1;
261
+ await new Promise((resolve) => setTimeout(resolve, checkAfterSecs * 1000));
349
262
  }
350
263
 
351
- /**
352
- * Get tweets with specified parameters
353
- */
354
- protected async getTweets(endpoint: string, params: Record<string, any> = {}): Promise<Tweet[]> {
355
- try {
356
- const defaultParams = {
357
- 'tweet.fields': 'id,text,created_at,author_id,conversation_id,public_metrics,attachments,entities,referenced_tweets',
358
- 'user.fields': 'id,name,username,profile_image_url',
359
- 'media.fields': 'url,preview_image_url,type,width,height',
360
- 'expansions': 'author_id,attachments.media_keys,referenced_tweets.id',
361
- 'max_results': 100
362
- };
363
-
364
- const response = await this.axiosInstance.get(endpoint, {
365
- params: { ...defaultParams, ...params }
366
- });
367
-
368
- return response.data.data || [];
369
- } catch (error) {
370
- LogError(`Failed to get tweets: ${error instanceof Error ? error.message : 'Unknown error'}`);
371
- throw error;
372
- }
264
+ throw new Error('Media processing timeout');
265
+ }
266
+
267
+ /**
268
+ * Get media category based on MIME type
269
+ */
270
+ private getMediaCategory(mimeType: string): string {
271
+ if (mimeType.startsWith('image/gif')) {
272
+ return 'tweet_gif';
273
+ } else if (mimeType.startsWith('image/')) {
274
+ return 'tweet_image';
275
+ } else if (mimeType.startsWith('video/')) {
276
+ return 'tweet_video';
373
277
  }
278
+ return 'tweet_image';
279
+ }
374
280
 
375
- /**
376
- * Get paginated tweets
377
- */
378
- protected async getPaginatedTweets(endpoint: string, params: Record<string, any> = {}, maxResults?: number): Promise<Tweet[]> {
379
- const tweets: Tweet[] = [];
380
- let paginationToken: string | undefined;
381
- const limit = params.max_results || 100;
382
-
383
- while (true) {
384
- const response = await this.axiosInstance.get(endpoint, {
385
- params: {
386
- ...params,
387
- max_results: limit,
388
- ...(paginationToken && { pagination_token: paginationToken })
389
- }
390
- });
391
-
392
- if (response.data.data && Array.isArray(response.data.data)) {
393
- tweets.push(...response.data.data);
394
- }
395
-
396
- // Check if we've reached max results
397
- if (maxResults && tweets.length >= maxResults) {
398
- return tweets.slice(0, maxResults);
399
- }
400
-
401
- // Check for more pages
402
- paginationToken = response.data.meta?.next_token;
403
- if (!paginationToken) {
404
- break;
405
- }
406
- }
281
+ /**
282
+ * Validate media file meets Twitter requirements
283
+ */
284
+ protected validateMediaFile(file: MediaFile): void {
285
+ const supportedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4'];
407
286
 
408
- return tweets;
287
+ if (!supportedTypes.includes(file.mimeType)) {
288
+ throw new Error(`Unsupported media type: ${file.mimeType}. Supported types: ${supportedTypes.join(', ')}`);
409
289
  }
410
290
 
411
- /**
412
- * Convert Twitter tweet to common format
413
- */
414
- protected normalizePost(tweet: Tweet): SocialPost {
415
- return {
416
- id: tweet.id,
417
- platform: 'Twitter',
418
- profileId: tweet.author_id || '',
419
- content: tweet.text,
420
- mediaUrls: tweet.attachments?.media_keys || [],
421
- publishedAt: new Date(tweet.created_at),
422
- analytics: tweet.public_metrics ? {
423
- impressions: tweet.public_metrics.impression_count || 0,
424
- engagements: (tweet.public_metrics.retweet_count || 0) +
425
- (tweet.public_metrics.reply_count || 0) +
426
- (tweet.public_metrics.like_count || 0) +
427
- (tweet.public_metrics.quote_count || 0),
428
- clicks: 0, // Not available in public metrics
429
- shares: tweet.public_metrics.retweet_count || 0,
430
- comments: tweet.public_metrics.reply_count || 0,
431
- likes: tweet.public_metrics.like_count || 0,
432
- reach: tweet.public_metrics.impression_count || 0,
433
- platformMetrics: tweet.public_metrics
434
- } : undefined,
435
- platformSpecificData: {
436
- conversationId: tweet.conversation_id,
437
- referencedTweets: tweet.referenced_tweets,
438
- entities: tweet.entities
439
- }
440
- };
291
+ // Twitter media size limits
292
+ let maxSize: number;
293
+ if (file.mimeType === 'image/gif') {
294
+ maxSize = 15 * 1024 * 1024; // 15MB for GIFs
295
+ } else if (file.mimeType.startsWith('image/')) {
296
+ maxSize = 5 * 1024 * 1024; // 5MB for images
297
+ } else if (file.mimeType.startsWith('video/')) {
298
+ maxSize = 512 * 1024 * 1024; // 512MB for videos
299
+ } else {
300
+ maxSize = 5 * 1024 * 1024; // Default 5MB
441
301
  }
442
302
 
443
- /**
444
- * Normalize Twitter analytics to common format
445
- */
446
- protected normalizeAnalytics(twitterMetrics: TwitterMetrics): SocialAnalytics {
447
- return {
448
- impressions: twitterMetrics.impression_count || 0,
449
- engagements: twitterMetrics.engagement_count || 0,
450
- clicks: twitterMetrics.url_link_clicks || 0,
451
- shares: twitterMetrics.retweet_count || 0,
452
- comments: twitterMetrics.reply_count || 0,
453
- likes: twitterMetrics.like_count || 0,
454
- reach: twitterMetrics.impression_count || 0,
455
- videoViews: twitterMetrics.video_view_count,
456
- platformMetrics: twitterMetrics
457
- };
303
+ if (file.size > maxSize) {
304
+ throw new Error(`File size exceeds limit. Max: ${maxSize / 1024 / 1024}MB, Got: ${file.size / 1024 / 1024}MB`);
458
305
  }
459
-
460
- /**
461
- * Search for tweets - implemented in search action
462
- */
463
- protected async searchPosts(params: SearchParams): Promise<SocialPost[]> {
464
- // This is implemented in the search-tweets.action.ts
465
- throw new Error('Search posts is implemented in TwitterSearchTweetsAction');
306
+ }
307
+
308
+ /**
309
+ * Create a tweet
310
+ */
311
+ protected async createTweet(tweetData: CreateTweetData): Promise<Tweet> {
312
+ try {
313
+ const response = await this.axiosInstance.post('/tweets', tweetData);
314
+ return response.data.data;
315
+ } catch (error) {
316
+ this.handleTwitterError(error as AxiosError);
466
317
  }
467
-
468
- /**
469
- * Handle Twitter-specific errors
470
- */
471
- protected handleTwitterError(error: AxiosError): never {
472
- if (error.response) {
473
- const { status, data } = error.response;
474
- const errorData = data as any;
475
-
476
- switch (status) {
477
- case 400:
478
- throw new Error(`Bad Request: ${errorData.detail || errorData.message || 'Invalid request parameters'}`);
479
- case 401:
480
- throw new Error('Unauthorized: Invalid or expired access token');
481
- case 403:
482
- throw new Error('Forbidden: Insufficient permissions. Ensure the app has required Twitter scopes.');
483
- case 404:
484
- throw new Error('Not Found: Resource does not exist');
485
- case 429:
486
- throw new Error('Rate Limit Exceeded: Too many requests');
487
- case 500:
488
- throw new Error('Internal Server Error: Twitter service error');
489
- case 503:
490
- throw new Error('Service Unavailable: Twitter service temporarily unavailable');
491
- default:
492
- throw new Error(`Twitter API Error (${status}): ${errorData.detail || errorData.message || 'Unknown error'}`);
493
- }
494
- } else if (error.request) {
495
- throw new Error('Network Error: No response from Twitter');
496
- } else {
497
- throw new Error(`Request Error: ${error.message}`);
498
- }
318
+ }
319
+
320
+ /**
321
+ * Delete a tweet
322
+ */
323
+ protected async deleteTweet(tweetId: string): Promise<void> {
324
+ try {
325
+ await this.axiosInstance.delete(`/tweets/${tweetId}`);
326
+ } catch (error) {
327
+ this.handleTwitterError(error as AxiosError);
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Get tweets with specified parameters
333
+ */
334
+ protected async getTweets(endpoint: string, params: Record<string, any> = {}): Promise<Tweet[]> {
335
+ try {
336
+ const defaultParams = {
337
+ 'tweet.fields': 'id,text,created_at,author_id,conversation_id,public_metrics,attachments,entities,referenced_tweets',
338
+ 'user.fields': 'id,name,username,profile_image_url',
339
+ 'media.fields': 'url,preview_image_url,type,width,height',
340
+ expansions: 'author_id,attachments.media_keys,referenced_tweets.id',
341
+ max_results: 100,
342
+ };
343
+
344
+ const response = await this.axiosInstance.get(endpoint, {
345
+ params: { ...defaultParams, ...params },
346
+ });
347
+
348
+ return response.data.data || [];
349
+ } catch (error) {
350
+ LogError(`Failed to get tweets: ${error instanceof Error ? error.message : 'Unknown error'}`);
351
+ throw error;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Get paginated tweets
357
+ */
358
+ protected async getPaginatedTweets(endpoint: string, params: Record<string, any> = {}, maxResults?: number): Promise<Tweet[]> {
359
+ const tweets: Tweet[] = [];
360
+ let paginationToken: string | undefined;
361
+ const limit = params.max_results || 100;
362
+
363
+ while (true) {
364
+ const response = await this.axiosInstance.get(endpoint, {
365
+ params: {
366
+ ...params,
367
+ max_results: limit,
368
+ ...(paginationToken && { pagination_token: paginationToken }),
369
+ },
370
+ });
371
+
372
+ if (response.data.data && Array.isArray(response.data.data)) {
373
+ tweets.push(...response.data.data);
374
+ }
375
+
376
+ // Check if we've reached max results
377
+ if (maxResults && tweets.length >= maxResults) {
378
+ return tweets.slice(0, maxResults);
379
+ }
380
+
381
+ // Check for more pages
382
+ paginationToken = response.data.meta?.next_token;
383
+ if (!paginationToken) {
384
+ break;
385
+ }
499
386
  }
500
387
 
501
- /**
502
- * Parse Twitter-specific rate limit headers
503
- */
504
- protected parseRateLimitHeaders(headers: any): { remaining: number; reset: Date; limit: number; } | null {
505
- const remaining = headers['x-rate-limit-remaining'];
506
- const reset = headers['x-rate-limit-reset'];
507
- const limit = headers['x-rate-limit-limit'];
508
-
509
- if (remaining !== undefined && reset && limit) {
510
- return {
511
- remaining: parseInt(remaining),
512
- reset: new Date(parseInt(reset) * 1000), // Unix timestamp to Date
513
- limit: parseInt(limit)
514
- };
515
- }
516
-
517
- return null;
388
+ return tweets;
389
+ }
390
+
391
+ /**
392
+ * Convert Twitter tweet to common format
393
+ */
394
+ protected normalizePost(tweet: Tweet): SocialPost {
395
+ return {
396
+ id: tweet.id,
397
+ platform: 'Twitter',
398
+ profileId: tweet.author_id || '',
399
+ content: tweet.text,
400
+ mediaUrls: tweet.attachments?.media_keys || [],
401
+ publishedAt: new Date(tweet.created_at),
402
+ analytics: tweet.public_metrics
403
+ ? {
404
+ impressions: tweet.public_metrics.impression_count || 0,
405
+ engagements:
406
+ (tweet.public_metrics.retweet_count || 0) +
407
+ (tweet.public_metrics.reply_count || 0) +
408
+ (tweet.public_metrics.like_count || 0) +
409
+ (tweet.public_metrics.quote_count || 0),
410
+ clicks: 0, // Not available in public metrics
411
+ shares: tweet.public_metrics.retweet_count || 0,
412
+ comments: tweet.public_metrics.reply_count || 0,
413
+ likes: tweet.public_metrics.like_count || 0,
414
+ reach: tweet.public_metrics.impression_count || 0,
415
+ platformMetrics: tweet.public_metrics,
416
+ }
417
+ : undefined,
418
+ platformSpecificData: {
419
+ conversationId: tweet.conversation_id,
420
+ referencedTweets: tweet.referenced_tweets,
421
+ entities: tweet.entities,
422
+ },
423
+ };
424
+ }
425
+
426
+ /**
427
+ * Normalize Twitter analytics to common format
428
+ */
429
+ protected normalizeAnalytics(twitterMetrics: TwitterMetrics): SocialAnalytics {
430
+ return {
431
+ impressions: twitterMetrics.impression_count || 0,
432
+ engagements: twitterMetrics.engagement_count || 0,
433
+ clicks: twitterMetrics.url_link_clicks || 0,
434
+ shares: twitterMetrics.retweet_count || 0,
435
+ comments: twitterMetrics.reply_count || 0,
436
+ likes: twitterMetrics.like_count || 0,
437
+ reach: twitterMetrics.impression_count || 0,
438
+ videoViews: twitterMetrics.video_view_count,
439
+ platformMetrics: twitterMetrics,
440
+ };
441
+ }
442
+
443
+ /**
444
+ * Search for tweets - implemented in search action
445
+ */
446
+ protected async searchPosts(params: SearchParams): Promise<SocialPost[]> {
447
+ // This is implemented in the search-tweets.action.ts
448
+ throw new Error('Search posts is implemented in TwitterSearchTweetsAction');
449
+ }
450
+
451
+ /**
452
+ * Handle Twitter-specific errors
453
+ */
454
+ protected handleTwitterError(error: AxiosError): never {
455
+ if (error.response) {
456
+ const { status, data } = error.response;
457
+ const errorData = data as any;
458
+
459
+ switch (status) {
460
+ case 400:
461
+ throw new Error(`Bad Request: ${errorData.detail || errorData.message || 'Invalid request parameters'}`);
462
+ case 401:
463
+ throw new Error('Unauthorized: Invalid or expired access token');
464
+ case 403:
465
+ throw new Error('Forbidden: Insufficient permissions. Ensure the app has required Twitter scopes.');
466
+ case 404:
467
+ throw new Error('Not Found: Resource does not exist');
468
+ case 429:
469
+ throw new Error('Rate Limit Exceeded: Too many requests');
470
+ case 500:
471
+ throw new Error('Internal Server Error: Twitter service error');
472
+ case 503:
473
+ throw new Error('Service Unavailable: Twitter service temporarily unavailable');
474
+ default:
475
+ throw new Error(`Twitter API Error (${status}): ${errorData.detail || errorData.message || 'Unknown error'}`);
476
+ }
477
+ } else if (error.request) {
478
+ throw new Error('Network Error: No response from Twitter');
479
+ } else {
480
+ throw new Error(`Request Error: ${error.message}`);
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Parse Twitter-specific rate limit headers
486
+ */
487
+ protected parseRateLimitHeaders(headers: any): { remaining: number; reset: Date; limit: number } | null {
488
+ const remaining = headers['x-rate-limit-remaining'];
489
+ const reset = headers['x-rate-limit-reset'];
490
+ const limit = headers['x-rate-limit-limit'];
491
+
492
+ if (remaining !== undefined && reset && limit) {
493
+ return {
494
+ remaining: parseInt(remaining),
495
+ reset: new Date(parseInt(reset) * 1000), // Unix timestamp to Date
496
+ limit: parseInt(limit),
497
+ };
518
498
  }
519
499
 
520
- /**
521
- * Build search query with operators
522
- */
523
- protected buildSearchQuery(params: SearchParams): string {
524
- const parts: string[] = [];
500
+ return null;
501
+ }
525
502
 
526
- if (params.query) {
527
- parts.push(params.query);
528
- }
503
+ /**
504
+ * Build search query with operators
505
+ */
506
+ protected buildSearchQuery(params: SearchParams): string {
507
+ const parts: string[] = [];
529
508
 
530
- if (params.hashtags && params.hashtags.length > 0) {
531
- const hashtagQuery = params.hashtags
532
- .map(tag => tag.startsWith('#') ? tag : `#${tag}`)
533
- .join(' OR ');
534
- parts.push(`(${hashtagQuery})`);
535
- }
509
+ if (params.query) {
510
+ parts.push(params.query);
511
+ }
536
512
 
537
- return parts.join(' ');
513
+ if (params.hashtags && params.hashtags.length > 0) {
514
+ const hashtagQuery = params.hashtags.map((tag) => (tag.startsWith('#') ? tag : `#${tag}`)).join(' OR ');
515
+ parts.push(`(${hashtagQuery})`);
538
516
  }
539
517
 
540
- /**
541
- * Format date for Twitter API (RFC 3339)
542
- */
543
- protected formatTwitterDate(date: Date | string): string {
544
- if (typeof date === 'string') {
545
- date = new Date(date);
546
- }
547
- return date.toISOString();
518
+ return parts.join(' ');
519
+ }
520
+
521
+ /**
522
+ * Format date for Twitter API (RFC 3339)
523
+ */
524
+ protected formatTwitterDate(date: Date | string): string {
525
+ if (typeof date === 'string') {
526
+ date = new Date(date);
548
527
  }
528
+ return date.toISOString();
529
+ }
549
530
  }
550
531
 
551
532
  /**
552
533
  * Twitter-specific interfaces
553
534
  */
554
535
  export interface TwitterUser {
555
- id: string;
556
- name: string;
557
- username: string;
558
- profile_image_url?: string;
559
- description?: string;
560
- created_at: string;
561
- verified?: boolean;
536
+ id: string;
537
+ name: string;
538
+ username: string;
539
+ profile_image_url?: string;
540
+ description?: string;
541
+ created_at: string;
542
+ verified?: boolean;
562
543
  }
563
544
 
564
545
  export interface CreateTweetData {
565
- text: string;
566
- media?: {
567
- media_ids: string[];
568
- };
569
- poll?: {
570
- options: string[];
571
- duration_minutes: number;
572
- };
573
- reply?: {
574
- in_reply_to_tweet_id: string;
575
- };
576
- quote_tweet_id?: string;
546
+ text: string;
547
+ media?: {
548
+ media_ids: string[];
549
+ };
550
+ poll?: {
551
+ options: string[];
552
+ duration_minutes: number;
553
+ };
554
+ reply?: {
555
+ in_reply_to_tweet_id: string;
556
+ };
557
+ quote_tweet_id?: string;
577
558
  }
578
559
 
579
560
  export interface Tweet {
580
- id: string;
581
- text: string;
582
- created_at: string;
583
- author_id?: string;
584
- conversation_id?: string;
585
- public_metrics?: {
586
- retweet_count: number;
587
- reply_count: number;
588
- like_count: number;
589
- quote_count: number;
590
- bookmark_count: number;
591
- impression_count: number;
592
- };
593
- attachments?: {
594
- media_keys?: string[];
595
- poll_ids?: string[];
596
- };
597
- entities?: {
598
- hashtags?: Array<{ start: number; end: number; tag: string }>;
599
- mentions?: Array<{ start: number; end: number; username: string }>;
600
- urls?: Array<{ start: number; end: number; url: string; expanded_url: string }>;
601
- };
602
- referenced_tweets?: Array<{
603
- type: 'retweeted' | 'quoted' | 'replied_to';
604
- id: string;
605
- }>;
606
- }
607
-
608
- export interface TwitterMetrics {
609
- impression_count: number;
610
- engagement_count: number;
561
+ id: string;
562
+ text: string;
563
+ created_at: string;
564
+ author_id?: string;
565
+ conversation_id?: string;
566
+ public_metrics?: {
611
567
  retweet_count: number;
612
568
  reply_count: number;
613
569
  like_count: number;
614
570
  quote_count: number;
615
571
  bookmark_count: number;
616
- url_link_clicks: number;
617
- user_profile_clicks: number;
618
- video_view_count?: number;
572
+ impression_count: number;
573
+ };
574
+ attachments?: {
575
+ media_keys?: string[];
576
+ poll_ids?: string[];
577
+ };
578
+ entities?: {
579
+ hashtags?: Array<{ start: number; end: number; tag: string }>;
580
+ mentions?: Array<{ start: number; end: number; username: string }>;
581
+ urls?: Array<{ start: number; end: number; url: string; expanded_url: string }>;
582
+ };
583
+ referenced_tweets?: Array<{
584
+ type: 'retweeted' | 'quoted' | 'replied_to';
585
+ id: string;
586
+ }>;
587
+ }
588
+
589
+ export interface TwitterMetrics {
590
+ impression_count: number;
591
+ engagement_count: number;
592
+ retweet_count: number;
593
+ reply_count: number;
594
+ like_count: number;
595
+ quote_count: number;
596
+ bookmark_count: number;
597
+ url_link_clicks: number;
598
+ user_profile_clicks: number;
599
+ video_view_count?: number;
619
600
  }
620
601
 
621
602
  export interface TwitterSearchParams {
622
- query: string;
623
- start_time?: string;
624
- end_time?: string;
625
- max_results?: number;
626
- next_token?: string;
627
- since_id?: string;
628
- until_id?: string;
629
- sort_order?: 'recency' | 'relevancy';
630
- }
603
+ query: string;
604
+ start_time?: string;
605
+ end_time?: string;
606
+ max_results?: number;
607
+ next_token?: string;
608
+ since_id?: string;
609
+ until_id?: string;
610
+ sort_order?: 'recency' | 'relevancy';
611
+ }