@opencoredev/social-sdk 0.1.1 → 0.2.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/README.md +10 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud/common.d.ts.map +1 -1
- package/dist/cloud/common.js +48 -20
- package/dist/cloud/common.js.map +1 -1
- package/dist/cloud/lifecycle.d.ts.map +1 -1
- package/dist/cloud/lifecycle.js +5 -0
- package/dist/cloud/lifecycle.js.map +1 -1
- package/dist/cloud/media.d.ts.map +1 -1
- package/dist/cloud/media.js +12 -8
- package/dist/cloud/media.js.map +1 -1
- package/dist/cloud/outcomes.d.ts.map +1 -1
- package/dist/cloud/outcomes.js +17 -9
- package/dist/cloud/outcomes.js.map +1 -1
- package/dist/cloud/post-for-me.d.ts.map +1 -1
- package/dist/cloud/post-for-me.js +99 -28
- package/dist/cloud/post-for-me.js.map +1 -1
- package/dist/cloud/zernio.d.ts.map +1 -1
- package/dist/cloud/zernio.js +100 -9
- package/dist/cloud/zernio.js.map +1 -1
- package/dist/core/adapter.d.ts +42 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/client.d.ts +35 -1
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +503 -110
- package/dist/core/client.js.map +1 -1
- package/dist/core/concurrency.d.ts.map +1 -1
- package/dist/core/concurrency.js +5 -3
- package/dist/core/concurrency.js.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +14 -7
- package/dist/core/errors.js.map +1 -1
- package/dist/core/idempotency.d.ts.map +1 -1
- package/dist/core/idempotency.js.map +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/pagination.d.ts.map +1 -1
- package/dist/core/pagination.js +1 -0
- package/dist/core/pagination.js.map +1 -1
- package/dist/core/types.d.ts +70 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +1 -1
- package/dist/platforms/bluesky.d.ts +131 -1
- package/dist/platforms/bluesky.d.ts.map +1 -1
- package/dist/platforms/bluesky.js +772 -44
- package/dist/platforms/bluesky.js.map +1 -1
- package/dist/platforms/instagram.d.ts +70 -3
- package/dist/platforms/instagram.d.ts.map +1 -1
- package/dist/platforms/instagram.js +403 -47
- package/dist/platforms/instagram.js.map +1 -1
- package/dist/platforms/linkedin.d.ts +55 -0
- package/dist/platforms/linkedin.d.ts.map +1 -1
- package/dist/platforms/linkedin.js +350 -39
- package/dist/platforms/linkedin.js.map +1 -1
- package/dist/platforms/threads.d.ts +37 -4
- package/dist/platforms/threads.d.ts.map +1 -1
- package/dist/platforms/threads.js +419 -44
- package/dist/platforms/threads.js.map +1 -1
- package/dist/platforms/tiktok.d.ts.map +1 -1
- package/dist/platforms/tiktok.js +109 -26
- package/dist/platforms/tiktok.js.map +1 -1
- package/dist/platforms/x-engagement.d.ts.map +1 -1
- package/dist/platforms/x-engagement.js +4 -0
- package/dist/platforms/x-engagement.js.map +1 -1
- package/dist/platforms/x.d.ts +227 -9
- package/dist/platforms/x.d.ts.map +1 -1
- package/dist/platforms/x.js +826 -49
- package/dist/platforms/x.js.map +1 -1
- package/dist/platforms/youtube-upload.d.ts.map +1 -1
- package/dist/platforms/youtube-upload.js +25 -9
- package/dist/platforms/youtube-upload.js.map +1 -1
- package/dist/platforms/youtube.d.ts +56 -2
- package/dist/platforms/youtube.d.ts.map +1 -1
- package/dist/platforms/youtube.js +571 -31
- package/dist/platforms/youtube.js.map +1 -1
- package/dist/server/connections.d.ts.map +1 -1
- package/dist/server/connections.js +4 -2
- package/dist/server/connections.js.map +1 -1
- package/dist/server/credentials.d.ts.map +1 -1
- package/dist/server/credentials.js +10 -16
- package/dist/server/credentials.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/oauth.d.ts +5 -0
- package/dist/server/oauth.d.ts.map +1 -1
- package/dist/server/oauth.js +118 -47
- package/dist/server/oauth.js.map +1 -1
- package/dist/server/webhooks.d.ts.map +1 -1
- package/dist/server/webhooks.js +11 -4
- package/dist/server/webhooks.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +153 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/transport/binary.d.ts.map +1 -1
- package/dist/transport/binary.js.map +1 -1
- package/dist/transport/budget.d.ts.map +1 -1
- package/dist/transport/budget.js.map +1 -1
- package/dist/transport/http.d.ts +2 -1
- package/dist/transport/http.d.ts.map +1 -1
- package/dist/transport/http.js +42 -12
- package/dist/transport/http.js.map +1 -1
- package/dist/transport/json.d.ts +7 -1
- package/dist/transport/json.d.ts.map +1 -1
- package/dist/transport/json.js +7 -1
- package/dist/transport/json.js.map +1 -1
- package/dist/transport/upload.d.ts +1 -0
- package/dist/transport/upload.d.ts.map +1 -1
- package/dist/transport/upload.js +38 -17
- package/dist/transport/upload.js.map +1 -1
- package/dist/transport/validation.d.ts.map +1 -1
- package/dist/transport/validation.js +1 -0
- package/dist/transport/validation.js.map +1 -1
- package/package.json +2 -1
package/dist/platforms/x.js
CHANGED
|
@@ -1,16 +1,66 @@
|
|
|
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. */
|
|
1
2
|
import { remainingBudget } from "../transport/budget.js";
|
|
2
3
|
import twitterText from "twitter-text";
|
|
3
4
|
import { defineAdapter } from "../core/adapter.js";
|
|
5
|
+
import { connectedAccountRef, profileRef } from "../core/types.js";
|
|
4
6
|
import { SocialError } from "../core/errors.js";
|
|
5
7
|
import { managedHttp, publicFields } from "../cloud/common.js";
|
|
6
8
|
import { createHttp, HttpError } from "../transport/http.js";
|
|
7
|
-
import { array, object, optionalNumber, optionalString, string } from "../transport/validation.js";
|
|
9
|
+
import { array, object as parseObject, optionalNumber, optionalString, string, } from "../transport/validation.js";
|
|
10
|
+
// oxlint-disable-next-line anti-slop/no-unknown-parameters, anti-slop/require-safety-comment-for-type-assertion -- transport parser validates the provider boundary.
|
|
11
|
+
const object = (value) => parseObject(value);
|
|
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.
|
|
15
|
+
const conversationHashWidth = 11;
|
|
16
|
+
const maxConversationHashes = 1200;
|
|
17
|
+
function conversationHash(id) {
|
|
18
|
+
let h1 = 0xdeadbeef;
|
|
19
|
+
let h2 = 0x41c6ce57;
|
|
20
|
+
for (let index = 0; index < id.length; index++) {
|
|
21
|
+
const code = id.charCodeAt(index);
|
|
22
|
+
h1 = Math.imul(h1 ^ code, 2654435761);
|
|
23
|
+
h2 = Math.imul(h2 ^ code, 1597334677);
|
|
24
|
+
}
|
|
25
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
26
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
27
|
+
const value = 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
28
|
+
return value.toString(36).padStart(conversationHashWidth, "0");
|
|
29
|
+
}
|
|
8
30
|
export { xLike, xUnlike } from "./x-engagement.js";
|
|
9
31
|
export function x(options) {
|
|
10
32
|
const request = managedHttp("https://api.x.com", {
|
|
11
|
-
apiKey: options.auth.accessToken,
|
|
33
|
+
apiKey: options.auth.accessToken ?? "app-auth-placeholder",
|
|
34
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
12
35
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
13
36
|
});
|
|
37
|
+
const appRequest = () => {
|
|
38
|
+
if (!options.appBearerToken?.trim())
|
|
39
|
+
throw new SocialError({
|
|
40
|
+
code: "missing_permission",
|
|
41
|
+
operation: "x.app-auth",
|
|
42
|
+
message: "This X endpoint requires an app-only bearer token. Configure appBearerToken.",
|
|
43
|
+
});
|
|
44
|
+
return managedHttp("https://api.x.com", {
|
|
45
|
+
apiKey: options.appBearerToken,
|
|
46
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated external boundary or fixture contract.
|
|
47
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
const requireUserToken = (operation) => {
|
|
51
|
+
if (!options.auth.accessToken?.trim())
|
|
52
|
+
throw new SocialError({
|
|
53
|
+
code: "missing_permission",
|
|
54
|
+
operation,
|
|
55
|
+
message: "This X operation requires a user access token.",
|
|
56
|
+
});
|
|
57
|
+
return request;
|
|
58
|
+
};
|
|
59
|
+
const readRequest = (path) => ["/2/users/me", "/liked_tweets", "/mentions", "/timelines/reverse_chronological"].some((suffix) => path.includes(suffix))
|
|
60
|
+
? requireUserToken("x.read")
|
|
61
|
+
: options.auth.accessToken?.trim()
|
|
62
|
+
? request
|
|
63
|
+
: appRequest();
|
|
14
64
|
const http = createHttp(options.fetch ? { fetch: options.fetch } : {});
|
|
15
65
|
const now = () => (options.clock?.() ?? new Date()).toISOString();
|
|
16
66
|
const authorize = (ref, context) => {
|
|
@@ -40,6 +90,14 @@ export function x(options) {
|
|
|
40
90
|
accountId: options.auth.userId,
|
|
41
91
|
},
|
|
42
92
|
displayName: string(user["name"]),
|
|
93
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
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.
|
|
43
101
|
...(typeof user["username"] === "string" ? { handle: user["username"] } : {}),
|
|
44
102
|
status: "connected",
|
|
45
103
|
};
|
|
@@ -55,6 +113,7 @@ export function x(options) {
|
|
|
55
113
|
operation: "posts.read",
|
|
56
114
|
message: "X post identity or author does not match the declared reference.",
|
|
57
115
|
});
|
|
116
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
58
117
|
return result;
|
|
59
118
|
}
|
|
60
119
|
async function listPosts(account, input, context) {
|
|
@@ -68,10 +127,12 @@ export function x(options) {
|
|
|
68
127
|
});
|
|
69
128
|
const result = object(await request(`/2/users/${encodeURIComponent(account.accountId)}/tweets`, context, undefined, {
|
|
70
129
|
"tweet.fields": "id,text,author_id,created_at,conversation_id",
|
|
130
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
71
131
|
...(input.cursor === undefined ? {} : { pagination_token: input.cursor }),
|
|
132
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
72
133
|
...(input.limit === undefined ? {} : { max_results: String(input.limit) }),
|
|
73
134
|
}));
|
|
74
|
-
const items = array(result["data"]).map((entry) => {
|
|
135
|
+
const items = (result["data"] === undefined ? [] : array(result["data"])).map((entry) => {
|
|
75
136
|
const row = object(entry);
|
|
76
137
|
const authorId = optionalString(row["author_id"]);
|
|
77
138
|
if (authorId !== account.accountId)
|
|
@@ -86,9 +147,152 @@ export function x(options) {
|
|
|
86
147
|
const nextCursor = optionalString(meta["next_token"]);
|
|
87
148
|
return {
|
|
88
149
|
items,
|
|
150
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
151
|
+
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function searchPosts(account, input, context, nativeInput) {
|
|
155
|
+
authorize(account, context);
|
|
156
|
+
const query = input.query;
|
|
157
|
+
const scope = input.scope ?? "recent";
|
|
158
|
+
const maxLimit = scope === "all" ? 500 : 100;
|
|
159
|
+
if (!query.trim() || query.length > 4096)
|
|
160
|
+
throw new SocialError({
|
|
161
|
+
code: "invalid_input",
|
|
162
|
+
operation: "search.posts",
|
|
163
|
+
message: "X search queries must contain 1-4096 characters.",
|
|
164
|
+
});
|
|
165
|
+
if (input.limit !== undefined &&
|
|
166
|
+
(!Number.isSafeInteger(input.limit) || input.limit < 10 || input.limit > maxLimit))
|
|
167
|
+
throw new SocialError({
|
|
168
|
+
code: "invalid_input",
|
|
169
|
+
operation: "search.posts",
|
|
170
|
+
message: `X ${scope} search limits must be integers from 10 through ${maxLimit}.`,
|
|
171
|
+
});
|
|
172
|
+
for (const [name, value] of [
|
|
173
|
+
["startTime", input.startTime],
|
|
174
|
+
["endTime", input.endTime],
|
|
175
|
+
]) {
|
|
176
|
+
if (value !== undefined && !Number.isFinite(Date.parse(value)))
|
|
177
|
+
throw new SocialError({
|
|
178
|
+
code: "invalid_input",
|
|
179
|
+
operation: "search.posts",
|
|
180
|
+
message: `${name} must be an ISO 8601 timestamp.`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (input.startTime !== undefined &&
|
|
184
|
+
input.endTime !== undefined &&
|
|
185
|
+
Date.parse(input.startTime) >= Date.parse(input.endTime))
|
|
186
|
+
throw new SocialError({
|
|
187
|
+
code: "invalid_input",
|
|
188
|
+
operation: "search.posts",
|
|
189
|
+
message: "startTime must be earlier than endTime.",
|
|
190
|
+
});
|
|
191
|
+
const result = object(await (options.auth.accessToken?.trim() ? request : appRequest())(`/2/tweets/search/${scope}`, context, undefined, {
|
|
192
|
+
query,
|
|
193
|
+
"tweet.fields": [
|
|
194
|
+
"id",
|
|
195
|
+
"text",
|
|
196
|
+
"edit_history_tweet_ids",
|
|
197
|
+
...(nativeInput?.tweetFields ?? [
|
|
198
|
+
"author_id",
|
|
199
|
+
"created_at",
|
|
200
|
+
"conversation_id",
|
|
201
|
+
"public_metrics",
|
|
202
|
+
"lang",
|
|
203
|
+
"entities",
|
|
204
|
+
"attachments",
|
|
205
|
+
"referenced_tweets",
|
|
206
|
+
]),
|
|
207
|
+
]
|
|
208
|
+
.filter((field, index, fields) => fields.indexOf(field) === index)
|
|
209
|
+
.join(","),
|
|
210
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
211
|
+
...(input.cursor === undefined ? {} : { next_token: input.cursor }),
|
|
212
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
213
|
+
...(input.limit === undefined ? {} : { max_results: String(input.limit) }),
|
|
214
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
215
|
+
...(input.startTime === undefined ? {} : { start_time: input.startTime }),
|
|
216
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
217
|
+
...(input.endTime === undefined ? {} : { end_time: input.endTime }),
|
|
218
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
219
|
+
...(nativeInput?.sortOrder === undefined ? {} : { sort_order: nativeInput.sortOrder }),
|
|
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(",") }),
|
|
232
|
+
}));
|
|
233
|
+
const requestedFields = nativeInput?.tweetFields ?? [
|
|
234
|
+
"id",
|
|
235
|
+
"text",
|
|
236
|
+
"author_id",
|
|
237
|
+
"created_at",
|
|
238
|
+
"conversation_id",
|
|
239
|
+
"public_metrics",
|
|
240
|
+
"lang",
|
|
241
|
+
"entities",
|
|
242
|
+
"attachments",
|
|
243
|
+
"referenced_tweets",
|
|
244
|
+
];
|
|
245
|
+
const items = (result["data"] === undefined ? [] : array(result["data"])).map((entry) => {
|
|
246
|
+
const row = object(entry);
|
|
247
|
+
return Object.fromEntries(["id", "text", ...requestedFields].flatMap((field, index, fields) => fields.indexOf(field) === index && field in row ? [[field, row[field]]] : []));
|
|
248
|
+
});
|
|
249
|
+
const meta = result["meta"] === undefined ? {} : object(result["meta"]);
|
|
250
|
+
const nextCursor = optionalString(meta["next_token"]);
|
|
251
|
+
const includes = result["includes"];
|
|
252
|
+
return {
|
|
253
|
+
items,
|
|
254
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
89
255
|
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
256
|
+
...(includes === undefined
|
|
257
|
+
? {}
|
|
258
|
+
: {
|
|
259
|
+
// SAFETY: X's `includes` member is a JSON object validated by the transport boundary.
|
|
260
|
+
metadata: { includes: object(includes) },
|
|
261
|
+
}),
|
|
90
262
|
};
|
|
91
263
|
}
|
|
264
|
+
async function pageRequest(path, account, input, context, query = {}, fields = "id,name,username,description,created_at,public_metrics", resource = "users") {
|
|
265
|
+
authorize(account, context);
|
|
266
|
+
const isTweets = resource === "tweets";
|
|
267
|
+
const minLimit = 1;
|
|
268
|
+
const maxLimit = 100;
|
|
269
|
+
if (input.limit !== undefined &&
|
|
270
|
+
(!Number.isInteger(input.limit) || input.limit < minLimit || input.limit > maxLimit))
|
|
271
|
+
throw new SocialError({
|
|
272
|
+
code: "invalid_input",
|
|
273
|
+
operation: path,
|
|
274
|
+
message: `X limits must be integers from ${minLimit} through ${maxLimit}.`,
|
|
275
|
+
});
|
|
276
|
+
const result = object(await readRequest(path)(path, context, undefined, {
|
|
277
|
+
...query,
|
|
278
|
+
...(isTweets
|
|
279
|
+
? {
|
|
280
|
+
"tweet.fields": "id,text,author_id,created_at,conversation_id,public_metrics,referenced_tweets",
|
|
281
|
+
expansions: "author_id,referenced_tweets.id",
|
|
282
|
+
}
|
|
283
|
+
: path.endsWith("/owned_lists") || path.endsWith("/pinned_lists")
|
|
284
|
+
? { "list.fields": fields }
|
|
285
|
+
: fields
|
|
286
|
+
? { "user.fields": fields }
|
|
287
|
+
: {}),
|
|
288
|
+
...(input.cursor === undefined ? {} : { pagination_token: input.cursor }),
|
|
289
|
+
...(input.limit === undefined ? {} : { max_results: String(input.limit) }),
|
|
290
|
+
}));
|
|
291
|
+
const items = (result["data"] === undefined ? [] : array(result["data"])).map((entry) => object(entry));
|
|
292
|
+
const meta = result["meta"] === undefined ? {} : object(result["meta"]);
|
|
293
|
+
const nextCursor = optionalString(meta["next_token"]);
|
|
294
|
+
return { items, ...(nextCursor === undefined ? {} : { nextCursor }) };
|
|
295
|
+
}
|
|
92
296
|
async function getAccountMetrics(account, context) {
|
|
93
297
|
authorize(account, context);
|
|
94
298
|
const result = object(await request(`/2/users/${encodeURIComponent(account.accountId)}`, context, undefined, {
|
|
@@ -137,6 +341,7 @@ export function x(options) {
|
|
|
137
341
|
"tweet.fields": "conversation_id",
|
|
138
342
|
}))["data"]);
|
|
139
343
|
if (root["id"] !== ref.postId ||
|
|
344
|
+
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- validated boundary or fixture contract.
|
|
140
345
|
(typeof parent["conversation_id"] === "string" &&
|
|
141
346
|
parent["conversation_id"] !== root["conversation_id"]))
|
|
142
347
|
throw new SocialError({
|
|
@@ -147,6 +352,7 @@ export function x(options) {
|
|
|
147
352
|
}
|
|
148
353
|
}
|
|
149
354
|
async function uploadImage(media, context) {
|
|
355
|
+
requireUserToken("media.upload");
|
|
150
356
|
if (media.source.kind !== "blob" ||
|
|
151
357
|
media.kind !== "image" ||
|
|
152
358
|
media.source.blob.size > 5 * 1024 * 1024)
|
|
@@ -166,6 +372,7 @@ export function x(options) {
|
|
|
166
372
|
headers: { Authorization: `Bearer ${options.auth.accessToken}` },
|
|
167
373
|
body,
|
|
168
374
|
timeoutMs: remainingBudget(context),
|
|
375
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
169
376
|
...(context.signal ? { signal: context.signal } : {}),
|
|
170
377
|
});
|
|
171
378
|
}
|
|
@@ -215,7 +422,8 @@ export function x(options) {
|
|
|
215
422
|
},
|
|
216
423
|
};
|
|
217
424
|
}
|
|
218
|
-
|
|
425
|
+
let nativeAdapter;
|
|
426
|
+
const adapter = defineAdapter({
|
|
219
427
|
id: "x",
|
|
220
428
|
capabilities: {
|
|
221
429
|
schemaVersion: 1,
|
|
@@ -227,7 +435,7 @@ export function x(options) {
|
|
|
227
435
|
platform: "x",
|
|
228
436
|
operation: "posts.publish",
|
|
229
437
|
availability: "available",
|
|
230
|
-
formats: ["text", "image"
|
|
438
|
+
formats: ["text", "image"],
|
|
231
439
|
requiredScopes: ["tweet.read", "tweet.write", "users.read", "media.write"],
|
|
232
440
|
notes: "User-context OAuth2 token; current X API access/billing required. Up to four static JPEG/PNG image Blobs, each at most 5 MiB. Video/GIF processing is outside this slice.",
|
|
233
441
|
},
|
|
@@ -236,6 +444,7 @@ export function x(options) {
|
|
|
236
444
|
"posts.list",
|
|
237
445
|
"posts.read",
|
|
238
446
|
"analytics.read",
|
|
447
|
+
"analytics.account.read",
|
|
239
448
|
"comments.write",
|
|
240
449
|
"posts.repost",
|
|
241
450
|
"posts.quote",
|
|
@@ -244,29 +453,150 @@ export function x(options) {
|
|
|
244
453
|
"bookmarks.read",
|
|
245
454
|
"bookmarks.write",
|
|
246
455
|
"follows.write",
|
|
456
|
+
"mentions.read",
|
|
457
|
+
"likes.read",
|
|
458
|
+
"likes.write",
|
|
459
|
+
"lists.read",
|
|
460
|
+
"lists.write",
|
|
461
|
+
"timelines.read",
|
|
247
462
|
].map((operation) => ({
|
|
248
463
|
platform: "x",
|
|
249
464
|
operation,
|
|
250
465
|
availability: "available",
|
|
466
|
+
...(["likes.read"].includes(operation)
|
|
467
|
+
? { requiredScopes: ["like.read", "tweet.read", "users.read"] }
|
|
468
|
+
: ["likes.write"].includes(operation)
|
|
469
|
+
? { requiredScopes: ["like.write", "tweet.read", "users.read"] }
|
|
470
|
+
: ["lists.read"].includes(operation)
|
|
471
|
+
? { requiredScopes: ["list.read", "tweet.read", "users.read"] }
|
|
472
|
+
: ["lists.write"].includes(operation)
|
|
473
|
+
? { requiredScopes: ["list.write", "tweet.read", "users.read"] }
|
|
474
|
+
: ["timelines.read", "mentions.read", "posts.list"].includes(operation)
|
|
475
|
+
? { requiredScopes: ["tweet.read", "users.read"] }
|
|
476
|
+
: ["posts.quote", "posts.delete", "posts.publish"].includes(operation)
|
|
477
|
+
? { requiredScopes: ["tweet.read", "tweet.write", "users.read"] }
|
|
478
|
+
: {}),
|
|
251
479
|
})),
|
|
252
480
|
{
|
|
253
481
|
platform: "x",
|
|
254
|
-
operation: "
|
|
482
|
+
operation: "posts.removeFromPlatform",
|
|
483
|
+
availability: "available",
|
|
484
|
+
requiredScopes: ["tweet.read", "tweet.write", "users.read"],
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
platform: "x",
|
|
488
|
+
operation: "graph.read",
|
|
489
|
+
availability: "available",
|
|
490
|
+
requiredScopes: ["users.read", "follows.read", "mute.read", "block.read"],
|
|
491
|
+
notes: "The required read scope depends on the requested relationship kind.",
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
platform: "x",
|
|
495
|
+
operation: "profiles.read",
|
|
496
|
+
availability: "available",
|
|
497
|
+
requiredScopes: ["users.read"],
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
platform: "x",
|
|
501
|
+
operation: "graph.follow",
|
|
502
|
+
availability: "available",
|
|
503
|
+
requiredScopes: ["users.read", "follows.write"],
|
|
504
|
+
notes: "Follow writes require an eligible paid X API tier.",
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
platform: "x",
|
|
508
|
+
operation: "graph.unfollow",
|
|
509
|
+
availability: "available",
|
|
510
|
+
requiredScopes: ["users.read", "follows.write"],
|
|
511
|
+
notes: "Follow writes require an eligible paid X API tier.",
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
platform: "x",
|
|
515
|
+
operation: "graph.mute",
|
|
516
|
+
availability: "available",
|
|
517
|
+
requiredScopes: ["users.read", "mute.write"],
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
platform: "x",
|
|
521
|
+
operation: "graph.unmute",
|
|
522
|
+
availability: "available",
|
|
523
|
+
requiredScopes: ["users.read", "mute.write"],
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
platform: "x",
|
|
527
|
+
operation: "graph.block",
|
|
528
|
+
availability: "available",
|
|
529
|
+
requiredScopes: ["users.read", "block.write"],
|
|
530
|
+
notes: "Enterprise plan only.",
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
platform: "x",
|
|
534
|
+
operation: "graph.unblock",
|
|
535
|
+
availability: "available",
|
|
536
|
+
requiredScopes: ["users.read", "block.write"],
|
|
537
|
+
notes: "Enterprise plan only.",
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
platform: "x",
|
|
541
|
+
operation: "lists.pinned.read",
|
|
542
|
+
availability: "available",
|
|
543
|
+
requiredScopes: ["users.read", "list.read"],
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
platform: "x",
|
|
547
|
+
operation: "lists.pinned.write",
|
|
548
|
+
availability: "available",
|
|
549
|
+
requiredScopes: ["users.read", "list.write"],
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
platform: "x",
|
|
553
|
+
operation: "messages.conversation.write",
|
|
255
554
|
availability: "available",
|
|
555
|
+
requiredScopes: ["dm.write"],
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
platform: "x",
|
|
559
|
+
operation: "messages.group.write",
|
|
560
|
+
availability: "available",
|
|
561
|
+
requiredScopes: ["dm.write"],
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
platform: "x",
|
|
565
|
+
operation: "search.posts",
|
|
566
|
+
availability: "available",
|
|
567
|
+
requiredScopes: ["tweet.read", "users.read"],
|
|
568
|
+
notes: "Recent search covers the last 7 days. Full-archive search requires an eligible X API pay-per-use or Enterprise plan.",
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
platform: "x",
|
|
572
|
+
operation: "media.video",
|
|
573
|
+
availability: "not-implemented-by-adapter",
|
|
256
574
|
formats: ["video"],
|
|
257
575
|
},
|
|
258
|
-
{
|
|
576
|
+
{
|
|
577
|
+
platform: "x",
|
|
578
|
+
operation: "media.gif",
|
|
579
|
+
availability: "not-implemented-by-adapter",
|
|
580
|
+
},
|
|
259
581
|
{
|
|
260
582
|
platform: "x",
|
|
261
583
|
operation: "messages.read",
|
|
262
|
-
availability: "
|
|
263
|
-
requiredScopes: ["dm.read"],
|
|
584
|
+
availability: "available",
|
|
585
|
+
requiredScopes: ["dm.read", "users.read", "tweet.read"],
|
|
586
|
+
notes: "Requires a user-context token and an X API tier that includes Direct Messages.",
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
platform: "x",
|
|
590
|
+
operation: "messages.write",
|
|
591
|
+
availability: "available",
|
|
592
|
+
requiredScopes: ["dm.write", "dm.read", "users.read", "tweet.read"],
|
|
593
|
+
notes: "Requires a user-context token and an X API tier that includes Direct Messages.",
|
|
264
594
|
},
|
|
265
595
|
{
|
|
266
596
|
platform: "x",
|
|
267
597
|
operation: "streams.read",
|
|
268
|
-
availability: "
|
|
269
|
-
notes: "Filtered stream
|
|
598
|
+
availability: "not-implemented-by-adapter",
|
|
599
|
+
notes: "Filtered stream rules and streaming transport are not implemented by this adapter.",
|
|
270
600
|
},
|
|
271
601
|
],
|
|
272
602
|
},
|
|
@@ -279,6 +609,134 @@ export function x(options) {
|
|
|
279
609
|
return readAccount(context);
|
|
280
610
|
},
|
|
281
611
|
},
|
|
612
|
+
graph: {
|
|
613
|
+
async getProfile(account, input, context) {
|
|
614
|
+
const value = input.profileId !== undefined
|
|
615
|
+
? await nativeAdapter.getUserById({ account, userId: input.profileId, context })
|
|
616
|
+
: input.handle !== undefined
|
|
617
|
+
? await nativeAdapter.getUserByUsername({ account, username: input.handle, context })
|
|
618
|
+
: await nativeAdapter.getMe({ account, context });
|
|
619
|
+
const user = object(object(value)["data"]);
|
|
620
|
+
const id = string(user["id"]);
|
|
621
|
+
return {
|
|
622
|
+
ref: profileRef({
|
|
623
|
+
backend: context.backendInstance,
|
|
624
|
+
platform: "x",
|
|
625
|
+
accountId: account.accountId,
|
|
626
|
+
profileId: id,
|
|
627
|
+
}),
|
|
628
|
+
...(typeof user["name"] === "string" ? { displayName: user["name"] } : {}),
|
|
629
|
+
...(typeof user["username"] === "string" ? { handle: user["username"] } : {}),
|
|
630
|
+
...(typeof user["description"] === "string" ? { bio: user["description"] } : {}),
|
|
631
|
+
native: user,
|
|
632
|
+
};
|
|
633
|
+
},
|
|
634
|
+
async listRelationships(account, input, context) {
|
|
635
|
+
const pagination = {
|
|
636
|
+
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
|
|
637
|
+
...(input.limit === undefined ? {} : { limit: input.limit }),
|
|
638
|
+
};
|
|
639
|
+
const result = input.kind === "following"
|
|
640
|
+
? await nativeAdapter.following({
|
|
641
|
+
account,
|
|
642
|
+
...pagination,
|
|
643
|
+
userId: account.accountId,
|
|
644
|
+
context,
|
|
645
|
+
})
|
|
646
|
+
: input.kind === "followers"
|
|
647
|
+
? await nativeAdapter.followers({
|
|
648
|
+
account,
|
|
649
|
+
...pagination,
|
|
650
|
+
userId: account.accountId,
|
|
651
|
+
context,
|
|
652
|
+
})
|
|
653
|
+
: input.kind === "blocked"
|
|
654
|
+
? await nativeAdapter.blockedUsers({
|
|
655
|
+
account,
|
|
656
|
+
...pagination,
|
|
657
|
+
context,
|
|
658
|
+
})
|
|
659
|
+
: await nativeAdapter.mutedUsers({
|
|
660
|
+
account,
|
|
661
|
+
...pagination,
|
|
662
|
+
context,
|
|
663
|
+
});
|
|
664
|
+
return {
|
|
665
|
+
items: result.items.map((entry) => {
|
|
666
|
+
const user = object(entry);
|
|
667
|
+
const id = string(user["id"]);
|
|
668
|
+
return {
|
|
669
|
+
profile: profileRef({
|
|
670
|
+
backend: context.backendInstance,
|
|
671
|
+
platform: "x",
|
|
672
|
+
accountId: account.accountId,
|
|
673
|
+
profileId: id,
|
|
674
|
+
}),
|
|
675
|
+
relationship: input.kind === "following"
|
|
676
|
+
? "following"
|
|
677
|
+
: input.kind === "followers"
|
|
678
|
+
? "follower"
|
|
679
|
+
: input.kind,
|
|
680
|
+
};
|
|
681
|
+
}),
|
|
682
|
+
...(result.nextCursor === undefined ? {} : { nextCursor: result.nextCursor }),
|
|
683
|
+
};
|
|
684
|
+
},
|
|
685
|
+
async follow(target, context) {
|
|
686
|
+
const account = connectedAccountRef({
|
|
687
|
+
backend: target.backend,
|
|
688
|
+
platform: "x",
|
|
689
|
+
accountId: target.accountId,
|
|
690
|
+
});
|
|
691
|
+
await nativeAdapter.followUser({ account, userId: target.profileId, context });
|
|
692
|
+
return { profile: target, relationship: "following" };
|
|
693
|
+
},
|
|
694
|
+
async unfollow(target, context) {
|
|
695
|
+
const account = connectedAccountRef({
|
|
696
|
+
backend: target.backend,
|
|
697
|
+
platform: "x",
|
|
698
|
+
accountId: target.accountId,
|
|
699
|
+
});
|
|
700
|
+
await nativeAdapter.unfollowUser({ account, userId: target.profileId, context });
|
|
701
|
+
},
|
|
702
|
+
async block(target, context) {
|
|
703
|
+
const account = connectedAccountRef({
|
|
704
|
+
backend: target.backend,
|
|
705
|
+
platform: "x",
|
|
706
|
+
accountId: target.accountId,
|
|
707
|
+
});
|
|
708
|
+
await nativeAdapter.blockUser({ account, userId: target.profileId, context });
|
|
709
|
+
return { profile: target, relationship: "blocked" };
|
|
710
|
+
},
|
|
711
|
+
async unblock(target, context) {
|
|
712
|
+
const account = connectedAccountRef({
|
|
713
|
+
backend: target.backend,
|
|
714
|
+
platform: "x",
|
|
715
|
+
accountId: target.accountId,
|
|
716
|
+
});
|
|
717
|
+
await nativeAdapter.unblockUser({ account, userId: target.profileId, context });
|
|
718
|
+
},
|
|
719
|
+
async mute(target, context) {
|
|
720
|
+
const account = connectedAccountRef({
|
|
721
|
+
backend: target.backend,
|
|
722
|
+
platform: "x",
|
|
723
|
+
accountId: target.accountId,
|
|
724
|
+
});
|
|
725
|
+
await nativeAdapter.muteUser({ account, userId: target.profileId, context });
|
|
726
|
+
return { profile: target, relationship: "muted" };
|
|
727
|
+
},
|
|
728
|
+
async unmute(target, context) {
|
|
729
|
+
const account = connectedAccountRef({
|
|
730
|
+
backend: target.backend,
|
|
731
|
+
platform: "x",
|
|
732
|
+
accountId: target.accountId,
|
|
733
|
+
});
|
|
734
|
+
await nativeAdapter.unmuteUser({ account, userId: target.profileId, context });
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
search: {
|
|
738
|
+
posts: (account, input, context) => searchPosts(account, input, context),
|
|
739
|
+
},
|
|
282
740
|
posts: {
|
|
283
741
|
list: listPosts,
|
|
284
742
|
prepareTarget(target) {
|
|
@@ -324,8 +782,11 @@ export function x(options) {
|
|
|
324
782
|
ids.push(await uploadImage(media, context));
|
|
325
783
|
const settings = target.options === undefined ? {} : object(target.options);
|
|
326
784
|
return createPost(target.account, target.content.text ?? "", context, {
|
|
785
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
327
786
|
...(ids.length ? { media: { media_ids: ids } } : {}),
|
|
787
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
328
788
|
...(target.replyTo ? { reply: { in_reply_to_tweet_id: target.replyTo.postId } } : {}),
|
|
789
|
+
// oxlint-disable-next-line anti-slop/no-conditional-empty-object-spread -- validated boundary or fixture contract.
|
|
329
790
|
...(settings["replySettings"] && settings["replySettings"] !== "everyone"
|
|
330
791
|
? { reply_settings: string(settings["replySettings"]) }
|
|
331
792
|
: {}),
|
|
@@ -340,6 +801,18 @@ export function x(options) {
|
|
|
340
801
|
"conversation_id",
|
|
341
802
|
]);
|
|
342
803
|
},
|
|
804
|
+
async removeFromPlatform(ref, context) {
|
|
805
|
+
authorize(ref, context);
|
|
806
|
+
await nativeAdapter.deletePost({
|
|
807
|
+
account: connectedAccountRef({
|
|
808
|
+
backend: ref.backend,
|
|
809
|
+
platform: "x",
|
|
810
|
+
accountId: ref.accountId,
|
|
811
|
+
}),
|
|
812
|
+
postId: ref.postId,
|
|
813
|
+
context,
|
|
814
|
+
});
|
|
815
|
+
},
|
|
343
816
|
},
|
|
344
817
|
comments: {
|
|
345
818
|
async list() {
|
|
@@ -368,6 +841,114 @@ export function x(options) {
|
|
|
368
841
|
return { ...ref, commentId: result.post.postId };
|
|
369
842
|
},
|
|
370
843
|
},
|
|
844
|
+
messages: {
|
|
845
|
+
async listConversations(account, input, context) {
|
|
846
|
+
// X has no conversation-list endpoint. This page is derived from /2/dm_events,
|
|
847
|
+
// with the latest event for each distinct dm_conversation_id.
|
|
848
|
+
requireUserToken("messages.read");
|
|
849
|
+
if (input.limit !== undefined &&
|
|
850
|
+
(!Number.isInteger(input.limit) || input.limit < 1 || input.limit > 100))
|
|
851
|
+
throw new SocialError({
|
|
852
|
+
code: "invalid_input",
|
|
853
|
+
operation: "messages.read",
|
|
854
|
+
message: "X DM limits must be integers from 1 through 100.",
|
|
855
|
+
});
|
|
856
|
+
// The cursor carries the event cursor plus fixed-width hashes of the conversations
|
|
857
|
+
// already returned, so later event pages skip them. To stay under the client cursor
|
|
858
|
+
// size limit, the walk ends after maxConversationHashes distinct conversations
|
|
859
|
+
// rather than forgetting earlier ones and returning them again.
|
|
860
|
+
let eventCursor;
|
|
861
|
+
const seen = [];
|
|
862
|
+
if (input.cursor !== undefined) {
|
|
863
|
+
try {
|
|
864
|
+
const state = parseObject(JSON.parse(input.cursor));
|
|
865
|
+
eventCursor = optionalString(state["c"]);
|
|
866
|
+
const hashes = string(state["s"]);
|
|
867
|
+
if (hashes.length % conversationHashWidth !== 0)
|
|
868
|
+
throw new Error("bad cursor");
|
|
869
|
+
for (let at = 0; at < hashes.length; at += conversationHashWidth)
|
|
870
|
+
seen.push(hashes.slice(at, at + conversationHashWidth));
|
|
871
|
+
}
|
|
872
|
+
catch {
|
|
873
|
+
throw new SocialError({
|
|
874
|
+
code: "invalid_input",
|
|
875
|
+
operation: "messages.read",
|
|
876
|
+
message: "Use a cursor returned by listConversations.",
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const seenSet = new Set(seen);
|
|
881
|
+
const target = input.limit ?? 100;
|
|
882
|
+
const items = [];
|
|
883
|
+
// A walk that has reached the conversation cap has no next page.
|
|
884
|
+
const pageFrom = (c) => seen.length >= maxConversationHashes
|
|
885
|
+
? { items }
|
|
886
|
+
: { items, nextCursor: JSON.stringify({ c, s: seen.join("") }) };
|
|
887
|
+
for (let fetches = 0; fetches < 10; fetches++) {
|
|
888
|
+
const page = await nativeAdapter.listDirectMessages({
|
|
889
|
+
account,
|
|
890
|
+
...(eventCursor === undefined ? {} : { cursor: eventCursor }),
|
|
891
|
+
...(input.limit === undefined ? {} : { limit: input.limit }),
|
|
892
|
+
context,
|
|
893
|
+
});
|
|
894
|
+
for (const event of page.items) {
|
|
895
|
+
const conversationId = optionalString(event["dm_conversation_id"]);
|
|
896
|
+
if (conversationId === undefined)
|
|
897
|
+
continue;
|
|
898
|
+
const hash = conversationHash(conversationId);
|
|
899
|
+
if (seenSet.has(hash))
|
|
900
|
+
continue;
|
|
901
|
+
if (seen.length >= maxConversationHashes)
|
|
902
|
+
return { items };
|
|
903
|
+
// Stop mid-page and reread this event page next time; seen IDs skip the rest.
|
|
904
|
+
if (items.length === target)
|
|
905
|
+
return pageFrom(eventCursor);
|
|
906
|
+
seenSet.add(hash);
|
|
907
|
+
seen.push(hash);
|
|
908
|
+
items.push(event);
|
|
909
|
+
}
|
|
910
|
+
if (page.nextCursor === undefined)
|
|
911
|
+
return { items };
|
|
912
|
+
eventCursor = page.nextCursor;
|
|
913
|
+
if (items.length === target)
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
return pageFrom(eventCursor);
|
|
917
|
+
},
|
|
918
|
+
async listMessages(conversation, input, context) {
|
|
919
|
+
requireUserToken("messages.read");
|
|
920
|
+
return nativeAdapter.listDirectMessages({
|
|
921
|
+
account: connectedAccountRef({
|
|
922
|
+
backend: conversation.backend,
|
|
923
|
+
platform: "x",
|
|
924
|
+
accountId: conversation.accountId,
|
|
925
|
+
}),
|
|
926
|
+
conversationId: conversation.conversationId,
|
|
927
|
+
...(input.cursor === undefined ? {} : { cursor: input.cursor }),
|
|
928
|
+
...(input.limit === undefined ? {} : { limit: input.limit }),
|
|
929
|
+
context,
|
|
930
|
+
});
|
|
931
|
+
},
|
|
932
|
+
async send(conversation, content, context) {
|
|
933
|
+
requireUserToken("messages.write");
|
|
934
|
+
if (!content.text.trim())
|
|
935
|
+
throw new SocialError({
|
|
936
|
+
code: "invalid_input",
|
|
937
|
+
operation: "messages.write",
|
|
938
|
+
message: "X direct messages require non-empty text.",
|
|
939
|
+
});
|
|
940
|
+
return nativeAdapter.sendConversationMessage({
|
|
941
|
+
account: connectedAccountRef({
|
|
942
|
+
backend: conversation.backend,
|
|
943
|
+
platform: "x",
|
|
944
|
+
accountId: conversation.accountId,
|
|
945
|
+
}),
|
|
946
|
+
conversationId: conversation.conversationId,
|
|
947
|
+
text: content.text,
|
|
948
|
+
context,
|
|
949
|
+
});
|
|
950
|
+
},
|
|
951
|
+
},
|
|
371
952
|
analytics: {
|
|
372
953
|
getAccountMetrics,
|
|
373
954
|
async getPostMetrics(ref, context) {
|
|
@@ -378,6 +959,8 @@ export function x(options) {
|
|
|
378
959
|
["reposts", "retweet_count"],
|
|
379
960
|
["replies", "reply_count"],
|
|
380
961
|
["quotes", "quote_count"],
|
|
962
|
+
["impressions", "impression_count"],
|
|
963
|
+
["bookmarks", "bookmark_count"],
|
|
381
964
|
].flatMap(([name, key]) => {
|
|
382
965
|
const value = key ? optionalNumber(counts[key]) : undefined;
|
|
383
966
|
return value === undefined || !name
|
|
@@ -396,16 +979,24 @@ export function x(options) {
|
|
|
396
979
|
});
|
|
397
980
|
},
|
|
398
981
|
},
|
|
399
|
-
native: {
|
|
982
|
+
native: (nativeAdapter = {
|
|
983
|
+
async searchRecentPosts({ account, search, context }) {
|
|
984
|
+
return searchPosts(account, { ...search, scope: "recent" }, context, search);
|
|
985
|
+
},
|
|
986
|
+
async searchAllPosts({ account, search, context }) {
|
|
987
|
+
return searchPosts(account, { ...search, scope: "all" }, context, search);
|
|
988
|
+
},
|
|
400
989
|
readPost,
|
|
401
990
|
async repost({ account, postId, context }) {
|
|
402
991
|
authorize(account, context);
|
|
992
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
403
993
|
return object(await request(`/2/users/${encodeURIComponent(account.accountId)}/retweets`, context, {
|
|
404
994
|
tweet_id: postId,
|
|
405
995
|
}));
|
|
406
996
|
},
|
|
407
997
|
async quote({ account, text, quotedPostId, context }) {
|
|
408
998
|
authorize(account, context);
|
|
999
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
409
1000
|
return object(await request("/2/tweets", context, { text, quote_tweet_id: quotedPostId }));
|
|
410
1001
|
},
|
|
411
1002
|
async deletePost({ account, postId, context }) {
|
|
@@ -423,6 +1014,7 @@ export function x(options) {
|
|
|
423
1014
|
operation: "x.polls.create",
|
|
424
1015
|
message: "X polls require 2-4 options and a duration from 5 minutes to 7 days.",
|
|
425
1016
|
});
|
|
1017
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
426
1018
|
return object(await request("/2/tweets", context, {
|
|
427
1019
|
text,
|
|
428
1020
|
poll: { options: [...pollOptions], duration_minutes: durationMinutes },
|
|
@@ -430,6 +1022,7 @@ export function x(options) {
|
|
|
430
1022
|
},
|
|
431
1023
|
async bookmarks({ account, context }) {
|
|
432
1024
|
authorize(account, context);
|
|
1025
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
433
1026
|
return object(await request(`/2/users/${encodeURIComponent(account.accountId)}/bookmarks`, context));
|
|
434
1027
|
},
|
|
435
1028
|
async bookmark({ account, postId, context }) {
|
|
@@ -444,6 +1037,7 @@ export function x(options) {
|
|
|
444
1037
|
},
|
|
445
1038
|
async follow({ account, userId, context }) {
|
|
446
1039
|
authorize(account, context);
|
|
1040
|
+
// oxlint-disable-next-line anti-slop/require-safety-comment-for-type-assertion -- validated boundary or fixture contract.
|
|
447
1041
|
return object(await request(`/2/users/${encodeURIComponent(account.accountId)}/following`, context, {
|
|
448
1042
|
target_user_id: userId,
|
|
449
1043
|
}));
|
|
@@ -452,60 +1046,243 @@ export function x(options) {
|
|
|
452
1046
|
authorize(account, context);
|
|
453
1047
|
await request(`/2/users/${encodeURIComponent(account.accountId)}/following/${encodeURIComponent(userId)}`, context, undefined, {}, "DELETE");
|
|
454
1048
|
},
|
|
455
|
-
async
|
|
1049
|
+
async listDirectMessages({ account, participantId, conversationId, cursor, limit, context }) {
|
|
456
1050
|
authorize(account, context);
|
|
457
|
-
if (!
|
|
1051
|
+
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 100))
|
|
458
1052
|
throw new SocialError({
|
|
459
1053
|
code: "invalid_input",
|
|
460
|
-
operation: "
|
|
461
|
-
message: "
|
|
1054
|
+
operation: "messages.read",
|
|
1055
|
+
message: "X DM limits must be integers from 1 through 100.",
|
|
462
1056
|
});
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
...(
|
|
1057
|
+
const path = conversationId
|
|
1058
|
+
? `/2/dm_conversations/${encodeURIComponent(conversationId)}/dm_events`
|
|
1059
|
+
: participantId
|
|
1060
|
+
? `/2/dm_conversations/with/${encodeURIComponent(participantId)}/dm_events`
|
|
1061
|
+
: "/2/dm_events";
|
|
1062
|
+
const result = object(await request(path, context, undefined, {
|
|
1063
|
+
"dm_event.fields": "id,text,event_type,created_at,dm_conversation_id,attachments,entities",
|
|
1064
|
+
expansions: "sender_id,participant_ids",
|
|
1065
|
+
...(cursor === undefined ? {} : { pagination_token: cursor }),
|
|
1066
|
+
...(limit === undefined ? {} : { max_results: String(limit) }),
|
|
473
1067
|
}));
|
|
1068
|
+
const meta = result["meta"] === undefined ? {} : object(result["meta"]);
|
|
1069
|
+
const nextCursor = optionalString(meta["next_token"]);
|
|
1070
|
+
return {
|
|
1071
|
+
items: result["data"] === undefined ? [] : array(result["data"]).map(object),
|
|
1072
|
+
...(nextCursor === undefined ? {} : { nextCursor }),
|
|
1073
|
+
};
|
|
1074
|
+
},
|
|
1075
|
+
async sendDirectMessage({ account, participantId, text, context }) {
|
|
1076
|
+
authorize(account, context);
|
|
1077
|
+
const userRequest = requireUserToken("messages.write");
|
|
1078
|
+
if (!text.trim())
|
|
1079
|
+
throw new SocialError({
|
|
1080
|
+
code: "invalid_input",
|
|
1081
|
+
operation: "messages.write",
|
|
1082
|
+
message: "X direct messages require non-empty text.",
|
|
1083
|
+
});
|
|
1084
|
+
return object(await userRequest(`/2/dm_conversations/with/${encodeURIComponent(participantId)}/messages`, context, { text }));
|
|
1085
|
+
},
|
|
1086
|
+
async sendConversationMessage({ account, conversationId, text, attachments, context }) {
|
|
1087
|
+
authorize(account, context);
|
|
1088
|
+
if (!text.trim())
|
|
1089
|
+
throw new SocialError({
|
|
1090
|
+
code: "invalid_input",
|
|
1091
|
+
operation: "messages.conversation.write",
|
|
1092
|
+
message: "X direct messages require non-empty text.",
|
|
1093
|
+
});
|
|
1094
|
+
return object(await requireUserToken("messages.conversation.write")(`/2/dm_conversations/${encodeURIComponent(conversationId)}/messages`, context, { text, ...(attachments === undefined ? {} : { attachments }) }));
|
|
474
1095
|
},
|
|
475
|
-
async
|
|
1096
|
+
async createGroupConversation({ account, participantIds, message, context }) {
|
|
476
1097
|
authorize(account, context);
|
|
477
|
-
if (
|
|
1098
|
+
if (!message.trim())
|
|
478
1099
|
throw new SocialError({
|
|
479
1100
|
code: "invalid_input",
|
|
480
|
-
operation: "
|
|
481
|
-
message: "
|
|
1101
|
+
operation: "messages.group.write",
|
|
1102
|
+
message: "X direct messages require non-empty text.",
|
|
482
1103
|
});
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
url: new URL("https://api.x.com/2/media/upload"),
|
|
488
|
-
method: "POST",
|
|
489
|
-
headers: { Authorization: `Bearer ${options.auth.accessToken}` },
|
|
490
|
-
body,
|
|
491
|
-
timeoutMs: remainingBudget(context),
|
|
492
|
-
...(context.signal ? { signal: context.signal } : {}),
|
|
1104
|
+
return object(await requireUserToken("messages.group.write")("/2/dm_conversations", context, {
|
|
1105
|
+
conversation_type: "Group",
|
|
1106
|
+
participant_ids: [...participantIds],
|
|
1107
|
+
message: { text: message },
|
|
493
1108
|
}));
|
|
494
1109
|
},
|
|
495
|
-
async
|
|
1110
|
+
async getUserById({ account, userId, context }) {
|
|
496
1111
|
authorize(account, context);
|
|
497
|
-
|
|
1112
|
+
const result = object(await readRequest("users")(`/2/users/${encodeURIComponent(userId)}`, context, undefined, {
|
|
1113
|
+
"user.fields": "id,name,username,description,created_at,public_metrics,profile_image_url,verified",
|
|
1114
|
+
}));
|
|
1115
|
+
const errors = result["errors"];
|
|
1116
|
+
if (errors !== undefined && result["data"] === undefined) {
|
|
1117
|
+
const detail = array(errors)[0] === undefined
|
|
1118
|
+
? "X user was not found."
|
|
1119
|
+
: (optionalString(object(array(errors)[0])["detail"]) ?? "X user was not found.");
|
|
1120
|
+
throw new SocialError({ code: "not_found", operation: "profiles.read", message: detail });
|
|
1121
|
+
}
|
|
1122
|
+
return result;
|
|
498
1123
|
},
|
|
499
|
-
async
|
|
1124
|
+
async getUserByUsername({ account, username, context }) {
|
|
500
1125
|
authorize(account, context);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
headers: { Authorization: `Bearer ${options.auth.accessToken}` },
|
|
504
|
-
timeoutMs: remainingBudget(context),
|
|
505
|
-
...(context.signal ? { signal: context.signal } : {}),
|
|
1126
|
+
const result = object(await readRequest("users")(`/2/users/by/username/${encodeURIComponent(username)}`, context, undefined, {
|
|
1127
|
+
"user.fields": "id,name,username,description,created_at,public_metrics,profile_image_url,verified",
|
|
506
1128
|
}));
|
|
1129
|
+
if (result["errors"] !== undefined && result["data"] === undefined) {
|
|
1130
|
+
const first = array(result["errors"])[0];
|
|
1131
|
+
throw new SocialError({
|
|
1132
|
+
code: "not_found",
|
|
1133
|
+
operation: "profiles.read",
|
|
1134
|
+
message: first === undefined
|
|
1135
|
+
? "X user was not found."
|
|
1136
|
+
: (optionalString(object(first)["detail"]) ?? "X user was not found."),
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
return result;
|
|
507
1140
|
},
|
|
508
|
-
|
|
1141
|
+
async getMe({ account, context }) {
|
|
1142
|
+
authorize(account, context);
|
|
1143
|
+
return object(await readRequest("/2/users/me")("/2/users/me", context, undefined, {
|
|
1144
|
+
"user.fields": "id,name,username,description,created_at,public_metrics,profile_image_url,verified",
|
|
1145
|
+
}));
|
|
1146
|
+
},
|
|
1147
|
+
async followers({ account, userId, cursor, limit, context }) {
|
|
1148
|
+
return pageRequest(`/2/users/${encodeURIComponent(userId)}/followers`, account, { cursor, limit }, context);
|
|
1149
|
+
},
|
|
1150
|
+
async following({ account, userId, cursor, limit, context }) {
|
|
1151
|
+
return pageRequest(`/2/users/${encodeURIComponent(userId)}/following`, account, { cursor, limit }, context);
|
|
1152
|
+
},
|
|
1153
|
+
async followUser({ account, userId, context }) {
|
|
1154
|
+
authorize(account, context);
|
|
1155
|
+
return object(await requireUserToken("graph.follow")(`/2/users/${encodeURIComponent(account.accountId)}/following`, context, {
|
|
1156
|
+
target_user_id: userId,
|
|
1157
|
+
}));
|
|
1158
|
+
},
|
|
1159
|
+
async unfollowUser({ account, userId, context }) {
|
|
1160
|
+
authorize(account, context);
|
|
1161
|
+
await requireUserToken("graph.unfollow")(`/2/users/${encodeURIComponent(account.accountId)}/following/${encodeURIComponent(userId)}`, context, undefined, {}, "DELETE");
|
|
1162
|
+
},
|
|
1163
|
+
async muteUser({ account, userId, context }) {
|
|
1164
|
+
authorize(account, context);
|
|
1165
|
+
return object(await requireUserToken("graph.mute")(`/2/users/${encodeURIComponent(account.accountId)}/muting`, context, {
|
|
1166
|
+
target_user_id: userId,
|
|
1167
|
+
}));
|
|
1168
|
+
},
|
|
1169
|
+
async unmuteUser({ account, userId, context }) {
|
|
1170
|
+
authorize(account, context);
|
|
1171
|
+
await requireUserToken("graph.unmute")(`/2/users/${encodeURIComponent(account.accountId)}/muting/${encodeURIComponent(userId)}`, context, undefined, {}, "DELETE");
|
|
1172
|
+
},
|
|
1173
|
+
async mutedUsers({ account, cursor, limit, context }) {
|
|
1174
|
+
return pageRequest(`/2/users/${encodeURIComponent(account.accountId)}/muting`, account, { cursor, limit }, context);
|
|
1175
|
+
},
|
|
1176
|
+
async blockUser({ account, userId, context }) {
|
|
1177
|
+
authorize(account, context);
|
|
1178
|
+
return object(await requireUserToken("graph.block")(`/2/users/${encodeURIComponent(account.accountId)}/blocking`, context, {
|
|
1179
|
+
target_user_id: userId,
|
|
1180
|
+
}));
|
|
1181
|
+
},
|
|
1182
|
+
async unblockUser({ account, userId, context }) {
|
|
1183
|
+
authorize(account, context);
|
|
1184
|
+
await requireUserToken("graph.unblock")(`/2/users/${encodeURIComponent(account.accountId)}/blocking/${encodeURIComponent(userId)}`, context, undefined, {}, "DELETE");
|
|
1185
|
+
},
|
|
1186
|
+
async blockedUsers({ account, cursor, limit, context }) {
|
|
1187
|
+
return pageRequest(`/2/users/${encodeURIComponent(account.accountId)}/blocking`, account, { cursor, limit }, context);
|
|
1188
|
+
},
|
|
1189
|
+
async like({ account, postId, context }) {
|
|
1190
|
+
authorize(account, context);
|
|
1191
|
+
await requireUserToken("likes.write")(`/2/users/${encodeURIComponent(account.accountId)}/likes`, context, {
|
|
1192
|
+
tweet_id: postId,
|
|
1193
|
+
});
|
|
1194
|
+
},
|
|
1195
|
+
async unlike({ account, postId, context }) {
|
|
1196
|
+
authorize(account, context);
|
|
1197
|
+
await requireUserToken("likes.write")(`/2/users/${encodeURIComponent(account.accountId)}/likes/${encodeURIComponent(postId)}`, context, undefined, {}, "DELETE");
|
|
1198
|
+
},
|
|
1199
|
+
async likedPosts({ account, userId, cursor, limit, context }) {
|
|
1200
|
+
return pageRequest(`/2/users/${encodeURIComponent(userId ?? account.accountId)}/liked_tweets`, account, { cursor, limit }, context, {}, "id,name,description,created_at,public_metrics", "tweets");
|
|
1201
|
+
},
|
|
1202
|
+
async likingUsers({ account, postId, cursor, limit, context }) {
|
|
1203
|
+
return pageRequest(`/2/tweets/${encodeURIComponent(postId)}/liking_users`, account, { cursor, limit }, context, {}, "id,name,description,created_at,public_metrics", "users");
|
|
1204
|
+
},
|
|
1205
|
+
async createList({ account, name, description, isPrivate, context }) {
|
|
1206
|
+
authorize(account, context);
|
|
1207
|
+
return object(await request("/2/lists", context, {
|
|
1208
|
+
name,
|
|
1209
|
+
...(description === undefined ? {} : { description }),
|
|
1210
|
+
...(isPrivate === undefined ? {} : { private: isPrivate }),
|
|
1211
|
+
}));
|
|
1212
|
+
},
|
|
1213
|
+
async updateList({ account, listId, name, description, isPrivate, context }) {
|
|
1214
|
+
authorize(account, context);
|
|
1215
|
+
return object(await request(`/2/lists/${encodeURIComponent(listId)}`, context, {
|
|
1216
|
+
...(name === undefined ? {} : { name }),
|
|
1217
|
+
...(description === undefined ? {} : { description }),
|
|
1218
|
+
...(isPrivate === undefined ? {} : { private: isPrivate }),
|
|
1219
|
+
}, {}, "PUT"));
|
|
1220
|
+
},
|
|
1221
|
+
async deleteList({ account, listId, context }) {
|
|
1222
|
+
authorize(account, context);
|
|
1223
|
+
await request(`/2/lists/${encodeURIComponent(listId)}`, context, undefined, {}, "DELETE");
|
|
1224
|
+
},
|
|
1225
|
+
async getList({ account, listId, context }) {
|
|
1226
|
+
authorize(account, context);
|
|
1227
|
+
return object(await request(`/2/lists/${encodeURIComponent(listId)}`, context));
|
|
1228
|
+
},
|
|
1229
|
+
async listMembers({ account, listId, cursor, limit, context }) {
|
|
1230
|
+
return pageRequest(`/2/lists/${encodeURIComponent(listId)}/members`, account, { cursor, limit }, context, {}, "id,name,description,private,member_count,follower_count");
|
|
1231
|
+
},
|
|
1232
|
+
async addListMember({ account, listId, userId, context }) {
|
|
1233
|
+
authorize(account, context);
|
|
1234
|
+
return object(await request(`/2/lists/${encodeURIComponent(listId)}/members`, context, {
|
|
1235
|
+
user_id: userId,
|
|
1236
|
+
}));
|
|
1237
|
+
},
|
|
1238
|
+
async removeListMember({ account, listId, userId, context }) {
|
|
1239
|
+
authorize(account, context);
|
|
1240
|
+
await request(`/2/lists/${encodeURIComponent(listId)}/members/${encodeURIComponent(userId)}`, context, undefined, {}, "DELETE");
|
|
1241
|
+
},
|
|
1242
|
+
async followList({ account, listId, context }) {
|
|
1243
|
+
authorize(account, context);
|
|
1244
|
+
return object(await request(`/2/users/${encodeURIComponent(account.accountId)}/followed_lists`, context, { list_id: listId }));
|
|
1245
|
+
},
|
|
1246
|
+
async unfollowList({ account, listId, context }) {
|
|
1247
|
+
authorize(account, context);
|
|
1248
|
+
await request(`/2/users/${encodeURIComponent(account.accountId)}/followed_lists/${encodeURIComponent(listId)}`, context, undefined, {}, "DELETE");
|
|
1249
|
+
},
|
|
1250
|
+
async ownedLists({ account, cursor, limit, context }) {
|
|
1251
|
+
return pageRequest(`/2/users/${encodeURIComponent(account.accountId)}/owned_lists`, account, { cursor, limit }, context, {}, "id,name,description,private,member_count,follower_count");
|
|
1252
|
+
},
|
|
1253
|
+
async pinList({ account, listId, context }) {
|
|
1254
|
+
authorize(account, context);
|
|
1255
|
+
return object(await requireUserToken("lists.pinned.write")(`/2/users/${encodeURIComponent(account.accountId)}/pinned_lists`, context, {
|
|
1256
|
+
list_id: listId,
|
|
1257
|
+
}));
|
|
1258
|
+
},
|
|
1259
|
+
async unpinList({ account, listId, context }) {
|
|
1260
|
+
authorize(account, context);
|
|
1261
|
+
await requireUserToken("lists.pinned.write")(`/2/users/${encodeURIComponent(account.accountId)}/pinned_lists/${encodeURIComponent(listId)}`, context, undefined, {}, "DELETE");
|
|
1262
|
+
},
|
|
1263
|
+
async pinnedLists({ account, context }) {
|
|
1264
|
+
authorize(account, context);
|
|
1265
|
+
const result = object(await readRequest("pinned_lists")(`/2/users/${encodeURIComponent(account.accountId)}/pinned_lists`, context, undefined, { "list.fields": "id,name,description,private,member_count,follower_count" }));
|
|
1266
|
+
return { items: result["data"] === undefined ? [] : array(result["data"]).map(object) };
|
|
1267
|
+
},
|
|
1268
|
+
async listPosts({ account, listId, cursor, limit, context }) {
|
|
1269
|
+
return pageRequest(`/2/lists/${encodeURIComponent(listId)}/tweets`, account, { cursor, limit }, context, {}, "id,name,username,description,created_at,public_metrics", "tweets");
|
|
1270
|
+
},
|
|
1271
|
+
async userPosts({ account, userId, cursor, limit, context }) {
|
|
1272
|
+
return pageRequest(`/2/users/${encodeURIComponent(userId)}/tweets`, account, { cursor, limit }, context, {}, "id,name,username,description,created_at,public_metrics", "tweets");
|
|
1273
|
+
},
|
|
1274
|
+
async homeTimeline({ account, cursor, limit, context }) {
|
|
1275
|
+
return pageRequest(`/2/users/${encodeURIComponent(account.accountId)}/timelines/reverse_chronological`, account, { cursor, limit }, context, {}, "id,name,username,description,created_at,public_metrics", "tweets");
|
|
1276
|
+
},
|
|
1277
|
+
async mentions({ account, userId, cursor, limit, context }) {
|
|
1278
|
+
return pageRequest(`/2/users/${encodeURIComponent(userId ?? account.accountId)}/mentions`, account, { cursor, limit }, context, {}, "id,name,username,description,created_at,public_metrics", "tweets");
|
|
1279
|
+
},
|
|
1280
|
+
async undoRepost({ account, postId, context }) {
|
|
1281
|
+
authorize(account, context);
|
|
1282
|
+
await request(`/2/users/${encodeURIComponent(account.accountId)}/retweets/${encodeURIComponent(postId)}`, context, undefined, {}, "DELETE");
|
|
1283
|
+
},
|
|
1284
|
+
}),
|
|
509
1285
|
});
|
|
1286
|
+
return adapter;
|
|
510
1287
|
}
|
|
511
1288
|
//# sourceMappingURL=x.js.map
|