@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
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { SocialError } from "../core/errors.js";
|
|
2
|
+
import { definedFields } from "../core/fields.js";
|
|
3
|
+
import { remainingBudget } from "../transport/budget.js";
|
|
4
|
+
import { abortable, retryDelay } from "../transport/http.js";
|
|
5
|
+
import { parseJson } from "../transport/json.js";
|
|
6
|
+
import { isJsonObject, isString } from "../transport/validation.js";
|
|
7
|
+
// Filtered stream contract, checked 2026-09-24 against X API v2 OpenAPI 2.168:
|
|
8
|
+
// https://docs.x.com/x-api/posts/filtered-stream/introduction
|
|
9
|
+
// https://docs.x.com/x-api/stream/stream-filtered-posts
|
|
10
|
+
// https://docs.x.com/x-api/stream/get-stream-rules
|
|
11
|
+
// https://docs.x.com/x-api/stream/update-stream-rules
|
|
12
|
+
// https://docs.x.com/x-api/fundamentals/handling-disconnections
|
|
13
|
+
// https://docs.x.com/x-api/fundamentals/recovery-and-redundancy
|
|
14
|
+
/** X sends a `\r\n` keep-alive at least every 20 seconds and recommends a 20-second read timeout. */
|
|
15
|
+
export const xStreamDefaultStallTimeoutMs = 20_000;
|
|
16
|
+
/** Upper bound for one newline-delimited stream message held in memory. */
|
|
17
|
+
const maxMessageLength = 1024 * 1024;
|
|
18
|
+
const ruleIdPattern = /^[0-9]{1,19}$/;
|
|
19
|
+
function jsonObjectValue(value) {
|
|
20
|
+
return value !== undefined && isJsonObject(value) ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
function objectArray(value) {
|
|
23
|
+
if (!Array.isArray(value))
|
|
24
|
+
return undefined;
|
|
25
|
+
return value.flatMap((entry) => {
|
|
26
|
+
const row = jsonObjectValue(entry);
|
|
27
|
+
return row === undefined ? [] : [row];
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function stringValue(value) {
|
|
31
|
+
return value !== undefined && isString(value) ? value : undefined;
|
|
32
|
+
}
|
|
33
|
+
function invalidResponse(operation, message) {
|
|
34
|
+
return new SocialError({
|
|
35
|
+
code: "upstream_failure",
|
|
36
|
+
operation,
|
|
37
|
+
message,
|
|
38
|
+
retryDisposition: { kind: "never" },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/** Parse one rule object from X, rejecting malformed identifiers. */
|
|
42
|
+
export function parseStreamRule(value, operation) {
|
|
43
|
+
const row = jsonObjectValue(value);
|
|
44
|
+
const id = stringValue(row?.["id"]);
|
|
45
|
+
const ruleValue = stringValue(row?.["value"]);
|
|
46
|
+
const tag = stringValue(row?.["tag"]);
|
|
47
|
+
if (id === undefined || !ruleIdPattern.test(id) || ruleValue === undefined)
|
|
48
|
+
throw invalidResponse(operation, "X returned a filtered-stream rule without an id or value.");
|
|
49
|
+
return { id, value: ruleValue, ...definedFields({ tag }) };
|
|
50
|
+
}
|
|
51
|
+
/** Parse the rule-mutation response while keeping per-rule errors. */
|
|
52
|
+
export function parseRulesUpdate(value, dryRun) {
|
|
53
|
+
const operation = "x.streamRules.update";
|
|
54
|
+
const body = jsonObjectValue(value);
|
|
55
|
+
if (body === undefined)
|
|
56
|
+
throw invalidResponse(operation, "X returned a non-object rule update.");
|
|
57
|
+
const data = body["data"];
|
|
58
|
+
const meta = jsonObjectValue(body["meta"]);
|
|
59
|
+
const summary = jsonObjectValue(meta?.["summary"]);
|
|
60
|
+
if (data !== undefined && !Array.isArray(data))
|
|
61
|
+
throw invalidResponse(operation, "X returned a rule update whose data is not an array.");
|
|
62
|
+
return {
|
|
63
|
+
dryRun,
|
|
64
|
+
rules: (data ?? []).map((entry) => parseStreamRule(entry, operation)),
|
|
65
|
+
...definedFields({ summary }),
|
|
66
|
+
errors: objectArray(body["errors"]) ?? [],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Validate one rule for local bounds. X enforces the tier-specific limit. */
|
|
70
|
+
export function validateRuleInput(rule) {
|
|
71
|
+
if (!rule.value.trim() || rule.value.length > 2048)
|
|
72
|
+
throw new SocialError({
|
|
73
|
+
code: "invalid_input",
|
|
74
|
+
operation: "x.streamRules.add",
|
|
75
|
+
message: "X filtered-stream rule values must contain 1-2048 characters.",
|
|
76
|
+
});
|
|
77
|
+
if (rule.tag !== undefined && !rule.tag.trim())
|
|
78
|
+
throw new SocialError({
|
|
79
|
+
code: "invalid_input",
|
|
80
|
+
operation: "x.streamRules.add",
|
|
81
|
+
message: "X filtered-stream rule tags must not be empty when provided.",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
export function validateRuleIds(ids, operation, max) {
|
|
85
|
+
if (ids.length === 0 || ids.length > max || ids.some((id) => !ruleIdPattern.test(id)))
|
|
86
|
+
throw new SocialError({
|
|
87
|
+
code: "invalid_input",
|
|
88
|
+
operation,
|
|
89
|
+
message: `Provide 1-${max} numeric X filtered-stream rule IDs.`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Classify one newline-delimited message. */
|
|
93
|
+
export function parseStreamMessage(line) {
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = parseJson(line);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw invalidResponse("streams.read", "X filtered stream sent an invalid JSON message.");
|
|
100
|
+
}
|
|
101
|
+
const message = jsonObjectValue(parsed);
|
|
102
|
+
if (message === undefined)
|
|
103
|
+
throw invalidResponse("streams.read", "X filtered stream sent a non-object message.");
|
|
104
|
+
const post = jsonObjectValue(message["data"]);
|
|
105
|
+
const errors = objectArray(message["errors"]);
|
|
106
|
+
if (post !== undefined) {
|
|
107
|
+
const includes = jsonObjectValue(message["includes"]);
|
|
108
|
+
const rules = objectArray(message["matching_rules"]) ?? [];
|
|
109
|
+
return {
|
|
110
|
+
kind: "post",
|
|
111
|
+
post,
|
|
112
|
+
matchingRules: rules.flatMap((rule) => {
|
|
113
|
+
const id = stringValue(rule["id"]);
|
|
114
|
+
const tag = stringValue(rule["tag"]);
|
|
115
|
+
return id === undefined ? [] : [{ id, ...definedFields({ tag }) }];
|
|
116
|
+
}),
|
|
117
|
+
...definedFields({
|
|
118
|
+
includes,
|
|
119
|
+
errors: errors !== undefined && errors.length > 0 ? errors : undefined,
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (errors !== undefined && errors.length > 0)
|
|
124
|
+
return { kind: "error", errors };
|
|
125
|
+
return { kind: "other", message };
|
|
126
|
+
}
|
|
127
|
+
function isoTime(value, name) {
|
|
128
|
+
if (value !== undefined && !Number.isFinite(Date.parse(value)))
|
|
129
|
+
throw new SocialError({
|
|
130
|
+
code: "invalid_input",
|
|
131
|
+
operation: "streams.read",
|
|
132
|
+
message: `${name} must be an ISO 8601 timestamp.`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/** Validate caller options and build the connection URL. Performs no I/O. */
|
|
136
|
+
export function streamUrl(options) {
|
|
137
|
+
const backfill = options.backfillMinutes;
|
|
138
|
+
if (backfill !== undefined && (!Number.isSafeInteger(backfill) || backfill < 0 || backfill > 5))
|
|
139
|
+
throw new SocialError({
|
|
140
|
+
code: "invalid_input",
|
|
141
|
+
operation: "streams.read",
|
|
142
|
+
message: "backfillMinutes must be an integer from 1 through 5, or 0 for no backfill.",
|
|
143
|
+
});
|
|
144
|
+
const stall = options.stallTimeoutMs;
|
|
145
|
+
if (stall !== undefined && (!Number.isSafeInteger(stall) || stall < 1000 || stall > 600_000))
|
|
146
|
+
throw new SocialError({
|
|
147
|
+
code: "invalid_input",
|
|
148
|
+
operation: "streams.read",
|
|
149
|
+
message: "stallTimeoutMs must be an integer from 1000 through 600000.",
|
|
150
|
+
});
|
|
151
|
+
isoTime(options.startTime, "startTime");
|
|
152
|
+
isoTime(options.endTime, "endTime");
|
|
153
|
+
if (options.startTime !== undefined &&
|
|
154
|
+
options.endTime !== undefined &&
|
|
155
|
+
Date.parse(options.startTime) >= Date.parse(options.endTime))
|
|
156
|
+
throw new SocialError({
|
|
157
|
+
code: "invalid_input",
|
|
158
|
+
operation: "streams.read",
|
|
159
|
+
message: "startTime must be earlier than endTime.",
|
|
160
|
+
});
|
|
161
|
+
const url = new URL("https://api.x.com/2/tweets/search/stream");
|
|
162
|
+
const unique = (values) => values.filter((value, index) => values.indexOf(value) === index).join(",");
|
|
163
|
+
if (options.tweetFields?.length)
|
|
164
|
+
url.searchParams.set("tweet.fields", unique(options.tweetFields));
|
|
165
|
+
if (options.expansions?.length)
|
|
166
|
+
url.searchParams.set("expansions", unique(options.expansions));
|
|
167
|
+
if (options.userFields?.length)
|
|
168
|
+
url.searchParams.set("user.fields", unique(options.userFields));
|
|
169
|
+
if (options.mediaFields?.length)
|
|
170
|
+
url.searchParams.set("media.fields", unique(options.mediaFields));
|
|
171
|
+
if (backfill !== undefined && backfill > 0)
|
|
172
|
+
url.searchParams.set("backfill_minutes", String(backfill));
|
|
173
|
+
if (options.startTime !== undefined)
|
|
174
|
+
url.searchParams.set("start_time", options.startTime);
|
|
175
|
+
if (options.endTime !== undefined)
|
|
176
|
+
url.searchParams.set("end_time", options.endTime);
|
|
177
|
+
return url;
|
|
178
|
+
}
|
|
179
|
+
function statusError(response, context) {
|
|
180
|
+
const status = response.status;
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
let delay = retryDelay(response.headers.get("retry-after"), now);
|
|
183
|
+
const reset = Number(response.headers.get("x-rate-limit-reset") ?? Number.NaN);
|
|
184
|
+
if (delay === undefined && Number.isFinite(reset) && reset >= 0)
|
|
185
|
+
delay = Math.max(0, reset * 1000 - now);
|
|
186
|
+
const common = {
|
|
187
|
+
operation: "streams.read",
|
|
188
|
+
backend: context.backendInstance,
|
|
189
|
+
correlationId: context.correlationId,
|
|
190
|
+
upstreamStatus: status,
|
|
191
|
+
};
|
|
192
|
+
if (status === 401)
|
|
193
|
+
return new SocialError({
|
|
194
|
+
...common,
|
|
195
|
+
code: "reconnect_required",
|
|
196
|
+
message: "X rejected the app-only bearer token for the filtered stream.",
|
|
197
|
+
retryDisposition: { kind: "after-reconnect" },
|
|
198
|
+
});
|
|
199
|
+
if (status === 403)
|
|
200
|
+
return new SocialError({
|
|
201
|
+
...common,
|
|
202
|
+
code: "missing_permission",
|
|
203
|
+
message: "X denied the filtered stream. It needs an app-only bearer token and pay-per-use or Enterprise access; backfill and recovery need Enterprise.",
|
|
204
|
+
retryDisposition: { kind: "never" },
|
|
205
|
+
});
|
|
206
|
+
if (status === 402)
|
|
207
|
+
return new SocialError({
|
|
208
|
+
...common,
|
|
209
|
+
code: "billing_required",
|
|
210
|
+
message: "X requires API credits or a plan change for the filtered stream.",
|
|
211
|
+
retryDisposition: { kind: "never" },
|
|
212
|
+
});
|
|
213
|
+
if (status === 429)
|
|
214
|
+
return new SocialError({
|
|
215
|
+
...common,
|
|
216
|
+
code: "rate_limited",
|
|
217
|
+
message: "X rate-limited the filtered stream connection or the connection limit is reached. Close other connections and back off before reconnecting.",
|
|
218
|
+
retryDisposition: delay === undefined ? { kind: "never" } : { kind: "after-delay", delayMs: delay },
|
|
219
|
+
});
|
|
220
|
+
return new SocialError({
|
|
221
|
+
...common,
|
|
222
|
+
code: "upstream_failure",
|
|
223
|
+
message: `X filtered stream connection failed with HTTP ${status}.`,
|
|
224
|
+
retryDisposition: { kind: "never" },
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Open one filtered-stream connection and yield its messages. The connection is made when
|
|
229
|
+
* iteration starts and closes when the caller stops iterating, the context signal aborts, the
|
|
230
|
+
* stall timeout elapses, or X ends the response. This function never reconnects.
|
|
231
|
+
*/
|
|
232
|
+
export async function* readFilteredStream(config, options, context) {
|
|
233
|
+
const url = streamUrl(options);
|
|
234
|
+
const stallTimeoutMs = options.stallTimeoutMs ?? xStreamDefaultStallTimeoutMs;
|
|
235
|
+
const outer = context.signal;
|
|
236
|
+
const common = {
|
|
237
|
+
operation: "streams.read",
|
|
238
|
+
backend: context.backendInstance,
|
|
239
|
+
correlationId: context.correlationId,
|
|
240
|
+
};
|
|
241
|
+
if (outer?.aborted)
|
|
242
|
+
throw new SocialError({
|
|
243
|
+
...common,
|
|
244
|
+
code: "cancelled",
|
|
245
|
+
message: "The filtered stream was cancelled before connecting.",
|
|
246
|
+
});
|
|
247
|
+
const connectBudget = remainingBudget(context);
|
|
248
|
+
const controller = new AbortController();
|
|
249
|
+
const connectTimeout = new Error("connect timeout");
|
|
250
|
+
const stalled = new Error("stalled");
|
|
251
|
+
const onAbort = () => controller.abort(outer?.reason);
|
|
252
|
+
outer?.addEventListener("abort", onAbort, { once: true });
|
|
253
|
+
let timer = setTimeout(() => controller.abort(connectTimeout), connectBudget);
|
|
254
|
+
let reader;
|
|
255
|
+
const interrupted = (phase) => {
|
|
256
|
+
const reason = controller.signal.reason;
|
|
257
|
+
if (outer?.aborted)
|
|
258
|
+
return new SocialError({
|
|
259
|
+
...common,
|
|
260
|
+
code: "cancelled",
|
|
261
|
+
message: "The filtered stream was cancelled by the caller.",
|
|
262
|
+
});
|
|
263
|
+
if (reason === connectTimeout || reason === stalled)
|
|
264
|
+
return new SocialError({
|
|
265
|
+
...common,
|
|
266
|
+
code: "timeout",
|
|
267
|
+
message: reason === stalled
|
|
268
|
+
? `X filtered stream sent no data or keep-alive within ${stallTimeoutMs} ms. Reconnect explicitly.`
|
|
269
|
+
: "X filtered stream did not connect within the operation budget.",
|
|
270
|
+
retryDisposition: { kind: "never" },
|
|
271
|
+
});
|
|
272
|
+
return new SocialError({
|
|
273
|
+
...common,
|
|
274
|
+
code: "upstream_failure",
|
|
275
|
+
message: phase === "connect"
|
|
276
|
+
? "The filtered stream connection failed before X responded."
|
|
277
|
+
: "The filtered stream connection dropped. Reconnect explicitly.",
|
|
278
|
+
retryDisposition: { kind: "never" },
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
try {
|
|
282
|
+
let response;
|
|
283
|
+
try {
|
|
284
|
+
const pending = (config.fetch ?? globalThis.fetch)(url, {
|
|
285
|
+
method: "GET",
|
|
286
|
+
headers: { Authorization: `Bearer ${config.bearerToken}` },
|
|
287
|
+
signal: controller.signal,
|
|
288
|
+
redirect: "error",
|
|
289
|
+
});
|
|
290
|
+
void pending.then((late) => {
|
|
291
|
+
if (controller.signal.aborted)
|
|
292
|
+
void late.body?.cancel().catch(() => undefined);
|
|
293
|
+
}, () => undefined);
|
|
294
|
+
response = await abortable(pending, controller.signal);
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
throw interrupted("connect");
|
|
298
|
+
}
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
void response.body?.cancel().catch(() => undefined);
|
|
302
|
+
throw statusError(response, context);
|
|
303
|
+
}
|
|
304
|
+
if (!response.body)
|
|
305
|
+
throw invalidResponse("streams.read", "X returned an empty stream body.");
|
|
306
|
+
reader = response.body.getReader();
|
|
307
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
308
|
+
let buffer = "";
|
|
309
|
+
for (;;) {
|
|
310
|
+
timer = setTimeout(() => controller.abort(stalled), stallTimeoutMs);
|
|
311
|
+
let chunk;
|
|
312
|
+
try {
|
|
313
|
+
chunk = await abortable(reader.read(), controller.signal);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
throw interrupted("read");
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
clearTimeout(timer);
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
buffer += chunk.done ? decoder.decode() : decoder.decode(chunk.value, { stream: true });
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
throw invalidResponse("streams.read", "X filtered stream sent invalid UTF-8.");
|
|
326
|
+
}
|
|
327
|
+
const lines = buffer.split("\n");
|
|
328
|
+
buffer = chunk.done ? "" : (lines.pop() ?? "");
|
|
329
|
+
if (buffer.length > maxMessageLength)
|
|
330
|
+
throw invalidResponse("streams.read", "X filtered stream message exceeds 1 MiB.");
|
|
331
|
+
for (const line of lines) {
|
|
332
|
+
if (line.length > maxMessageLength)
|
|
333
|
+
throw invalidResponse("streams.read", "X filtered stream message exceeds 1 MiB.");
|
|
334
|
+
const text = line.trim();
|
|
335
|
+
// Blank lines are X keep-alive heartbeats.
|
|
336
|
+
if (text)
|
|
337
|
+
yield parseStreamMessage(text);
|
|
338
|
+
}
|
|
339
|
+
if (chunk.done)
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
finally {
|
|
344
|
+
clearTimeout(timer);
|
|
345
|
+
outer?.removeEventListener("abort", onAbort);
|
|
346
|
+
if (!controller.signal.aborted)
|
|
347
|
+
controller.abort(new Error("stream closed"));
|
|
348
|
+
void reader?.cancel().catch(() => undefined);
|
|
349
|
+
}
|
|
350
|
+
}
|
package/dist/platforms/x.d.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import type { AdapterOperationContext, ConnectedAccountRef, JsonObject, Page, PlatformPostRef, SearchPostsInput } from "../core/types.js";
|
|
2
|
+
import { type XStreamEvent, type XStreamOptions, type XStreamRule, type XStreamRuleInput, type XStreamRulesUpdate } from "./x-stream.js";
|
|
2
3
|
export interface XAuthorization {
|
|
3
4
|
readonly userId: string;
|
|
4
5
|
readonly accessToken?: string;
|
|
5
6
|
readonly handle?: string;
|
|
6
7
|
}
|
|
7
8
|
export { xLike, xUnlike, type XEngagementOptions, type XEngagementResult } from "./x-engagement.js";
|
|
9
|
+
export { xStreamDefaultStallTimeoutMs, type XMatchingRule, type XStreamEvent, type XStreamOptions, type XStreamRule, type XStreamRuleInput, type XStreamRulesUpdate, } from "./x-stream.js";
|
|
8
10
|
export interface XOptions {
|
|
9
11
|
readonly auth: XAuthorization;
|
|
10
12
|
/** App-only bearer token for full-archive search and filtered stream. */
|
|
11
13
|
readonly appBearerToken?: string;
|
|
12
14
|
readonly fetch?: typeof globalThis.fetch;
|
|
13
15
|
readonly clock?: () => Date;
|
|
16
|
+
/**
|
|
17
|
+
* OAuth 2.0 client secret (or legacy OAuth 1.0 consumer secret) that X uses to sign
|
|
18
|
+
* webhook deliveries and CRC responses.
|
|
19
|
+
*/
|
|
20
|
+
readonly webhookSecret?: string;
|
|
14
21
|
}
|
|
15
22
|
export type XTweetField = "attachments" | "author_id" | "conversation_id" | "created_at" | "entities" | "geo" | "id" | "lang" | "public_metrics" | "referenced_tweets" | "text";
|
|
16
23
|
export type XTweetExpansion = "attachments.media_keys" | "attachments.poll_ids" | "author_id" | "entities.mentions.username" | "geo.place_id" | "in_reply_to_user_id" | "referenced_tweets.id";
|
|
@@ -26,6 +33,16 @@ export interface XSearchInput extends SearchPostsInput {
|
|
|
26
33
|
export interface XUploadedMedia {
|
|
27
34
|
readonly mediaId: string;
|
|
28
35
|
}
|
|
36
|
+
/** A confirmed X post edit. X assigns every edited version a new post ID. */
|
|
37
|
+
export interface XUpdatedPost {
|
|
38
|
+
/** Reference to the new version created by the edit. */
|
|
39
|
+
readonly post: PlatformPostRef;
|
|
40
|
+
/** The post ID that was passed as `previous_post_id`. */
|
|
41
|
+
readonly previousPostId: string;
|
|
42
|
+
readonly text: string;
|
|
43
|
+
/** Oldest-first edit chain, when X returns it. The first entry is the original post ID. */
|
|
44
|
+
readonly editHistoryPostIds?: readonly string[];
|
|
45
|
+
}
|
|
29
46
|
export interface XNative {
|
|
30
47
|
readonly searchRecentPosts: (input: {
|
|
31
48
|
readonly account: ConnectedAccountRef;
|
|
@@ -54,6 +71,18 @@ export interface XNative {
|
|
|
54
71
|
readonly postId: string;
|
|
55
72
|
readonly context: AdapterOperationContext;
|
|
56
73
|
}) => Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Edits the text of a recent post with `POST /2/tweets` and `edit_options.previous_post_id`.
|
|
76
|
+
* X decides eligibility (X Premium, own post, edit window, edit count) and returns a new post ID.
|
|
77
|
+
* Sources, accessed 2026-09-24: https://docs.x.com/x-api/posts/create-post,
|
|
78
|
+
* https://docs.x.com/x-api/fundamentals/edit-posts, https://docs.x.com/changelog (2025-10-03).
|
|
79
|
+
*/
|
|
80
|
+
readonly updatePost: (input: {
|
|
81
|
+
readonly account: ConnectedAccountRef;
|
|
82
|
+
readonly postId: string;
|
|
83
|
+
readonly text: string;
|
|
84
|
+
readonly context: AdapterOperationContext;
|
|
85
|
+
}) => Promise<XUpdatedPost>;
|
|
57
86
|
/** Uploads one MP4 Blob (up to 512 MiB) and waits for processing. Returns an attachable media ID. */
|
|
58
87
|
readonly uploadVideo: (input: {
|
|
59
88
|
readonly account: ConnectedAccountRef;
|
|
@@ -310,5 +339,48 @@ export interface XNative {
|
|
|
310
339
|
readonly postId: string;
|
|
311
340
|
readonly context: AdapterOperationContext;
|
|
312
341
|
}) => Promise<void>;
|
|
342
|
+
/** Lists the app's filtered-stream rules. Requires `appBearerToken`. */
|
|
343
|
+
readonly listStreamRules: (input: {
|
|
344
|
+
readonly account: ConnectedAccountRef;
|
|
345
|
+
readonly ids?: readonly string[];
|
|
346
|
+
readonly cursor?: string;
|
|
347
|
+
readonly limit?: number;
|
|
348
|
+
readonly context: AdapterOperationContext;
|
|
349
|
+
}) => Promise<Page<XStreamRule>>;
|
|
350
|
+
/** Adds filtered-stream rules in one request. Set `dryRun` to validate without saving. */
|
|
351
|
+
readonly addStreamRules: (input: {
|
|
352
|
+
readonly account: ConnectedAccountRef;
|
|
353
|
+
readonly rules: readonly XStreamRuleInput[];
|
|
354
|
+
readonly dryRun?: boolean;
|
|
355
|
+
readonly context: AdapterOperationContext;
|
|
356
|
+
}) => Promise<XStreamRulesUpdate>;
|
|
357
|
+
/** Deletes filtered-stream rules by ID in one request. Set `dryRun` to validate without saving. */
|
|
358
|
+
readonly deleteStreamRules: (input: {
|
|
359
|
+
readonly account: ConnectedAccountRef;
|
|
360
|
+
readonly ids: readonly string[];
|
|
361
|
+
readonly dryRun?: boolean;
|
|
362
|
+
readonly context: AdapterOperationContext;
|
|
363
|
+
}) => Promise<XStreamRulesUpdate>;
|
|
364
|
+
/**
|
|
365
|
+
* Opens one filtered-stream connection when iteration starts. It yields until the caller
|
|
366
|
+
* stops, `context.signal` aborts, the stall timeout elapses, or X closes the response. It
|
|
367
|
+
* never reconnects on its own. Requires `appBearerToken`.
|
|
368
|
+
*/
|
|
369
|
+
readonly stream: (input: XStreamOptions & {
|
|
370
|
+
readonly account: ConnectedAccountRef;
|
|
371
|
+
readonly context: AdapterOperationContext;
|
|
372
|
+
}) => AsyncGenerator<XStreamEvent, void, undefined>;
|
|
373
|
+
/**
|
|
374
|
+
* Hides or unhides a reply in a conversation the authenticated user started.
|
|
375
|
+
* Calls `PUT /2/tweets/:id/hidden` and returns the hidden state X reports.
|
|
376
|
+
*/
|
|
377
|
+
readonly hideReply: (input: {
|
|
378
|
+
readonly account: ConnectedAccountRef;
|
|
379
|
+
readonly replyId: string;
|
|
380
|
+
readonly hidden: boolean;
|
|
381
|
+
readonly context: AdapterOperationContext;
|
|
382
|
+
}) => Promise<{
|
|
383
|
+
readonly hidden: boolean;
|
|
384
|
+
}>;
|
|
313
385
|
}
|
|
314
386
|
export declare function x(options: XOptions): import("../core/adapter.js").SocialAdapter<XNative>;
|