@hasna/connectors 1.3.36 → 1.3.37
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/bin/index.js +167 -241
- package/bin/mcp.js +190 -267
- package/bin/serve.js +264 -341
- package/connectors/bluesky/README.md +34 -0
- package/connectors/bluesky/package.json +41 -0
- package/connectors/bluesky/src/api/client.test.ts +122 -0
- package/connectors/bluesky/src/api/client.ts +194 -0
- package/connectors/bluesky/src/api/index.ts +108 -0
- package/connectors/bluesky/src/index.ts +17 -0
- package/connectors/bluesky/src/types/index.ts +46 -0
- package/connectors/bluesky/tsconfig.json +16 -0
- package/connectors/x/src/api/media.ts +4 -3
- package/dashboard/dist/assets/index-ClBXkNbL.css +1 -0
- package/dashboard/dist/assets/index-DvfmyAO4.js +284 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/.types/connectors/bluesky/src/api/client.d.ts +73 -0
- package/dist/.types/connectors/bluesky/src/api/index.d.ts +76 -0
- package/dist/.types/connectors/bluesky/src/index.d.ts +11 -0
- package/dist/.types/connectors/bluesky/src/types/index.d.ts +33 -0
- package/dist/.types/connectors/x/src/api/client.d.ts +96 -0
- package/dist/.types/connectors/x/src/api/index.d.ts +80 -0
- package/dist/.types/connectors/x/src/api/media.d.ts +54 -0
- package/dist/.types/connectors/x/src/api/oauth.d.ts +82 -0
- package/dist/.types/connectors/x/src/api/oauth1.d.ts +68 -0
- package/dist/.types/connectors/x/src/api/tweets.d.ts +175 -0
- package/dist/.types/connectors/x/src/api/users.d.ts +127 -0
- package/dist/.types/connectors/x/src/cli/index.d.ts +2 -0
- package/dist/.types/connectors/x/src/index.d.ts +6 -0
- package/dist/.types/connectors/x/src/types/index.d.ts +156 -0
- package/dist/.types/connectors/x/src/utils/config.d.ts +99 -0
- package/dist/.types/connectors/x/src/utils/output.d.ts +8 -0
- package/dist/.types/src/social/bluesky.d.ts +65 -0
- package/dist/.types/src/social/errors.d.ts +9 -0
- package/dist/.types/src/social/index.d.ts +16 -0
- package/dist/.types/src/social/mastodon.d.ts +32 -0
- package/dist/.types/src/social/types.d.ts +73 -0
- package/dist/.types/src/social/util.d.ts +5 -0
- package/dist/.types/src/social/x.d.ts +82 -0
- package/dist/cli/components/App.d.ts +1 -2
- package/dist/cli/components/CategorySelect.d.ts +1 -2
- package/dist/cli/components/ConnectorSelect.d.ts +1 -2
- package/dist/cli/components/Header.d.ts +1 -2
- package/dist/cli/components/InstallProgress.d.ts +1 -2
- package/dist/cli/components/SearchView.d.ts +1 -2
- package/dist/index.js +122 -109
- package/dist/social/index.js +1531 -0
- package/package.json +7 -3
- package/dashboard/dist/assets/index-DJjxFlTl.css +0 -1
- package/dashboard/dist/assets/index-DNPZ58_i.js +0 -284
|
@@ -0,0 +1,1531 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// src/social/errors.ts
|
|
5
|
+
class ConnectorOperationNotSupported extends Error {
|
|
6
|
+
connector;
|
|
7
|
+
operation;
|
|
8
|
+
constructor(connector, operation) {
|
|
9
|
+
super(`Connector "${connector}" does not support operation "${operation}"`);
|
|
10
|
+
this.name = "ConnectorOperationNotSupported";
|
|
11
|
+
this.connector = connector;
|
|
12
|
+
this.operation = operation;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// connectors/x/src/types/index.ts
|
|
17
|
+
class XApiError extends Error {
|
|
18
|
+
statusCode;
|
|
19
|
+
errors;
|
|
20
|
+
constructor(message, statusCode, errors) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "XApiError";
|
|
23
|
+
this.statusCode = statusCode;
|
|
24
|
+
this.errors = errors;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// connectors/x/src/api/oauth.ts
|
|
29
|
+
var TOKEN_URL = "https://api.twitter.com/2/oauth2/token";
|
|
30
|
+
async function tokenRequest(url, body, config) {
|
|
31
|
+
body.append("client_id", config.clientId);
|
|
32
|
+
if (config.clientSecret) {
|
|
33
|
+
body.append("client_secret", config.clientSecret);
|
|
34
|
+
}
|
|
35
|
+
return fetch(url, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
38
|
+
body: body.toString()
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function refreshAccessToken(config, refreshToken) {
|
|
42
|
+
const body = new URLSearchParams({
|
|
43
|
+
grant_type: "refresh_token",
|
|
44
|
+
refresh_token: refreshToken
|
|
45
|
+
});
|
|
46
|
+
const response = await tokenRequest(TOKEN_URL, body, config);
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
const errorText = await response.text();
|
|
49
|
+
throw new Error(`Token refresh failed: ${errorText}`);
|
|
50
|
+
}
|
|
51
|
+
const data = await response.json();
|
|
52
|
+
return {
|
|
53
|
+
accessToken: data.access_token,
|
|
54
|
+
refreshToken: data.refresh_token || refreshToken,
|
|
55
|
+
expiresIn: data.expires_in,
|
|
56
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
57
|
+
scope: data.scope,
|
|
58
|
+
tokenType: data.token_type
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// connectors/x/src/api/oauth1.ts
|
|
63
|
+
import { createHmac, randomBytes } from "crypto";
|
|
64
|
+
function generateNonce() {
|
|
65
|
+
return randomBytes(16).toString("hex");
|
|
66
|
+
}
|
|
67
|
+
function generateTimestamp() {
|
|
68
|
+
return Math.floor(Date.now() / 1000).toString();
|
|
69
|
+
}
|
|
70
|
+
function percentEncode(str) {
|
|
71
|
+
return encodeURIComponent(str).replace(/!/g, "%21").replace(/\*/g, "%2A").replace(/'/g, "%27").replace(/\(/g, "%28").replace(/\)/g, "%29");
|
|
72
|
+
}
|
|
73
|
+
function createSignatureBaseString(method, url, params) {
|
|
74
|
+
const sortedParams = Object.keys(params).sort().map((key) => `${percentEncode(key)}=${percentEncode(params[key])}`).join("&");
|
|
75
|
+
const baseUrl = url.split("?")[0];
|
|
76
|
+
return `${method.toUpperCase()}&${percentEncode(baseUrl)}&${percentEncode(sortedParams)}`;
|
|
77
|
+
}
|
|
78
|
+
function createSignature(baseString, consumerSecret, tokenSecret = "") {
|
|
79
|
+
const signingKey = `${percentEncode(consumerSecret)}&${percentEncode(tokenSecret)}`;
|
|
80
|
+
return createHmac("sha1", signingKey).update(baseString).digest("base64");
|
|
81
|
+
}
|
|
82
|
+
function buildOAuth1Header(config, method, url, additionalParams = {}) {
|
|
83
|
+
const oauthParams = {
|
|
84
|
+
oauth_consumer_key: config.consumerKey,
|
|
85
|
+
oauth_nonce: generateNonce(),
|
|
86
|
+
oauth_signature_method: "HMAC-SHA1",
|
|
87
|
+
oauth_timestamp: generateTimestamp(),
|
|
88
|
+
oauth_version: "1.0"
|
|
89
|
+
};
|
|
90
|
+
if (config.accessToken) {
|
|
91
|
+
oauthParams["oauth_token"] = config.accessToken;
|
|
92
|
+
}
|
|
93
|
+
const allParams = { ...oauthParams, ...additionalParams };
|
|
94
|
+
const baseString = createSignatureBaseString(method, url, allParams);
|
|
95
|
+
const signature = createSignature(baseString, config.consumerSecret, config.accessTokenSecret);
|
|
96
|
+
oauthParams["oauth_signature"] = signature;
|
|
97
|
+
const headerParams = Object.keys(oauthParams).sort().map((key) => `${percentEncode(key)}="${percentEncode(oauthParams[key])}"`).join(", ");
|
|
98
|
+
return `OAuth ${headerParams}`;
|
|
99
|
+
}
|
|
100
|
+
async function oauth1Request(config, method, url, body, additionalHeaders) {
|
|
101
|
+
let bodyParams = {};
|
|
102
|
+
let requestBody;
|
|
103
|
+
const headers = { ...additionalHeaders };
|
|
104
|
+
if (body) {
|
|
105
|
+
if (body instanceof FormData) {
|
|
106
|
+
requestBody = body;
|
|
107
|
+
} else if (typeof body === "string") {
|
|
108
|
+
requestBody = body;
|
|
109
|
+
} else {
|
|
110
|
+
bodyParams = body;
|
|
111
|
+
requestBody = new URLSearchParams(body).toString();
|
|
112
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const authHeader = buildOAuth1Header(config, method, url, bodyParams);
|
|
116
|
+
headers["Authorization"] = authHeader;
|
|
117
|
+
const response = await fetch(url, {
|
|
118
|
+
method,
|
|
119
|
+
headers,
|
|
120
|
+
body: requestBody
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) {
|
|
123
|
+
const errorText = await response.text();
|
|
124
|
+
throw new Error(`OAuth 1.0a request failed: ${response.status} - ${errorText}`);
|
|
125
|
+
}
|
|
126
|
+
const contentType = response.headers.get("content-type") || "";
|
|
127
|
+
if (contentType.includes("application/json")) {
|
|
128
|
+
return await response.json();
|
|
129
|
+
}
|
|
130
|
+
return await response.text();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class OAuth1Client {
|
|
134
|
+
config;
|
|
135
|
+
constructor(config) {
|
|
136
|
+
if (!config.consumerKey || !config.consumerSecret) {
|
|
137
|
+
throw new Error("Consumer key and secret are required");
|
|
138
|
+
}
|
|
139
|
+
this.config = config;
|
|
140
|
+
}
|
|
141
|
+
hasUserTokens() {
|
|
142
|
+
return !!(this.config.accessToken && this.config.accessTokenSecret);
|
|
143
|
+
}
|
|
144
|
+
setTokens(oauthToken, oauthTokenSecret) {
|
|
145
|
+
this.config.accessToken = oauthToken;
|
|
146
|
+
this.config.accessTokenSecret = oauthTokenSecret;
|
|
147
|
+
}
|
|
148
|
+
async get(url, params) {
|
|
149
|
+
const urlWithParams = params ? `${url}?${new URLSearchParams(params).toString()}` : url;
|
|
150
|
+
return oauth1Request(this.config, "GET", urlWithParams);
|
|
151
|
+
}
|
|
152
|
+
async post(url, body, headers) {
|
|
153
|
+
return oauth1Request(this.config, "POST", url, body, headers);
|
|
154
|
+
}
|
|
155
|
+
buildAuthHeader(method, url, params) {
|
|
156
|
+
return buildOAuth1Header(this.config, method, url, params);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// connectors/x/src/api/client.ts
|
|
161
|
+
var DEFAULT_BASE_URL = "https://api.twitter.com";
|
|
162
|
+
|
|
163
|
+
class XClient {
|
|
164
|
+
apiKey;
|
|
165
|
+
apiSecret;
|
|
166
|
+
bearerToken;
|
|
167
|
+
baseUrl;
|
|
168
|
+
accessToken;
|
|
169
|
+
refreshToken;
|
|
170
|
+
tokenExpiresAt;
|
|
171
|
+
clientId;
|
|
172
|
+
clientSecret;
|
|
173
|
+
oauth1Client;
|
|
174
|
+
onTokenRefresh;
|
|
175
|
+
constructor(config) {
|
|
176
|
+
if (!config.apiKey || !config.apiSecret) {
|
|
177
|
+
throw new Error("API key and API secret are required");
|
|
178
|
+
}
|
|
179
|
+
this.apiKey = config.apiKey;
|
|
180
|
+
this.apiSecret = config.apiSecret;
|
|
181
|
+
this.bearerToken = config.bearerToken;
|
|
182
|
+
this.baseUrl = config.baseUrl || DEFAULT_BASE_URL;
|
|
183
|
+
this.accessToken = config.accessToken;
|
|
184
|
+
this.refreshToken = config.refreshToken;
|
|
185
|
+
this.tokenExpiresAt = config.tokenExpiresAt;
|
|
186
|
+
this.clientId = config.clientId;
|
|
187
|
+
this.clientSecret = config.clientSecret;
|
|
188
|
+
if (config.oauth1AccessToken && config.oauth1AccessTokenSecret) {
|
|
189
|
+
this.oauth1Client = new OAuth1Client({
|
|
190
|
+
consumerKey: config.apiKey,
|
|
191
|
+
consumerSecret: config.apiSecret,
|
|
192
|
+
accessToken: config.oauth1AccessToken,
|
|
193
|
+
accessTokenSecret: config.oauth1AccessTokenSecret
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
setTokenRefreshCallback(callback) {
|
|
198
|
+
this.onTokenRefresh = callback;
|
|
199
|
+
}
|
|
200
|
+
async getAppBearerToken() {
|
|
201
|
+
if (this.bearerToken) {
|
|
202
|
+
return this.bearerToken;
|
|
203
|
+
}
|
|
204
|
+
const credentials = Buffer.from(`${this.apiKey}:${this.apiSecret}`).toString("base64");
|
|
205
|
+
const response = await fetch(`${this.baseUrl}/oauth2/token`, {
|
|
206
|
+
method: "POST",
|
|
207
|
+
headers: {
|
|
208
|
+
Authorization: `Basic ${credentials}`,
|
|
209
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
210
|
+
},
|
|
211
|
+
body: "grant_type=client_credentials"
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
const errorText = await response.text();
|
|
215
|
+
throw new XApiError(`Failed to get bearer token: ${errorText}`, response.status);
|
|
216
|
+
}
|
|
217
|
+
const data = await response.json();
|
|
218
|
+
this.bearerToken = data.access_token;
|
|
219
|
+
return this.bearerToken;
|
|
220
|
+
}
|
|
221
|
+
isTokenExpired() {
|
|
222
|
+
if (!this.tokenExpiresAt)
|
|
223
|
+
return true;
|
|
224
|
+
return Date.now() > this.tokenExpiresAt - 5 * 60 * 1000;
|
|
225
|
+
}
|
|
226
|
+
async refreshUserToken() {
|
|
227
|
+
if (!this.refreshToken || !this.clientId) {
|
|
228
|
+
throw new Error("Cannot refresh token: missing refresh token or client ID");
|
|
229
|
+
}
|
|
230
|
+
const oauth2Config = {
|
|
231
|
+
clientId: this.clientId,
|
|
232
|
+
clientSecret: this.clientSecret,
|
|
233
|
+
redirectUri: "http://localhost:3000/callback"
|
|
234
|
+
};
|
|
235
|
+
const tokens = await refreshAccessToken(oauth2Config, this.refreshToken);
|
|
236
|
+
this.accessToken = tokens.accessToken;
|
|
237
|
+
this.refreshToken = tokens.refreshToken || this.refreshToken;
|
|
238
|
+
this.tokenExpiresAt = tokens.expiresAt;
|
|
239
|
+
if (this.onTokenRefresh) {
|
|
240
|
+
this.onTokenRefresh({
|
|
241
|
+
accessToken: tokens.accessToken,
|
|
242
|
+
refreshToken: tokens.refreshToken,
|
|
243
|
+
expiresAt: tokens.expiresAt
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return this.accessToken;
|
|
247
|
+
}
|
|
248
|
+
async getAccessToken(preferUser = true) {
|
|
249
|
+
if (preferUser && this.accessToken) {
|
|
250
|
+
if (this.isTokenExpired() && this.refreshToken && this.clientId) {
|
|
251
|
+
const token2 = await this.refreshUserToken();
|
|
252
|
+
return { token: token2, isUserContext: true };
|
|
253
|
+
}
|
|
254
|
+
if (!this.isTokenExpired()) {
|
|
255
|
+
return { token: this.accessToken, isUserContext: true };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const token = await this.getAppBearerToken();
|
|
259
|
+
return { token, isUserContext: false };
|
|
260
|
+
}
|
|
261
|
+
getAuthMethod() {
|
|
262
|
+
if (this.accessToken && !this.isTokenExpired()) {
|
|
263
|
+
return "oauth2-user";
|
|
264
|
+
}
|
|
265
|
+
if (this.oauth1Client?.hasUserTokens()) {
|
|
266
|
+
return "oauth1-user";
|
|
267
|
+
}
|
|
268
|
+
return "app-only";
|
|
269
|
+
}
|
|
270
|
+
getAuthStatus() {
|
|
271
|
+
const method = this.getAuthMethod();
|
|
272
|
+
return {
|
|
273
|
+
method,
|
|
274
|
+
isAuthenticated: method !== "app-only",
|
|
275
|
+
expiresAt: this.tokenExpiresAt,
|
|
276
|
+
hasOAuth1: this.oauth1Client?.hasUserTokens()
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
hasUserContext() {
|
|
280
|
+
return !!(this.accessToken && !this.isTokenExpired());
|
|
281
|
+
}
|
|
282
|
+
hasOAuth1() {
|
|
283
|
+
return this.oauth1Client?.hasUserTokens() ?? false;
|
|
284
|
+
}
|
|
285
|
+
getOAuth1Client() {
|
|
286
|
+
return this.oauth1Client;
|
|
287
|
+
}
|
|
288
|
+
setUserTokens(tokens) {
|
|
289
|
+
this.accessToken = tokens.accessToken;
|
|
290
|
+
this.refreshToken = tokens.refreshToken;
|
|
291
|
+
this.tokenExpiresAt = tokens.expiresAt;
|
|
292
|
+
}
|
|
293
|
+
setOAuth1Tokens(oauthToken, oauthTokenSecret) {
|
|
294
|
+
this.oauth1Client = new OAuth1Client({
|
|
295
|
+
consumerKey: this.apiKey,
|
|
296
|
+
consumerSecret: this.apiSecret,
|
|
297
|
+
accessToken: oauthToken,
|
|
298
|
+
accessTokenSecret: oauthTokenSecret
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
buildUrl(path, params) {
|
|
302
|
+
const url = new URL(`${this.baseUrl}${path}`);
|
|
303
|
+
if (params) {
|
|
304
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
305
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
306
|
+
if (Array.isArray(value)) {
|
|
307
|
+
url.searchParams.append(key, value.join(","));
|
|
308
|
+
} else {
|
|
309
|
+
url.searchParams.append(key, String(value));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return url.toString();
|
|
315
|
+
}
|
|
316
|
+
async request(path, options = {}) {
|
|
317
|
+
const { method = "GET", params, body, headers = {}, authMethod } = options;
|
|
318
|
+
const preferUser = authMethod === "oauth2-user" || authMethod === "oauth1-user" || ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
319
|
+
const url = this.buildUrl(path, params);
|
|
320
|
+
const isWriteOp = ["POST", "PUT", "PATCH", "DELETE"].includes(method);
|
|
321
|
+
const useOAuth1 = authMethod === "oauth1-user" || isWriteOp && !this.accessToken && this.oauth1Client?.hasUserTokens();
|
|
322
|
+
let requestHeaders;
|
|
323
|
+
if (useOAuth1 && this.oauth1Client) {
|
|
324
|
+
const oauth1Header = this.oauth1Client.buildAuthHeader(method, url);
|
|
325
|
+
requestHeaders = {
|
|
326
|
+
Authorization: oauth1Header,
|
|
327
|
+
Accept: "application/json",
|
|
328
|
+
...headers
|
|
329
|
+
};
|
|
330
|
+
} else {
|
|
331
|
+
const { token } = await this.getAccessToken(preferUser);
|
|
332
|
+
requestHeaders = {
|
|
333
|
+
Authorization: `Bearer ${token}`,
|
|
334
|
+
Accept: "application/json",
|
|
335
|
+
...headers
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (body && ["POST", "PUT", "PATCH"].includes(method)) {
|
|
339
|
+
requestHeaders["Content-Type"] = "application/json";
|
|
340
|
+
}
|
|
341
|
+
const fetchOptions = {
|
|
342
|
+
method,
|
|
343
|
+
headers: requestHeaders
|
|
344
|
+
};
|
|
345
|
+
if (body && ["POST", "PUT", "PATCH"].includes(method)) {
|
|
346
|
+
fetchOptions.body = typeof body === "string" ? body : JSON.stringify(body);
|
|
347
|
+
}
|
|
348
|
+
const response = await fetch(url, fetchOptions);
|
|
349
|
+
if (response.status === 204) {
|
|
350
|
+
return {};
|
|
351
|
+
}
|
|
352
|
+
let data;
|
|
353
|
+
const contentType = response.headers.get("content-type") || "";
|
|
354
|
+
if (contentType.includes("application/json")) {
|
|
355
|
+
const text = await response.text();
|
|
356
|
+
if (text) {
|
|
357
|
+
try {
|
|
358
|
+
data = JSON.parse(text);
|
|
359
|
+
} catch {
|
|
360
|
+
data = text;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
data = await response.text();
|
|
365
|
+
}
|
|
366
|
+
if (!response.ok) {
|
|
367
|
+
const errorMessage = typeof data === "object" && data !== null ? JSON.stringify(data) : String(data || response.statusText);
|
|
368
|
+
throw new XApiError(errorMessage, response.status);
|
|
369
|
+
}
|
|
370
|
+
return data;
|
|
371
|
+
}
|
|
372
|
+
async get(path, params) {
|
|
373
|
+
return this.request(path, { method: "GET", params });
|
|
374
|
+
}
|
|
375
|
+
async post(path, body, params) {
|
|
376
|
+
return this.request(path, { method: "POST", body, params });
|
|
377
|
+
}
|
|
378
|
+
async delete(path, params) {
|
|
379
|
+
return this.request(path, { method: "DELETE", params });
|
|
380
|
+
}
|
|
381
|
+
getApiKeyPreview() {
|
|
382
|
+
if (this.apiKey.length > 10) {
|
|
383
|
+
return `${this.apiKey.substring(0, 6)}...${this.apiKey.substring(this.apiKey.length - 4)}`;
|
|
384
|
+
}
|
|
385
|
+
return "***";
|
|
386
|
+
}
|
|
387
|
+
hasBearerToken() {
|
|
388
|
+
return !!this.bearerToken;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// connectors/x/src/api/tweets.ts
|
|
393
|
+
class TweetsApi {
|
|
394
|
+
client;
|
|
395
|
+
constructor(client) {
|
|
396
|
+
this.client = client;
|
|
397
|
+
}
|
|
398
|
+
async get(id, options) {
|
|
399
|
+
const params = {};
|
|
400
|
+
if (options?.tweetFields?.length) {
|
|
401
|
+
params["tweet.fields"] = options.tweetFields.join(",");
|
|
402
|
+
}
|
|
403
|
+
if (options?.userFields?.length) {
|
|
404
|
+
params["user.fields"] = options.userFields.join(",");
|
|
405
|
+
}
|
|
406
|
+
if (options?.expansions?.length) {
|
|
407
|
+
params["expansions"] = options.expansions.join(",");
|
|
408
|
+
}
|
|
409
|
+
if (options?.mediaFields?.length) {
|
|
410
|
+
params["media.fields"] = options.mediaFields.join(",");
|
|
411
|
+
}
|
|
412
|
+
return this.client.get(`/2/tweets/${id}`, params);
|
|
413
|
+
}
|
|
414
|
+
async getMany(ids, options) {
|
|
415
|
+
const params = {
|
|
416
|
+
ids: ids.join(",")
|
|
417
|
+
};
|
|
418
|
+
if (options?.tweetFields?.length) {
|
|
419
|
+
params["tweet.fields"] = options.tweetFields.join(",");
|
|
420
|
+
}
|
|
421
|
+
if (options?.userFields?.length) {
|
|
422
|
+
params["user.fields"] = options.userFields.join(",");
|
|
423
|
+
}
|
|
424
|
+
if (options?.expansions?.length) {
|
|
425
|
+
params["expansions"] = options.expansions.join(",");
|
|
426
|
+
}
|
|
427
|
+
if (options?.mediaFields?.length) {
|
|
428
|
+
params["media.fields"] = options.mediaFields.join(",");
|
|
429
|
+
}
|
|
430
|
+
return this.client.get("/2/tweets", params);
|
|
431
|
+
}
|
|
432
|
+
async searchRecent(options) {
|
|
433
|
+
const params = {
|
|
434
|
+
query: options.query,
|
|
435
|
+
max_results: options.maxResults || 10,
|
|
436
|
+
next_token: options.nextToken,
|
|
437
|
+
start_time: options.startTime,
|
|
438
|
+
end_time: options.endTime,
|
|
439
|
+
since_id: options.sinceId,
|
|
440
|
+
until_id: options.untilId,
|
|
441
|
+
sort_order: options.sortOrder,
|
|
442
|
+
"tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
|
|
443
|
+
expansions: "author_id",
|
|
444
|
+
"user.fields": "id,name,username,profile_image_url"
|
|
445
|
+
};
|
|
446
|
+
return this.client.get("/2/tweets/search/recent", params);
|
|
447
|
+
}
|
|
448
|
+
async countRecent(query, options) {
|
|
449
|
+
const params = {
|
|
450
|
+
query,
|
|
451
|
+
start_time: options?.startTime,
|
|
452
|
+
end_time: options?.endTime,
|
|
453
|
+
granularity: options?.granularity || "hour"
|
|
454
|
+
};
|
|
455
|
+
return this.client.get("/2/tweets/counts/recent", params);
|
|
456
|
+
}
|
|
457
|
+
async getUserTimeline(userId, options) {
|
|
458
|
+
const exclude = [];
|
|
459
|
+
if (options?.excludeReplies)
|
|
460
|
+
exclude.push("replies");
|
|
461
|
+
if (options?.excludeRetweets)
|
|
462
|
+
exclude.push("retweets");
|
|
463
|
+
const params = {
|
|
464
|
+
max_results: options?.maxResults || 10,
|
|
465
|
+
pagination_token: options?.paginationToken,
|
|
466
|
+
start_time: options?.startTime,
|
|
467
|
+
end_time: options?.endTime,
|
|
468
|
+
since_id: options?.sinceId,
|
|
469
|
+
until_id: options?.untilId,
|
|
470
|
+
exclude: exclude.length ? exclude.join(",") : undefined,
|
|
471
|
+
"tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
|
|
472
|
+
expansions: "author_id",
|
|
473
|
+
"user.fields": "id,name,username,profile_image_url"
|
|
474
|
+
};
|
|
475
|
+
return this.client.get(`/2/users/${userId}/tweets`, params);
|
|
476
|
+
}
|
|
477
|
+
async getUserMentions(userId, options) {
|
|
478
|
+
const params = {
|
|
479
|
+
max_results: options?.maxResults || 10,
|
|
480
|
+
pagination_token: options?.paginationToken,
|
|
481
|
+
start_time: options?.startTime,
|
|
482
|
+
end_time: options?.endTime,
|
|
483
|
+
since_id: options?.sinceId,
|
|
484
|
+
until_id: options?.untilId,
|
|
485
|
+
"tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
|
|
486
|
+
expansions: "author_id",
|
|
487
|
+
"user.fields": "id,name,username,profile_image_url"
|
|
488
|
+
};
|
|
489
|
+
return this.client.get(`/2/users/${userId}/mentions`, params);
|
|
490
|
+
}
|
|
491
|
+
async getLikingUsers(tweetId, options) {
|
|
492
|
+
const params = {
|
|
493
|
+
max_results: options?.maxResults || 100,
|
|
494
|
+
pagination_token: options?.paginationToken,
|
|
495
|
+
"user.fields": "id,name,username,profile_image_url,public_metrics"
|
|
496
|
+
};
|
|
497
|
+
return this.client.get(`/2/tweets/${tweetId}/liking_users`, params);
|
|
498
|
+
}
|
|
499
|
+
async getRetweetedBy(tweetId, options) {
|
|
500
|
+
const params = {
|
|
501
|
+
max_results: options?.maxResults || 100,
|
|
502
|
+
pagination_token: options?.paginationToken,
|
|
503
|
+
"user.fields": "id,name,username,profile_image_url,public_metrics"
|
|
504
|
+
};
|
|
505
|
+
return this.client.get(`/2/tweets/${tweetId}/retweeted_by`, params);
|
|
506
|
+
}
|
|
507
|
+
async getQuoteTweets(tweetId, options) {
|
|
508
|
+
const params = {
|
|
509
|
+
max_results: options?.maxResults || 10,
|
|
510
|
+
pagination_token: options?.paginationToken,
|
|
511
|
+
"tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
|
|
512
|
+
expansions: "author_id",
|
|
513
|
+
"user.fields": "id,name,username,profile_image_url"
|
|
514
|
+
};
|
|
515
|
+
return this.client.get(`/2/tweets/${tweetId}/quote_tweets`, params);
|
|
516
|
+
}
|
|
517
|
+
async create(options) {
|
|
518
|
+
const body = {
|
|
519
|
+
text: options.text
|
|
520
|
+
};
|
|
521
|
+
if (options.mediaIds?.length) {
|
|
522
|
+
body.media = { media_ids: options.mediaIds };
|
|
523
|
+
}
|
|
524
|
+
if (options.replyToTweetId) {
|
|
525
|
+
body.reply = { in_reply_to_tweet_id: options.replyToTweetId };
|
|
526
|
+
}
|
|
527
|
+
if (options.quoteTweetId) {
|
|
528
|
+
body.quote_tweet_id = options.quoteTweetId;
|
|
529
|
+
}
|
|
530
|
+
if (options.pollOptions?.length) {
|
|
531
|
+
body.poll = {
|
|
532
|
+
options: options.pollOptions,
|
|
533
|
+
duration_minutes: options.pollDurationMinutes || 1440
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
return this.client.post("/2/tweets", body);
|
|
537
|
+
}
|
|
538
|
+
async delete(tweetId) {
|
|
539
|
+
return this.client.delete(`/2/tweets/${tweetId}`);
|
|
540
|
+
}
|
|
541
|
+
async like(userId, tweetId) {
|
|
542
|
+
return this.client.post(`/2/users/${userId}/likes`, {
|
|
543
|
+
tweet_id: tweetId
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
async unlike(userId, tweetId) {
|
|
547
|
+
return this.client.delete(`/2/users/${userId}/likes/${tweetId}`);
|
|
548
|
+
}
|
|
549
|
+
async retweet(userId, tweetId) {
|
|
550
|
+
return this.client.post(`/2/users/${userId}/retweets`, {
|
|
551
|
+
tweet_id: tweetId
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
async unretweet(userId, tweetId) {
|
|
555
|
+
return this.client.delete(`/2/users/${userId}/retweets/${tweetId}`);
|
|
556
|
+
}
|
|
557
|
+
async getBookmarks(userId, options) {
|
|
558
|
+
return this.client.request(`/2/users/${userId}/bookmarks`, {
|
|
559
|
+
method: "GET",
|
|
560
|
+
params: {
|
|
561
|
+
max_results: options?.maxResults || 10,
|
|
562
|
+
pagination_token: options?.paginationToken,
|
|
563
|
+
"tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
|
|
564
|
+
expansions: "author_id",
|
|
565
|
+
"user.fields": "id,name,username,profile_image_url"
|
|
566
|
+
},
|
|
567
|
+
authMethod: "oauth2-user"
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
async bookmark(userId, tweetId) {
|
|
571
|
+
return this.client.post(`/2/users/${userId}/bookmarks`, {
|
|
572
|
+
tweet_id: tweetId
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
async removeBookmark(userId, tweetId) {
|
|
576
|
+
return this.client.delete(`/2/users/${userId}/bookmarks/${tweetId}`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// connectors/x/src/api/users.ts
|
|
581
|
+
class UsersApi {
|
|
582
|
+
client;
|
|
583
|
+
constructor(client) {
|
|
584
|
+
this.client = client;
|
|
585
|
+
}
|
|
586
|
+
async getById(id, options) {
|
|
587
|
+
const params = {};
|
|
588
|
+
if (options?.userFields?.length) {
|
|
589
|
+
params["user.fields"] = options.userFields.join(",");
|
|
590
|
+
}
|
|
591
|
+
if (options?.tweetFields?.length) {
|
|
592
|
+
params["tweet.fields"] = options.tweetFields.join(",");
|
|
593
|
+
}
|
|
594
|
+
if (options?.expansions?.length) {
|
|
595
|
+
params["expansions"] = options.expansions.join(",");
|
|
596
|
+
}
|
|
597
|
+
return this.client.get(`/2/users/${id}`, params);
|
|
598
|
+
}
|
|
599
|
+
async getByUsername(username, options) {
|
|
600
|
+
const params = {
|
|
601
|
+
"user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified,protected,location,url"
|
|
602
|
+
};
|
|
603
|
+
if (options?.userFields?.length) {
|
|
604
|
+
params["user.fields"] = options.userFields.join(",");
|
|
605
|
+
}
|
|
606
|
+
if (options?.tweetFields?.length) {
|
|
607
|
+
params["tweet.fields"] = options.tweetFields.join(",");
|
|
608
|
+
}
|
|
609
|
+
if (options?.expansions?.length) {
|
|
610
|
+
params["expansions"] = options.expansions.join(",");
|
|
611
|
+
}
|
|
612
|
+
return this.client.get(`/2/users/by/username/${username}`, params);
|
|
613
|
+
}
|
|
614
|
+
async getMany(ids, options) {
|
|
615
|
+
const params = {
|
|
616
|
+
ids: ids.join(","),
|
|
617
|
+
"user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified,protected"
|
|
618
|
+
};
|
|
619
|
+
if (options?.userFields?.length) {
|
|
620
|
+
params["user.fields"] = options.userFields.join(",");
|
|
621
|
+
}
|
|
622
|
+
if (options?.tweetFields?.length) {
|
|
623
|
+
params["tweet.fields"] = options.tweetFields.join(",");
|
|
624
|
+
}
|
|
625
|
+
if (options?.expansions?.length) {
|
|
626
|
+
params["expansions"] = options.expansions.join(",");
|
|
627
|
+
}
|
|
628
|
+
return this.client.get("/2/users", params);
|
|
629
|
+
}
|
|
630
|
+
async getManyByUsernames(usernames, options) {
|
|
631
|
+
const params = {
|
|
632
|
+
usernames: usernames.join(","),
|
|
633
|
+
"user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified,protected"
|
|
634
|
+
};
|
|
635
|
+
if (options?.userFields?.length) {
|
|
636
|
+
params["user.fields"] = options.userFields.join(",");
|
|
637
|
+
}
|
|
638
|
+
if (options?.tweetFields?.length) {
|
|
639
|
+
params["tweet.fields"] = options.tweetFields.join(",");
|
|
640
|
+
}
|
|
641
|
+
if (options?.expansions?.length) {
|
|
642
|
+
params["expansions"] = options.expansions.join(",");
|
|
643
|
+
}
|
|
644
|
+
return this.client.get("/2/users/by", params);
|
|
645
|
+
}
|
|
646
|
+
async getFollowers(userId, options) {
|
|
647
|
+
const params = {
|
|
648
|
+
max_results: options?.maxResults || 100,
|
|
649
|
+
pagination_token: options?.paginationToken,
|
|
650
|
+
"user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified"
|
|
651
|
+
};
|
|
652
|
+
return this.client.get(`/2/users/${userId}/followers`, params);
|
|
653
|
+
}
|
|
654
|
+
async getFollowing(userId, options) {
|
|
655
|
+
const params = {
|
|
656
|
+
max_results: options?.maxResults || 100,
|
|
657
|
+
pagination_token: options?.paginationToken,
|
|
658
|
+
"user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified"
|
|
659
|
+
};
|
|
660
|
+
return this.client.get(`/2/users/${userId}/following`, params);
|
|
661
|
+
}
|
|
662
|
+
async getLikedTweets(userId, options) {
|
|
663
|
+
return this.client.request(`/2/users/${userId}/liked_tweets`, {
|
|
664
|
+
method: "GET",
|
|
665
|
+
params: {
|
|
666
|
+
max_results: options?.maxResults || 10,
|
|
667
|
+
pagination_token: options?.paginationToken,
|
|
668
|
+
"tweet.fields": "id,text,author_id,created_at,public_metrics,source,lang",
|
|
669
|
+
expansions: "author_id",
|
|
670
|
+
"user.fields": "id,name,username,profile_image_url"
|
|
671
|
+
},
|
|
672
|
+
authMethod: "oauth2-user"
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
async getOwnedLists(userId, options) {
|
|
676
|
+
const params = {
|
|
677
|
+
max_results: options?.maxResults || 100,
|
|
678
|
+
pagination_token: options?.paginationToken,
|
|
679
|
+
"list.fields": "id,name,description,follower_count,member_count,private,owner_id,created_at"
|
|
680
|
+
};
|
|
681
|
+
return this.client.get(`/2/users/${userId}/owned_lists`, params);
|
|
682
|
+
}
|
|
683
|
+
async getListMemberships(userId, options) {
|
|
684
|
+
const params = {
|
|
685
|
+
max_results: options?.maxResults || 100,
|
|
686
|
+
pagination_token: options?.paginationToken,
|
|
687
|
+
"list.fields": "id,name,description,follower_count,member_count,private,owner_id,created_at"
|
|
688
|
+
};
|
|
689
|
+
return this.client.get(`/2/users/${userId}/list_memberships`, params);
|
|
690
|
+
}
|
|
691
|
+
async me() {
|
|
692
|
+
return this.client.request("/2/users/me", {
|
|
693
|
+
method: "GET",
|
|
694
|
+
params: {
|
|
695
|
+
"user.fields": "id,name,username,created_at,description,profile_image_url,public_metrics,verified,protected,location,url"
|
|
696
|
+
},
|
|
697
|
+
authMethod: "oauth2-user"
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
async follow(userId, targetUserId) {
|
|
701
|
+
return this.client.post(`/2/users/${userId}/following`, { target_user_id: targetUserId });
|
|
702
|
+
}
|
|
703
|
+
async unfollow(userId, targetUserId) {
|
|
704
|
+
return this.client.delete(`/2/users/${userId}/following/${targetUserId}`);
|
|
705
|
+
}
|
|
706
|
+
async block(userId, targetUserId) {
|
|
707
|
+
return this.client.post(`/2/users/${userId}/blocking`, { target_user_id: targetUserId });
|
|
708
|
+
}
|
|
709
|
+
async unblock(userId, targetUserId) {
|
|
710
|
+
return this.client.delete(`/2/users/${userId}/blocking/${targetUserId}`);
|
|
711
|
+
}
|
|
712
|
+
async mute(userId, targetUserId) {
|
|
713
|
+
return this.client.post(`/2/users/${userId}/muting`, { target_user_id: targetUserId });
|
|
714
|
+
}
|
|
715
|
+
async unmute(userId, targetUserId) {
|
|
716
|
+
return this.client.delete(`/2/users/${userId}/muting/${targetUserId}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// connectors/x/src/api/media.ts
|
|
721
|
+
var UPLOAD_URL = "https://upload.twitter.com/1.1/media/upload.json";
|
|
722
|
+
var MIME_TYPES = {
|
|
723
|
+
".jpg": "image/jpeg",
|
|
724
|
+
".jpeg": "image/jpeg",
|
|
725
|
+
".png": "image/png",
|
|
726
|
+
".gif": "image/gif",
|
|
727
|
+
".webp": "image/webp",
|
|
728
|
+
".mp4": "video/mp4",
|
|
729
|
+
".mov": "video/quicktime"
|
|
730
|
+
};
|
|
731
|
+
var MAX_SIZES = {
|
|
732
|
+
image: 5 * 1024 * 1024,
|
|
733
|
+
gif: 15 * 1024 * 1024,
|
|
734
|
+
video: 512 * 1024 * 1024
|
|
735
|
+
};
|
|
736
|
+
var CHUNK_SIZE = 5 * 1024 * 1024;
|
|
737
|
+
function getMimeType(filePath) {
|
|
738
|
+
const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0] || "";
|
|
739
|
+
return MIME_TYPES[ext] || "application/octet-stream";
|
|
740
|
+
}
|
|
741
|
+
function getMediaCategory(mimeType) {
|
|
742
|
+
if (mimeType.startsWith("video/")) {
|
|
743
|
+
return "tweet_video";
|
|
744
|
+
}
|
|
745
|
+
if (mimeType === "image/gif") {
|
|
746
|
+
return "tweet_gif";
|
|
747
|
+
}
|
|
748
|
+
return "tweet_image";
|
|
749
|
+
}
|
|
750
|
+
function requiresChunkedUpload(mimeType, fileSize) {
|
|
751
|
+
if (mimeType.startsWith("video/")) {
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
if (mimeType === "image/gif" && fileSize > 5 * 1024 * 1024) {
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
return false;
|
|
758
|
+
}
|
|
759
|
+
async function simpleUpload(oauth1Client, fileData, mimeType) {
|
|
760
|
+
const base64Data = fileData.toString("base64");
|
|
761
|
+
const result = await oauth1Client.post(UPLOAD_URL, {
|
|
762
|
+
media_data: base64Data
|
|
763
|
+
});
|
|
764
|
+
return result;
|
|
765
|
+
}
|
|
766
|
+
async function chunkedUpload(oauth1Client, fileData, mimeType, category) {
|
|
767
|
+
const totalBytes = fileData.length;
|
|
768
|
+
const initResult = await oauth1Client.post(UPLOAD_URL, {
|
|
769
|
+
command: "INIT",
|
|
770
|
+
total_bytes: totalBytes.toString(),
|
|
771
|
+
media_type: mimeType,
|
|
772
|
+
media_category: category
|
|
773
|
+
});
|
|
774
|
+
const mediaId = initResult.media_id_string;
|
|
775
|
+
let segmentIndex = 0;
|
|
776
|
+
let offset = 0;
|
|
777
|
+
while (offset < totalBytes) {
|
|
778
|
+
const chunk = fileData.subarray(offset, Math.min(offset + CHUNK_SIZE, totalBytes));
|
|
779
|
+
const chunkBase64 = chunk.toString("base64");
|
|
780
|
+
await oauth1Client.post(UPLOAD_URL, {
|
|
781
|
+
command: "APPEND",
|
|
782
|
+
media_id: mediaId,
|
|
783
|
+
media_data: chunkBase64,
|
|
784
|
+
segment_index: segmentIndex.toString()
|
|
785
|
+
});
|
|
786
|
+
offset += CHUNK_SIZE;
|
|
787
|
+
segmentIndex++;
|
|
788
|
+
}
|
|
789
|
+
const finalizeResult = await oauth1Client.post(UPLOAD_URL, {
|
|
790
|
+
command: "FINALIZE",
|
|
791
|
+
media_id: mediaId
|
|
792
|
+
});
|
|
793
|
+
return finalizeResult;
|
|
794
|
+
}
|
|
795
|
+
async function waitForProcessing(oauth1Client, mediaId, maxWaitMs = 60000) {
|
|
796
|
+
const startTime = Date.now();
|
|
797
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
798
|
+
const status = await oauth1Client.get(UPLOAD_URL, {
|
|
799
|
+
command: "STATUS",
|
|
800
|
+
media_id: mediaId
|
|
801
|
+
});
|
|
802
|
+
if (!status.processing_info) {
|
|
803
|
+
return status;
|
|
804
|
+
}
|
|
805
|
+
const { state, check_after_secs, error } = status.processing_info;
|
|
806
|
+
if (state === "succeeded") {
|
|
807
|
+
return status;
|
|
808
|
+
}
|
|
809
|
+
if (state === "failed") {
|
|
810
|
+
throw new Error(`Media processing failed: ${error?.message || "Unknown error"}`);
|
|
811
|
+
}
|
|
812
|
+
const waitMs = (check_after_secs || 5) * 1000;
|
|
813
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
814
|
+
}
|
|
815
|
+
throw new Error("Media processing timed out");
|
|
816
|
+
}
|
|
817
|
+
async function addAltText(oauth1Client, mediaId, altText) {
|
|
818
|
+
const url = "https://upload.twitter.com/1.1/media/metadata/create.json";
|
|
819
|
+
await oauth1Client.post(url, JSON.stringify({
|
|
820
|
+
media_id: mediaId,
|
|
821
|
+
alt_text: { text: altText }
|
|
822
|
+
}), { "Content-Type": "application/json" });
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
class MediaApi {
|
|
826
|
+
oauth1Client;
|
|
827
|
+
constructor(oauth1Client) {
|
|
828
|
+
this.oauth1Client = oauth1Client;
|
|
829
|
+
}
|
|
830
|
+
async uploadFile(filePath, options = {}) {
|
|
831
|
+
const { readFileSync } = await import("fs");
|
|
832
|
+
const fileData = readFileSync(filePath);
|
|
833
|
+
const mimeType = getMimeType(filePath);
|
|
834
|
+
return this.uploadBuffer(fileData, mimeType, {
|
|
835
|
+
...options,
|
|
836
|
+
category: options.category || getMediaCategory(mimeType)
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
async uploadBuffer(data, mimeType, options = {}) {
|
|
840
|
+
const fileSize = data.length;
|
|
841
|
+
const category = options.category || getMediaCategory(mimeType);
|
|
842
|
+
const maxSize = mimeType.startsWith("video/") ? MAX_SIZES.video : mimeType === "image/gif" ? MAX_SIZES.gif : MAX_SIZES.image;
|
|
843
|
+
if (fileSize > maxSize) {
|
|
844
|
+
throw new Error(`File size ${fileSize} exceeds maximum ${maxSize} bytes for ${mimeType}`);
|
|
845
|
+
}
|
|
846
|
+
let result;
|
|
847
|
+
if (requiresChunkedUpload(mimeType, fileSize)) {
|
|
848
|
+
result = await chunkedUpload(this.oauth1Client, data, mimeType, category);
|
|
849
|
+
if (result.processing_info) {
|
|
850
|
+
result = await waitForProcessing(this.oauth1Client, result.media_id_string);
|
|
851
|
+
}
|
|
852
|
+
} else {
|
|
853
|
+
result = await simpleUpload(this.oauth1Client, data, mimeType);
|
|
854
|
+
}
|
|
855
|
+
if (options.altText) {
|
|
856
|
+
await addAltText(this.oauth1Client, result.media_id_string, options.altText);
|
|
857
|
+
}
|
|
858
|
+
return result;
|
|
859
|
+
}
|
|
860
|
+
async uploadFromUrl(url, options = {}) {
|
|
861
|
+
const response = await fetch(url);
|
|
862
|
+
if (!response.ok) {
|
|
863
|
+
throw new Error(`Failed to fetch media from URL: ${response.statusText}`);
|
|
864
|
+
}
|
|
865
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
866
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
867
|
+
return this.uploadBuffer(buffer, contentType, options);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// connectors/x/src/api/index.ts
|
|
872
|
+
class X {
|
|
873
|
+
client;
|
|
874
|
+
mediaApi;
|
|
875
|
+
tweets;
|
|
876
|
+
users;
|
|
877
|
+
constructor(config) {
|
|
878
|
+
this.client = new XClient(config);
|
|
879
|
+
this.tweets = new TweetsApi(this.client);
|
|
880
|
+
this.users = new UsersApi(this.client);
|
|
881
|
+
if (config.oauth1AccessToken && config.oauth1AccessTokenSecret) {
|
|
882
|
+
const oauth1Client = new OAuth1Client({
|
|
883
|
+
consumerKey: config.apiKey,
|
|
884
|
+
consumerSecret: config.apiSecret,
|
|
885
|
+
accessToken: config.oauth1AccessToken,
|
|
886
|
+
accessTokenSecret: config.oauth1AccessTokenSecret
|
|
887
|
+
});
|
|
888
|
+
this.mediaApi = new MediaApi(oauth1Client);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
get media() {
|
|
892
|
+
return this.mediaApi;
|
|
893
|
+
}
|
|
894
|
+
hasMediaUpload() {
|
|
895
|
+
return !!this.mediaApi;
|
|
896
|
+
}
|
|
897
|
+
static fromEnv() {
|
|
898
|
+
const apiKey = process.env.X_API_KEY;
|
|
899
|
+
const apiSecret = process.env.X_API_SECRET;
|
|
900
|
+
const bearerToken = process.env.X_BEARER_TOKEN;
|
|
901
|
+
const accessToken = process.env.X_ACCESS_TOKEN;
|
|
902
|
+
const refreshToken = process.env.X_REFRESH_TOKEN;
|
|
903
|
+
const clientId = process.env.X_CLIENT_ID;
|
|
904
|
+
const clientSecret = process.env.X_CLIENT_SECRET;
|
|
905
|
+
const oauth1AccessToken = process.env.X_OAUTH1_ACCESS_TOKEN;
|
|
906
|
+
const oauth1AccessTokenSecret = process.env.X_OAUTH1_ACCESS_TOKEN_SECRET;
|
|
907
|
+
if (!apiKey || !apiSecret) {
|
|
908
|
+
throw new Error("X_API_KEY and X_API_SECRET environment variables are required");
|
|
909
|
+
}
|
|
910
|
+
return new X({
|
|
911
|
+
apiKey,
|
|
912
|
+
apiSecret,
|
|
913
|
+
bearerToken,
|
|
914
|
+
accessToken,
|
|
915
|
+
refreshToken,
|
|
916
|
+
clientId,
|
|
917
|
+
clientSecret,
|
|
918
|
+
oauth1AccessToken,
|
|
919
|
+
oauth1AccessTokenSecret
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
getApiKeyPreview() {
|
|
923
|
+
return this.client.getApiKeyPreview();
|
|
924
|
+
}
|
|
925
|
+
hasBearerToken() {
|
|
926
|
+
return this.client.hasBearerToken();
|
|
927
|
+
}
|
|
928
|
+
hasUserContext() {
|
|
929
|
+
return this.client.hasUserContext();
|
|
930
|
+
}
|
|
931
|
+
hasOAuth1() {
|
|
932
|
+
return this.client.hasOAuth1();
|
|
933
|
+
}
|
|
934
|
+
getAuthStatus() {
|
|
935
|
+
return this.client.getAuthStatus();
|
|
936
|
+
}
|
|
937
|
+
setUserTokens(tokens) {
|
|
938
|
+
this.client.setUserTokens(tokens);
|
|
939
|
+
}
|
|
940
|
+
setOAuth1Tokens(oauthToken, oauthTokenSecret) {
|
|
941
|
+
this.client.setOAuth1Tokens(oauthToken, oauthTokenSecret);
|
|
942
|
+
const oauth1Client = this.client.getOAuth1Client();
|
|
943
|
+
if (oauth1Client) {
|
|
944
|
+
this.mediaApi = new MediaApi(oauth1Client);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
setTokenRefreshCallback(callback) {
|
|
948
|
+
this.client.setTokenRefreshCallback(callback);
|
|
949
|
+
}
|
|
950
|
+
getClient() {
|
|
951
|
+
return this.client;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// src/social/util.ts
|
|
956
|
+
function decodeBase64(dataBase64) {
|
|
957
|
+
if (typeof dataBase64 !== "string" || dataBase64.length === 0) {
|
|
958
|
+
throw new Error("media.upload requires a non-empty base64 string in `dataBase64`");
|
|
959
|
+
}
|
|
960
|
+
return Buffer.from(dataBase64, "base64");
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/social/x.ts
|
|
964
|
+
class XAdapter {
|
|
965
|
+
x;
|
|
966
|
+
me;
|
|
967
|
+
constructor(x) {
|
|
968
|
+
this.x = x;
|
|
969
|
+
}
|
|
970
|
+
static fromCredentials(creds) {
|
|
971
|
+
if (!creds || !creds.apiKey || !creds.apiSecret) {
|
|
972
|
+
throw new Error("x credentials require `apiKey` and `apiSecret`");
|
|
973
|
+
}
|
|
974
|
+
const x = new X({
|
|
975
|
+
apiKey: creds.apiKey,
|
|
976
|
+
apiSecret: creds.apiSecret,
|
|
977
|
+
bearerToken: creds.bearerToken,
|
|
978
|
+
accessToken: creds.accessToken,
|
|
979
|
+
refreshToken: creds.refreshToken,
|
|
980
|
+
clientId: creds.clientId,
|
|
981
|
+
clientSecret: creds.clientSecret,
|
|
982
|
+
oauth1AccessToken: creds.oauth1AccessToken,
|
|
983
|
+
oauth1AccessTokenSecret: creds.oauth1AccessTokenSecret
|
|
984
|
+
});
|
|
985
|
+
return new XAdapter(x);
|
|
986
|
+
}
|
|
987
|
+
async resolveMe() {
|
|
988
|
+
if (this.me)
|
|
989
|
+
return this.me;
|
|
990
|
+
const res = await this.x.users.me();
|
|
991
|
+
this.me = { id: res.data.id, username: res.data.username, displayName: res.data.name };
|
|
992
|
+
return this.me;
|
|
993
|
+
}
|
|
994
|
+
async accountMe() {
|
|
995
|
+
const me = await this.resolveMe();
|
|
996
|
+
return {
|
|
997
|
+
id: me.id,
|
|
998
|
+
username: me.username,
|
|
999
|
+
displayName: me.displayName,
|
|
1000
|
+
url: `https://x.com/${me.username}`
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
async postCreate(input) {
|
|
1004
|
+
const res = await this.x.tweets.create({
|
|
1005
|
+
text: input.text,
|
|
1006
|
+
replyToTweetId: input.replyToId,
|
|
1007
|
+
mediaIds: input.mediaIds
|
|
1008
|
+
});
|
|
1009
|
+
const id = res.data.id;
|
|
1010
|
+
const me = await this.resolveMe();
|
|
1011
|
+
return { id, url: `https://x.com/${me.username}/status/${id}` };
|
|
1012
|
+
}
|
|
1013
|
+
async postDelete(input) {
|
|
1014
|
+
const res = await this.x.tweets.delete(input.id);
|
|
1015
|
+
return { id: input.id, deleted: Boolean(res.data.deleted) };
|
|
1016
|
+
}
|
|
1017
|
+
async mediaUpload(input) {
|
|
1018
|
+
if (!this.x.media) {
|
|
1019
|
+
throw new ConnectorOperationNotSupported("x", "media.upload (requires OAuth 1.0a credentials: oauth1AccessToken + oauth1AccessTokenSecret)");
|
|
1020
|
+
}
|
|
1021
|
+
const buffer = decodeBase64(input.dataBase64);
|
|
1022
|
+
const res = await this.x.media.uploadBuffer(buffer, input.mimeType, {
|
|
1023
|
+
altText: input.altText
|
|
1024
|
+
});
|
|
1025
|
+
return { mediaId: res.media_id_string };
|
|
1026
|
+
}
|
|
1027
|
+
async mentionsList(input = {}) {
|
|
1028
|
+
const me = await this.resolveMe();
|
|
1029
|
+
const res = await this.x.tweets.getUserMentions(me.id, {
|
|
1030
|
+
sinceId: input.sinceId,
|
|
1031
|
+
maxResults: input.limit
|
|
1032
|
+
});
|
|
1033
|
+
const handleById = new Map;
|
|
1034
|
+
for (const u of res.includes?.users ?? []) {
|
|
1035
|
+
handleById.set(u.id, u.username);
|
|
1036
|
+
}
|
|
1037
|
+
const items = (res.data ?? []).map((t) => ({
|
|
1038
|
+
id: t.id,
|
|
1039
|
+
text: t.text,
|
|
1040
|
+
authorId: t.author_id,
|
|
1041
|
+
authorHandle: t.author_id ? handleById.get(t.author_id) : undefined,
|
|
1042
|
+
createdAt: t.created_at
|
|
1043
|
+
}));
|
|
1044
|
+
return { items };
|
|
1045
|
+
}
|
|
1046
|
+
async analyticsPost(input) {
|
|
1047
|
+
const res = await this.x.tweets.get(input.id, {
|
|
1048
|
+
tweetFields: ["public_metrics"]
|
|
1049
|
+
});
|
|
1050
|
+
const metrics = res.data.public_metrics ?? {};
|
|
1051
|
+
return { metrics };
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/social/mastodon.ts
|
|
1056
|
+
class MastodonAdapter {
|
|
1057
|
+
accessToken;
|
|
1058
|
+
baseUrl;
|
|
1059
|
+
fetchImpl;
|
|
1060
|
+
constructor(creds, fetchImpl) {
|
|
1061
|
+
if (!creds || !creds.accessToken || !creds.baseUrl) {
|
|
1062
|
+
throw new Error("mastodon credentials require `accessToken` and `baseUrl`");
|
|
1063
|
+
}
|
|
1064
|
+
this.accessToken = creds.accessToken;
|
|
1065
|
+
this.baseUrl = creds.baseUrl.replace(/\/+$/, "");
|
|
1066
|
+
this.fetchImpl = fetchImpl ?? globalThis.fetch;
|
|
1067
|
+
}
|
|
1068
|
+
static fromCredentials(creds, fetchImpl) {
|
|
1069
|
+
return new MastodonAdapter(creds, fetchImpl);
|
|
1070
|
+
}
|
|
1071
|
+
async call(path, options = {}) {
|
|
1072
|
+
const { method = "GET", body, query } = options;
|
|
1073
|
+
let url = `${this.baseUrl}${path}`;
|
|
1074
|
+
if (query) {
|
|
1075
|
+
const qs = new URLSearchParams;
|
|
1076
|
+
for (const [k, v] of Object.entries(query)) {
|
|
1077
|
+
if (v !== undefined && v !== null && v !== "")
|
|
1078
|
+
qs.append(k, String(v));
|
|
1079
|
+
}
|
|
1080
|
+
const s = qs.toString();
|
|
1081
|
+
if (s)
|
|
1082
|
+
url += `?${s}`;
|
|
1083
|
+
}
|
|
1084
|
+
const headers = {
|
|
1085
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
1086
|
+
Accept: "application/json"
|
|
1087
|
+
};
|
|
1088
|
+
let serializedBody;
|
|
1089
|
+
if (body !== undefined && method !== "GET") {
|
|
1090
|
+
headers["Content-Type"] = "application/json";
|
|
1091
|
+
serializedBody = JSON.stringify(body);
|
|
1092
|
+
}
|
|
1093
|
+
const res = await this.fetchImpl(url, { method, headers, body: serializedBody });
|
|
1094
|
+
const text = await res.text();
|
|
1095
|
+
const data = text ? JSON.parse(text) : {};
|
|
1096
|
+
if (!res.ok) {
|
|
1097
|
+
const message = data && (data.error || data.message) || res.statusText || "Mastodon request failed";
|
|
1098
|
+
throw new Error(`Mastodon ${res.status}: ${message}`);
|
|
1099
|
+
}
|
|
1100
|
+
return data;
|
|
1101
|
+
}
|
|
1102
|
+
async accountMe() {
|
|
1103
|
+
const acct = await this.call("/api/v1/accounts/verify_credentials");
|
|
1104
|
+
return {
|
|
1105
|
+
id: acct.id,
|
|
1106
|
+
username: acct.username,
|
|
1107
|
+
displayName: acct.display_name,
|
|
1108
|
+
url: acct.url
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
async postCreate(input) {
|
|
1112
|
+
const body = { status: input.text };
|
|
1113
|
+
if (input.replyToId)
|
|
1114
|
+
body.in_reply_to_id = input.replyToId;
|
|
1115
|
+
if (input.mediaIds && input.mediaIds.length > 0)
|
|
1116
|
+
body.media_ids = input.mediaIds;
|
|
1117
|
+
const status = await this.call("/api/v1/statuses", {
|
|
1118
|
+
method: "POST",
|
|
1119
|
+
body
|
|
1120
|
+
});
|
|
1121
|
+
return { id: status.id, url: status.url };
|
|
1122
|
+
}
|
|
1123
|
+
async postDelete(input) {
|
|
1124
|
+
await this.call(`/api/v1/statuses/${input.id}`, { method: "DELETE" });
|
|
1125
|
+
return { id: input.id, deleted: true };
|
|
1126
|
+
}
|
|
1127
|
+
async mediaUpload(input) {
|
|
1128
|
+
const buffer = decodeBase64(input.dataBase64);
|
|
1129
|
+
const form = new FormData;
|
|
1130
|
+
const bytes = Uint8Array.from(buffer);
|
|
1131
|
+
const blob = new Blob([bytes], { type: input.mimeType });
|
|
1132
|
+
form.append("file", blob);
|
|
1133
|
+
if (input.altText)
|
|
1134
|
+
form.append("description", input.altText);
|
|
1135
|
+
const res = await this.fetchImpl(`${this.baseUrl}/api/v2/media`, {
|
|
1136
|
+
method: "POST",
|
|
1137
|
+
headers: { Authorization: `Bearer ${this.accessToken}`, Accept: "application/json" },
|
|
1138
|
+
body: form
|
|
1139
|
+
});
|
|
1140
|
+
const text = await res.text();
|
|
1141
|
+
const data = text ? JSON.parse(text) : {};
|
|
1142
|
+
if (!res.ok) {
|
|
1143
|
+
const message = data && (data.error || data.message) || res.statusText || "media upload failed";
|
|
1144
|
+
throw new Error(`Mastodon ${res.status}: ${message}`);
|
|
1145
|
+
}
|
|
1146
|
+
return { mediaId: String(data.id) };
|
|
1147
|
+
}
|
|
1148
|
+
async mentionsList(input = {}) {
|
|
1149
|
+
const notifications = await this.call("/api/v1/notifications", {
|
|
1150
|
+
query: { types: "mention", since_id: input.sinceId, limit: input.limit }
|
|
1151
|
+
});
|
|
1152
|
+
const items = notifications.filter((n) => n.type === "mention" && n.status).map((n) => ({
|
|
1153
|
+
id: n.status.id,
|
|
1154
|
+
text: stripHtml(n.status.content ?? ""),
|
|
1155
|
+
authorId: n.status.account?.id,
|
|
1156
|
+
authorHandle: n.status.account?.acct,
|
|
1157
|
+
createdAt: n.status.created_at
|
|
1158
|
+
}));
|
|
1159
|
+
return { items };
|
|
1160
|
+
}
|
|
1161
|
+
async analyticsPost(input) {
|
|
1162
|
+
const status = await this.call(`/api/v1/statuses/${input.id}`);
|
|
1163
|
+
return {
|
|
1164
|
+
metrics: {
|
|
1165
|
+
reblogsCount: status.reblogs_count ?? 0,
|
|
1166
|
+
favouritesCount: status.favourites_count ?? 0,
|
|
1167
|
+
repliesCount: status.replies_count ?? 0
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
function stripHtml(html) {
|
|
1173
|
+
return html.replace(/<[^>]*>/g, "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").trim();
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// connectors/bluesky/src/types/index.ts
|
|
1177
|
+
class BlueskyApiError extends Error {
|
|
1178
|
+
statusCode;
|
|
1179
|
+
errorCode;
|
|
1180
|
+
constructor(message, statusCode, errorCode) {
|
|
1181
|
+
super(message);
|
|
1182
|
+
this.name = "BlueskyApiError";
|
|
1183
|
+
this.statusCode = statusCode;
|
|
1184
|
+
this.errorCode = errorCode;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// connectors/bluesky/src/api/client.ts
|
|
1189
|
+
var DEFAULT_PDS = "https://bsky.social";
|
|
1190
|
+
|
|
1191
|
+
class BlueskyClient {
|
|
1192
|
+
identifier;
|
|
1193
|
+
appPassword;
|
|
1194
|
+
pds;
|
|
1195
|
+
session;
|
|
1196
|
+
constructor(config) {
|
|
1197
|
+
if (!config?.identifier || !config?.appPassword) {
|
|
1198
|
+
throw new Error("bluesky credentials require `identifier` and `appPassword`");
|
|
1199
|
+
}
|
|
1200
|
+
this.identifier = config.identifier;
|
|
1201
|
+
this.appPassword = config.appPassword;
|
|
1202
|
+
this.pds = (config.pds || DEFAULT_PDS).replace(/\/+$/, "");
|
|
1203
|
+
}
|
|
1204
|
+
async xrpc(nsid, options = {}) {
|
|
1205
|
+
const { method = "GET", params, body, auth = true, contentType, rawBody } = options;
|
|
1206
|
+
const url = new URL(`${this.pds}/xrpc/${nsid}`);
|
|
1207
|
+
if (params) {
|
|
1208
|
+
for (const [k, v] of Object.entries(params)) {
|
|
1209
|
+
if (v !== undefined && v !== null && v !== "")
|
|
1210
|
+
url.searchParams.append(k, String(v));
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
const headers = { Accept: "application/json" };
|
|
1214
|
+
if (auth) {
|
|
1215
|
+
const session = await this.ensureSession();
|
|
1216
|
+
headers.Authorization = `Bearer ${session.accessJwt}`;
|
|
1217
|
+
}
|
|
1218
|
+
const init = { method, headers };
|
|
1219
|
+
if (rawBody !== undefined) {
|
|
1220
|
+
headers["Content-Type"] = contentType || "application/octet-stream";
|
|
1221
|
+
init.body = rawBody;
|
|
1222
|
+
} else if (body !== undefined && method !== "GET") {
|
|
1223
|
+
headers["Content-Type"] = contentType || "application/json";
|
|
1224
|
+
init.body = JSON.stringify(body);
|
|
1225
|
+
}
|
|
1226
|
+
const response = await fetch(url.toString(), init);
|
|
1227
|
+
if (response.status === 204)
|
|
1228
|
+
return {};
|
|
1229
|
+
let data;
|
|
1230
|
+
const text = await response.text();
|
|
1231
|
+
if (text) {
|
|
1232
|
+
try {
|
|
1233
|
+
data = JSON.parse(text);
|
|
1234
|
+
} catch {
|
|
1235
|
+
data = text;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
if (!response.ok) {
|
|
1239
|
+
const errBody = typeof data === "object" && data !== null ? data : {};
|
|
1240
|
+
const message = errBody.message || errBody.error || response.statusText || "Request failed";
|
|
1241
|
+
throw new BlueskyApiError(message, response.status, errBody.error);
|
|
1242
|
+
}
|
|
1243
|
+
return data;
|
|
1244
|
+
}
|
|
1245
|
+
async ensureSession() {
|
|
1246
|
+
if (this.session)
|
|
1247
|
+
return this.session;
|
|
1248
|
+
const session = await this.xrpc("com.atproto.server.createSession", {
|
|
1249
|
+
method: "POST",
|
|
1250
|
+
auth: false,
|
|
1251
|
+
body: { identifier: this.identifier, password: this.appPassword }
|
|
1252
|
+
});
|
|
1253
|
+
this.session = session;
|
|
1254
|
+
return session;
|
|
1255
|
+
}
|
|
1256
|
+
async createRecord(collection, record) {
|
|
1257
|
+
const session = await this.ensureSession();
|
|
1258
|
+
return this.xrpc("com.atproto.repo.createRecord", {
|
|
1259
|
+
method: "POST",
|
|
1260
|
+
body: { repo: session.did, collection, record }
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
async deleteRecord(uri) {
|
|
1264
|
+
const session = await this.ensureSession();
|
|
1265
|
+
const parsed = parseAtUri(uri);
|
|
1266
|
+
await this.xrpc("com.atproto.repo.deleteRecord", {
|
|
1267
|
+
method: "POST",
|
|
1268
|
+
body: { repo: session.did, collection: parsed.collection, rkey: parsed.rkey }
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
async uploadBlob(data, mimeType) {
|
|
1272
|
+
return this.xrpc("com.atproto.repo.uploadBlob", {
|
|
1273
|
+
method: "POST",
|
|
1274
|
+
rawBody: data,
|
|
1275
|
+
contentType: mimeType || "application/octet-stream"
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
async listNotifications(options) {
|
|
1279
|
+
return this.xrpc("app.bsky.notification.listNotifications", {
|
|
1280
|
+
method: "GET",
|
|
1281
|
+
params: { limit: options?.limit, cursor: options?.cursor }
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
async getPosts(uris) {
|
|
1285
|
+
const url = new URL(`${this.pds}/xrpc/app.bsky.feed.getPosts`);
|
|
1286
|
+
for (const u of uris)
|
|
1287
|
+
url.searchParams.append("uris", u);
|
|
1288
|
+
const session = await this.ensureSession();
|
|
1289
|
+
const response = await fetch(url.toString(), {
|
|
1290
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${session.accessJwt}` }
|
|
1291
|
+
});
|
|
1292
|
+
const text = await response.text();
|
|
1293
|
+
const data = text ? JSON.parse(text) : {};
|
|
1294
|
+
if (!response.ok) {
|
|
1295
|
+
throw new BlueskyApiError(data?.message || data?.error || response.statusText, response.status, data?.error);
|
|
1296
|
+
}
|
|
1297
|
+
return data;
|
|
1298
|
+
}
|
|
1299
|
+
getSession() {
|
|
1300
|
+
return this.session;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
function parseAtUri(uri) {
|
|
1304
|
+
const match = /^at:\/\/([^/]+)\/([^/]+)\/(.+)$/.exec(uri);
|
|
1305
|
+
if (!match) {
|
|
1306
|
+
throw new Error(`Invalid at:// URI: ${uri}`);
|
|
1307
|
+
}
|
|
1308
|
+
return { repo: match[1], collection: match[2], rkey: match[3] };
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// connectors/bluesky/src/api/index.ts
|
|
1312
|
+
var POST_COLLECTION = "app.bsky.feed.post";
|
|
1313
|
+
|
|
1314
|
+
class Bluesky {
|
|
1315
|
+
client;
|
|
1316
|
+
constructor(config) {
|
|
1317
|
+
this.client = new BlueskyClient(config);
|
|
1318
|
+
}
|
|
1319
|
+
static fromEnv() {
|
|
1320
|
+
const identifier = process.env.BLUESKY_IDENTIFIER;
|
|
1321
|
+
const appPassword = process.env.BLUESKY_APP_PASSWORD;
|
|
1322
|
+
const pds = process.env.BLUESKY_PDS;
|
|
1323
|
+
if (!identifier || !appPassword) {
|
|
1324
|
+
throw new Error("BLUESKY_IDENTIFIER and BLUESKY_APP_PASSWORD environment variables are required");
|
|
1325
|
+
}
|
|
1326
|
+
return new Bluesky({ identifier, appPassword, pds });
|
|
1327
|
+
}
|
|
1328
|
+
async me() {
|
|
1329
|
+
return this.client.ensureSession();
|
|
1330
|
+
}
|
|
1331
|
+
async createPost(options) {
|
|
1332
|
+
const record = {
|
|
1333
|
+
$type: POST_COLLECTION,
|
|
1334
|
+
text: options.text,
|
|
1335
|
+
createdAt: options.createdAt || new Date().toISOString()
|
|
1336
|
+
};
|
|
1337
|
+
if (options.embed) {
|
|
1338
|
+
record.embed = options.embed;
|
|
1339
|
+
}
|
|
1340
|
+
if (options.replyToUri) {
|
|
1341
|
+
const { parent, root } = await this.resolveReplyRefs(options.replyToUri);
|
|
1342
|
+
record.reply = { root, parent };
|
|
1343
|
+
}
|
|
1344
|
+
return this.client.createRecord(POST_COLLECTION, record);
|
|
1345
|
+
}
|
|
1346
|
+
async deletePost(uri) {
|
|
1347
|
+
return this.client.deleteRecord(uri);
|
|
1348
|
+
}
|
|
1349
|
+
async uploadBlob(data, mimeType) {
|
|
1350
|
+
const res = await this.client.uploadBlob(data, mimeType);
|
|
1351
|
+
return res.blob;
|
|
1352
|
+
}
|
|
1353
|
+
async listNotifications(options) {
|
|
1354
|
+
return this.client.listNotifications(options);
|
|
1355
|
+
}
|
|
1356
|
+
async getPosts(uris) {
|
|
1357
|
+
return this.client.getPosts(uris);
|
|
1358
|
+
}
|
|
1359
|
+
getClient() {
|
|
1360
|
+
return this.client;
|
|
1361
|
+
}
|
|
1362
|
+
async resolveReplyRefs(parentUri) {
|
|
1363
|
+
parseAtUri(parentUri);
|
|
1364
|
+
const posts = await this.client.getPosts([parentUri]);
|
|
1365
|
+
const match = posts.posts.find((p) => p.uri === parentUri);
|
|
1366
|
+
if (!match || !match.cid) {
|
|
1367
|
+
throw new Error(`Cannot resolve parent post for reply: ${parentUri}`);
|
|
1368
|
+
}
|
|
1369
|
+
const parent = { uri: match.uri, cid: match.cid };
|
|
1370
|
+
const root = match.record?.reply?.root ?? parent;
|
|
1371
|
+
return { parent, root };
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/social/bluesky.ts
|
|
1376
|
+
function postUrl(handle, uri) {
|
|
1377
|
+
const m = /^at:\/\/[^/]+\/app\.bsky\.feed\.post\/(.+)$/.exec(uri);
|
|
1378
|
+
if (!m)
|
|
1379
|
+
return;
|
|
1380
|
+
return `https://bsky.app/profile/${handle}/post/${m[1]}`;
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
class BlueskyAdapter {
|
|
1384
|
+
bsky;
|
|
1385
|
+
handle;
|
|
1386
|
+
constructor(bsky) {
|
|
1387
|
+
this.bsky = bsky;
|
|
1388
|
+
}
|
|
1389
|
+
static fromCredentials(creds) {
|
|
1390
|
+
if (!creds || !creds.identifier || !creds.appPassword) {
|
|
1391
|
+
throw new Error("bluesky credentials require `identifier` and `appPassword`");
|
|
1392
|
+
}
|
|
1393
|
+
const bsky = new Bluesky({
|
|
1394
|
+
identifier: creds.identifier,
|
|
1395
|
+
appPassword: creds.appPassword,
|
|
1396
|
+
pds: creds.pds
|
|
1397
|
+
});
|
|
1398
|
+
return new BlueskyAdapter(bsky);
|
|
1399
|
+
}
|
|
1400
|
+
async resolveHandle() {
|
|
1401
|
+
if (this.handle)
|
|
1402
|
+
return this.handle;
|
|
1403
|
+
const me = await this.bsky.me();
|
|
1404
|
+
this.handle = me.handle;
|
|
1405
|
+
return this.handle;
|
|
1406
|
+
}
|
|
1407
|
+
async accountMe() {
|
|
1408
|
+
const me = await this.bsky.me();
|
|
1409
|
+
this.handle = me.handle;
|
|
1410
|
+
return {
|
|
1411
|
+
id: me.did,
|
|
1412
|
+
username: me.handle,
|
|
1413
|
+
displayName: me.handle,
|
|
1414
|
+
url: `https://bsky.app/profile/${me.handle}`
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
async postCreate(input) {
|
|
1418
|
+
let embed;
|
|
1419
|
+
if (input.mediaIds && input.mediaIds.length > 0) {
|
|
1420
|
+
const images = input.mediaIds.map((m) => {
|
|
1421
|
+
const blob = JSON.parse(m);
|
|
1422
|
+
return { alt: "", image: blob };
|
|
1423
|
+
});
|
|
1424
|
+
embed = { $type: "app.bsky.embed.images", images };
|
|
1425
|
+
}
|
|
1426
|
+
const res = await this.bsky.createPost({
|
|
1427
|
+
text: input.text,
|
|
1428
|
+
replyToUri: input.replyToId,
|
|
1429
|
+
embed
|
|
1430
|
+
});
|
|
1431
|
+
const handle = await this.resolveHandle();
|
|
1432
|
+
return { id: res.uri, url: postUrl(handle, res.uri) };
|
|
1433
|
+
}
|
|
1434
|
+
async postDelete(input) {
|
|
1435
|
+
await this.bsky.deletePost(input.id);
|
|
1436
|
+
return { id: input.id, deleted: true };
|
|
1437
|
+
}
|
|
1438
|
+
async mediaUpload(input) {
|
|
1439
|
+
const buffer = decodeBase64(input.dataBase64);
|
|
1440
|
+
const blob = await this.bsky.uploadBlob(buffer, input.mimeType);
|
|
1441
|
+
return { mediaId: JSON.stringify(blob) };
|
|
1442
|
+
}
|
|
1443
|
+
async mentionsList(input = {}) {
|
|
1444
|
+
const res = await this.bsky.listNotifications({ limit: input.limit });
|
|
1445
|
+
const items = res.notifications.filter((n) => n.reason === "mention" || n.reason === "reply").map((n) => ({
|
|
1446
|
+
id: n.uri,
|
|
1447
|
+
text: n.record?.text ?? "",
|
|
1448
|
+
authorId: n.author?.did,
|
|
1449
|
+
authorHandle: n.author?.handle,
|
|
1450
|
+
createdAt: n.record?.createdAt ?? n.indexedAt
|
|
1451
|
+
}));
|
|
1452
|
+
return { items };
|
|
1453
|
+
}
|
|
1454
|
+
async analyticsPost(input) {
|
|
1455
|
+
const res = await this.bsky.getPosts([input.id]);
|
|
1456
|
+
const post = res.posts.find((p) => p.uri === input.id) ?? res.posts[0];
|
|
1457
|
+
if (!post) {
|
|
1458
|
+
throw new ConnectorOperationNotSupported("bluesky", `analytics.post (post not found: ${input.id})`);
|
|
1459
|
+
}
|
|
1460
|
+
const metrics = {
|
|
1461
|
+
likeCount: post.likeCount ?? 0,
|
|
1462
|
+
repostCount: post.repostCount ?? 0,
|
|
1463
|
+
replyCount: post.replyCount ?? 0,
|
|
1464
|
+
quoteCount: post.quoteCount ?? 0
|
|
1465
|
+
};
|
|
1466
|
+
return { metrics };
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// src/social/index.ts
|
|
1471
|
+
var SUPPORTED_CONNECTORS = ["x", "mastodon", "bluesky"];
|
|
1472
|
+
var SUPPORTED_OPERATIONS = {
|
|
1473
|
+
x: ["account.me", "post.create", "post.delete", "media.upload", "mentions.list", "analytics.post"],
|
|
1474
|
+
mastodon: ["account.me", "post.create", "post.delete", "media.upload", "mentions.list", "analytics.post"],
|
|
1475
|
+
bluesky: ["account.me", "post.create", "post.delete", "media.upload", "mentions.list", "analytics.post"]
|
|
1476
|
+
};
|
|
1477
|
+
function listSocialConnectors() {
|
|
1478
|
+
return [...SUPPORTED_CONNECTORS];
|
|
1479
|
+
}
|
|
1480
|
+
function getSocialOperations(connector) {
|
|
1481
|
+
const ops = SUPPORTED_OPERATIONS[connector];
|
|
1482
|
+
if (!ops) {
|
|
1483
|
+
throw new ConnectorOperationNotSupported(connector, "*");
|
|
1484
|
+
}
|
|
1485
|
+
return [...ops];
|
|
1486
|
+
}
|
|
1487
|
+
function buildAdapter(connector, credentials) {
|
|
1488
|
+
switch (connector) {
|
|
1489
|
+
case "x":
|
|
1490
|
+
return XAdapter.fromCredentials(credentials);
|
|
1491
|
+
case "mastodon":
|
|
1492
|
+
return MastodonAdapter.fromCredentials(credentials);
|
|
1493
|
+
case "bluesky":
|
|
1494
|
+
return BlueskyAdapter.fromCredentials(credentials);
|
|
1495
|
+
default:
|
|
1496
|
+
throw new ConnectorOperationNotSupported(connector, "*");
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
async function runSocialOperation(args) {
|
|
1500
|
+
const { connector, operation, input = {}, credentials = {} } = args;
|
|
1501
|
+
if (!SUPPORTED_CONNECTORS.includes(connector)) {
|
|
1502
|
+
throw new ConnectorOperationNotSupported(connector, operation);
|
|
1503
|
+
}
|
|
1504
|
+
const supported = SUPPORTED_OPERATIONS[connector] ?? [];
|
|
1505
|
+
if (!supported.includes(operation)) {
|
|
1506
|
+
throw new ConnectorOperationNotSupported(connector, operation);
|
|
1507
|
+
}
|
|
1508
|
+
const adapter = buildAdapter(connector, credentials);
|
|
1509
|
+
switch (operation) {
|
|
1510
|
+
case "account.me":
|
|
1511
|
+
return adapter.accountMe(input);
|
|
1512
|
+
case "post.create":
|
|
1513
|
+
return adapter.postCreate(input);
|
|
1514
|
+
case "post.delete":
|
|
1515
|
+
return adapter.postDelete(input);
|
|
1516
|
+
case "media.upload":
|
|
1517
|
+
return adapter.mediaUpload(input);
|
|
1518
|
+
case "mentions.list":
|
|
1519
|
+
return adapter.mentionsList(input);
|
|
1520
|
+
case "analytics.post":
|
|
1521
|
+
return adapter.analyticsPost(input);
|
|
1522
|
+
default:
|
|
1523
|
+
throw new ConnectorOperationNotSupported(connector, operation);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
export {
|
|
1527
|
+
runSocialOperation,
|
|
1528
|
+
listSocialConnectors,
|
|
1529
|
+
getSocialOperations,
|
|
1530
|
+
ConnectorOperationNotSupported
|
|
1531
|
+
};
|