@classytic/social 0.1.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/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +368 -0
- package/dist/base-Bw7e52V8.mjs +246 -0
- package/dist/base-Bw7e52V8.mjs.map +1 -0
- package/dist/base-DBtKFiSX.d.mts +226 -0
- package/dist/base-DBtKFiSX.d.mts.map +1 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client/index.d.mts +44 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +154 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/common/index.d.mts +3 -0
- package/dist/common/index.mjs +7 -0
- package/dist/contracts-Cdwa4zlg.d.mts +121 -0
- package/dist/contracts-Cdwa4zlg.d.mts.map +1 -0
- package/dist/contracts-lCa069IK.mjs +221 -0
- package/dist/contracts-lCa069IK.mjs.map +1 -0
- package/dist/env-Bl0cwwjC.mjs +955 -0
- package/dist/env-Bl0cwwjC.mjs.map +1 -0
- package/dist/env-DxOZHf0p.d.mts +394 -0
- package/dist/env-DxOZHf0p.d.mts.map +1 -0
- package/dist/errors-Cm6LeKf7.mjs +32 -0
- package/dist/errors-Cm6LeKf7.mjs.map +1 -0
- package/dist/facebook-l_4CghaA.mjs +95 -0
- package/dist/facebook-l_4CghaA.mjs.map +1 -0
- package/dist/http-DpcLSR1M.mjs +197 -0
- package/dist/http-DpcLSR1M.mjs.map +1 -0
- package/dist/index.d.mts +42 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +71 -0
- package/dist/index.mjs.map +1 -0
- package/dist/instagram-BGaeUFU2.mjs +90 -0
- package/dist/instagram-BGaeUFU2.mjs.map +1 -0
- package/dist/linkedin-70whtVKa.mjs +101 -0
- package/dist/linkedin-70whtVKa.mjs.map +1 -0
- package/dist/meta-D3vcJU1c.mjs +126 -0
- package/dist/meta-D3vcJU1c.mjs.map +1 -0
- package/dist/pkce-jq5II68b.mjs +72 -0
- package/dist/pkce-jq5II68b.mjs.map +1 -0
- package/dist/polling-DZ1apXtA.mjs +25 -0
- package/dist/polling-DZ1apXtA.mjs.map +1 -0
- package/dist/providers/facebook.d.mts +135 -0
- package/dist/providers/facebook.d.mts.map +1 -0
- package/dist/providers/facebook.mjs +450 -0
- package/dist/providers/facebook.mjs.map +1 -0
- package/dist/providers/instagram.d.mts +122 -0
- package/dist/providers/instagram.d.mts.map +1 -0
- package/dist/providers/instagram.mjs +496 -0
- package/dist/providers/instagram.mjs.map +1 -0
- package/dist/providers/linkedin.d.mts +145 -0
- package/dist/providers/linkedin.d.mts.map +1 -0
- package/dist/providers/linkedin.mjs +574 -0
- package/dist/providers/linkedin.mjs.map +1 -0
- package/dist/providers/reddit.d.mts +102 -0
- package/dist/providers/reddit.d.mts.map +1 -0
- package/dist/providers/reddit.mjs +657 -0
- package/dist/providers/reddit.mjs.map +1 -0
- package/dist/providers/telegram.d.mts +139 -0
- package/dist/providers/telegram.d.mts.map +1 -0
- package/dist/providers/telegram.mjs +517 -0
- package/dist/providers/telegram.mjs.map +1 -0
- package/dist/providers/tiktok.d.mts +116 -0
- package/dist/providers/tiktok.d.mts.map +1 -0
- package/dist/providers/tiktok.mjs +676 -0
- package/dist/providers/tiktok.mjs.map +1 -0
- package/dist/providers/twitter.d.mts +150 -0
- package/dist/providers/twitter.d.mts.map +1 -0
- package/dist/providers/twitter.mjs +628 -0
- package/dist/providers/twitter.mjs.map +1 -0
- package/dist/providers/whatsapp.d.mts +79 -0
- package/dist/providers/whatsapp.d.mts.map +1 -0
- package/dist/providers/whatsapp.mjs +376 -0
- package/dist/providers/whatsapp.mjs.map +1 -0
- package/dist/providers/youtube.d.mts +153 -0
- package/dist/providers/youtube.d.mts.map +1 -0
- package/dist/providers/youtube.mjs +902 -0
- package/dist/providers/youtube.mjs.map +1 -0
- package/dist/reddit-B10kS4Se.mjs +126 -0
- package/dist/reddit-B10kS4Se.mjs.map +1 -0
- package/dist/schemas/index.d.mts +819 -0
- package/dist/schemas/index.d.mts.map +1 -0
- package/dist/schemas/index.mjs +31 -0
- package/dist/schemas/index.mjs.map +1 -0
- package/dist/security-BXhfebWm.d.mts +338 -0
- package/dist/security-BXhfebWm.d.mts.map +1 -0
- package/dist/shared-Fvc6xQku.mjs +100 -0
- package/dist/shared-Fvc6xQku.mjs.map +1 -0
- package/dist/telegram-FaUHpZgB.mjs +107 -0
- package/dist/telegram-FaUHpZgB.mjs.map +1 -0
- package/dist/tiktok-B_bMk4G-.mjs +94 -0
- package/dist/tiktok-B_bMk4G-.mjs.map +1 -0
- package/dist/twitter-BC22zfuc.mjs +98 -0
- package/dist/twitter-BC22zfuc.mjs.map +1 -0
- package/dist/types-BFE4psYI.d.mts +102 -0
- package/dist/types-BFE4psYI.d.mts.map +1 -0
- package/dist/types-Bv27tcT0.d.mts +230 -0
- package/dist/types-Bv27tcT0.d.mts.map +1 -0
- package/dist/types-BwkKyqpi.d.mts +253 -0
- package/dist/types-BwkKyqpi.d.mts.map +1 -0
- package/dist/types-CJrHMDV9.mjs +27 -0
- package/dist/types-CJrHMDV9.mjs.map +1 -0
- package/dist/types-ClbVc2rc.d.mts +117 -0
- package/dist/types-ClbVc2rc.d.mts.map +1 -0
- package/dist/types-D91N16Ym.d.mts +242 -0
- package/dist/types-D91N16Ym.d.mts.map +1 -0
- package/dist/types-DfLp_ibQ.d.mts +178 -0
- package/dist/types-DfLp_ibQ.d.mts.map +1 -0
- package/dist/types-DfjDgEoJ.d.mts +88 -0
- package/dist/types-DfjDgEoJ.d.mts.map +1 -0
- package/dist/types-Dp5Z9VBr.mjs +23 -0
- package/dist/types-Dp5Z9VBr.mjs.map +1 -0
- package/dist/types-hriBJTsU.d.mts +129 -0
- package/dist/types-hriBJTsU.d.mts.map +1 -0
- package/dist/types-rn6UuLL8.d.mts +184 -0
- package/dist/types-rn6UuLL8.d.mts.map +1 -0
- package/dist/whatsapp-CFp7ryR4.mjs +101 -0
- package/dist/whatsapp-CFp7ryR4.mjs.map +1 -0
- package/dist/youtube-Bs0fdY7H.mjs +98 -0
- package/dist/youtube-Bs0fdY7H.mjs.map +1 -0
- package/package.json +148 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import { t as PlatformProvider } from "../base-Bw7e52V8.mjs";
|
|
2
|
+
import { t as SocialError } from "../errors-Cm6LeKf7.mjs";
|
|
3
|
+
import { t as httpRequest } from "../http-DpcLSR1M.mjs";
|
|
4
|
+
import { n as generateCodeChallenge, r as generateCodeVerifier, t as PkceStore } from "../pkce-jq5II68b.mjs";
|
|
5
|
+
import { t as TwitterCredentialsSchema } from "../twitter-BC22zfuc.mjs";
|
|
6
|
+
import { t as TWITTER_ERROR_HINTS } from "../types-Dp5Z9VBr.mjs";
|
|
7
|
+
|
|
8
|
+
//#region src/providers/twitter/index.ts
|
|
9
|
+
/**
|
|
10
|
+
* Twitter/X Provider
|
|
11
|
+
* ==================
|
|
12
|
+
* Full Twitter API v2 integration with OAuth 2.0 + PKCE.
|
|
13
|
+
*
|
|
14
|
+
* Supports:
|
|
15
|
+
* - OAuth 2.0 with PKCE (authorization code flow)
|
|
16
|
+
* - Tweet CRUD (create, delete, search, get)
|
|
17
|
+
* - Reply and quote tweets
|
|
18
|
+
* - Like / unlike / retweet / unretweet
|
|
19
|
+
* - User lookup (by ID, username, or authenticated user)
|
|
20
|
+
* - Direct messages (send)
|
|
21
|
+
* - Followers / following lists
|
|
22
|
+
* - Bookmarks
|
|
23
|
+
* - Media upload (via v1.1 chunked upload)
|
|
24
|
+
*
|
|
25
|
+
* @see https://developer.x.com/en/docs/x-api
|
|
26
|
+
*/
|
|
27
|
+
const API_BASE = "https://api.twitter.com/2";
|
|
28
|
+
const AUTH_URL = "https://x.com/i/oauth2/authorize";
|
|
29
|
+
const TOKEN_URL = "https://api.twitter.com/2/oauth2/token";
|
|
30
|
+
const REVOKE_URL = "https://api.twitter.com/2/oauth2/revoke";
|
|
31
|
+
const DEFAULT_SCOPES = [
|
|
32
|
+
"tweet.read",
|
|
33
|
+
"tweet.write",
|
|
34
|
+
"tweet.moderate.write",
|
|
35
|
+
"users.read",
|
|
36
|
+
"follows.read",
|
|
37
|
+
"follows.write",
|
|
38
|
+
"offline.access",
|
|
39
|
+
"like.read",
|
|
40
|
+
"like.write",
|
|
41
|
+
"dm.write",
|
|
42
|
+
"dm.read",
|
|
43
|
+
"list.read",
|
|
44
|
+
"list.write",
|
|
45
|
+
"bookmark.read",
|
|
46
|
+
"bookmark.write",
|
|
47
|
+
"block.read",
|
|
48
|
+
"mute.read",
|
|
49
|
+
"media.write"
|
|
50
|
+
];
|
|
51
|
+
const SCOPE_DESCRIPTIONS = {
|
|
52
|
+
"tweet.read": "Read tweets on your behalf",
|
|
53
|
+
"tweet.write": "Create, delete, and manage tweets",
|
|
54
|
+
"tweet.moderate.write": "Hide/unhide replies to your tweets",
|
|
55
|
+
"users.read": "Read user profile information",
|
|
56
|
+
"follows.read": "View followers and following lists",
|
|
57
|
+
"follows.write": "Follow and unfollow users",
|
|
58
|
+
"offline.access": "Stay connected (refresh tokens)",
|
|
59
|
+
"like.read": "View liked tweets",
|
|
60
|
+
"like.write": "Like and unlike tweets",
|
|
61
|
+
"dm.write": "Send direct messages",
|
|
62
|
+
"dm.read": "Read direct messages",
|
|
63
|
+
"list.read": "Read lists",
|
|
64
|
+
"list.write": "Create and manage lists",
|
|
65
|
+
"bookmark.read": "View bookmarks",
|
|
66
|
+
"bookmark.write": "Create and manage bookmarks",
|
|
67
|
+
"block.read": "View blocked accounts",
|
|
68
|
+
"mute.read": "View muted accounts"
|
|
69
|
+
};
|
|
70
|
+
const pkceVerifiers = new PkceStore(600 * 1e3);
|
|
71
|
+
var TwitterProvider = class extends PlatformProvider {
|
|
72
|
+
constructor(config = {}) {
|
|
73
|
+
super(config);
|
|
74
|
+
this.name = "twitter";
|
|
75
|
+
this.displayName = "Twitter / X";
|
|
76
|
+
this.authType = "oauth2";
|
|
77
|
+
}
|
|
78
|
+
getAuthUrl(state, credData, options) {
|
|
79
|
+
const clientId = credData?.clientId;
|
|
80
|
+
if (!clientId) throw new SocialError("twitter", "Client ID is required", { statusCode: 400 });
|
|
81
|
+
const redirectUri = credData?.redirectUri || this.config.redirectUri || `http://localhost:${this.config.port || 8060}/api/oauth/twitter/callback`;
|
|
82
|
+
const codeVerifier = generateCodeVerifier();
|
|
83
|
+
pkceVerifiers.set(state, codeVerifier);
|
|
84
|
+
return `${AUTH_URL}?${new URLSearchParams({
|
|
85
|
+
response_type: "code",
|
|
86
|
+
client_id: clientId,
|
|
87
|
+
redirect_uri: redirectUri,
|
|
88
|
+
scope: DEFAULT_SCOPES.join(" "),
|
|
89
|
+
state,
|
|
90
|
+
code_challenge: codeVerifier,
|
|
91
|
+
code_challenge_method: "plain"
|
|
92
|
+
}).toString()}`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Async auth URL generation with proper S256 PKCE challenge.
|
|
96
|
+
* Prefer this over getAuthUrl() when async is acceptable.
|
|
97
|
+
*/
|
|
98
|
+
async getAuthUrlAsync(state, credData) {
|
|
99
|
+
const clientId = credData?.clientId;
|
|
100
|
+
if (!clientId) throw new SocialError("twitter", "Client ID is required", { statusCode: 400 });
|
|
101
|
+
const redirectUri = credData?.redirectUri || this.config.redirectUri || `http://localhost:${this.config.port || 8060}/api/oauth/twitter/callback`;
|
|
102
|
+
const codeVerifier = generateCodeVerifier();
|
|
103
|
+
pkceVerifiers.set(state, codeVerifier);
|
|
104
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
105
|
+
return `${AUTH_URL}?${new URLSearchParams({
|
|
106
|
+
response_type: "code",
|
|
107
|
+
client_id: clientId,
|
|
108
|
+
redirect_uri: redirectUri,
|
|
109
|
+
scope: DEFAULT_SCOPES.join(" "),
|
|
110
|
+
state,
|
|
111
|
+
code_challenge: codeChallenge,
|
|
112
|
+
code_challenge_method: "S256"
|
|
113
|
+
}).toString()}`;
|
|
114
|
+
}
|
|
115
|
+
async exchangeCode(code, credData, state) {
|
|
116
|
+
const clientId = credData?.clientId;
|
|
117
|
+
const clientSecret = credData?.clientSecret;
|
|
118
|
+
if (!clientId) throw new SocialError("twitter", "Client ID is required for token exchange", { statusCode: 400 });
|
|
119
|
+
const redirectUri = credData?.redirectUri || this.config.redirectUri || `http://localhost:${this.config.port || 8060}/api/oauth/twitter/callback`;
|
|
120
|
+
const codeVerifier = state ? pkceVerifiers.take(state) : void 0;
|
|
121
|
+
const body = new URLSearchParams({
|
|
122
|
+
grant_type: "authorization_code",
|
|
123
|
+
code,
|
|
124
|
+
redirect_uri: redirectUri,
|
|
125
|
+
client_id: clientId,
|
|
126
|
+
...codeVerifier ? { code_verifier: codeVerifier } : {}
|
|
127
|
+
});
|
|
128
|
+
const headers = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
129
|
+
if (clientSecret) headers["Authorization"] = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
|
|
130
|
+
const response = await fetch(TOKEN_URL, {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers,
|
|
133
|
+
body: body.toString()
|
|
134
|
+
});
|
|
135
|
+
const data = await response.json();
|
|
136
|
+
if (!response.ok) throw new SocialError("twitter", `Token exchange failed: ${data.error_description || data.error || response.statusText}`, {
|
|
137
|
+
statusCode: response.status,
|
|
138
|
+
errorCode: data.error,
|
|
139
|
+
hint: "Ensure your Client ID, Client Secret, and redirect URI are correct.",
|
|
140
|
+
originalError: new Error(JSON.stringify(data))
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
access_token: data.access_token,
|
|
144
|
+
refresh_token: data.refresh_token,
|
|
145
|
+
expires_in: data.expires_in,
|
|
146
|
+
token_type: data.token_type,
|
|
147
|
+
scope: data.scope
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async refreshToken(refreshToken, credData) {
|
|
151
|
+
const clientId = credData?.clientId;
|
|
152
|
+
const clientSecret = credData?.clientSecret;
|
|
153
|
+
if (!clientId) throw new SocialError("twitter", "Client ID is required for token refresh", { statusCode: 400 });
|
|
154
|
+
const body = new URLSearchParams({
|
|
155
|
+
grant_type: "refresh_token",
|
|
156
|
+
refresh_token: refreshToken,
|
|
157
|
+
client_id: clientId
|
|
158
|
+
});
|
|
159
|
+
const headers = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
160
|
+
if (clientSecret) headers["Authorization"] = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
|
|
161
|
+
const response = await fetch(TOKEN_URL, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers,
|
|
164
|
+
body: body.toString()
|
|
165
|
+
});
|
|
166
|
+
const data = await response.json();
|
|
167
|
+
if (!response.ok) throw new SocialError("twitter", `Token refresh failed: ${data.error_description || data.error || response.statusText}`, {
|
|
168
|
+
statusCode: response.status,
|
|
169
|
+
errorCode: data.error,
|
|
170
|
+
hint: "The refresh token may have been revoked. Re-authenticate."
|
|
171
|
+
});
|
|
172
|
+
return {
|
|
173
|
+
access_token: data.access_token,
|
|
174
|
+
refresh_token: data.refresh_token,
|
|
175
|
+
expires_in: data.expires_in,
|
|
176
|
+
token_type: data.token_type,
|
|
177
|
+
scope: data.scope
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
async revokeToken(accessToken, credData) {
|
|
181
|
+
const clientId = credData?.clientId;
|
|
182
|
+
if (!clientId) return;
|
|
183
|
+
const body = new URLSearchParams({
|
|
184
|
+
token: accessToken,
|
|
185
|
+
token_type_hint: "access_token",
|
|
186
|
+
client_id: clientId
|
|
187
|
+
});
|
|
188
|
+
await fetch(REVOKE_URL, {
|
|
189
|
+
method: "POST",
|
|
190
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
191
|
+
body: body.toString()
|
|
192
|
+
}).catch(() => {});
|
|
193
|
+
}
|
|
194
|
+
async getAccountInfo(accessToken) {
|
|
195
|
+
const user = (await this._api("GET", "/users/me?user.fields=id,name,username,description,profile_image_url,public_metrics,verified,verified_type,created_at,location,url", accessToken)).data;
|
|
196
|
+
return {
|
|
197
|
+
id: user.id,
|
|
198
|
+
name: user.name,
|
|
199
|
+
username: user.username,
|
|
200
|
+
profileImage: user.profile_image_url ?? null,
|
|
201
|
+
description: user.description,
|
|
202
|
+
location: user.location,
|
|
203
|
+
url: user.url,
|
|
204
|
+
verified: user.verified,
|
|
205
|
+
verifiedType: user.verified_type,
|
|
206
|
+
followersCount: user.public_metrics?.followers_count,
|
|
207
|
+
followingCount: user.public_metrics?.following_count,
|
|
208
|
+
tweetCount: user.public_metrics?.tweet_count,
|
|
209
|
+
listedCount: user.public_metrics?.listed_count,
|
|
210
|
+
createdAt: user.created_at
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
async testCredential(credentialData) {
|
|
214
|
+
try {
|
|
215
|
+
const tokenData = typeof credentialData.oauthTokenData === "string" ? JSON.parse(credentialData.oauthTokenData) : credentialData.oauthTokenData;
|
|
216
|
+
if (!tokenData?.access_token) return {
|
|
217
|
+
status: "Error",
|
|
218
|
+
message: "No access token found. Complete OAuth authorization first."
|
|
219
|
+
};
|
|
220
|
+
const info = await this.getAccountInfo(tokenData.access_token);
|
|
221
|
+
return {
|
|
222
|
+
status: "OK",
|
|
223
|
+
message: `Connected as @${info.username} (${info.name})`,
|
|
224
|
+
data: {
|
|
225
|
+
id: info.id,
|
|
226
|
+
username: info.username,
|
|
227
|
+
name: info.name
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
} catch (err) {
|
|
231
|
+
return {
|
|
232
|
+
status: "Error",
|
|
233
|
+
message: err instanceof Error ? err.message : String(err)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Create a tweet (or reply / quote tweet).
|
|
239
|
+
*/
|
|
240
|
+
async createTweet(accessToken, params) {
|
|
241
|
+
const body = { text: params.text };
|
|
242
|
+
if (params.replyTo) body.reply = { in_reply_to_tweet_id: params.replyTo };
|
|
243
|
+
if (params.quoteTweetId) body.quote_tweet_id = params.quoteTweetId;
|
|
244
|
+
if (params.mediaIds?.length) body.media = { media_ids: params.mediaIds };
|
|
245
|
+
if (params.poll) body.poll = {
|
|
246
|
+
options: params.poll.options,
|
|
247
|
+
duration_minutes: params.poll.durationMinutes
|
|
248
|
+
};
|
|
249
|
+
if (params.replySettings) body.reply_settings = params.replySettings;
|
|
250
|
+
const result = await this._api("POST", "/tweets", accessToken, body);
|
|
251
|
+
return {
|
|
252
|
+
id: result.data.id,
|
|
253
|
+
text: result.data.text,
|
|
254
|
+
editHistoryTweetIds: result.data.edit_history_tweet_ids
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Delete a tweet by ID.
|
|
259
|
+
*/
|
|
260
|
+
async deleteTweet(accessToken, tweetId) {
|
|
261
|
+
return { deleted: (await this._api("DELETE", `/tweets/${tweetId}`, accessToken)).data?.deleted ?? true };
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Get a single tweet by ID.
|
|
265
|
+
*/
|
|
266
|
+
async getTweet(accessToken, tweetId, fields) {
|
|
267
|
+
const tweetFields = fields?.join(",") || "id,text,author_id,created_at,public_metrics,entities,conversation_id,reply_settings,source,lang";
|
|
268
|
+
const data = await this._api("GET", `/tweets/${tweetId}?tweet.fields=${tweetFields}`, accessToken);
|
|
269
|
+
if (!data.data) throw new SocialError("twitter", `Tweet ${tweetId} not found`, {
|
|
270
|
+
statusCode: 404,
|
|
271
|
+
errorCode: "TWEET_NOT_FOUND",
|
|
272
|
+
hint: TWITTER_ERROR_HINTS["TWEET_NOT_FOUND"]
|
|
273
|
+
});
|
|
274
|
+
return this._parseTweet(data.data);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Search recent tweets (last 7 days on Basic tier).
|
|
278
|
+
*/
|
|
279
|
+
async searchTweets(accessToken, params) {
|
|
280
|
+
const qs = new URLSearchParams({ query: params.query });
|
|
281
|
+
if (params.maxResults) qs.set("max_results", String(params.maxResults));
|
|
282
|
+
if (params.nextToken) qs.set("next_token", params.nextToken);
|
|
283
|
+
if (params.startTime) qs.set("start_time", params.startTime);
|
|
284
|
+
if (params.endTime) qs.set("end_time", params.endTime);
|
|
285
|
+
if (params.sortOrder) qs.set("sort_order", params.sortOrder);
|
|
286
|
+
if (params.tweetFields?.length) qs.set("tweet.fields", params.tweetFields.join(","));
|
|
287
|
+
if (params.userFields?.length) qs.set("user.fields", params.userFields.join(","));
|
|
288
|
+
if (params.expansions?.length) qs.set("expansions", params.expansions.join(","));
|
|
289
|
+
if (!params.tweetFields?.length) qs.set("tweet.fields", "id,text,author_id,created_at,public_metrics,entities,source,lang");
|
|
290
|
+
const result = await this._api("GET", `/tweets/search/recent?${qs.toString()}`, accessToken);
|
|
291
|
+
return {
|
|
292
|
+
tweets: (result.data || []).map((t) => this._parseTweet(t)),
|
|
293
|
+
meta: {
|
|
294
|
+
newestId: result.meta?.newest_id,
|
|
295
|
+
oldestId: result.meta?.oldest_id,
|
|
296
|
+
resultCount: result.meta?.result_count || 0,
|
|
297
|
+
nextToken: result.meta?.next_token
|
|
298
|
+
},
|
|
299
|
+
includes: result.includes
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Like a tweet.
|
|
304
|
+
*/
|
|
305
|
+
async likeTweet(accessToken, tweetId) {
|
|
306
|
+
const me = await this._getMyId(accessToken);
|
|
307
|
+
return { liked: (await this._api("POST", `/users/${me}/likes`, accessToken, { tweet_id: tweetId })).data?.liked ?? true };
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Unlike a tweet.
|
|
311
|
+
*/
|
|
312
|
+
async unlikeTweet(accessToken, tweetId) {
|
|
313
|
+
const me = await this._getMyId(accessToken);
|
|
314
|
+
return { liked: (await this._api("DELETE", `/users/${me}/likes/${tweetId}`, accessToken)).data?.liked ?? false };
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Retweet a tweet.
|
|
318
|
+
*/
|
|
319
|
+
async retweet(accessToken, tweetId) {
|
|
320
|
+
const me = await this._getMyId(accessToken);
|
|
321
|
+
return { retweeted: (await this._api("POST", `/users/${me}/retweets`, accessToken, { tweet_id: tweetId })).data?.retweeted ?? true };
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Remove a retweet.
|
|
325
|
+
*/
|
|
326
|
+
async unretweet(accessToken, tweetId) {
|
|
327
|
+
const me = await this._getMyId(accessToken);
|
|
328
|
+
return { retweeted: (await this._api("DELETE", `/users/${me}/retweets/${tweetId}`, accessToken)).data?.retweeted ?? false };
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Bookmark a tweet.
|
|
332
|
+
*/
|
|
333
|
+
async bookmarkTweet(accessToken, tweetId) {
|
|
334
|
+
const me = await this._getMyId(accessToken);
|
|
335
|
+
return { bookmarked: (await this._api("POST", `/users/${me}/bookmarks`, accessToken, { tweet_id: tweetId })).data?.bookmarked ?? true };
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Remove a bookmark.
|
|
339
|
+
*/
|
|
340
|
+
async removeBookmark(accessToken, tweetId) {
|
|
341
|
+
const me = await this._getMyId(accessToken);
|
|
342
|
+
return { bookmarked: (await this._api("DELETE", `/users/${me}/bookmarks/${tweetId}`, accessToken)).data?.bookmarked ?? false };
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Follow a user.
|
|
346
|
+
*/
|
|
347
|
+
async followUser(accessToken, targetUserId) {
|
|
348
|
+
const me = await this._getMyId(accessToken);
|
|
349
|
+
const result = await this._api("POST", `/users/${me}/following`, accessToken, { target_user_id: targetUserId });
|
|
350
|
+
return {
|
|
351
|
+
following: result.data?.following ?? true,
|
|
352
|
+
pendingFollow: result.data?.pending_follow ?? false
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Unfollow a user.
|
|
357
|
+
*/
|
|
358
|
+
async unfollowUser(accessToken, targetUserId) {
|
|
359
|
+
const me = await this._getMyId(accessToken);
|
|
360
|
+
return { following: (await this._api("DELETE", `/users/${me}/following/${targetUserId}`, accessToken)).data?.following ?? false };
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Get followers of a user.
|
|
364
|
+
*/
|
|
365
|
+
async getFollowers(accessToken, userId, opts = {}) {
|
|
366
|
+
const qs = new URLSearchParams();
|
|
367
|
+
if (opts.maxResults) qs.set("max_results", String(opts.maxResults));
|
|
368
|
+
if (opts.paginationToken) qs.set("pagination_token", opts.paginationToken);
|
|
369
|
+
qs.set("user.fields", (opts.userFields || [
|
|
370
|
+
"id",
|
|
371
|
+
"name",
|
|
372
|
+
"username",
|
|
373
|
+
"profile_image_url",
|
|
374
|
+
"public_metrics",
|
|
375
|
+
"verified"
|
|
376
|
+
]).join(","));
|
|
377
|
+
const result = await this._api("GET", `/users/${userId}/followers?${qs.toString()}`, accessToken);
|
|
378
|
+
return {
|
|
379
|
+
users: (result.data || []).map((u) => this._parseUser(u)),
|
|
380
|
+
nextToken: result.meta?.next_token
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Get users that a user is following.
|
|
385
|
+
*/
|
|
386
|
+
async getFollowing(accessToken, userId, opts = {}) {
|
|
387
|
+
const qs = new URLSearchParams();
|
|
388
|
+
if (opts.maxResults) qs.set("max_results", String(opts.maxResults));
|
|
389
|
+
if (opts.paginationToken) qs.set("pagination_token", opts.paginationToken);
|
|
390
|
+
qs.set("user.fields", (opts.userFields || [
|
|
391
|
+
"id",
|
|
392
|
+
"name",
|
|
393
|
+
"username",
|
|
394
|
+
"profile_image_url",
|
|
395
|
+
"public_metrics",
|
|
396
|
+
"verified"
|
|
397
|
+
]).join(","));
|
|
398
|
+
const result = await this._api("GET", `/users/${userId}/following?${qs.toString()}`, accessToken);
|
|
399
|
+
return {
|
|
400
|
+
users: (result.data || []).map((u) => this._parseUser(u)),
|
|
401
|
+
nextToken: result.meta?.next_token
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Get a user by username.
|
|
406
|
+
*/
|
|
407
|
+
async getUserByUsername(accessToken, username, fields) {
|
|
408
|
+
const clean = username.replace(/^@/, "");
|
|
409
|
+
const userFields = fields?.join(",") || "id,name,username,description,profile_image_url,public_metrics,verified,verified_type,created_at,location,url";
|
|
410
|
+
const data = await this._api("GET", `/users/by/username/${clean}?user.fields=${userFields}`, accessToken);
|
|
411
|
+
if (!data.data) throw new SocialError("twitter", `User @${clean} not found`, {
|
|
412
|
+
statusCode: 404,
|
|
413
|
+
errorCode: "USER_NOT_FOUND",
|
|
414
|
+
hint: TWITTER_ERROR_HINTS["USER_NOT_FOUND"]
|
|
415
|
+
});
|
|
416
|
+
return this._parseUser(data.data);
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get a user by ID.
|
|
420
|
+
*/
|
|
421
|
+
async getUserById(accessToken, userId, fields) {
|
|
422
|
+
const userFields = fields?.join(",") || "id,name,username,description,profile_image_url,public_metrics,verified,verified_type,created_at,location,url";
|
|
423
|
+
const data = await this._api("GET", `/users/${userId}?user.fields=${userFields}`, accessToken);
|
|
424
|
+
if (!data.data) throw new SocialError("twitter", `User ${userId} not found`, {
|
|
425
|
+
statusCode: 404,
|
|
426
|
+
errorCode: "USER_NOT_FOUND",
|
|
427
|
+
hint: TWITTER_ERROR_HINTS["USER_NOT_FOUND"]
|
|
428
|
+
});
|
|
429
|
+
return this._parseUser(data.data);
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Send a direct message to a user.
|
|
433
|
+
*/
|
|
434
|
+
async sendDirectMessage(accessToken, params) {
|
|
435
|
+
const body = { text: params.text };
|
|
436
|
+
if (params.mediaId) body.attachments = [{ media_id: params.mediaId }];
|
|
437
|
+
const result = await this._api("POST", `/dm_conversations/with/${params.participantId}/messages`, accessToken, body);
|
|
438
|
+
return {
|
|
439
|
+
dmConversationId: result.data?.dm_conversation_id ?? "",
|
|
440
|
+
dmEventId: result.data?.dm_event_id ?? ""
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Mute a user.
|
|
445
|
+
*/
|
|
446
|
+
async muteUser(accessToken, targetUserId) {
|
|
447
|
+
const me = await this._getMyId(accessToken);
|
|
448
|
+
return { muting: (await this._api("POST", `/users/${me}/muting`, accessToken, { target_user_id: targetUserId })).data?.muting ?? true };
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Unmute a user.
|
|
452
|
+
*/
|
|
453
|
+
async unmuteUser(accessToken, targetUserId) {
|
|
454
|
+
const me = await this._getMyId(accessToken);
|
|
455
|
+
return { muting: (await this._api("DELETE", `/users/${me}/muting/${targetUserId}`, accessToken)).data?.muting ?? false };
|
|
456
|
+
}
|
|
457
|
+
async uploadPhoto(params) {
|
|
458
|
+
const { tokens, caption, title } = params;
|
|
459
|
+
const text = caption || title || "";
|
|
460
|
+
if (!text) throw new SocialError("twitter", "Tweet text or caption is required", { statusCode: 400 });
|
|
461
|
+
const result = await this.createTweet(tokens.access_token, { text });
|
|
462
|
+
return {
|
|
463
|
+
platformPostId: result.id,
|
|
464
|
+
platformUrl: `https://x.com/i/status/${result.id}`,
|
|
465
|
+
status: "published",
|
|
466
|
+
uploadedAt: /* @__PURE__ */ new Date()
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
async deletePost(accessToken, tweetId) {
|
|
470
|
+
return this.deleteTweet(accessToken, tweetId);
|
|
471
|
+
}
|
|
472
|
+
async sendMessage(accessToken, participantId, text) {
|
|
473
|
+
return this.sendDirectMessage(accessToken, {
|
|
474
|
+
participantId,
|
|
475
|
+
text
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
getCredentialZodSchema() {
|
|
479
|
+
return TwitterCredentialsSchema;
|
|
480
|
+
}
|
|
481
|
+
getCredentialSchema() {
|
|
482
|
+
return [{
|
|
483
|
+
name: "clientId",
|
|
484
|
+
displayName: "Client ID",
|
|
485
|
+
type: "text",
|
|
486
|
+
required: true,
|
|
487
|
+
description: "OAuth 2.0 Client ID from the Twitter Developer Portal",
|
|
488
|
+
placeholder: "abc123..."
|
|
489
|
+
}, {
|
|
490
|
+
name: "clientSecret",
|
|
491
|
+
displayName: "Client Secret",
|
|
492
|
+
type: "password",
|
|
493
|
+
required: true,
|
|
494
|
+
description: "OAuth 2.0 Client Secret (for confidential clients)",
|
|
495
|
+
placeholder: "secret..."
|
|
496
|
+
}];
|
|
497
|
+
}
|
|
498
|
+
getMetadata() {
|
|
499
|
+
return {
|
|
500
|
+
name: this.name,
|
|
501
|
+
displayName: this.displayName,
|
|
502
|
+
authType: this.authType,
|
|
503
|
+
icon: "twitter",
|
|
504
|
+
brandColor: "#000000",
|
|
505
|
+
description: "Post tweets, reply, search, and manage your X/Twitter account",
|
|
506
|
+
scopes: DEFAULT_SCOPES,
|
|
507
|
+
scopeDescriptions: SCOPE_DESCRIPTIONS,
|
|
508
|
+
setupGuide: [
|
|
509
|
+
{
|
|
510
|
+
step: 1,
|
|
511
|
+
title: "Create a Developer App",
|
|
512
|
+
description: "Go to developer.x.com → Projects & Apps → Create a new app. Select \"Web App\" as the app type."
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
step: 2,
|
|
516
|
+
title: "Set up OAuth 2.0",
|
|
517
|
+
description: "Under \"User authentication settings\", enable OAuth 2.0. Set the callback URL to your redirect URI. Select \"Web App\" type."
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
step: 3,
|
|
521
|
+
title: "Copy credentials",
|
|
522
|
+
description: "Copy the Client ID and Client Secret from the \"Keys and tokens\" tab."
|
|
523
|
+
},
|
|
524
|
+
{
|
|
525
|
+
step: 4,
|
|
526
|
+
title: "API Access Level",
|
|
527
|
+
description: "Ensure your app has at least Basic API access (Free tier is very limited). Apply for Pro if you need search and analytics."
|
|
528
|
+
}
|
|
529
|
+
],
|
|
530
|
+
supportsScheduling: false,
|
|
531
|
+
supportsEnvironment: false,
|
|
532
|
+
redirectUriPattern: "/api/oauth/twitter/callback",
|
|
533
|
+
credentialSchema: this.getCredentialSchema()
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
/** Cached user ID for the authenticated user (per-request) */
|
|
537
|
+
_myIdCache = /* @__PURE__ */ new Map();
|
|
538
|
+
async _getMyId(accessToken) {
|
|
539
|
+
const cached = this._myIdCache.get(accessToken);
|
|
540
|
+
if (cached) return cached;
|
|
541
|
+
const id = (await this._api("GET", "/users/me", accessToken)).data.id;
|
|
542
|
+
this._myIdCache.set(accessToken, id);
|
|
543
|
+
return id;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Core API request helper.
|
|
547
|
+
*/
|
|
548
|
+
async _api(method, endpoint, accessToken, body) {
|
|
549
|
+
const url = endpoint.startsWith("http") ? endpoint : `${API_BASE}${endpoint}`;
|
|
550
|
+
const result = await httpRequest("twitter", {
|
|
551
|
+
method: method.toUpperCase(),
|
|
552
|
+
url,
|
|
553
|
+
bearer: accessToken,
|
|
554
|
+
headers: { "User-Agent": "web:com.classytic.social:v0.1.0" },
|
|
555
|
+
json: body,
|
|
556
|
+
timeout: 3e4,
|
|
557
|
+
retry: { attempts: 2 },
|
|
558
|
+
parseError: (raw, status) => {
|
|
559
|
+
if (raw && typeof raw === "object") {
|
|
560
|
+
const r = raw;
|
|
561
|
+
const errors = r.errors;
|
|
562
|
+
const detail = errors?.[0]?.detail || r.detail || r.error_description || r.error;
|
|
563
|
+
const errorTitle = errors?.[0]?.title || r.title || "API_ERROR";
|
|
564
|
+
const hintKey = errorTitle.toUpperCase().replace(/ /g, "_");
|
|
565
|
+
return {
|
|
566
|
+
message: `Twitter API error (${status}): ${detail || "Unknown"}`,
|
|
567
|
+
errorCode: errorTitle,
|
|
568
|
+
hint: TWITTER_ERROR_HINTS[hintKey] || TWITTER_ERROR_HINTS[String(status)] || null
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
if (result.status === 204) return { data: { deleted: true } };
|
|
575
|
+
return result.data;
|
|
576
|
+
}
|
|
577
|
+
_parseTweet(raw) {
|
|
578
|
+
return {
|
|
579
|
+
id: raw.id,
|
|
580
|
+
text: raw.text,
|
|
581
|
+
authorId: raw.author_id,
|
|
582
|
+
createdAt: raw.created_at,
|
|
583
|
+
conversationId: raw.conversation_id,
|
|
584
|
+
inReplyToUserId: raw.in_reply_to_user_id,
|
|
585
|
+
publicMetrics: raw.public_metrics ? {
|
|
586
|
+
retweetCount: raw.public_metrics.retweet_count ?? 0,
|
|
587
|
+
replyCount: raw.public_metrics.reply_count ?? 0,
|
|
588
|
+
likeCount: raw.public_metrics.like_count ?? 0,
|
|
589
|
+
quoteCount: raw.public_metrics.quote_count ?? 0,
|
|
590
|
+
bookmarkCount: raw.public_metrics.bookmark_count,
|
|
591
|
+
impressionCount: raw.public_metrics.impression_count
|
|
592
|
+
} : void 0,
|
|
593
|
+
entities: raw.entities,
|
|
594
|
+
attachments: raw.attachments,
|
|
595
|
+
source: raw.source,
|
|
596
|
+
lang: raw.lang,
|
|
597
|
+
replySettings: raw.reply_settings,
|
|
598
|
+
editHistoryTweetIds: raw.edit_history_tweet_ids
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
_parseUser(raw) {
|
|
602
|
+
return {
|
|
603
|
+
id: raw.id,
|
|
604
|
+
name: raw.name,
|
|
605
|
+
username: raw.username,
|
|
606
|
+
createdAt: raw.created_at,
|
|
607
|
+
description: raw.description,
|
|
608
|
+
location: raw.location,
|
|
609
|
+
profileImageUrl: raw.profile_image_url,
|
|
610
|
+
protected: raw.protected,
|
|
611
|
+
publicMetrics: raw.public_metrics ? {
|
|
612
|
+
followersCount: raw.public_metrics.followers_count ?? 0,
|
|
613
|
+
followingCount: raw.public_metrics.following_count ?? 0,
|
|
614
|
+
tweetCount: raw.public_metrics.tweet_count ?? 0,
|
|
615
|
+
listedCount: raw.public_metrics.listed_count ?? 0,
|
|
616
|
+
likeCount: raw.public_metrics.like_count
|
|
617
|
+
} : void 0,
|
|
618
|
+
url: raw.url,
|
|
619
|
+
verified: raw.verified,
|
|
620
|
+
verifiedType: raw.verified_type,
|
|
621
|
+
pinnedTweetId: raw.pinned_tweet_id
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
//#endregion
|
|
627
|
+
export { TwitterProvider };
|
|
628
|
+
//# sourceMappingURL=twitter.mjs.map
|