@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.
Files changed (117) hide show
  1. package/README.md +10 -2
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +1 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cloud/common.d.ts.map +1 -1
  6. package/dist/cloud/common.js +48 -20
  7. package/dist/cloud/common.js.map +1 -1
  8. package/dist/cloud/lifecycle.d.ts.map +1 -1
  9. package/dist/cloud/lifecycle.js +5 -0
  10. package/dist/cloud/lifecycle.js.map +1 -1
  11. package/dist/cloud/media.d.ts.map +1 -1
  12. package/dist/cloud/media.js +12 -8
  13. package/dist/cloud/media.js.map +1 -1
  14. package/dist/cloud/outcomes.d.ts.map +1 -1
  15. package/dist/cloud/outcomes.js +17 -9
  16. package/dist/cloud/outcomes.js.map +1 -1
  17. package/dist/cloud/post-for-me.d.ts.map +1 -1
  18. package/dist/cloud/post-for-me.js +99 -28
  19. package/dist/cloud/post-for-me.js.map +1 -1
  20. package/dist/cloud/zernio.d.ts.map +1 -1
  21. package/dist/cloud/zernio.js +100 -9
  22. package/dist/cloud/zernio.js.map +1 -1
  23. package/dist/core/adapter.d.ts +42 -1
  24. package/dist/core/adapter.d.ts.map +1 -1
  25. package/dist/core/adapter.js.map +1 -1
  26. package/dist/core/client.d.ts +35 -1
  27. package/dist/core/client.d.ts.map +1 -1
  28. package/dist/core/client.js +503 -110
  29. package/dist/core/client.js.map +1 -1
  30. package/dist/core/concurrency.d.ts.map +1 -1
  31. package/dist/core/concurrency.js +5 -3
  32. package/dist/core/concurrency.js.map +1 -1
  33. package/dist/core/errors.d.ts.map +1 -1
  34. package/dist/core/errors.js +14 -7
  35. package/dist/core/errors.js.map +1 -1
  36. package/dist/core/idempotency.d.ts.map +1 -1
  37. package/dist/core/idempotency.js.map +1 -1
  38. package/dist/core/index.d.ts.map +1 -1
  39. package/dist/core/index.js.map +1 -1
  40. package/dist/core/pagination.d.ts.map +1 -1
  41. package/dist/core/pagination.js +1 -0
  42. package/dist/core/pagination.js.map +1 -1
  43. package/dist/core/types.d.ts +70 -2
  44. package/dist/core/types.d.ts.map +1 -1
  45. package/dist/core/types.js +10 -0
  46. package/dist/core/types.js.map +1 -1
  47. package/dist/platforms/bluesky.d.ts +131 -1
  48. package/dist/platforms/bluesky.d.ts.map +1 -1
  49. package/dist/platforms/bluesky.js +772 -44
  50. package/dist/platforms/bluesky.js.map +1 -1
  51. package/dist/platforms/instagram.d.ts +70 -3
  52. package/dist/platforms/instagram.d.ts.map +1 -1
  53. package/dist/platforms/instagram.js +403 -47
  54. package/dist/platforms/instagram.js.map +1 -1
  55. package/dist/platforms/linkedin.d.ts +55 -0
  56. package/dist/platforms/linkedin.d.ts.map +1 -1
  57. package/dist/platforms/linkedin.js +350 -39
  58. package/dist/platforms/linkedin.js.map +1 -1
  59. package/dist/platforms/threads.d.ts +37 -4
  60. package/dist/platforms/threads.d.ts.map +1 -1
  61. package/dist/platforms/threads.js +419 -44
  62. package/dist/platforms/threads.js.map +1 -1
  63. package/dist/platforms/tiktok.d.ts.map +1 -1
  64. package/dist/platforms/tiktok.js +109 -26
  65. package/dist/platforms/tiktok.js.map +1 -1
  66. package/dist/platforms/x-engagement.d.ts.map +1 -1
  67. package/dist/platforms/x-engagement.js +4 -0
  68. package/dist/platforms/x-engagement.js.map +1 -1
  69. package/dist/platforms/x.d.ts +227 -9
  70. package/dist/platforms/x.d.ts.map +1 -1
  71. package/dist/platforms/x.js +826 -49
  72. package/dist/platforms/x.js.map +1 -1
  73. package/dist/platforms/youtube-upload.d.ts.map +1 -1
  74. package/dist/platforms/youtube-upload.js +25 -9
  75. package/dist/platforms/youtube-upload.js.map +1 -1
  76. package/dist/platforms/youtube.d.ts +56 -2
  77. package/dist/platforms/youtube.d.ts.map +1 -1
  78. package/dist/platforms/youtube.js +571 -31
  79. package/dist/platforms/youtube.js.map +1 -1
  80. package/dist/server/connections.d.ts.map +1 -1
  81. package/dist/server/connections.js +4 -2
  82. package/dist/server/connections.js.map +1 -1
  83. package/dist/server/credentials.d.ts.map +1 -1
  84. package/dist/server/credentials.js +10 -16
  85. package/dist/server/credentials.js.map +1 -1
  86. package/dist/server/index.d.ts.map +1 -1
  87. package/dist/server/index.js.map +1 -1
  88. package/dist/server/oauth.d.ts +5 -0
  89. package/dist/server/oauth.d.ts.map +1 -1
  90. package/dist/server/oauth.js +118 -47
  91. package/dist/server/oauth.js.map +1 -1
  92. package/dist/server/webhooks.d.ts.map +1 -1
  93. package/dist/server/webhooks.js +11 -4
  94. package/dist/server/webhooks.js.map +1 -1
  95. package/dist/testing/index.d.ts.map +1 -1
  96. package/dist/testing/index.js +153 -1
  97. package/dist/testing/index.js.map +1 -1
  98. package/dist/transport/binary.d.ts.map +1 -1
  99. package/dist/transport/binary.js.map +1 -1
  100. package/dist/transport/budget.d.ts.map +1 -1
  101. package/dist/transport/budget.js.map +1 -1
  102. package/dist/transport/http.d.ts +2 -1
  103. package/dist/transport/http.d.ts.map +1 -1
  104. package/dist/transport/http.js +42 -12
  105. package/dist/transport/http.js.map +1 -1
  106. package/dist/transport/json.d.ts +7 -1
  107. package/dist/transport/json.d.ts.map +1 -1
  108. package/dist/transport/json.js +7 -1
  109. package/dist/transport/json.js.map +1 -1
  110. package/dist/transport/upload.d.ts +1 -0
  111. package/dist/transport/upload.d.ts.map +1 -1
  112. package/dist/transport/upload.js +38 -17
  113. package/dist/transport/upload.js.map +1 -1
  114. package/dist/transport/validation.d.ts.map +1 -1
  115. package/dist/transport/validation.js +1 -0
  116. package/dist/transport/validation.js.map +1 -1
  117. package/package.json +2 -1
