@memberjunction/actions-bizapps-social 2.112.0 → 2.113.1
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 +13 -0
- package/dist/base/base-social.action.d.ts.map +1 -1
- package/dist/base/base-social.action.js +24 -18
- 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 +34 -35
- 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 +36 -34
- 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 +27 -25
- 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 +23 -19
- 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 +32 -28
- 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 +44 -42
- 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 +39 -37
- 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 +59 -44
- 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 +31 -33
- 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 +32 -28
- 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 +26 -24
- 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 +34 -32
- 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 +52 -43
- 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 +28 -30
- 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 +20 -18
- 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 +26 -27
- 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 +59 -38
- 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 +25 -23
- 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 +60 -56
- 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 +27 -25
- 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 +45 -55
- 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 +29 -31
- 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 +23 -25
- 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 +30 -32
- 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 +30 -28
- 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 +38 -33
- 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 +26 -25
- 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 +29 -25
- 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 +47 -40
- 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 +31 -30
- 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 +58 -56
- 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 +68 -58
- 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 +25 -22
- package/dist/providers/youtube/youtube-base.action.js.map +1 -1
- package/package.json +6 -5
- package/src/base/base-social.action.ts +224 -217
- package/src/providers/buffer/buffer-base.action.ts +441 -435
- package/src/providers/facebook/actions/boost-post.action.ts +386 -350
- package/src/providers/facebook/actions/create-album.action.ts +307 -291
- package/src/providers/facebook/actions/create-post.action.ts +227 -224
- package/src/providers/facebook/actions/get-page-insights.action.ts +403 -383
- package/src/providers/facebook/actions/get-page-posts.action.ts +225 -214
- package/src/providers/facebook/actions/get-post-insights.action.ts +316 -300
- package/src/providers/facebook/actions/respond-to-comments.action.ts +336 -319
- package/src/providers/facebook/actions/schedule-post.action.ts +292 -289
- package/src/providers/facebook/actions/search-posts.action.ts +413 -399
- package/src/providers/facebook/facebook-base.action.ts +670 -653
- package/src/providers/hootsuite/actions/bulk-schedule-posts.action.ts +257 -257
- package/src/providers/hootsuite/actions/create-scheduled-post.action.ts +189 -184
- package/src/providers/hootsuite/actions/delete-scheduled-post.action.ts +161 -160
- package/src/providers/hootsuite/actions/get-analytics.action.ts +254 -249
- package/src/providers/hootsuite/actions/get-scheduled-posts.action.ts +207 -206
- package/src/providers/hootsuite/actions/get-social-profiles.action.ts +205 -206
- package/src/providers/hootsuite/actions/search-posts.action.ts +369 -351
- package/src/providers/hootsuite/actions/update-scheduled-post.action.ts +209 -211
- package/src/providers/hootsuite/hootsuite-base.action.ts +307 -301
- package/src/providers/instagram/actions/create-post.action.ts +296 -276
- package/src/providers/instagram/actions/create-story.action.ts +394 -378
- package/src/providers/instagram/actions/get-account-insights.action.ts +420 -384
- package/src/providers/instagram/actions/get-business-posts.action.ts +242 -233
- package/src/providers/instagram/actions/get-comments.action.ts +377 -365
- package/src/providers/instagram/actions/get-post-insights.action.ts +273 -265
- package/src/providers/instagram/actions/schedule-post.action.ts +235 -233
- package/src/providers/instagram/actions/search-posts.action.ts +538 -512
- package/src/providers/instagram/instagram-base.action.ts +393 -368
- package/src/providers/linkedin/actions/create-article.action.ts +266 -275
- package/src/providers/linkedin/actions/create-post.action.ts +177 -179
- package/src/providers/linkedin/actions/get-followers.action.ts +211 -211
- package/src/providers/linkedin/actions/get-organization-posts.action.ts +147 -146
- package/src/providers/linkedin/actions/get-personal-posts.action.ts +139 -138
- package/src/providers/linkedin/actions/get-post-analytics.action.ts +189 -190
- package/src/providers/linkedin/actions/schedule-post.action.ts +189 -191
- package/src/providers/linkedin/actions/search-posts.action.ts +283 -275
- package/src/providers/linkedin/linkedin-base.action.ts +421 -407
- package/src/providers/tiktok/tiktok-base.action.ts +320 -305
- package/src/providers/twitter/actions/create-thread.action.ts +207 -203
- package/src/providers/twitter/actions/create-tweet.action.ts +188 -187
- package/src/providers/twitter/actions/delete-tweet.action.ts +129 -128
- package/src/providers/twitter/actions/get-analytics.action.ts +411 -402
- package/src/providers/twitter/actions/get-mentions.action.ts +219 -218
- package/src/providers/twitter/actions/get-timeline.action.ts +233 -232
- package/src/providers/twitter/actions/schedule-tweet.action.ts +222 -221
- package/src/providers/twitter/actions/search-tweets.action.ts +543 -540
- package/src/providers/twitter/twitter-base.action.ts +560 -541
- 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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
288
|
-
throw new Error(`Unsupported media type: ${file.mimeType}. Supported types: ${supportedTypes.join(', ')}`);
|
|
408
|
+
return tweets;
|
|
289
409
|
}
|
|
290
410
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
501
|
-
|
|
520
|
+
/**
|
|
521
|
+
* Build search query with operators
|
|
522
|
+
*/
|
|
523
|
+
protected buildSearchQuery(params: SearchParams): string {
|
|
524
|
+
const parts: string[] = [];
|
|
502
525
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
protected buildSearchQuery(params: SearchParams): string {
|
|
507
|
-
const parts: string[] = [];
|
|
526
|
+
if (params.query) {
|
|
527
|
+
parts.push(params.query);
|
|
528
|
+
}
|
|
508
529
|
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
+
}
|