@opencoredev/social-sdk 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-request.d.ts +15 -0
- package/dist/cli-request.js +193 -0
- package/dist/cli.d.ts +4 -3
- package/dist/cli.js +19 -21
- package/dist/cloud/common.d.ts +7 -6
- package/dist/cloud/common.js +35 -54
- package/dist/cloud/lifecycle.js +31 -35
- package/dist/cloud/media.d.ts +2 -2
- package/dist/cloud/media.js +13 -3
- package/dist/cloud/outcomes.d.ts +4 -3
- package/dist/cloud/outcomes.js +8 -15
- package/dist/cloud/post-for-me.js +41 -49
- package/dist/cloud/zernio.js +58 -98
- package/dist/core/client.js +79 -99
- package/dist/core/fields.d.ts +14 -0
- package/dist/core/fields.js +14 -0
- package/dist/core/idempotency.d.ts +7 -2
- package/dist/core/idempotency.js +37 -20
- package/dist/core/pagination.js +8 -7
- package/dist/core/types.d.ts +3 -2
- package/dist/platforms/bluesky.d.ts +65 -1
- package/dist/platforms/bluesky.js +675 -276
- package/dist/platforms/instagram.d.ts +2 -0
- package/dist/platforms/instagram.js +130 -105
- package/dist/platforms/linkedin.d.ts +58 -1
- package/dist/platforms/linkedin.js +877 -107
- package/dist/platforms/threads.d.ts +13 -1
- package/dist/platforms/threads.js +204 -302
- package/dist/platforms/tiktok.d.ts +4 -0
- package/dist/platforms/tiktok.js +140 -124
- package/dist/platforms/webhook-adapter.d.ts +9 -0
- package/dist/platforms/webhook-adapter.js +24 -0
- package/dist/platforms/x-engagement.js +7 -12
- package/dist/platforms/x-stream.d.ts +83 -0
- package/dist/platforms/x-stream.js +350 -0
- package/dist/platforms/x.d.ts +72 -0
- package/dist/platforms/x.js +328 -119
- package/dist/platforms/youtube-upload.d.ts +1 -1
- package/dist/platforms/youtube-upload.js +6 -2
- package/dist/platforms/youtube.d.ts +28 -4
- package/dist/platforms/youtube.js +291 -133
- package/dist/server/bluesky-oauth.d.ts +177 -0
- package/dist/server/bluesky-oauth.js +1229 -0
- package/dist/server/connections.d.ts +14 -0
- package/dist/server/connections.js +10 -2
- package/dist/server/egress.d.ts +14 -0
- package/dist/server/egress.js +115 -0
- package/dist/server/oauth-internal.d.ts +6 -0
- package/dist/server/oauth-internal.js +66 -0
- package/dist/server/oauth.d.ts +1 -1
- package/dist/server/oauth.js +46 -99
- package/dist/server/webhooks.d.ts +136 -3
- package/dist/server/webhooks.js +639 -25
- package/dist/testing/index.js +14 -28
- package/dist/transport/http.d.ts +1 -1
- package/dist/transport/http.js +0 -1
- package/dist/transport/json.d.ts +7 -0
- package/dist/transport/json.js +32 -4
- package/dist/transport/upload.d.ts +1 -1
- package/dist/transport/upload.js +46 -38
- package/dist/transport/validation.d.ts +16 -5
- package/dist/transport/validation.js +29 -7
- package/package.json +2 -2
package/dist/platforms/x.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
/* oxlint-disable anti-slop/no-conditional-empty-object-spread, anti-slop/no-runtime-typeof, anti-slop/require-readable-spacing, anti-slop/require-safety-comment-for-type-assertion -- validated external boundary or fixture contract. */
|
|
2
1
|
import { remainingBudget } from "../transport/budget.js";
|
|
2
|
+
import { definedFields } from "../core/fields.js";
|
|
3
3
|
import { isValidXText } from "./x-text.js";
|
|
4
4
|
import { defineAdapter } from "../core/adapter.js";
|
|
5
5
|
import { connectedAccountRef, profileRef } from "../core/types.js";
|
|
6
6
|
import { SocialError } from "../core/errors.js";
|
|
7
|
-
import { managedHttp, publicFields } from "../cloud/common.js";
|
|
7
|
+
import { managedHttp, optionsObject, publicFields } from "../cloud/common.js";
|
|
8
|
+
import { verifyXWebhook } from "../server/webhooks.js";
|
|
9
|
+
import { directWebhooks, webhookCapability } from "./webhook-adapter.js";
|
|
8
10
|
import { createHttp, HttpError } from "../transport/http.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// 53-bit conversation ID hashes, 11 base36 characters each. listConversations returns at
|
|
13
|
-
// most 1,200 distinct conversations, which keeps its cursor well under the client's 16,384
|
|
14
|
-
// character cursor limit.
|
|
11
|
+
import { isJsonValue } from "../transport/json.js";
|
|
12
|
+
import { parseRulesUpdate, parseStreamRule, readFilteredStream, validateRuleIds, validateRuleInput, } from "./x-stream.js";
|
|
13
|
+
import { array, isString, object, optionalBoolean, optionalNumber, optionalString, string, } from "../transport/validation.js";
|
|
15
14
|
const conversationHashWidth = 11;
|
|
16
15
|
const maxConversationHashes = 1200;
|
|
16
|
+
const replyFields = [
|
|
17
|
+
"id",
|
|
18
|
+
"text",
|
|
19
|
+
"author_id",
|
|
20
|
+
"created_at",
|
|
21
|
+
"conversation_id",
|
|
22
|
+
"in_reply_to_user_id",
|
|
23
|
+
"referenced_tweets",
|
|
24
|
+
"public_metrics",
|
|
25
|
+
];
|
|
17
26
|
function conversationHash(id) {
|
|
18
27
|
let h1 = 0xdeadbeef;
|
|
19
28
|
let h2 = 0x41c6ce57;
|
|
@@ -28,11 +37,11 @@ function conversationHash(id) {
|
|
|
28
37
|
return value.toString(36).padStart(conversationHashWidth, "0");
|
|
29
38
|
}
|
|
30
39
|
export { xLike, xUnlike } from "./x-engagement.js";
|
|
40
|
+
export { xStreamDefaultStallTimeoutMs, } from "./x-stream.js";
|
|
31
41
|
export function x(options) {
|
|
32
42
|
const request = managedHttp("https://api.x.com", {
|
|
33
43
|
apiKey: options.auth.accessToken ?? "app-auth-placeholder",
|
|
34
|
-
|
|
35
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
44
|
+
...definedFields({ fetch: options.fetch }),
|
|
36
45
|
});
|
|
37
46
|
const appRequest = () => {
|
|
38
47
|
if (!options.appBearerToken?.trim())
|
|
@@ -43,8 +52,7 @@ export function x(options) {
|
|
|
43
52
|
});
|
|
44
53
|
return managedHttp("https://api.x.com", {
|
|
45
54
|
apiKey: options.appBearerToken,
|
|
46
|
-
|
|
47
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
55
|
+
...definedFields({ fetch: options.fetch }),
|
|
48
56
|
});
|
|
49
57
|
};
|
|
50
58
|
const requireUserToken = (operation) => {
|
|
@@ -61,7 +69,7 @@ export function x(options) {
|
|
|
61
69
|
: options.auth.accessToken?.trim()
|
|
62
70
|
? request
|
|
63
71
|
: appRequest();
|
|
64
|
-
const http = createHttp(
|
|
72
|
+
const http = createHttp(definedFields({ fetch: options.fetch }));
|
|
65
73
|
const now = () => (options.clock?.() ?? new Date()).toISOString();
|
|
66
74
|
const authorize = (ref, context) => {
|
|
67
75
|
if (ref.backend !== context.backendInstance ||
|
|
@@ -90,15 +98,7 @@ export function x(options) {
|
|
|
90
98
|
accountId: options.auth.userId,
|
|
91
99
|
},
|
|
92
100
|
displayName: string(user["name"]),
|
|
93
|
-
|
|
94
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
95
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- provider payload is validated at this adapter boundary.
|
|
96
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
97
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
98
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
99
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
100
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated external boundary or fixture contract.
|
|
101
|
-
...(typeof user["username"] === "string" ? { handle: user["username"] } : {}),
|
|
101
|
+
...definedFields({ handle: optionalString(user["username"]) }),
|
|
102
102
|
status: "connected",
|
|
103
103
|
};
|
|
104
104
|
}
|
|
@@ -113,7 +113,6 @@ export function x(options) {
|
|
|
113
113
|
operation: "posts.read",
|
|
114
114
|
message: "X post identity or author does not match the declared reference.",
|
|
115
115
|
});
|
|
116
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
117
116
|
return result;
|
|
118
117
|
}
|
|
119
118
|
async function listPosts(account, input, context) {
|
|
@@ -127,10 +126,10 @@ export function x(options) {
|
|
|
127
126
|
});
|
|
128
127
|
const result = object(await request(`/2/users/${encodeURIComponent(account.accountId)}/tweets`, context, undefined, {
|
|
129
128
|
"tweet.fields": "id,text,author_id,created_at,conversation_id",
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
129
|
+
...definedFields({
|
|
130
|
+
pagination_token: input.cursor,
|
|
131
|
+
max_results: input.limit?.toString(),
|
|
132
|
+
}),
|
|
134
133
|
}));
|
|
135
134
|
const items = (result["data"] === undefined ? [] : array(result["data"])).map((entry) => {
|
|
136
135
|
const row = object(entry);
|
|
@@ -147,8 +146,7 @@ export function x(options) {
|
|
|
147
146
|
const nextCursor = optionalString(meta["next_token"]);
|
|
148
147
|
return {
|
|
149
148
|
items,
|
|
150
|
-
|
|
151
|
-
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
149
|
+
...definedFields({ nextCursor }),
|
|
152
150
|
};
|
|
153
151
|
}
|
|
154
152
|
async function searchPosts(account, input, context, nativeInput) {
|
|
@@ -207,28 +205,16 @@ export function x(options) {
|
|
|
207
205
|
]
|
|
208
206
|
.filter((field, index, fields) => fields.indexOf(field) === index)
|
|
209
207
|
.join(","),
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
221
|
-
...(nativeInput?.expansions === undefined
|
|
222
|
-
? {}
|
|
223
|
-
: { expansions: nativeInput.expansions.join(",") }),
|
|
224
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
225
|
-
...(nativeInput?.userFields === undefined
|
|
226
|
-
? {}
|
|
227
|
-
: { "user.fields": nativeInput.userFields.join(",") }),
|
|
228
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
229
|
-
...(nativeInput?.mediaFields === undefined
|
|
230
|
-
? {}
|
|
231
|
-
: { "media.fields": nativeInput.mediaFields.join(",") }),
|
|
208
|
+
...definedFields({
|
|
209
|
+
next_token: input.cursor,
|
|
210
|
+
max_results: input.limit?.toString(),
|
|
211
|
+
start_time: input.startTime,
|
|
212
|
+
end_time: input.endTime,
|
|
213
|
+
sort_order: nativeInput?.sortOrder,
|
|
214
|
+
expansions: nativeInput?.expansions?.join(","),
|
|
215
|
+
"user.fields": nativeInput?.userFields?.join(","),
|
|
216
|
+
"media.fields": nativeInput?.mediaFields?.join(","),
|
|
217
|
+
}),
|
|
232
218
|
}));
|
|
233
219
|
const requestedFields = nativeInput?.tweetFields ?? [
|
|
234
220
|
"id",
|
|
@@ -244,23 +230,97 @@ export function x(options) {
|
|
|
244
230
|
];
|
|
245
231
|
const items = (result["data"] === undefined ? [] : array(result["data"])).map((entry) => {
|
|
246
232
|
const row = object(entry);
|
|
247
|
-
|
|
233
|
+
const picked = {};
|
|
234
|
+
// A Set keeps first-seen order, so "id" and "text" lead and repeated fields appear once.
|
|
235
|
+
for (const field of new Set(["id", "text", ...requestedFields])) {
|
|
236
|
+
const value = row[field];
|
|
237
|
+
if (value !== undefined)
|
|
238
|
+
picked[field] = value;
|
|
239
|
+
}
|
|
240
|
+
return picked;
|
|
248
241
|
});
|
|
249
242
|
const meta = result["meta"] === undefined ? {} : object(result["meta"]);
|
|
250
243
|
const nextCursor = optionalString(meta["next_token"]);
|
|
251
244
|
const includes = result["includes"];
|
|
252
245
|
return {
|
|
253
246
|
items,
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
: {
|
|
259
|
-
// SAFETY: X's `includes` member is a JSON object validated by the transport boundary.
|
|
260
|
-
metadata: { includes: object(includes) },
|
|
261
|
-
}),
|
|
247
|
+
...definedFields({
|
|
248
|
+
nextCursor,
|
|
249
|
+
metadata: includes === undefined ? undefined : { includes: object(includes) },
|
|
250
|
+
}),
|
|
262
251
|
};
|
|
263
252
|
}
|
|
253
|
+
// Replies come from recent search with the standalone `conversation_id:` operator, which is
|
|
254
|
+
// the method X documents for reading a conversation. Sources, accessed 2026-09-24:
|
|
255
|
+
// https://docs.x.com/x-api/fundamentals/conversation-id
|
|
256
|
+
// https://docs.x.com/x-api/posts/search/integrate/operators
|
|
257
|
+
// https://docs.x.com/x-api/posts/search-recent-posts (max_results 10-100, next_token)
|
|
258
|
+
// https://docs.x.com/x-api/posts/search/introduction (recent search covers the last 7 days)
|
|
259
|
+
// https://docs.x.com/x-api/fundamentals/rate-limits (450/15min per app, 300/15min per user)
|
|
260
|
+
async function listReplies(post, input, context) {
|
|
261
|
+
authorize(post, context);
|
|
262
|
+
if (!/^[0-9]{1,19}$/.test(post.postId))
|
|
263
|
+
throw new SocialError({
|
|
264
|
+
code: "invalid_input",
|
|
265
|
+
operation: "comments.read",
|
|
266
|
+
message: "X post IDs must be numeric.",
|
|
267
|
+
});
|
|
268
|
+
if (input.limit !== undefined &&
|
|
269
|
+
(!Number.isSafeInteger(input.limit) || input.limit < 10 || input.limit > 100))
|
|
270
|
+
throw new SocialError({
|
|
271
|
+
code: "invalid_input",
|
|
272
|
+
operation: "comments.read",
|
|
273
|
+
message: "X reply limits must be integers from 10 through 100.",
|
|
274
|
+
});
|
|
275
|
+
const query = {
|
|
276
|
+
query: `conversation_id:${post.postId}`,
|
|
277
|
+
"tweet.fields": replyFields.join(","),
|
|
278
|
+
};
|
|
279
|
+
if (input.cursor !== undefined) {
|
|
280
|
+
query["next_token"] = input.cursor;
|
|
281
|
+
}
|
|
282
|
+
if (input.limit !== undefined) {
|
|
283
|
+
query["max_results"] = String(input.limit);
|
|
284
|
+
}
|
|
285
|
+
const result = object(await (options.auth.accessToken?.trim() ? request : appRequest())("/2/tweets/search/recent", context, undefined, query));
|
|
286
|
+
const items = (result["data"] === undefined ? [] : array(result["data"])).flatMap((entry) => {
|
|
287
|
+
const row = object(entry);
|
|
288
|
+
const id = string(row["id"]);
|
|
289
|
+
if (row["conversation_id"] !== post.postId)
|
|
290
|
+
throw new SocialError({
|
|
291
|
+
code: "unauthorized",
|
|
292
|
+
operation: "comments.read",
|
|
293
|
+
message: "X returned a post from a different conversation.",
|
|
294
|
+
});
|
|
295
|
+
// The conversation root shares its own conversation_id; it is not a reply.
|
|
296
|
+
if (id === post.postId)
|
|
297
|
+
return [];
|
|
298
|
+
const references = row["referenced_tweets"] === undefined
|
|
299
|
+
? undefined
|
|
300
|
+
: array(row["referenced_tweets"]).map((reference) => {
|
|
301
|
+
const item = object(reference);
|
|
302
|
+
return { type: string(item["type"]), id: string(item["id"]) };
|
|
303
|
+
});
|
|
304
|
+
const counts = row["public_metrics"] === undefined ? undefined : object(row["public_metrics"]);
|
|
305
|
+
const metrics = counts === undefined
|
|
306
|
+
? undefined
|
|
307
|
+
: Object.fromEntries(Object.keys(counts).flatMap((name) => {
|
|
308
|
+
const value = optionalNumber(counts[name]);
|
|
309
|
+
return value === undefined ? [] : [[name, value]];
|
|
310
|
+
}));
|
|
311
|
+
const itemFields = definedFields({
|
|
312
|
+
...publicFields(row, replyFields),
|
|
313
|
+
referenced_tweets: references,
|
|
314
|
+
public_metrics: metrics,
|
|
315
|
+
});
|
|
316
|
+
return [itemFields];
|
|
317
|
+
});
|
|
318
|
+
const meta = result["meta"] === undefined ? {} : object(result["meta"]);
|
|
319
|
+
const nextCursor = optionalString(meta["next_token"]);
|
|
320
|
+
if (nextCursor === undefined)
|
|
321
|
+
return { items };
|
|
322
|
+
return { items, nextCursor };
|
|
323
|
+
}
|
|
264
324
|
async function pageRequest(path, account, input, context, query = {}, fields = "id,name,username,description,created_at,public_metrics", resource = "users") {
|
|
265
325
|
authorize(account, context);
|
|
266
326
|
const isTweets = resource === "tweets";
|
|
@@ -285,13 +345,12 @@ export function x(options) {
|
|
|
285
345
|
: fields
|
|
286
346
|
? { "user.fields": fields }
|
|
287
347
|
: {}),
|
|
288
|
-
...(
|
|
289
|
-
...(input.limit === undefined ? {} : { max_results: String(input.limit) }),
|
|
348
|
+
...definedFields({ pagination_token: input.cursor, max_results: input.limit?.toString() }),
|
|
290
349
|
}));
|
|
291
|
-
const items = (result["data"] === undefined ? [] : array(result["data"])).map(
|
|
350
|
+
const items = (result["data"] === undefined ? [] : array(result["data"])).map(object);
|
|
292
351
|
const meta = result["meta"] === undefined ? {} : object(result["meta"]);
|
|
293
352
|
const nextCursor = optionalString(meta["next_token"]);
|
|
294
|
-
return { items, ...(
|
|
353
|
+
return { items, ...definedFields({ nextCursor }) };
|
|
295
354
|
}
|
|
296
355
|
async function getAccountMetrics(account, context) {
|
|
297
356
|
authorize(account, context);
|
|
@@ -341,8 +400,7 @@ export function x(options) {
|
|
|
341
400
|
"tweet.fields": "conversation_id",
|
|
342
401
|
}))["data"]);
|
|
343
402
|
if (root["id"] !== ref.postId ||
|
|
344
|
-
|
|
345
|
-
(typeof parent["conversation_id"] === "string" &&
|
|
403
|
+
(isString(parent["conversation_id"]) &&
|
|
346
404
|
parent["conversation_id"] !== root["conversation_id"]))
|
|
347
405
|
throw new SocialError({
|
|
348
406
|
code: "unauthorized",
|
|
@@ -372,8 +430,7 @@ export function x(options) {
|
|
|
372
430
|
headers: { Authorization: `Bearer ${options.auth.accessToken}` },
|
|
373
431
|
body,
|
|
374
432
|
timeoutMs: remainingBudget(context),
|
|
375
|
-
|
|
376
|
-
...(context.signal ? { signal: context.signal } : {}),
|
|
433
|
+
...definedFields({ signal: context.signal }),
|
|
377
434
|
});
|
|
378
435
|
}
|
|
379
436
|
catch (error) {
|
|
@@ -484,11 +541,11 @@ export function x(options) {
|
|
|
484
541
|
Authorization: `Bearer ${options.auth.accessToken}`,
|
|
485
542
|
"Content-Type": "application/json",
|
|
486
543
|
},
|
|
487
|
-
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- FINALIZE carries no body.
|
|
488
|
-
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
489
544
|
timeoutMs: remainingBudget(context),
|
|
490
|
-
|
|
491
|
-
|
|
545
|
+
...definedFields({
|
|
546
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
547
|
+
signal: context.signal,
|
|
548
|
+
}),
|
|
492
549
|
});
|
|
493
550
|
}
|
|
494
551
|
catch (error) {
|
|
@@ -510,8 +567,7 @@ export function x(options) {
|
|
|
510
567
|
headers: { Authorization: `Bearer ${options.auth.accessToken}` },
|
|
511
568
|
body,
|
|
512
569
|
timeoutMs: remainingBudget(context),
|
|
513
|
-
|
|
514
|
-
...(context.signal ? { signal: context.signal } : {}),
|
|
570
|
+
...definedFields({ signal: context.signal }),
|
|
515
571
|
});
|
|
516
572
|
}
|
|
517
573
|
catch (error) {
|
|
@@ -529,8 +585,7 @@ export function x(options) {
|
|
|
529
585
|
method: "GET",
|
|
530
586
|
headers: { Authorization: `Bearer ${options.auth.accessToken}` },
|
|
531
587
|
timeoutMs: remainingBudget(context),
|
|
532
|
-
|
|
533
|
-
...(context.signal ? { signal: context.signal } : {}),
|
|
588
|
+
...definedFields({ signal: context.signal }),
|
|
534
589
|
});
|
|
535
590
|
}
|
|
536
591
|
catch (error) {
|
|
@@ -692,6 +747,7 @@ export function x(options) {
|
|
|
692
747
|
apiRevision: "X API v2 / OpenAPI 2.168",
|
|
693
748
|
runtime: ["node22", "node24", "bun"],
|
|
694
749
|
capabilities: [
|
|
750
|
+
webhookCapability("x", "Verifies X-Twitter-Webhooks-Signature-OAuth2 or the legacy X-Twitter-Webhooks-Signature and decodes Account Activity deliveries. Answer the CRC GET with answerXWebhookChallenge."),
|
|
695
751
|
{
|
|
696
752
|
platform: "x",
|
|
697
753
|
operation: "posts.publish",
|
|
@@ -744,6 +800,14 @@ export function x(options) {
|
|
|
744
800
|
availability: "available",
|
|
745
801
|
requiredScopes: ["tweet.read", "tweet.write", "users.read"],
|
|
746
802
|
},
|
|
803
|
+
{
|
|
804
|
+
platform: "x",
|
|
805
|
+
operation: "posts.update",
|
|
806
|
+
availability: "available",
|
|
807
|
+
formats: ["text"],
|
|
808
|
+
requiredScopes: ["tweet.read", "tweet.write", "users.read"],
|
|
809
|
+
notes: "Text-only edit through native.updatePost. X requires X Premium, the account's own post, and a recent post within X's edit window and edit count. Polls, replies to others, reposts, and scheduled posts are not editable. Each edit creates a new post ID.",
|
|
810
|
+
},
|
|
747
811
|
{
|
|
748
812
|
platform: "x",
|
|
749
813
|
operation: "graph.read",
|
|
@@ -821,6 +885,13 @@ export function x(options) {
|
|
|
821
885
|
availability: "available",
|
|
822
886
|
requiredScopes: ["dm.write"],
|
|
823
887
|
},
|
|
888
|
+
{
|
|
889
|
+
platform: "x",
|
|
890
|
+
operation: "comments.read",
|
|
891
|
+
availability: "available",
|
|
892
|
+
requiredScopes: ["tweet.read", "users.read"],
|
|
893
|
+
notes: "Replies come from recent search with conversation_id, so only replies from the last 7 days are returned. Pass the conversation's root post. Page limits are 10-100. Recent search allows 450 requests per 15 minutes per app and 300 per user, and X bills post reads under the app's plan.",
|
|
894
|
+
},
|
|
824
895
|
{
|
|
825
896
|
platform: "x",
|
|
826
897
|
operation: "search.posts",
|
|
@@ -850,6 +921,12 @@ export function x(options) {
|
|
|
850
921
|
requiredScopes: ["dm.read", "users.read", "tweet.read"],
|
|
851
922
|
notes: "Requires a user-context token and an X API tier that includes Direct Messages.",
|
|
852
923
|
},
|
|
924
|
+
{
|
|
925
|
+
platform: "x",
|
|
926
|
+
operation: "notifications.read",
|
|
927
|
+
availability: "unsupported-by-platform",
|
|
928
|
+
notes: "X API v2 has no notifications list endpoint. Use mentions.read, or the Account Activity or X Activity API webhooks and streams.",
|
|
929
|
+
},
|
|
853
930
|
{
|
|
854
931
|
platform: "x",
|
|
855
932
|
operation: "messages.write",
|
|
@@ -857,11 +934,30 @@ export function x(options) {
|
|
|
857
934
|
requiredScopes: ["dm.write", "dm.read", "users.read", "tweet.read"],
|
|
858
935
|
notes: "Requires a user-context token and an X API tier that includes Direct Messages.",
|
|
859
936
|
},
|
|
937
|
+
{
|
|
938
|
+
platform: "x",
|
|
939
|
+
operation: "comments.moderate",
|
|
940
|
+
availability: "available",
|
|
941
|
+
requiredScopes: ["tweet.moderate.write", "tweet.read", "users.read"],
|
|
942
|
+
notes: "Native hideReply hides or unhides replies in conversations the authenticated user started. Requires a user-context token.",
|
|
943
|
+
},
|
|
860
944
|
{
|
|
861
945
|
platform: "x",
|
|
862
946
|
operation: "streams.read",
|
|
947
|
+
availability: "available",
|
|
948
|
+
notes: "Filtered stream via native stream, listStreamRules, addStreamRules, and deleteStreamRules with the app-only appBearerToken. Needs X API pay-per-use (1 connection, 1,000 rules of up to 1,024 characters) or Enterprise (multiple connections, 25,000+ rules of up to 2,048 characters). backfillMinutes and startTime/endTime recovery need Enterprise. One caller-controlled connection per iteration; no automatic reconnect.",
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
platform: "x",
|
|
952
|
+
operation: "posts.schedule",
|
|
863
953
|
availability: "not-implemented-by-adapter",
|
|
864
|
-
notes: "
|
|
954
|
+
notes: "X API v2 has no scheduled-post field; POST /2/tweets publishes immediately. X Ads API scheduled Tweets (ads-api.x.com/12/accounts/:account_id/scheduled_tweets) need Ads API approval, an ads account, and OAuth 1.0a-signed requests, which this OAuth 2.0 adapter does not implement (https://docs.x.com/x-ads-api/fundamentals/making-authenticated-requests, checked 2026-09-24). Scheduled Tweets default to nullcast=true (promoted-only, not on the public timeline); organic nullcast=false Tweets can only be created by the ads account's full promotable user (https://docs.x.com/x-ads-api/creatives, checked 2026-09-24). Use an application-owned job runner to publish at a chosen time.",
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
platform: "x",
|
|
958
|
+
operation: "profile.update",
|
|
959
|
+
availability: "unsupported-by-platform",
|
|
960
|
+
notes: "X API v2 (OpenAPI 2.168, https://docs.x.com/openapi.json, checked 2026-09-24) has no endpoint that writes the user's profile. The v1.1 POST account/update_profile reference is no longer published on docs.x.com (its developer.x.com URL redirects to https://docs.x.com/overview), and v1.1 user writes require OAuth 1.0a.",
|
|
865
961
|
},
|
|
866
962
|
],
|
|
867
963
|
},
|
|
@@ -890,16 +986,17 @@ export function x(options) {
|
|
|
890
986
|
accountId: account.accountId,
|
|
891
987
|
profileId: id,
|
|
892
988
|
}),
|
|
893
|
-
...(
|
|
894
|
-
|
|
895
|
-
|
|
989
|
+
...definedFields({
|
|
990
|
+
displayName: optionalString(user["name"]),
|
|
991
|
+
handle: optionalString(user["username"]),
|
|
992
|
+
bio: optionalString(user["description"]),
|
|
993
|
+
}),
|
|
896
994
|
native: user,
|
|
897
995
|
};
|
|
898
996
|
},
|
|
899
997
|
async listRelationships(account, input, context) {
|
|
900
998
|
const pagination = {
|
|
901
|
-
...(
|
|
902
|
-
...(input.limit === undefined ? {} : { limit: input.limit }),
|
|
999
|
+
...definedFields({ cursor: input.cursor, limit: input.limit }),
|
|
903
1000
|
};
|
|
904
1001
|
const result = input.kind === "following"
|
|
905
1002
|
? await nativeAdapter.following({
|
|
@@ -944,7 +1041,7 @@ export function x(options) {
|
|
|
944
1041
|
: input.kind,
|
|
945
1042
|
};
|
|
946
1043
|
}),
|
|
947
|
-
...(
|
|
1044
|
+
...definedFields({ nextCursor: result.nextCursor }),
|
|
948
1045
|
};
|
|
949
1046
|
},
|
|
950
1047
|
async follow(target, context) {
|
|
@@ -1013,14 +1110,14 @@ export function x(options) {
|
|
|
1013
1110
|
if (text && !isValidXText(text))
|
|
1014
1111
|
fail("x.text", "Text exceeds X's weighted 280-character limit or contains invalid characters.");
|
|
1015
1112
|
if (target.schedule || target.content.link)
|
|
1016
|
-
fail("x.operation", "
|
|
1113
|
+
fail("x.operation", "X API v2 cannot schedule posts; use an application job runner. Place URLs explicitly in text.");
|
|
1017
1114
|
if (target.replyTo &&
|
|
1018
1115
|
(target.replyTo.platform !== "x" ||
|
|
1019
1116
|
target.replyTo.backend !== target.account.backend ||
|
|
1020
1117
|
target.replyTo.accountId !== target.account.accountId ||
|
|
1021
1118
|
target.replyTo.kind !== "platform-post"))
|
|
1022
1119
|
fail("x.reply", "Use a platform-post reply reference authorized for this account and backend.");
|
|
1023
|
-
const settings =
|
|
1120
|
+
const settings = optionsObject(target);
|
|
1024
1121
|
if (Object.keys(settings).some((key) => key !== "replySettings") ||
|
|
1025
1122
|
(settings["replySettings"] !== undefined &&
|
|
1026
1123
|
!["everyone", "following", "mentionedUsers"].includes(String(settings["replySettings"]))))
|
|
@@ -1063,18 +1160,15 @@ export function x(options) {
|
|
|
1063
1160
|
const ids = [];
|
|
1064
1161
|
for (const media of target.content.media ?? [])
|
|
1065
1162
|
ids.push(await uploadXMedia(media, context));
|
|
1066
|
-
const
|
|
1163
|
+
const replySettings = optionsObject(target)["replySettings"];
|
|
1067
1164
|
const hasVideo = (target.content.media ?? []).some((media) => chunkedCategory(media) === "tweet_video");
|
|
1068
1165
|
try {
|
|
1069
1166
|
return await createPost(target.account, target.content.text ?? "", context, {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
...(settings["replySettings"] && settings["replySettings"] !== "everyone"
|
|
1076
|
-
? { reply_settings: string(settings["replySettings"]) }
|
|
1077
|
-
: {}),
|
|
1167
|
+
...definedFields({
|
|
1168
|
+
media: ids.length ? { media_ids: ids } : undefined,
|
|
1169
|
+
reply: target.replyTo ? { in_reply_to_tweet_id: target.replyTo.postId } : undefined,
|
|
1170
|
+
reply_settings: replySettings && replySettings !== "everyone" ? string(replySettings) : undefined,
|
|
1171
|
+
}),
|
|
1078
1172
|
}, target.targetIndex);
|
|
1079
1173
|
}
|
|
1080
1174
|
catch (error) {
|
|
@@ -1116,12 +1210,8 @@ export function x(options) {
|
|
|
1116
1210
|
},
|
|
1117
1211
|
},
|
|
1118
1212
|
comments: {
|
|
1119
|
-
async list() {
|
|
1120
|
-
|
|
1121
|
-
code: "unsupported_capability",
|
|
1122
|
-
operation: "comments.read",
|
|
1123
|
-
message: "X reply search is not implemented in this slice.",
|
|
1124
|
-
});
|
|
1213
|
+
async list(ref, input, context) {
|
|
1214
|
+
return listReplies(ref, input, context);
|
|
1125
1215
|
},
|
|
1126
1216
|
async reply(ref, content, context) {
|
|
1127
1217
|
await validateReplyParent(ref, context);
|
|
@@ -1162,7 +1252,10 @@ export function x(options) {
|
|
|
1162
1252
|
const seen = [];
|
|
1163
1253
|
if (input.cursor !== undefined) {
|
|
1164
1254
|
try {
|
|
1165
|
-
const
|
|
1255
|
+
const parsed = JSON.parse(input.cursor);
|
|
1256
|
+
if (!isJsonValue(parsed))
|
|
1257
|
+
throw new Error("bad cursor");
|
|
1258
|
+
const state = object(parsed);
|
|
1166
1259
|
eventCursor = optionalString(state["c"]);
|
|
1167
1260
|
const hashes = string(state["s"]);
|
|
1168
1261
|
if (hashes.length % conversationHashWidth !== 0)
|
|
@@ -1188,8 +1281,7 @@ export function x(options) {
|
|
|
1188
1281
|
for (let fetches = 0; fetches < 10; fetches++) {
|
|
1189
1282
|
const page = await nativeAdapter.listDirectMessages({
|
|
1190
1283
|
account,
|
|
1191
|
-
...(
|
|
1192
|
-
...(input.limit === undefined ? {} : { limit: input.limit }),
|
|
1284
|
+
...definedFields({ cursor: eventCursor, limit: input.limit }),
|
|
1193
1285
|
context,
|
|
1194
1286
|
});
|
|
1195
1287
|
for (const event of page.items) {
|
|
@@ -1225,8 +1317,7 @@ export function x(options) {
|
|
|
1225
1317
|
accountId: conversation.accountId,
|
|
1226
1318
|
}),
|
|
1227
1319
|
conversationId: conversation.conversationId,
|
|
1228
|
-
...(
|
|
1229
|
-
...(input.limit === undefined ? {} : { limit: input.limit }),
|
|
1320
|
+
...definedFields({ cursor: input.cursor, limit: input.limit }),
|
|
1230
1321
|
context,
|
|
1231
1322
|
});
|
|
1232
1323
|
},
|
|
@@ -1280,6 +1371,7 @@ export function x(options) {
|
|
|
1280
1371
|
});
|
|
1281
1372
|
},
|
|
1282
1373
|
},
|
|
1374
|
+
webhooks: directWebhooks("x", (input) => verifyXWebhook({ ...input, secret: options.webhookSecret ?? "" }), now),
|
|
1283
1375
|
native: (nativeAdapter = {
|
|
1284
1376
|
async searchRecentPosts({ account, search, context }) {
|
|
1285
1377
|
return searchPosts(account, { ...search, scope: "recent" }, context, search);
|
|
@@ -1290,20 +1382,65 @@ export function x(options) {
|
|
|
1290
1382
|
readPost,
|
|
1291
1383
|
async repost({ account, postId, context }) {
|
|
1292
1384
|
authorize(account, context);
|
|
1293
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
1294
1385
|
return object(await request(`/2/users/${encodeURIComponent(account.accountId)}/retweets`, context, {
|
|
1295
1386
|
tweet_id: postId,
|
|
1296
1387
|
}));
|
|
1297
1388
|
},
|
|
1298
1389
|
async quote({ account, text, quotedPostId, context }) {
|
|
1299
1390
|
authorize(account, context);
|
|
1300
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
1301
1391
|
return object(await request("/2/tweets", context, { text, quote_tweet_id: quotedPostId }));
|
|
1302
1392
|
},
|
|
1303
1393
|
async deletePost({ account, postId, context }) {
|
|
1304
1394
|
authorize(account, context);
|
|
1305
1395
|
await request(`/2/tweets/${encodeURIComponent(postId)}`, context, undefined, {}, "DELETE");
|
|
1306
1396
|
},
|
|
1397
|
+
async updatePost({ account, postId, text, context }) {
|
|
1398
|
+
authorize(account, context);
|
|
1399
|
+
if (!/^[0-9]{1,19}$/.test(postId))
|
|
1400
|
+
throw new SocialError({
|
|
1401
|
+
code: "invalid_input",
|
|
1402
|
+
operation: "posts.update",
|
|
1403
|
+
message: "X post edits require a numeric post ID.",
|
|
1404
|
+
});
|
|
1405
|
+
if (!text || !isValidXText(text))
|
|
1406
|
+
throw new SocialError({
|
|
1407
|
+
code: "invalid_input",
|
|
1408
|
+
operation: "posts.update",
|
|
1409
|
+
message: "Edited text is empty, exceeds X's weighted 280-character limit, or contains invalid characters.",
|
|
1410
|
+
});
|
|
1411
|
+
const result = object(await request("/2/tweets", context, {
|
|
1412
|
+
text,
|
|
1413
|
+
edit_options: { previous_post_id: postId },
|
|
1414
|
+
}));
|
|
1415
|
+
const data = result["data"] === undefined ? {} : object(result["data"]);
|
|
1416
|
+
const id = optionalString(data["id"]);
|
|
1417
|
+
if (!id)
|
|
1418
|
+
throw new SocialError({
|
|
1419
|
+
code: "ambiguous_outcome",
|
|
1420
|
+
operation: "posts.update",
|
|
1421
|
+
backend: context.backendInstance,
|
|
1422
|
+
correlationId: context.correlationId,
|
|
1423
|
+
message: "X edit response lacks a new post ID. Reconcile before retrying.",
|
|
1424
|
+
retryDisposition: { kind: "reconcile-first" },
|
|
1425
|
+
});
|
|
1426
|
+
const history = data["edit_history_post_ids"] ?? data["edit_history_tweet_ids"];
|
|
1427
|
+
const editHistoryPostIds = Array.isArray(history)
|
|
1428
|
+
? history.filter((entry) => typeof entry === "string")
|
|
1429
|
+
: undefined;
|
|
1430
|
+
return {
|
|
1431
|
+
post: {
|
|
1432
|
+
kind: "platform-post",
|
|
1433
|
+
version: 1,
|
|
1434
|
+
backend: account.backend,
|
|
1435
|
+
platform: "x",
|
|
1436
|
+
accountId: account.accountId,
|
|
1437
|
+
postId: id,
|
|
1438
|
+
},
|
|
1439
|
+
previousPostId: postId,
|
|
1440
|
+
text: optionalString(data["text"]) ?? text,
|
|
1441
|
+
...definedFields({ editHistoryPostIds }),
|
|
1442
|
+
};
|
|
1443
|
+
},
|
|
1307
1444
|
async uploadVideo({ account, video, context }) {
|
|
1308
1445
|
authorize(account, context);
|
|
1309
1446
|
const media = {
|
|
@@ -1333,7 +1470,6 @@ export function x(options) {
|
|
|
1333
1470
|
operation: "x.polls.create",
|
|
1334
1471
|
message: "X polls require 2-4 options and a duration from 5 minutes to 7 days.",
|
|
1335
1472
|
});
|
|
1336
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
1337
1473
|
return object(await request("/2/tweets", context, {
|
|
1338
1474
|
text,
|
|
1339
1475
|
poll: { options: [...pollOptions], duration_minutes: durationMinutes },
|
|
@@ -1341,7 +1477,6 @@ export function x(options) {
|
|
|
1341
1477
|
},
|
|
1342
1478
|
async bookmarks({ account, context }) {
|
|
1343
1479
|
authorize(account, context);
|
|
1344
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
1345
1480
|
return object(await request(`/2/users/${encodeURIComponent(account.accountId)}/bookmarks`, context));
|
|
1346
1481
|
},
|
|
1347
1482
|
async bookmark({ account, postId, context }) {
|
|
@@ -1356,7 +1491,6 @@ export function x(options) {
|
|
|
1356
1491
|
},
|
|
1357
1492
|
async follow({ account, userId, context }) {
|
|
1358
1493
|
authorize(account, context);
|
|
1359
|
-
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
1360
1494
|
return object(await request(`/2/users/${encodeURIComponent(account.accountId)}/following`, context, {
|
|
1361
1495
|
target_user_id: userId,
|
|
1362
1496
|
}));
|
|
@@ -1381,14 +1515,13 @@ export function x(options) {
|
|
|
1381
1515
|
const result = object(await request(path, context, undefined, {
|
|
1382
1516
|
"dm_event.fields": "id,text,event_type,created_at,dm_conversation_id,attachments,entities",
|
|
1383
1517
|
expansions: "sender_id,participant_ids",
|
|
1384
|
-
...(
|
|
1385
|
-
...(limit === undefined ? {} : { max_results: String(limit) }),
|
|
1518
|
+
...definedFields({ pagination_token: cursor, max_results: limit?.toString() }),
|
|
1386
1519
|
}));
|
|
1387
1520
|
const meta = result["meta"] === undefined ? {} : object(result["meta"]);
|
|
1388
1521
|
const nextCursor = optionalString(meta["next_token"]);
|
|
1389
1522
|
return {
|
|
1390
1523
|
items: result["data"] === undefined ? [] : array(result["data"]).map(object),
|
|
1391
|
-
...(
|
|
1524
|
+
...definedFields({ nextCursor }),
|
|
1392
1525
|
};
|
|
1393
1526
|
},
|
|
1394
1527
|
async sendDirectMessage({ account, participantId, text, context }) {
|
|
@@ -1410,7 +1543,7 @@ export function x(options) {
|
|
|
1410
1543
|
operation: "messages.conversation.write",
|
|
1411
1544
|
message: "X direct messages require non-empty text.",
|
|
1412
1545
|
});
|
|
1413
|
-
return object(await requireUserToken("messages.conversation.write")(`/2/dm_conversations/${encodeURIComponent(conversationId)}/messages`, context, { text, ...(
|
|
1546
|
+
return object(await requireUserToken("messages.conversation.write")(`/2/dm_conversations/${encodeURIComponent(conversationId)}/messages`, context, { text, ...definedFields({ attachments }) }));
|
|
1414
1547
|
},
|
|
1415
1548
|
async createGroupConversation({ account, participantIds, message, context }) {
|
|
1416
1549
|
authorize(account, context);
|
|
@@ -1525,16 +1658,13 @@ export function x(options) {
|
|
|
1525
1658
|
authorize(account, context);
|
|
1526
1659
|
return object(await request("/2/lists", context, {
|
|
1527
1660
|
name,
|
|
1528
|
-
...(description
|
|
1529
|
-
...(isPrivate === undefined ? {} : { private: isPrivate }),
|
|
1661
|
+
...definedFields({ description, private: isPrivate }),
|
|
1530
1662
|
}));
|
|
1531
1663
|
},
|
|
1532
1664
|
async updateList({ account, listId, name, description, isPrivate, context }) {
|
|
1533
1665
|
authorize(account, context);
|
|
1534
1666
|
return object(await request(`/2/lists/${encodeURIComponent(listId)}`, context, {
|
|
1535
|
-
...(name
|
|
1536
|
-
...(description === undefined ? {} : { description }),
|
|
1537
|
-
...(isPrivate === undefined ? {} : { private: isPrivate }),
|
|
1667
|
+
...definedFields({ name, description, private: isPrivate }),
|
|
1538
1668
|
}, {}, "PUT"));
|
|
1539
1669
|
},
|
|
1540
1670
|
async deleteList({ account, listId, context }) {
|
|
@@ -1600,6 +1730,85 @@ export function x(options) {
|
|
|
1600
1730
|
authorize(account, context);
|
|
1601
1731
|
await request(`/2/users/${encodeURIComponent(account.accountId)}/retweets/${encodeURIComponent(postId)}`, context, undefined, {}, "DELETE");
|
|
1602
1732
|
},
|
|
1733
|
+
async listStreamRules({ account, ids, cursor, limit, context }) {
|
|
1734
|
+
authorize(account, context);
|
|
1735
|
+
if (ids !== undefined)
|
|
1736
|
+
validateRuleIds(ids, "x.streamRules.list", 1000);
|
|
1737
|
+
if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000))
|
|
1738
|
+
throw new SocialError({
|
|
1739
|
+
code: "invalid_input",
|
|
1740
|
+
operation: "x.streamRules.list",
|
|
1741
|
+
message: "X stream rule limits must be integers from 1 through 1000.",
|
|
1742
|
+
});
|
|
1743
|
+
const result = object(await appRequest()("/2/tweets/search/stream/rules", context, undefined, {
|
|
1744
|
+
...definedFields({
|
|
1745
|
+
ids: ids?.join(","),
|
|
1746
|
+
pagination_token: cursor,
|
|
1747
|
+
max_results: limit === undefined ? undefined : String(limit),
|
|
1748
|
+
}),
|
|
1749
|
+
}));
|
|
1750
|
+
const items = (result["data"] === undefined ? [] : array(result["data"])).map((entry) => parseStreamRule(object(entry), "x.streamRules.list"));
|
|
1751
|
+
const meta = result["meta"] === undefined ? {} : object(result["meta"]);
|
|
1752
|
+
const nextCursor = optionalString(meta["next_token"]);
|
|
1753
|
+
return { items, ...definedFields({ nextCursor }) };
|
|
1754
|
+
},
|
|
1755
|
+
async addStreamRules({ account, rules, dryRun = false, context }) {
|
|
1756
|
+
authorize(account, context);
|
|
1757
|
+
if (rules.length === 0)
|
|
1758
|
+
throw new SocialError({
|
|
1759
|
+
code: "invalid_input",
|
|
1760
|
+
operation: "x.streamRules.add",
|
|
1761
|
+
message: "Provide at least one X filtered-stream rule to add.",
|
|
1762
|
+
});
|
|
1763
|
+
for (const rule of rules)
|
|
1764
|
+
validateRuleInput(rule);
|
|
1765
|
+
const result = await appRequest()("/2/tweets/search/stream/rules", context, {
|
|
1766
|
+
add: rules.map((rule) => ({
|
|
1767
|
+
value: rule.value,
|
|
1768
|
+
...definedFields({ tag: rule.tag }),
|
|
1769
|
+
})),
|
|
1770
|
+
}, dryRun ? { dry_run: "true" } : {});
|
|
1771
|
+
return parseRulesUpdate(object(result), dryRun);
|
|
1772
|
+
},
|
|
1773
|
+
async deleteStreamRules({ account, ids, dryRun = false, context }) {
|
|
1774
|
+
authorize(account, context);
|
|
1775
|
+
validateRuleIds(ids, "x.streamRules.delete", 1000);
|
|
1776
|
+
const result = await appRequest()("/2/tweets/search/stream/rules", context, { delete: { ids: [...ids] } }, dryRun ? { dry_run: "true" } : {});
|
|
1777
|
+
return parseRulesUpdate(object(result), dryRun);
|
|
1778
|
+
},
|
|
1779
|
+
async *stream({ account, context, ...streamOptions }) {
|
|
1780
|
+
authorize(account, context);
|
|
1781
|
+
const bearerToken = options.appBearerToken;
|
|
1782
|
+
if (!bearerToken?.trim())
|
|
1783
|
+
throw new SocialError({
|
|
1784
|
+
code: "missing_permission",
|
|
1785
|
+
operation: "streams.read",
|
|
1786
|
+
message: "The X filtered stream requires an app-only bearer token. Configure appBearerToken.",
|
|
1787
|
+
});
|
|
1788
|
+
yield* readFilteredStream({ bearerToken, fetch: options.fetch }, streamOptions, context);
|
|
1789
|
+
},
|
|
1790
|
+
async hideReply({ account, replyId, hidden, context }) {
|
|
1791
|
+
authorize(account, context);
|
|
1792
|
+
if (!/^[0-9]{1,19}$/.test(replyId))
|
|
1793
|
+
throw new SocialError({
|
|
1794
|
+
code: "invalid_input",
|
|
1795
|
+
operation: "comments.moderate",
|
|
1796
|
+
message: "X reply IDs are numeric strings of 1 to 19 digits.",
|
|
1797
|
+
retryDisposition: { kind: "never" },
|
|
1798
|
+
});
|
|
1799
|
+
const result = object(await requireUserToken("comments.moderate")(`/2/tweets/${encodeURIComponent(replyId)}/hidden`, context, { hidden }, {}, "PUT"));
|
|
1800
|
+
const state = result["data"] === undefined
|
|
1801
|
+
? undefined
|
|
1802
|
+
: optionalBoolean(object(result["data"])["hidden"]);
|
|
1803
|
+
if (state === undefined)
|
|
1804
|
+
throw new SocialError({
|
|
1805
|
+
code: "ambiguous_outcome",
|
|
1806
|
+
operation: "comments.moderate",
|
|
1807
|
+
message: "X did not report the reply's hidden state.",
|
|
1808
|
+
retryDisposition: { kind: "reconcile-first" },
|
|
1809
|
+
});
|
|
1810
|
+
return { hidden: state };
|
|
1811
|
+
},
|
|
1603
1812
|
}),
|
|
1604
1813
|
});
|
|
1605
1814
|
return adapter;
|