@@ -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
- return defineAdapter({
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", "video"],
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: "media.video",
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
- { platform: "x", operation: "media.gif", availability: "available" },
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: "permission-required",
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: "approval-dependent",
269
- notes: "Filtered stream access depends on the X API tier.",
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 uploadVideo({ account, media, context }) {
1049
+ async listDirectMessages({ account, participantId, conversationId, cursor, limit, context }) {
456
1050
  authorize(account, context);
457
- if (!media.type.startsWith("video/") || media.size > 512 * 1024 * 1024)
1051
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 1 || limit > 100))
458
1052
  throw new SocialError({
459
1053
  code: "invalid_input",
460
- operation: "x.media.video",
461
- message: "Provide a video Blob no larger than 512 MiB.",
1054
+ operation: "messages.read",
1055
+ message: "X DM limits must be integers from 1 through 100.",
462
1056
  });
463
- const body = new FormData();
464
- body.set("media", media, "video");
465
- body.set("media_category", "tweet_video");
466
- return object(await http({
467
- url: new URL("https://api.x.com/2/media/upload"),
468
- method: "POST",
469
- headers: { Authorization: `Bearer ${options.auth.accessToken}` },
470
- body,
471
- timeoutMs: remainingBudget(context),
472
- ...(context.signal ? { signal: context.signal } : {}),
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 uploadGif({ account, media, context }) {
1096
+ async createGroupConversation({ account, participantIds, message, context }) {
476
1097
  authorize(account, context);
477
- if (media.type !== "image/gif")
1098
+ if (!message.trim())
478
1099
  throw new SocialError({
479
1100
  code: "invalid_input",
480
- operation: "x.media.gif",
481
- message: "GIF uploads require image/gif media.",
1101
+ operation: "messages.group.write",
1102
+ message: "X direct messages require non-empty text.",
482
1103
  });
483
- const body = new FormData();
484
- body.set("media", media, "image.gif");
485
- body.set("media_category", "tweet_gif");
486
- return object(await http({
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 listDirectMessages({ account, context }) {
1110
+ async getUserById({ account, userId, context }) {
496
1111
  authorize(account, context);
497
- return object(await request("/2/dm_conversations", context));
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 stream({ account, context }) {
1124
+ async getUserByUsername({ account, username, context }) {
500
1125
  authorize(account, context);
501
- return (await http({
502
- url: new URL("https://api.x.com/2/tweets/search/stream"),
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