@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
@@ -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/global';
5
+ import { LogStatus, LogError } from '@memberjunction/core';
6
6
  import FormData from 'form-data';
7
7
  import { BaseAction } from '@memberjunction/actions';
8
8
 
@@ -13,599 +13,618 @@ 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 ? 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);
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
+ );
83
87
  }
84
- );
88
+ return this._axiosInstance;
85
89
  }
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');
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
+ }
96
134
  }
97
135
 
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
- },
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;
116
150
  }
117
- );
151
+ }
118
152
 
119
- const { access_token, refresh_token: newRefreshToken, expires_in } = response.data;
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
+ }
120
237
 
121
- // Update stored tokens
122
- await this.updateStoredTokens(access_token, newRefreshToken || refreshToken, expires_in);
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
+ }
123
277
 
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;
278
+ throw new Error('Media processing timeout');
128
279
  }
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;
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';
145
293
  }
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
- },
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(', ')}`);
169
309
  }
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
- },
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
212
321
  }
213
- );
214
322
 
215
- // Step 4: Check processing status (for videos)
216
- if (file.mimeType.startsWith('video/')) {
217
- await this.waitForMediaProcessing(mediaId);
218
- }
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
+ }
219
327
 
220
- return mediaId;
221
- } catch (error) {
222
- LogError(`Failed to upload media to Twitter: ${error instanceof Error ? error.message : 'Unknown error'}`);
223
- throw error;
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
+ }
224
338
  }
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));
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
+ }
262
349
  }
263
350
 
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';
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
+ }
277
373
  }
278
- return 'tweet_image';
279
- }
280
374
 
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'];
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
+ }
286
407
 
287
- if (!supportedTypes.includes(file.mimeType)) {
288
- throw new Error(`Unsupported media type: ${file.mimeType}. Supported types: ${supportedTypes.join(', ')}`);
408
+ return tweets;
289
409
  }
290
410
 
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
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
+ };
301
441
  }
302
442
 
303
- if (file.size > maxSize) {
304
- throw new Error(`File size exceeds limit. Max: ${maxSize / 1024 / 1024}MB, Got: ${file.size / 1024 / 1024}MB`);
305
- }
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);
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
+ };
317
458
  }
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
- }
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');
386
466
  }
387
467
 
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}`);
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
+ }
481
499
  }
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
- };
500
+
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;
498
518
  }
499
519
 
500
- return null;
501
- }
520
+ /**
521
+ * Build search query with operators
522
+ */
523
+ protected buildSearchQuery(params: SearchParams): string {
524
+ const parts: string[] = [];
502
525
 
503
- /**
504
- * Build search query with operators
505
- */
506
- protected buildSearchQuery(params: SearchParams): string {
507
- const parts: string[] = [];
526
+ if (params.query) {
527
+ parts.push(params.query);
528
+ }
508
529
 
509
- if (params.query) {
510
- parts.push(params.query);
511
- }
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
+ }
512
536
 
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})`);
537
+ return parts.join(' ');
516
538
  }
517
539
 
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);
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();
527
548
  }
528
- return date.toISOString();
529
- }
530
549
  }
531
550
 
532
551
  /**
533
552
  * Twitter-specific interfaces
534
553
  */
535
554
  export interface TwitterUser {
536
- id: string;
537
- name: string;
538
- username: string;
539
- profile_image_url?: string;
540
- description?: string;
541
- created_at: string;
542
- verified?: boolean;
555
+ id: string;
556
+ name: string;
557
+ username: string;
558
+ profile_image_url?: string;
559
+ description?: string;
560
+ created_at: string;
561
+ verified?: boolean;
543
562
  }
544
563
 
545
564
  export interface CreateTweetData {
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;
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;
558
577
  }
559
578
 
560
579
  export interface Tweet {
561
- id: string;
562
- text: string;
563
- created_at: string;
564
- author_id?: string;
565
- conversation_id?: string;
566
- public_metrics?: {
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;
567
611
  retweet_count: number;
568
612
  reply_count: number;
569
613
  like_count: number;
570
614
  quote_count: number;
571
615
  bookmark_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;
616
+ url_link_clicks: number;
617
+ user_profile_clicks: number;
618
+ video_view_count?: number;
600
619
  }
601
620
 
602
621
  export interface TwitterSearchParams {
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
- }
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
+ }