@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +6 -6
- package/dist/base/base-social.action.d.ts.map +1 -1
- package/dist/base/base-social.action.js +18 -24
- package/dist/base/base-social.action.js.map +1 -1
- package/dist/providers/buffer/buffer-base.action.d.ts.map +1 -1
- package/dist/providers/buffer/buffer-base.action.js +35 -34
- package/dist/providers/buffer/buffer-base.action.js.map +1 -1
- package/dist/providers/facebook/actions/boost-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/boost-post.action.js +33 -33
- package/dist/providers/facebook/actions/boost-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/create-album.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/create-album.action.js +34 -36
- package/dist/providers/facebook/actions/create-album.action.js.map +1 -1
- package/dist/providers/facebook/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/create-post.action.js +20 -20
- package/dist/providers/facebook/actions/create-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-page-insights.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-page-insights.action.js +25 -27
- package/dist/providers/facebook/actions/get-page-insights.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-page-posts.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-page-posts.action.js +19 -23
- package/dist/providers/facebook/actions/get-page-posts.action.js.map +1 -1
- package/dist/providers/facebook/actions/get-post-insights.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/get-post-insights.action.js +28 -32
- package/dist/providers/facebook/actions/get-post-insights.action.js.map +1 -1
- package/dist/providers/facebook/actions/respond-to-comments.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/respond-to-comments.action.js +42 -44
- package/dist/providers/facebook/actions/respond-to-comments.action.js.map +1 -1
- package/dist/providers/facebook/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/schedule-post.action.js +29 -29
- package/dist/providers/facebook/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/facebook/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/facebook/actions/search-posts.action.js +37 -39
- package/dist/providers/facebook/actions/search-posts.action.js.map +1 -1
- package/dist/providers/facebook/facebook-base.action.d.ts.map +1 -1
- package/dist/providers/facebook/facebook-base.action.js +44 -59
- package/dist/providers/facebook/facebook-base.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js +33 -31
- package/dist/providers/hootsuite/actions/bulk-schedule-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.js +28 -32
- package/dist/providers/hootsuite/actions/create-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js +19 -19
- package/dist/providers/hootsuite/actions/delete-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-analytics.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-analytics.action.js +24 -26
- package/dist/providers/hootsuite/actions/get-analytics.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js +22 -22
- package/dist/providers/hootsuite/actions/get-scheduled-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/get-social-profiles.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/get-social-profiles.action.js +32 -34
- package/dist/providers/hootsuite/actions/get-social-profiles.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/search-posts.action.js +43 -52
- package/dist/providers/hootsuite/actions/search-posts.action.js.map +1 -1
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.js +30 -28
- package/dist/providers/hootsuite/actions/update-scheduled-post.action.js.map +1 -1
- package/dist/providers/hootsuite/hootsuite-base.action.d.ts.map +1 -1
- package/dist/providers/hootsuite/hootsuite-base.action.js +18 -20
- package/dist/providers/hootsuite/hootsuite-base.action.js.map +1 -1
- package/dist/providers/instagram/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/create-post.action.js +27 -26
- package/dist/providers/instagram/actions/create-post.action.js.map +1 -1
- package/dist/providers/instagram/actions/create-story.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/create-story.action.js +35 -35
- package/dist/providers/instagram/actions/create-story.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-account-insights.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-account-insights.action.js +38 -59
- package/dist/providers/instagram/actions/get-account-insights.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-business-posts.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-business-posts.action.js +29 -29
- package/dist/providers/instagram/actions/get-business-posts.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-comments.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-comments.action.js +36 -36
- package/dist/providers/instagram/actions/get-comments.action.js.map +1 -1
- package/dist/providers/instagram/actions/get-post-insights.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/get-post-insights.action.js +23 -25
- package/dist/providers/instagram/actions/get-post-insights.action.js.map +1 -1
- package/dist/providers/instagram/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/schedule-post.action.js +25 -25
- package/dist/providers/instagram/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/instagram/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/instagram/actions/search-posts.action.js +56 -60
- package/dist/providers/instagram/actions/search-posts.action.js.map +1 -1
- package/dist/providers/instagram/instagram-base.action.d.ts.map +1 -1
- package/dist/providers/instagram/instagram-base.action.js +25 -27
- package/dist/providers/instagram/instagram-base.action.js.map +1 -1
- package/dist/providers/linkedin/actions/create-article.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/create-article.action.js +55 -45
- package/dist/providers/linkedin/actions/create-article.action.js.map +1 -1
- package/dist/providers/linkedin/actions/create-post.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/create-post.action.js +31 -29
- package/dist/providers/linkedin/actions/create-post.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-followers.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-followers.action.js +28 -28
- package/dist/providers/linkedin/actions/get-followers.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-organization-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-organization-posts.action.js +20 -20
- package/dist/providers/linkedin/actions/get-organization-posts.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-personal-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-personal-posts.action.js +19 -19
- package/dist/providers/linkedin/actions/get-personal-posts.action.js.map +1 -1
- package/dist/providers/linkedin/actions/get-post-analytics.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/get-post-analytics.action.js +25 -23
- package/dist/providers/linkedin/actions/get-post-analytics.action.js.map +1 -1
- package/dist/providers/linkedin/actions/schedule-post.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/schedule-post.action.js +32 -30
- package/dist/providers/linkedin/actions/schedule-post.action.js.map +1 -1
- package/dist/providers/linkedin/actions/search-posts.action.d.ts.map +1 -1
- package/dist/providers/linkedin/actions/search-posts.action.js +28 -30
- package/dist/providers/linkedin/actions/search-posts.action.js.map +1 -1
- package/dist/providers/linkedin/linkedin-base.action.d.ts.map +1 -1
- package/dist/providers/linkedin/linkedin-base.action.js +33 -38
- package/dist/providers/linkedin/linkedin-base.action.js.map +1 -1
- package/dist/providers/tiktok/tiktok-base.action.d.ts.map +1 -1
- package/dist/providers/tiktok/tiktok-base.action.js +25 -26
- package/dist/providers/tiktok/tiktok-base.action.js.map +1 -1
- package/dist/providers/twitter/actions/create-thread.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/create-thread.action.js +25 -29
- package/dist/providers/twitter/actions/create-thread.action.js.map +1 -1
- package/dist/providers/twitter/actions/create-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/create-tweet.action.js +23 -23
- package/dist/providers/twitter/actions/create-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/delete-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/delete-tweet.action.js +19 -19
- package/dist/providers/twitter/actions/delete-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-analytics.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-analytics.action.js +40 -47
- package/dist/providers/twitter/actions/get-analytics.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-mentions.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-mentions.action.js +30 -31
- package/dist/providers/twitter/actions/get-mentions.action.js.map +1 -1
- package/dist/providers/twitter/actions/get-timeline.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/get-timeline.action.js +29 -29
- package/dist/providers/twitter/actions/get-timeline.action.js.map +1 -1
- package/dist/providers/twitter/actions/schedule-tweet.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/schedule-tweet.action.js +26 -26
- package/dist/providers/twitter/actions/schedule-tweet.action.js.map +1 -1
- package/dist/providers/twitter/actions/search-tweets.action.d.ts.map +1 -1
- package/dist/providers/twitter/actions/search-tweets.action.js +56 -58
- package/dist/providers/twitter/actions/search-tweets.action.js.map +1 -1
- package/dist/providers/twitter/twitter-base.action.d.ts.map +1 -1
- package/dist/providers/twitter/twitter-base.action.js +58 -68
- package/dist/providers/twitter/twitter-base.action.js.map +1 -1
- package/dist/providers/youtube/youtube-base.action.d.ts +1 -1
- package/dist/providers/youtube/youtube-base.action.d.ts.map +1 -1
- package/dist/providers/youtube/youtube-base.action.js +22 -25
- package/dist/providers/youtube/youtube-base.action.js.map +1 -1
- package/package.json +5 -6
- package/src/base/base-social.action.ts +217 -224
- package/src/providers/buffer/buffer-base.action.ts +435 -441
- package/src/providers/facebook/actions/boost-post.action.ts +350 -386
- package/src/providers/facebook/actions/create-album.action.ts +291 -307
- package/src/providers/facebook/actions/create-post.action.ts +224 -227
- package/src/providers/facebook/actions/get-page-insights.action.ts +383 -403
- package/src/providers/facebook/actions/get-page-posts.action.ts +214 -225
- package/src/providers/facebook/actions/get-post-insights.action.ts +300 -316
- package/src/providers/facebook/actions/respond-to-comments.action.ts +319 -336
- package/src/providers/facebook/actions/schedule-post.action.ts +289 -292
- package/src/providers/facebook/actions/search-posts.action.ts +399 -413
- package/src/providers/facebook/facebook-base.action.ts +653 -670
- package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
- package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +184 -189
- package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +160 -161
- package/src/providers/hootsuite/actions/get-analytics.action.ts +249 -254
- package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +206 -207
- package/src/providers/hootsuite/actions/get-social-profiles.action.ts +206 -205
- package/src/providers/hootsuite/actions/search-posts.action.ts +351 -369
- package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +211 -209
- package/src/providers/hootsuite/hootsuite-base.action.ts +301 -307
- package/src/providers/instagram/actions/create-post.action.ts +276 -296
- package/src/providers/instagram/actions/create-story.action.ts +378 -394
- package/src/providers/instagram/actions/get-account-insights.action.ts +384 -420
- package/src/providers/instagram/actions/get-business-posts.action.ts +233 -242
- package/src/providers/instagram/actions/get-comments.action.ts +365 -377
- package/src/providers/instagram/actions/get-post-insights.action.ts +265 -273
- package/src/providers/instagram/actions/schedule-post.action.ts +233 -235
- package/src/providers/instagram/actions/search-posts.action.ts +512 -538
- package/src/providers/instagram/instagram-base.action.ts +368 -393
- package/src/providers/linkedin/actions/create-article.action.ts +275 -266
- package/src/providers/linkedin/actions/create-post.action.ts +179 -177
- package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
- package/src/providers/linkedin/actions/get-organization-posts.action.ts +146 -147
- package/src/providers/linkedin/actions/get-personal-posts.action.ts +138 -139
- package/src/providers/linkedin/actions/get-post-analytics.action.ts +190 -189
- package/src/providers/linkedin/actions/schedule-post.action.ts +191 -189
- package/src/providers/linkedin/actions/search-posts.action.ts +275 -283
- package/src/providers/linkedin/linkedin-base.action.ts +407 -421
- package/src/providers/tiktok/tiktok-base.action.ts +305 -320
- package/src/providers/twitter/actions/create-thread.action.ts +203 -207
- package/src/providers/twitter/actions/create-tweet.action.ts +187 -188
- package/src/providers/twitter/actions/delete-tweet.action.ts +128 -129
- package/src/providers/twitter/actions/get-analytics.action.ts +402 -411
- package/src/providers/twitter/actions/get-mentions.action.ts +218 -219
- package/src/providers/twitter/actions/get-timeline.action.ts +232 -233
- package/src/providers/twitter/actions/schedule-tweet.action.ts +221 -222
- package/src/providers/twitter/actions/search-tweets.action.ts +540 -543
- package/src/providers/twitter/twitter-base.action.ts +541 -560
- 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/
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
84
|
+
);
|
|
89
85
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
522
|
-
*/
|
|
523
|
-
protected buildSearchQuery(params: SearchParams): string {
|
|
524
|
-
const parts: string[] = [];
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
525
502
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
503
|
+
/**
|
|
504
|
+
* Build search query with operators
|
|
505
|
+
*/
|
|
506
|
+
protected buildSearchQuery(params: SearchParams): string {
|
|
507
|
+
const parts: string[] = [];
|
|
529
508
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
.join(' OR ');
|
|
534
|
-
parts.push(`(${hashtagQuery})`);
|
|
535
|
-
}
|
|
509
|
+
if (params.query) {
|
|
510
|
+
parts.push(params.query);
|
|
511
|
+
}
|
|
536
512
|
|
|
537
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
+
}
|