@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.
Files changed (121) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/LICENSE +21 -0
  3. package/README.md +368 -0
  4. package/dist/base-Bw7e52V8.mjs +246 -0
  5. package/dist/base-Bw7e52V8.mjs.map +1 -0
  6. package/dist/base-DBtKFiSX.d.mts +226 -0
  7. package/dist/base-DBtKFiSX.d.mts.map +1 -0
  8. package/dist/chunk-DQk6qfdC.mjs +18 -0
  9. package/dist/client/index.d.mts +44 -0
  10. package/dist/client/index.d.mts.map +1 -0
  11. package/dist/client/index.mjs +154 -0
  12. package/dist/client/index.mjs.map +1 -0
  13. package/dist/common/index.d.mts +3 -0
  14. package/dist/common/index.mjs +7 -0
  15. package/dist/contracts-Cdwa4zlg.d.mts +121 -0
  16. package/dist/contracts-Cdwa4zlg.d.mts.map +1 -0
  17. package/dist/contracts-lCa069IK.mjs +221 -0
  18. package/dist/contracts-lCa069IK.mjs.map +1 -0
  19. package/dist/env-Bl0cwwjC.mjs +955 -0
  20. package/dist/env-Bl0cwwjC.mjs.map +1 -0
  21. package/dist/env-DxOZHf0p.d.mts +394 -0
  22. package/dist/env-DxOZHf0p.d.mts.map +1 -0
  23. package/dist/errors-Cm6LeKf7.mjs +32 -0
  24. package/dist/errors-Cm6LeKf7.mjs.map +1 -0
  25. package/dist/facebook-l_4CghaA.mjs +95 -0
  26. package/dist/facebook-l_4CghaA.mjs.map +1 -0
  27. package/dist/http-DpcLSR1M.mjs +197 -0
  28. package/dist/http-DpcLSR1M.mjs.map +1 -0
  29. package/dist/index.d.mts +42 -0
  30. package/dist/index.d.mts.map +1 -0
  31. package/dist/index.mjs +71 -0
  32. package/dist/index.mjs.map +1 -0
  33. package/dist/instagram-BGaeUFU2.mjs +90 -0
  34. package/dist/instagram-BGaeUFU2.mjs.map +1 -0
  35. package/dist/linkedin-70whtVKa.mjs +101 -0
  36. package/dist/linkedin-70whtVKa.mjs.map +1 -0
  37. package/dist/meta-D3vcJU1c.mjs +126 -0
  38. package/dist/meta-D3vcJU1c.mjs.map +1 -0
  39. package/dist/pkce-jq5II68b.mjs +72 -0
  40. package/dist/pkce-jq5II68b.mjs.map +1 -0
  41. package/dist/polling-DZ1apXtA.mjs +25 -0
  42. package/dist/polling-DZ1apXtA.mjs.map +1 -0
  43. package/dist/providers/facebook.d.mts +135 -0
  44. package/dist/providers/facebook.d.mts.map +1 -0
  45. package/dist/providers/facebook.mjs +450 -0
  46. package/dist/providers/facebook.mjs.map +1 -0
  47. package/dist/providers/instagram.d.mts +122 -0
  48. package/dist/providers/instagram.d.mts.map +1 -0
  49. package/dist/providers/instagram.mjs +496 -0
  50. package/dist/providers/instagram.mjs.map +1 -0
  51. package/dist/providers/linkedin.d.mts +145 -0
  52. package/dist/providers/linkedin.d.mts.map +1 -0
  53. package/dist/providers/linkedin.mjs +574 -0
  54. package/dist/providers/linkedin.mjs.map +1 -0
  55. package/dist/providers/reddit.d.mts +102 -0
  56. package/dist/providers/reddit.d.mts.map +1 -0
  57. package/dist/providers/reddit.mjs +657 -0
  58. package/dist/providers/reddit.mjs.map +1 -0
  59. package/dist/providers/telegram.d.mts +139 -0
  60. package/dist/providers/telegram.d.mts.map +1 -0
  61. package/dist/providers/telegram.mjs +517 -0
  62. package/dist/providers/telegram.mjs.map +1 -0
  63. package/dist/providers/tiktok.d.mts +116 -0
  64. package/dist/providers/tiktok.d.mts.map +1 -0
  65. package/dist/providers/tiktok.mjs +676 -0
  66. package/dist/providers/tiktok.mjs.map +1 -0
  67. package/dist/providers/twitter.d.mts +150 -0
  68. package/dist/providers/twitter.d.mts.map +1 -0
  69. package/dist/providers/twitter.mjs +628 -0
  70. package/dist/providers/twitter.mjs.map +1 -0
  71. package/dist/providers/whatsapp.d.mts +79 -0
  72. package/dist/providers/whatsapp.d.mts.map +1 -0
  73. package/dist/providers/whatsapp.mjs +376 -0
  74. package/dist/providers/whatsapp.mjs.map +1 -0
  75. package/dist/providers/youtube.d.mts +153 -0
  76. package/dist/providers/youtube.d.mts.map +1 -0
  77. package/dist/providers/youtube.mjs +902 -0
  78. package/dist/providers/youtube.mjs.map +1 -0
  79. package/dist/reddit-B10kS4Se.mjs +126 -0
  80. package/dist/reddit-B10kS4Se.mjs.map +1 -0
  81. package/dist/schemas/index.d.mts +819 -0
  82. package/dist/schemas/index.d.mts.map +1 -0
  83. package/dist/schemas/index.mjs +31 -0
  84. package/dist/schemas/index.mjs.map +1 -0
  85. package/dist/security-BXhfebWm.d.mts +338 -0
  86. package/dist/security-BXhfebWm.d.mts.map +1 -0
  87. package/dist/shared-Fvc6xQku.mjs +100 -0
  88. package/dist/shared-Fvc6xQku.mjs.map +1 -0
  89. package/dist/telegram-FaUHpZgB.mjs +107 -0
  90. package/dist/telegram-FaUHpZgB.mjs.map +1 -0
  91. package/dist/tiktok-B_bMk4G-.mjs +94 -0
  92. package/dist/tiktok-B_bMk4G-.mjs.map +1 -0
  93. package/dist/twitter-BC22zfuc.mjs +98 -0
  94. package/dist/twitter-BC22zfuc.mjs.map +1 -0
  95. package/dist/types-BFE4psYI.d.mts +102 -0
  96. package/dist/types-BFE4psYI.d.mts.map +1 -0
  97. package/dist/types-Bv27tcT0.d.mts +230 -0
  98. package/dist/types-Bv27tcT0.d.mts.map +1 -0
  99. package/dist/types-BwkKyqpi.d.mts +253 -0
  100. package/dist/types-BwkKyqpi.d.mts.map +1 -0
  101. package/dist/types-CJrHMDV9.mjs +27 -0
  102. package/dist/types-CJrHMDV9.mjs.map +1 -0
  103. package/dist/types-ClbVc2rc.d.mts +117 -0
  104. package/dist/types-ClbVc2rc.d.mts.map +1 -0
  105. package/dist/types-D91N16Ym.d.mts +242 -0
  106. package/dist/types-D91N16Ym.d.mts.map +1 -0
  107. package/dist/types-DfLp_ibQ.d.mts +178 -0
  108. package/dist/types-DfLp_ibQ.d.mts.map +1 -0
  109. package/dist/types-DfjDgEoJ.d.mts +88 -0
  110. package/dist/types-DfjDgEoJ.d.mts.map +1 -0
  111. package/dist/types-Dp5Z9VBr.mjs +23 -0
  112. package/dist/types-Dp5Z9VBr.mjs.map +1 -0
  113. package/dist/types-hriBJTsU.d.mts +129 -0
  114. package/dist/types-hriBJTsU.d.mts.map +1 -0
  115. package/dist/types-rn6UuLL8.d.mts +184 -0
  116. package/dist/types-rn6UuLL8.d.mts.map +1 -0
  117. package/dist/whatsapp-CFp7ryR4.mjs +101 -0
  118. package/dist/whatsapp-CFp7ryR4.mjs.map +1 -0
  119. package/dist/youtube-Bs0fdY7H.mjs +98 -0
  120. package/dist/youtube-Bs0fdY7H.mjs.map +1 -0
  121. 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