birdclaw 0.5.1 → 0.7.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/CHANGELOG.md +92 -1
- package/README.md +75 -5
- package/package.json +8 -2
- package/scripts/browser-perf.mjs +1 -0
- package/scripts/start-test-server.mjs +16 -3
- package/src/cli.ts +812 -37
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +37 -7
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/DmWorkspace.tsx +18 -8
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +818 -0
- package/src/components/ProfileAnalysisStream.tsx +428 -0
- package/src/components/ProfilePreview.tsx +120 -9
- package/src/components/SavedTimelineView.tsx +30 -8
- package/src/components/SyncNowButton.tsx +60 -25
- package/src/components/ThemeSlider.tsx +55 -50
- package/src/components/TimelineCard.tsx +240 -92
- package/src/components/TimelineRouteFrame.tsx +38 -8
- package/src/components/TweetMediaGrid.tsx +87 -38
- package/src/components/TweetRichText.tsx +45 -17
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +97 -13
- package/src/lib/account-sync-job.ts +666 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +128 -53
- package/src/lib/archive-finder.ts +78 -63
- package/src/lib/archive-import.ts +1593 -1291
- package/src/lib/authored-live.ts +262 -204
- package/src/lib/avatar-cache.ts +208 -43
- package/src/lib/backup.ts +1536 -954
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +582 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/config.ts +35 -2
- package/src/lib/conversation-surface.ts +79 -48
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +95 -4
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/geocoding.ts +296 -0
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/location.ts +137 -0
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +445 -288
- package/src/lib/mentions-live.ts +549 -354
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/network-map.ts +382 -0
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +1317 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +983 -203
- package/src/lib/research.ts +165 -120
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +325 -51
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +75 -3
- package/src/lib/ui.ts +31 -8
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +222 -149
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +1185 -405
- package/src/routeTree.gen.ts +273 -0
- package/src/routes/__root.tsx +24 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/data-sources.tsx +24 -0
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +133 -0
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +80 -55
- package/src/routes/api/search-discussion.tsx +169 -0
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +75 -29
- package/src/routes/api/xurl-rate-limits.tsx +24 -0
- package/src/routes/data-sources.tsx +255 -0
- package/src/routes/discuss.tsx +419 -0
- package/src/routes/dms.tsx +95 -28
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/network-map.tsx +1035 -0
- package/src/routes/profile-analyze.tsx +112 -0
- package/src/routes/profiles.$handle.tsx +228 -0
- package/src/routes/rate-limits.tsx +309 -0
- package/src/routes/today.tsx +455 -0
- package/src/styles.css +22 -0
package/src/lib/xurl.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { runEffectPromise } from "./effect-runtime";
|
|
3
5
|
import type {
|
|
4
6
|
FollowDirection,
|
|
7
|
+
LiveDataSourceAccount,
|
|
5
8
|
TransportStatus,
|
|
9
|
+
XurlDmEventsResponse,
|
|
6
10
|
XurlFollowUsersResponse,
|
|
7
11
|
XurlMentionsResponse,
|
|
8
12
|
XurlMentionUser,
|
|
@@ -21,6 +25,8 @@ const MEDIA_FIELDS =
|
|
|
21
25
|
"variants,preview_image_url,url,duration_ms,alt_text,type,width,height,public_metrics";
|
|
22
26
|
const RICH_USER_FIELDS =
|
|
23
27
|
"description,entities,location,public_metrics,profile_image_url,url,created_at,verified,verified_type";
|
|
28
|
+
const DM_EVENT_FIELDS =
|
|
29
|
+
"attachments,created_at,dm_conversation_id,entities,event_type,id,participant_ids,referenced_tweets,sender_id,text";
|
|
24
30
|
const THREAD_TWEET_FIELDS =
|
|
25
31
|
"created_at,conversation_id,entities,public_metrics,referenced_tweets,in_reply_to_user_id,attachments";
|
|
26
32
|
// X bookmarks pagination truncates above 90 until this bug is fixed:
|
|
@@ -31,6 +37,18 @@ type TimelineCollectionEndpoint = "liked_tweets" | "bookmarks";
|
|
|
31
37
|
type JsonCommandOptions = {
|
|
32
38
|
timeoutMs?: number;
|
|
33
39
|
deadlineMs?: number;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
onAttempt?: (attempt: XurlJsonCommandAttempt) => void;
|
|
42
|
+
};
|
|
43
|
+
export interface XurlJsonCommandAttempt {
|
|
44
|
+
args: readonly string[];
|
|
45
|
+
attempt: number;
|
|
46
|
+
status: "ok" | "rate_limited" | "error";
|
|
47
|
+
error?: Error;
|
|
48
|
+
}
|
|
49
|
+
type OAuth2UsernameCandidate = {
|
|
50
|
+
app?: string;
|
|
51
|
+
username?: string;
|
|
34
52
|
};
|
|
35
53
|
|
|
36
54
|
let transportStatusCache:
|
|
@@ -47,11 +65,22 @@ let authenticatedUserCache:
|
|
|
47
65
|
value?: Record<string, unknown> | null;
|
|
48
66
|
}
|
|
49
67
|
| undefined;
|
|
68
|
+
const oauth2CandidateCache = new Map<
|
|
69
|
+
string,
|
|
70
|
+
{ expiresAt: number; value: OAuth2UsernameCandidate }
|
|
71
|
+
>();
|
|
50
72
|
|
|
51
73
|
function liveWritesDisabled() {
|
|
52
74
|
return process.env.BIRDCLAW_DISABLE_LIVE_WRITES === "1";
|
|
53
75
|
}
|
|
54
76
|
|
|
77
|
+
function e2eFakeLiveWritesEnabled() {
|
|
78
|
+
return (
|
|
79
|
+
process.env.BIRDCLAW_E2E === "1" &&
|
|
80
|
+
process.env.BIRDCLAW_E2E_FAKE_LIVE_WRITES === "1"
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
55
84
|
function getJsonRetryBaseDelayMs() {
|
|
56
85
|
const value = Number(process.env.BIRDCLAW_XURL_RETRY_BASE_MS ?? "2000");
|
|
57
86
|
return Number.isFinite(value) && value >= 0 ? value : 2000;
|
|
@@ -90,6 +119,10 @@ function formatXurlCommandError(error: unknown, args: string[]) {
|
|
|
90
119
|
return new Error(formatExecError(error, `xurl ${args.join(" ")} failed`));
|
|
91
120
|
}
|
|
92
121
|
|
|
122
|
+
function normalizeError(error: unknown) {
|
|
123
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
124
|
+
}
|
|
125
|
+
|
|
93
126
|
function parseErrorPayload(error: unknown) {
|
|
94
127
|
const stdout =
|
|
95
128
|
typeof error === "object" &&
|
|
@@ -123,6 +156,17 @@ function getRetryDelayMs(error: unknown, attempt: number) {
|
|
|
123
156
|
return Math.min(baseDelay * 2 ** attempt, 30_000);
|
|
124
157
|
}
|
|
125
158
|
|
|
159
|
+
function emitJsonCommandAttempt(
|
|
160
|
+
options: JsonCommandOptions,
|
|
161
|
+
attempt: XurlJsonCommandAttempt,
|
|
162
|
+
) {
|
|
163
|
+
try {
|
|
164
|
+
options.onAttempt?.(attempt);
|
|
165
|
+
} catch {
|
|
166
|
+
// Telemetry observers must not affect xurl command behavior.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
126
170
|
function capTimelineCollectionMaxResults(
|
|
127
171
|
collection: TimelineCollectionEndpoint,
|
|
128
172
|
maxResults: number,
|
|
@@ -133,28 +177,23 @@ function capTimelineCollectionMaxResults(
|
|
|
133
177
|
: maxResults;
|
|
134
178
|
}
|
|
135
179
|
|
|
136
|
-
async function sleep(ms: number) {
|
|
137
|
-
if (ms <= 0) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
180
|
export function resetTransportStatusCache() {
|
|
144
181
|
transportStatusCache = undefined;
|
|
145
182
|
}
|
|
146
183
|
|
|
147
184
|
export function resetAuthenticatedUserCache() {
|
|
148
185
|
authenticatedUserCache = undefined;
|
|
186
|
+
oauth2CandidateCache.clear();
|
|
149
187
|
}
|
|
150
188
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
|
|
189
|
+
function hasXurlEffect() {
|
|
190
|
+
return Effect.tryPromise({
|
|
191
|
+
try: () => execFileAsync("xurl", ["version"]),
|
|
192
|
+
catch: normalizeError,
|
|
193
|
+
}).pipe(
|
|
194
|
+
Effect.as(true),
|
|
195
|
+
Effect.catchAll(() => Effect.succeed(false)),
|
|
196
|
+
);
|
|
158
197
|
}
|
|
159
198
|
|
|
160
199
|
function isUnauthenticatedXurlStatus(status: string) {
|
|
@@ -163,164 +202,553 @@ function isUnauthenticatedXurlStatus(status: string) {
|
|
|
163
202
|
);
|
|
164
203
|
}
|
|
165
204
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
return transportStatusCache.value;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (transportStatusCache?.pending) {
|
|
173
|
-
return transportStatusCache.pending;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const pending: Promise<TransportStatus> = (async () => {
|
|
177
|
-
const installed = await hasXurl();
|
|
205
|
+
function readTransportStatusEffect(): Effect.Effect<TransportStatus, never> {
|
|
206
|
+
return Effect.gen(function* () {
|
|
207
|
+
const installed = yield* hasXurlEffect();
|
|
178
208
|
if (!installed) {
|
|
179
209
|
return {
|
|
180
210
|
installed: false,
|
|
181
|
-
availableTransport: "local",
|
|
211
|
+
availableTransport: "local" as const,
|
|
182
212
|
statusText: "xurl not installed. local mode active.",
|
|
183
213
|
};
|
|
184
214
|
}
|
|
185
215
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
216
|
+
return yield* Effect.tryPromise({
|
|
217
|
+
try: () => execFileAsync("xurl", ["auth", "status"]),
|
|
218
|
+
catch: (cause) => cause,
|
|
219
|
+
}).pipe(
|
|
220
|
+
Effect.map(({ stdout }) => {
|
|
221
|
+
const rawStatus = stdout.trim();
|
|
222
|
+
|
|
223
|
+
if (isUnauthenticatedXurlStatus(rawStatus)) {
|
|
224
|
+
return {
|
|
225
|
+
installed: true,
|
|
226
|
+
availableTransport: "local" as const,
|
|
227
|
+
statusText:
|
|
228
|
+
"xurl installed but not authenticated. local (bird) mode active.",
|
|
229
|
+
rawStatus,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
189
232
|
|
|
190
|
-
if (isUnauthenticatedXurlStatus(rawStatus)) {
|
|
191
233
|
return {
|
|
192
234
|
installed: true,
|
|
193
|
-
availableTransport: "
|
|
194
|
-
statusText:
|
|
195
|
-
"xurl installed but not authenticated. local (bird) mode active.",
|
|
235
|
+
availableTransport: "xurl" as const,
|
|
236
|
+
statusText: "xurl available",
|
|
196
237
|
rawStatus,
|
|
197
238
|
};
|
|
198
|
-
}
|
|
239
|
+
}),
|
|
240
|
+
Effect.catchAll((error) =>
|
|
241
|
+
Effect.succeed({
|
|
242
|
+
installed: true,
|
|
243
|
+
availableTransport: "local" as const,
|
|
244
|
+
statusText: `xurl detected but auth unavailable: ${
|
|
245
|
+
error instanceof Error ? error.message : "unknown error"
|
|
246
|
+
}`,
|
|
247
|
+
}),
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
199
252
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
};
|
|
206
|
-
} catch (error) {
|
|
207
|
-
return {
|
|
208
|
-
installed: true,
|
|
209
|
-
availableTransport: "local",
|
|
210
|
-
statusText: `xurl detected but auth unavailable: ${
|
|
211
|
-
error instanceof Error ? error.message : "unknown error"
|
|
212
|
-
}`,
|
|
213
|
-
};
|
|
253
|
+
export function getTransportStatusEffect() {
|
|
254
|
+
return Effect.gen(function* () {
|
|
255
|
+
const now = Date.now();
|
|
256
|
+
if (transportStatusCache?.value && transportStatusCache.expiresAt > now) {
|
|
257
|
+
return transportStatusCache.value;
|
|
214
258
|
}
|
|
215
|
-
})();
|
|
216
259
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
260
|
+
if (transportStatusCache?.pending) {
|
|
261
|
+
const status = yield* Effect.tryPromise({
|
|
262
|
+
try: () => transportStatusCache?.pending ?? Promise.resolve(undefined),
|
|
263
|
+
catch: normalizeError,
|
|
264
|
+
});
|
|
265
|
+
if (status) return status;
|
|
266
|
+
}
|
|
221
267
|
|
|
222
|
-
|
|
223
|
-
|
|
268
|
+
const pending = runEffectPromise(readTransportStatusEffect());
|
|
269
|
+
|
|
270
|
+
transportStatusCache = {
|
|
271
|
+
expiresAt: 0,
|
|
272
|
+
pending,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const status = yield* Effect.tryPromise({
|
|
276
|
+
try: () => pending,
|
|
277
|
+
catch: normalizeError,
|
|
278
|
+
}).pipe(
|
|
279
|
+
Effect.catchAll((error) =>
|
|
280
|
+
Effect.sync(() => {
|
|
281
|
+
transportStatusCache = undefined;
|
|
282
|
+
}).pipe(Effect.flatMap(() => Effect.fail(error))),
|
|
283
|
+
),
|
|
284
|
+
);
|
|
224
285
|
transportStatusCache = {
|
|
225
286
|
expiresAt: Date.now() + TRANSPORT_STATUS_TTL_MS,
|
|
226
287
|
value: status,
|
|
227
288
|
};
|
|
228
289
|
return status;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function getTransportStatus(): Promise<TransportStatus> {
|
|
294
|
+
return runEffectPromise(getTransportStatusEffect());
|
|
233
295
|
}
|
|
234
296
|
|
|
235
|
-
|
|
297
|
+
function runShortcutEffect(args: string[]) {
|
|
298
|
+
return Effect.gen(function* () {
|
|
299
|
+
if (liveWritesDisabled()) {
|
|
300
|
+
if (e2eFakeLiveWritesEnabled()) {
|
|
301
|
+
return { ok: true, output: "e2e fake live write" };
|
|
302
|
+
}
|
|
303
|
+
return { ok: false, output: "live writes disabled" };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return yield* Effect.tryPromise({
|
|
307
|
+
try: () => execFileAsync("xurl", args),
|
|
308
|
+
catch: normalizeError,
|
|
309
|
+
}).pipe(
|
|
310
|
+
Effect.map(({ stdout, stderr }) => ({
|
|
311
|
+
ok: true,
|
|
312
|
+
output: stdout || stderr,
|
|
313
|
+
})),
|
|
314
|
+
Effect.catchAll((error) =>
|
|
315
|
+
Effect.succeed({
|
|
316
|
+
ok: false,
|
|
317
|
+
output: formatExecError(error, "xurl execution failed"),
|
|
318
|
+
}),
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function execXurlJsonEffect(
|
|
236
325
|
args: string[],
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
326
|
+
timeoutMs?: number,
|
|
327
|
+
signal?: AbortSignal,
|
|
328
|
+
): Effect.Effect<{ stdout: string; stderr: string }, Error> {
|
|
329
|
+
return Effect.tryPromise({
|
|
330
|
+
try: () => {
|
|
331
|
+
const controller =
|
|
332
|
+
(typeof timeoutMs === "number" &&
|
|
333
|
+
Number.isFinite(timeoutMs) &&
|
|
334
|
+
timeoutMs > 0) ||
|
|
335
|
+
signal
|
|
336
|
+
? new AbortController()
|
|
337
|
+
: undefined;
|
|
338
|
+
if (
|
|
339
|
+
typeof timeoutMs === "number" &&
|
|
340
|
+
Number.isFinite(timeoutMs) &&
|
|
341
|
+
timeoutMs <= 0
|
|
342
|
+
) {
|
|
343
|
+
throw new Error("xurl command timed out");
|
|
344
|
+
}
|
|
345
|
+
if (signal?.aborted) {
|
|
346
|
+
throw new Error("xurl command aborted");
|
|
347
|
+
}
|
|
348
|
+
const onAbort = () => controller?.abort();
|
|
349
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
350
|
+
const timeout =
|
|
351
|
+
controller &&
|
|
352
|
+
typeof timeoutMs === "number" &&
|
|
353
|
+
Number.isFinite(timeoutMs) &&
|
|
354
|
+
timeoutMs > 0
|
|
355
|
+
? setTimeout(() => controller.abort(), timeoutMs)
|
|
356
|
+
: undefined;
|
|
357
|
+
const result = controller
|
|
358
|
+
? execFileAsync("xurl", args, { signal: controller.signal })
|
|
359
|
+
: execFileAsync("xurl", args);
|
|
360
|
+
return result.finally(() => {
|
|
361
|
+
signal?.removeEventListener("abort", onAbort);
|
|
362
|
+
if (timeout) {
|
|
363
|
+
clearTimeout(timeout);
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
catch: normalizeError,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
241
370
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
ok: false,
|
|
248
|
-
output: formatExecError(error, "xurl execution failed"),
|
|
249
|
-
};
|
|
371
|
+
function getRemainingTimeoutMs(deadlineMs?: number) {
|
|
372
|
+
if (deadlineMs === undefined) return undefined;
|
|
373
|
+
const timeoutMs = Math.max(0, deadlineMs - Date.now());
|
|
374
|
+
if (timeoutMs <= 0) {
|
|
375
|
+
throw new Error("xurl OAuth2 fallback timed out");
|
|
250
376
|
}
|
|
377
|
+
return timeoutMs;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function execXurlTextEffect(
|
|
381
|
+
args: string[],
|
|
382
|
+
deadlineMs?: number,
|
|
383
|
+
): Effect.Effect<{ stdout: string; stderr: string }, Error> {
|
|
384
|
+
return Effect.tryPromise({
|
|
385
|
+
try: () => {
|
|
386
|
+
const timeoutMs = getRemainingTimeoutMs(deadlineMs);
|
|
387
|
+
const controller =
|
|
388
|
+
typeof timeoutMs === "number" &&
|
|
389
|
+
Number.isFinite(timeoutMs) &&
|
|
390
|
+
timeoutMs > 0
|
|
391
|
+
? new AbortController()
|
|
392
|
+
: undefined;
|
|
393
|
+
const timeout = controller
|
|
394
|
+
? setTimeout(() => controller.abort(), timeoutMs)
|
|
395
|
+
: undefined;
|
|
396
|
+
const result = controller
|
|
397
|
+
? execFileAsync("xurl", args, { signal: controller.signal })
|
|
398
|
+
: execFileAsync("xurl", args);
|
|
399
|
+
return result.finally(() => {
|
|
400
|
+
if (timeout) {
|
|
401
|
+
clearTimeout(timeout);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
},
|
|
405
|
+
catch: normalizeError,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function parseJsonPayloadEffect(
|
|
410
|
+
stdout: string,
|
|
411
|
+
args: string[],
|
|
412
|
+
): Effect.Effect<Record<string, unknown>, Error> {
|
|
413
|
+
return Effect.try({
|
|
414
|
+
try: () => JSON.parse(stdout) as Record<string, unknown>,
|
|
415
|
+
catch: (error) => formatXurlCommandError(error, args),
|
|
416
|
+
});
|
|
251
417
|
}
|
|
252
418
|
|
|
253
|
-
|
|
419
|
+
function runJsonCommandEffect(
|
|
254
420
|
args: string[],
|
|
255
421
|
options: JsonCommandOptions = {},
|
|
256
422
|
attempt = 0,
|
|
257
|
-
) {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const controller =
|
|
269
|
-
typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
270
|
-
? new AbortController()
|
|
423
|
+
): Effect.Effect<Record<string, unknown>, Error> {
|
|
424
|
+
return Effect.gen(function* () {
|
|
425
|
+
const deadlineMs =
|
|
426
|
+
options.deadlineMs ??
|
|
427
|
+
(typeof options.timeoutMs === "number" &&
|
|
428
|
+
Number.isFinite(options.timeoutMs) &&
|
|
429
|
+
options.timeoutMs > 0
|
|
430
|
+
? Date.now() + options.timeoutMs
|
|
431
|
+
: undefined);
|
|
432
|
+
const timeoutMs = deadlineMs
|
|
433
|
+
? Math.max(0, deadlineMs - Date.now())
|
|
271
434
|
: undefined;
|
|
272
|
-
|
|
273
|
-
|
|
435
|
+
return yield* execXurlJsonEffect(args, timeoutMs, options.signal).pipe(
|
|
436
|
+
Effect.flatMap(({ stdout }) => parseJsonPayloadEffect(stdout, args)),
|
|
437
|
+
Effect.tap(() =>
|
|
438
|
+
Effect.sync(() =>
|
|
439
|
+
emitJsonCommandAttempt(options, { args, attempt, status: "ok" }),
|
|
440
|
+
),
|
|
441
|
+
),
|
|
442
|
+
Effect.catchAll((error) => {
|
|
443
|
+
const retryDelayMs = getRetryDelayMs(error, attempt);
|
|
444
|
+
emitJsonCommandAttempt(options, {
|
|
445
|
+
args,
|
|
446
|
+
attempt,
|
|
447
|
+
status: retryDelayMs === null ? "error" : "rate_limited",
|
|
448
|
+
error,
|
|
449
|
+
});
|
|
450
|
+
if (retryDelayMs === null || attempt >= JSON_RETRY_LIMIT - 1) {
|
|
451
|
+
return Effect.fail(formatXurlCommandError(error, args));
|
|
452
|
+
}
|
|
453
|
+
if (options.signal?.aborted) {
|
|
454
|
+
return Effect.fail(formatXurlCommandError(error, args));
|
|
455
|
+
}
|
|
456
|
+
const remainingMs = deadlineMs
|
|
457
|
+
? Math.max(0, deadlineMs - Date.now())
|
|
458
|
+
: undefined;
|
|
459
|
+
if (remainingMs !== undefined && retryDelayMs >= remainingMs) {
|
|
460
|
+
return Effect.fail(formatXurlCommandError(error, args));
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return Effect.sleep(retryDelayMs).pipe(
|
|
464
|
+
Effect.flatMap(() =>
|
|
465
|
+
runJsonCommandEffect(args, { ...options, deadlineMs }, attempt + 1),
|
|
466
|
+
),
|
|
467
|
+
);
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function cleanXurlUsernameLabel(username?: string) {
|
|
474
|
+
const label = username?.trim().replace(/^@/, "");
|
|
475
|
+
return label &&
|
|
476
|
+
label !== "-" &&
|
|
477
|
+
label !== "–" &&
|
|
478
|
+
label !== "(none)" &&
|
|
479
|
+
label.toLowerCase() !== "none" &&
|
|
480
|
+
label.toLowerCase() !== "unknown" &&
|
|
481
|
+
/^[^\s]{1,128}$/.test(label)
|
|
482
|
+
? label
|
|
274
483
|
: undefined;
|
|
484
|
+
}
|
|
275
485
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
486
|
+
function comparableXurlUsername(username?: string) {
|
|
487
|
+
return cleanXurlUsernameLabel(username)?.toLowerCase();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function cleanXurlAppLabel(app?: string) {
|
|
491
|
+
const label = app?.trim();
|
|
492
|
+
return label && /^[^\s]{1,128}$/.test(label) ? label : undefined;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function parseOAuth2UsernamesFromStatus(rawStatus: string) {
|
|
496
|
+
const seen = new Set<string>();
|
|
497
|
+
const usernames: OAuth2UsernameCandidate[] = [];
|
|
498
|
+
let currentApp: string | undefined;
|
|
499
|
+
for (const line of rawStatus.split(/\r?\n/)) {
|
|
500
|
+
const appMatch = line.match(/^\s*(?:▸\s*)?([^\s]+)\s+\[client_id:/);
|
|
501
|
+
if (appMatch) {
|
|
502
|
+
currentApp = cleanXurlAppLabel(appMatch[1]);
|
|
503
|
+
continue;
|
|
285
504
|
}
|
|
286
|
-
const
|
|
287
|
-
|
|
505
|
+
const oauthMatch = line.match(/\boauth2:\s*([^\s]+)/);
|
|
506
|
+
if (oauthMatch) {
|
|
507
|
+
const username = cleanXurlUsernameLabel(oauthMatch[1]);
|
|
508
|
+
const key = username
|
|
509
|
+
? `${currentApp ?? "default"}:${username}`
|
|
510
|
+
: undefined;
|
|
511
|
+
if (username && key && !seen.has(key)) {
|
|
512
|
+
seen.add(key);
|
|
513
|
+
usernames.push({ app: currentApp, username });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return usernames;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function readOAuth2UsernameCandidatesEffect(
|
|
521
|
+
deadlineMs?: number,
|
|
522
|
+
): Effect.Effect<OAuth2UsernameCandidate[], never> {
|
|
523
|
+
return execXurlTextEffect(["auth", "status"], deadlineMs).pipe(
|
|
524
|
+
Effect.map(({ stdout }) => parseOAuth2UsernamesFromStatus(stdout)),
|
|
525
|
+
Effect.catchAll(() => Effect.succeed([])),
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
export function readXurlOAuth2AccountsEffect(): Effect.Effect<
|
|
530
|
+
LiveDataSourceAccount[],
|
|
531
|
+
never
|
|
532
|
+
> {
|
|
533
|
+
return readOAuth2UsernameCandidatesEffect().pipe(
|
|
534
|
+
Effect.map((candidates) =>
|
|
535
|
+
candidates.map((candidate) => ({
|
|
536
|
+
...(candidate.app ? { app: candidate.app } : {}),
|
|
537
|
+
...(candidate.username ? { username: candidate.username } : {}),
|
|
538
|
+
})),
|
|
539
|
+
),
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function readXurlOAuth2Accounts(): Promise<LiveDataSourceAccount[]> {
|
|
544
|
+
return runEffectPromise(readXurlOAuth2AccountsEffect());
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function lookupOAuth2UsernameForAccountEffect(
|
|
548
|
+
expectedUsername: string,
|
|
549
|
+
attemptedUsernames: Set<string>,
|
|
550
|
+
deadlineMs?: number,
|
|
551
|
+
knownCandidates?: OAuth2UsernameCandidate[],
|
|
552
|
+
) {
|
|
553
|
+
return Effect.gen(function* () {
|
|
554
|
+
const expected = comparableXurlUsername(expectedUsername);
|
|
555
|
+
if (!expected) return undefined;
|
|
556
|
+
|
|
557
|
+
const candidates =
|
|
558
|
+
knownCandidates ??
|
|
559
|
+
(yield* readOAuth2UsernameCandidatesEffect(deadlineMs));
|
|
560
|
+
for (const candidate of candidates) {
|
|
561
|
+
const candidateKey = `${candidate.app ?? "default"}:${candidate.username}`;
|
|
562
|
+
if (attemptedUsernames.has(candidateKey)) continue;
|
|
563
|
+
const payload = yield* runJsonCommandEffect(
|
|
564
|
+
oauth2ArgsForCandidate(candidate, ["/2/users/me"]),
|
|
565
|
+
{ deadlineMs },
|
|
566
|
+
).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
567
|
+
const user = payload ? authenticatedUserFromPayload(payload) : null;
|
|
568
|
+
const actual = comparableXurlUsername(String(user?.username ?? ""));
|
|
569
|
+
if (actual === expected) {
|
|
570
|
+
return candidate;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return undefined;
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function oauth2ArgsForCandidate(
|
|
579
|
+
candidate: OAuth2UsernameCandidate | undefined,
|
|
580
|
+
args: string[],
|
|
581
|
+
) {
|
|
582
|
+
return [
|
|
583
|
+
...(candidate?.app ? ["--app", candidate.app] : []),
|
|
584
|
+
"--auth",
|
|
585
|
+
"oauth2",
|
|
586
|
+
...(candidate?.username ? ["--username", candidate.username] : []),
|
|
587
|
+
...args,
|
|
588
|
+
];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function configuredOAuth2Candidate(primaryUsername: string | undefined) {
|
|
592
|
+
const app = cleanXurlAppLabel(process.env.BIRDCLAW_XURL_OAUTH2_APP);
|
|
593
|
+
const username = cleanXurlUsernameLabel(
|
|
594
|
+
process.env.BIRDCLAW_XURL_OAUTH2_USERNAME,
|
|
595
|
+
);
|
|
596
|
+
if (!app && !username) return undefined;
|
|
597
|
+
const effectiveUsername = username ?? primaryUsername;
|
|
598
|
+
if (!app && !effectiveUsername) return undefined;
|
|
599
|
+
return {
|
|
600
|
+
...(app ? { app } : {}),
|
|
601
|
+
...(effectiveUsername ? { username: effectiveUsername } : {}),
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function runOAuth2JsonCommandEffect({
|
|
606
|
+
args,
|
|
607
|
+
username,
|
|
608
|
+
options,
|
|
609
|
+
useConfiguredCandidate = true,
|
|
610
|
+
}: {
|
|
611
|
+
args: string[];
|
|
612
|
+
username?: string;
|
|
613
|
+
options?: JsonCommandOptions;
|
|
614
|
+
useConfiguredCandidate?: boolean;
|
|
615
|
+
}) {
|
|
616
|
+
const primaryUsername = cleanXurlUsernameLabel(username);
|
|
617
|
+
return Effect.gen(function* () {
|
|
618
|
+
const deadlineMs =
|
|
619
|
+
options?.deadlineMs ??
|
|
620
|
+
(typeof options?.timeoutMs === "number" &&
|
|
621
|
+
Number.isFinite(options.timeoutMs) &&
|
|
622
|
+
options.timeoutMs > 0
|
|
623
|
+
? Date.now() + options.timeoutMs
|
|
624
|
+
: undefined);
|
|
625
|
+
const scopedOptions = { ...options, deadlineMs };
|
|
626
|
+
let authCandidate: OAuth2UsernameCandidate | undefined = primaryUsername
|
|
627
|
+
? { username: primaryUsername }
|
|
288
628
|
: undefined;
|
|
289
|
-
|
|
290
|
-
|
|
629
|
+
const configuredCandidate = useConfiguredCandidate
|
|
630
|
+
? configuredOAuth2Candidate(primaryUsername)
|
|
631
|
+
: undefined;
|
|
632
|
+
if (configuredCandidate) {
|
|
633
|
+
authCandidate = configuredCandidate;
|
|
634
|
+
} else if (primaryUsername) {
|
|
635
|
+
const cacheKey = primaryUsername.toLowerCase();
|
|
636
|
+
const cachedCandidate = oauth2CandidateCache.get(cacheKey);
|
|
637
|
+
if (cachedCandidate && cachedCandidate.expiresAt > Date.now()) {
|
|
638
|
+
authCandidate = cachedCandidate.value;
|
|
639
|
+
} else {
|
|
640
|
+
const candidates =
|
|
641
|
+
yield* readOAuth2UsernameCandidatesEffect(deadlineMs);
|
|
642
|
+
const primaryCandidates = candidates.filter(
|
|
643
|
+
(candidate) => candidate.username === primaryUsername,
|
|
644
|
+
);
|
|
645
|
+
if (primaryCandidates.length === 1) {
|
|
646
|
+
authCandidate = primaryCandidates[0];
|
|
647
|
+
} else if (primaryCandidates.length > 1) {
|
|
648
|
+
const verifiedUsername = yield* lookupOAuth2UsernameForAccountEffect(
|
|
649
|
+
primaryUsername,
|
|
650
|
+
new Set(),
|
|
651
|
+
deadlineMs,
|
|
652
|
+
candidates,
|
|
653
|
+
);
|
|
654
|
+
authCandidate = verifiedUsername ??
|
|
655
|
+
primaryCandidates[0] ?? { username: primaryUsername };
|
|
656
|
+
} else {
|
|
657
|
+
const fallbackUsername = yield* lookupOAuth2UsernameForAccountEffect(
|
|
658
|
+
primaryUsername,
|
|
659
|
+
new Set(),
|
|
660
|
+
deadlineMs,
|
|
661
|
+
candidates,
|
|
662
|
+
);
|
|
663
|
+
if (fallbackUsername) {
|
|
664
|
+
authCandidate = fallbackUsername;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (authCandidate) {
|
|
668
|
+
oauth2CandidateCache.set(cacheKey, {
|
|
669
|
+
expiresAt: Date.now() + AUTHENTICATED_USER_TTL_MS,
|
|
670
|
+
value: authCandidate,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
291
674
|
}
|
|
292
675
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
} finally {
|
|
296
|
-
if (timeout) {
|
|
297
|
-
clearTimeout(timeout);
|
|
676
|
+
if (deadlineMs !== undefined && Date.now() >= deadlineMs) {
|
|
677
|
+
return yield* Effect.fail(new Error("xurl OAuth2 fallback timed out"));
|
|
298
678
|
}
|
|
299
|
-
|
|
679
|
+
|
|
680
|
+
return yield* runJsonCommandEffect(
|
|
681
|
+
oauth2ArgsForCandidate(authCandidate, args),
|
|
682
|
+
scopedOptions,
|
|
683
|
+
).pipe(
|
|
684
|
+
Effect.catchAll((error) => {
|
|
685
|
+
if (!primaryUsername) {
|
|
686
|
+
return Effect.fail(error);
|
|
687
|
+
}
|
|
688
|
+
oauth2CandidateCache.delete(primaryUsername.toLowerCase());
|
|
689
|
+
const attempted = new Set([
|
|
690
|
+
authCandidate
|
|
691
|
+
? `${authCandidate.app ?? "default"}:${authCandidate.username}`
|
|
692
|
+
: `default:${primaryUsername}`,
|
|
693
|
+
]);
|
|
694
|
+
if (authCandidate?.username !== primaryUsername) {
|
|
695
|
+
attempted.add(`default:${primaryUsername}`);
|
|
696
|
+
}
|
|
697
|
+
return lookupOAuth2UsernameForAccountEffect(
|
|
698
|
+
primaryUsername,
|
|
699
|
+
attempted,
|
|
700
|
+
deadlineMs,
|
|
701
|
+
).pipe(
|
|
702
|
+
Effect.flatMap((fallbackUsername) => {
|
|
703
|
+
if (!fallbackUsername) {
|
|
704
|
+
return Effect.fail(error);
|
|
705
|
+
}
|
|
706
|
+
oauth2CandidateCache.set(primaryUsername.toLowerCase(), {
|
|
707
|
+
expiresAt: Date.now() + AUTHENTICATED_USER_TTL_MS,
|
|
708
|
+
value: fallbackUsername,
|
|
709
|
+
});
|
|
710
|
+
return runJsonCommandEffect(
|
|
711
|
+
oauth2ArgsForCandidate(fallbackUsername, args),
|
|
712
|
+
scopedOptions,
|
|
713
|
+
);
|
|
714
|
+
}),
|
|
715
|
+
Effect.catchAll(() => Effect.fail(error)),
|
|
716
|
+
);
|
|
717
|
+
}),
|
|
718
|
+
);
|
|
719
|
+
});
|
|
300
720
|
}
|
|
301
721
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
722
|
+
function runMutationCommandEffect(args: string[]) {
|
|
723
|
+
return Effect.gen(function* () {
|
|
724
|
+
if (liveWritesDisabled()) {
|
|
725
|
+
if (e2eFakeLiveWritesEnabled()) {
|
|
726
|
+
return { ok: true, output: "e2e fake live write" };
|
|
727
|
+
}
|
|
728
|
+
return { ok: false, output: "live writes disabled" };
|
|
729
|
+
}
|
|
306
730
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
731
|
+
return yield* Effect.tryPromise({
|
|
732
|
+
try: () => execFileAsync("xurl", args),
|
|
733
|
+
catch: normalizeError,
|
|
734
|
+
}).pipe(
|
|
735
|
+
Effect.map(({ stdout, stderr }) => ({
|
|
736
|
+
ok: true,
|
|
737
|
+
output: stdout || stderr || "ok",
|
|
738
|
+
})),
|
|
739
|
+
Effect.catchAll((error) =>
|
|
740
|
+
Effect.succeed({
|
|
741
|
+
ok: false,
|
|
742
|
+
output: formatExecError(error, "xurl execution failed"),
|
|
743
|
+
}),
|
|
744
|
+
),
|
|
745
|
+
);
|
|
746
|
+
});
|
|
319
747
|
}
|
|
320
748
|
|
|
321
|
-
export
|
|
749
|
+
export function lookupUsersByIdsEffect(ids: string[]) {
|
|
322
750
|
if (ids.length === 0) {
|
|
323
|
-
return [];
|
|
751
|
+
return Effect.succeed([]);
|
|
324
752
|
}
|
|
325
753
|
|
|
326
754
|
const query = new URLSearchParams({
|
|
@@ -328,14 +756,28 @@ export async function lookupUsersByIds(ids: string[]) {
|
|
|
328
756
|
"user.fields":
|
|
329
757
|
"description,entities,location,public_metrics,profile_image_url,url,created_at,verified,verified_type",
|
|
330
758
|
});
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
759
|
+
return runJsonCommandEffect([`/2/users?${query.toString()}`]).pipe(
|
|
760
|
+
Effect.map((payload) =>
|
|
761
|
+
Array.isArray(payload.data) ? (payload.data as XurlMentionUser[]) : [],
|
|
762
|
+
),
|
|
763
|
+
);
|
|
334
764
|
}
|
|
335
765
|
|
|
336
|
-
export
|
|
766
|
+
export function lookupUsersByIds(ids: string[]) {
|
|
767
|
+
return runEffectPromise(lookupUsersByIdsEffect(ids));
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export function lookupUsersByHandlesEffect(
|
|
771
|
+
handles: string[],
|
|
772
|
+
options: {
|
|
773
|
+
auth?: "oauth2";
|
|
774
|
+
username?: string;
|
|
775
|
+
signal?: AbortSignal;
|
|
776
|
+
useConfiguredCandidate?: boolean;
|
|
777
|
+
} = {},
|
|
778
|
+
) {
|
|
337
779
|
if (handles.length === 0) {
|
|
338
|
-
return [];
|
|
780
|
+
return Effect.succeed([]);
|
|
339
781
|
}
|
|
340
782
|
|
|
341
783
|
const query = new URLSearchParams({
|
|
@@ -343,52 +785,135 @@ export async function lookupUsersByHandles(handles: string[]) {
|
|
|
343
785
|
"user.fields":
|
|
344
786
|
"description,entities,location,public_metrics,profile_image_url,url,created_at,verified,verified_type",
|
|
345
787
|
});
|
|
346
|
-
const
|
|
788
|
+
const args = [`/2/users/by?${query.toString()}`];
|
|
789
|
+
const command =
|
|
790
|
+
options.auth === "oauth2"
|
|
791
|
+
? runOAuth2JsonCommandEffect({
|
|
792
|
+
args,
|
|
793
|
+
username: options.username,
|
|
794
|
+
options: { signal: options.signal },
|
|
795
|
+
useConfiguredCandidate: options.useConfiguredCandidate,
|
|
796
|
+
})
|
|
797
|
+
: runJsonCommandEffect(args, { signal: options.signal });
|
|
798
|
+
return command.pipe(
|
|
799
|
+
Effect.map((payload) =>
|
|
800
|
+
Array.isArray(payload.data) ? (payload.data as XurlMentionUser[]) : [],
|
|
801
|
+
),
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function lookupUsersByHandles(
|
|
806
|
+
handles: string[],
|
|
807
|
+
options: {
|
|
808
|
+
auth?: "oauth2";
|
|
809
|
+
username?: string;
|
|
810
|
+
signal?: AbortSignal;
|
|
811
|
+
useConfiguredCandidate?: boolean;
|
|
812
|
+
} = {},
|
|
813
|
+
) {
|
|
814
|
+
return runEffectPromise(lookupUsersByHandlesEffect(handles, options));
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function authenticatedUserFromPayload(payload: Record<string, unknown>) {
|
|
347
818
|
const data = payload.data;
|
|
348
|
-
return
|
|
819
|
+
return data && typeof data === "object"
|
|
820
|
+
? (data as Record<string, unknown>)
|
|
821
|
+
: null;
|
|
349
822
|
}
|
|
350
823
|
|
|
351
|
-
export
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
authenticatedUserCache.expiresAt > now
|
|
357
|
-
) {
|
|
358
|
-
return authenticatedUserCache.value ?? null;
|
|
359
|
-
}
|
|
824
|
+
export function lookupAuthenticatedUserFreshEffect() {
|
|
825
|
+
return runJsonCommandEffect(["whoami"]).pipe(
|
|
826
|
+
Effect.map(authenticatedUserFromPayload),
|
|
827
|
+
);
|
|
828
|
+
}
|
|
360
829
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
830
|
+
export function lookupAuthenticatedOAuth2UserEffect(username?: string) {
|
|
831
|
+
return runOAuth2JsonCommandEffect({
|
|
832
|
+
args: ["whoami"],
|
|
833
|
+
username,
|
|
834
|
+
}).pipe(Effect.map(authenticatedUserFromPayload));
|
|
835
|
+
}
|
|
364
836
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
pending,
|
|
376
|
-
};
|
|
837
|
+
export function lookupAuthenticatedUserEffect() {
|
|
838
|
+
return Effect.gen(function* () {
|
|
839
|
+
const now = Date.now();
|
|
840
|
+
if (
|
|
841
|
+
authenticatedUserCache &&
|
|
842
|
+
"value" in authenticatedUserCache &&
|
|
843
|
+
authenticatedUserCache.expiresAt > now
|
|
844
|
+
) {
|
|
845
|
+
return authenticatedUserCache.value ?? null;
|
|
846
|
+
}
|
|
377
847
|
|
|
378
|
-
|
|
379
|
-
|
|
848
|
+
if (authenticatedUserCache?.pending) {
|
|
849
|
+
return yield* Effect.tryPromise({
|
|
850
|
+
try: () => authenticatedUserCache?.pending ?? Promise.resolve(null),
|
|
851
|
+
catch: normalizeError,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const pending = runEffectPromise(lookupAuthenticatedUserFreshEffect());
|
|
856
|
+
|
|
857
|
+
authenticatedUserCache = {
|
|
858
|
+
expiresAt: 0,
|
|
859
|
+
pending,
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const value = yield* Effect.tryPromise({
|
|
863
|
+
try: () => pending,
|
|
864
|
+
catch: normalizeError,
|
|
865
|
+
}).pipe(
|
|
866
|
+
Effect.catchAll((error) =>
|
|
867
|
+
Effect.sync(() => {
|
|
868
|
+
authenticatedUserCache = undefined;
|
|
869
|
+
}).pipe(Effect.flatMap(() => Effect.fail(error))),
|
|
870
|
+
),
|
|
871
|
+
);
|
|
380
872
|
authenticatedUserCache = {
|
|
381
873
|
expiresAt: Date.now() + AUTHENTICATED_USER_TTL_MS,
|
|
382
874
|
value,
|
|
383
875
|
};
|
|
384
876
|
return value;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
export function lookupAuthenticatedUser() {
|
|
881
|
+
return runEffectPromise(lookupAuthenticatedUserEffect());
|
|
389
882
|
}
|
|
390
883
|
|
|
391
|
-
export
|
|
884
|
+
export function lookupAuthenticatedUserFresh() {
|
|
885
|
+
return runEffectPromise(lookupAuthenticatedUserFreshEffect());
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function resolveUserIdEffect({
|
|
889
|
+
username,
|
|
890
|
+
userId,
|
|
891
|
+
}: {
|
|
892
|
+
username?: string;
|
|
893
|
+
userId?: string;
|
|
894
|
+
}) {
|
|
895
|
+
return Effect.gen(function* () {
|
|
896
|
+
if (userId) return userId;
|
|
897
|
+
if (username) {
|
|
898
|
+
const [user] = yield* lookupUsersByHandlesEffect([username]);
|
|
899
|
+
if (!user?.id) {
|
|
900
|
+
return yield* Effect.fail(
|
|
901
|
+
new Error(`Could not resolve Twitter user id for @${username}`),
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
return String(user.id);
|
|
905
|
+
}
|
|
906
|
+
const user = yield* lookupAuthenticatedUserEffect();
|
|
907
|
+
if (!user?.id) {
|
|
908
|
+
return yield* Effect.fail(
|
|
909
|
+
new Error("Could not resolve authenticated Twitter user id"),
|
|
910
|
+
);
|
|
911
|
+
}
|
|
912
|
+
return String(user.id);
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
export function listMentionsViaXurlEffect({
|
|
392
917
|
maxResults,
|
|
393
918
|
username,
|
|
394
919
|
userId,
|
|
@@ -402,45 +927,97 @@ export async function listMentionsViaXurl({
|
|
|
402
927
|
paginationToken?: string;
|
|
403
928
|
sinceId?: string;
|
|
404
929
|
startTime?: string;
|
|
930
|
+
}): Effect.Effect<XurlMentionsResponse, Error> {
|
|
931
|
+
return Effect.gen(function* () {
|
|
932
|
+
const resolvedUserId = yield* resolveUserIdEffect({ username, userId });
|
|
933
|
+
const query = new URLSearchParams({
|
|
934
|
+
max_results: String(maxResults),
|
|
935
|
+
expansions: AUTHOR_MEDIA_EXPANSIONS,
|
|
936
|
+
"tweet.fields": "created_at,conversation_id,entities,public_metrics",
|
|
937
|
+
"media.fields": MEDIA_FIELDS,
|
|
938
|
+
"user.fields":
|
|
939
|
+
"description,entities,location,public_metrics,profile_image_url,url,created_at,verified,verified_type",
|
|
940
|
+
});
|
|
941
|
+
if (paginationToken) {
|
|
942
|
+
query.set("pagination_token", paginationToken);
|
|
943
|
+
}
|
|
944
|
+
if (sinceId) {
|
|
945
|
+
query.set("since_id", sinceId);
|
|
946
|
+
}
|
|
947
|
+
if (startTime) {
|
|
948
|
+
query.set("start_time", startTime);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const payload = yield* runOAuth2JsonCommandEffect({
|
|
952
|
+
args: [`/2/users/${resolvedUserId}/mentions?${query.toString()}`],
|
|
953
|
+
username,
|
|
954
|
+
});
|
|
955
|
+
return toXurlMentionsResponse(payload);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export function listMentionsViaXurl(options: {
|
|
960
|
+
maxResults: number;
|
|
961
|
+
username?: string;
|
|
962
|
+
userId?: string;
|
|
963
|
+
paginationToken?: string;
|
|
964
|
+
sinceId?: string;
|
|
965
|
+
startTime?: string;
|
|
405
966
|
}): Promise<XurlMentionsResponse> {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
967
|
+
return runEffectPromise(listMentionsViaXurlEffect(options));
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export function listHomeTimelineViaXurlEffect({
|
|
971
|
+
maxResults,
|
|
972
|
+
username,
|
|
973
|
+
userId,
|
|
974
|
+
paginationToken,
|
|
975
|
+
timeoutMs,
|
|
976
|
+
}: {
|
|
977
|
+
maxResults: number;
|
|
978
|
+
username?: string;
|
|
979
|
+
userId?: string;
|
|
980
|
+
paginationToken?: string;
|
|
981
|
+
timeoutMs?: number;
|
|
982
|
+
}): Effect.Effect<XurlMentionsResponse, Error> {
|
|
983
|
+
return Effect.gen(function* () {
|
|
984
|
+
const resolvedUserId = yield* resolveUserIdEffect({ username, userId });
|
|
985
|
+
const query = new URLSearchParams({
|
|
986
|
+
max_results: String(maxResults),
|
|
987
|
+
expansions: AUTHOR_MEDIA_EXPANSIONS,
|
|
988
|
+
"tweet.fields":
|
|
989
|
+
"created_at,conversation_id,entities,public_metrics,referenced_tweets",
|
|
990
|
+
"media.fields": MEDIA_FIELDS,
|
|
991
|
+
"user.fields": RICH_USER_FIELDS,
|
|
992
|
+
});
|
|
993
|
+
if (paginationToken) {
|
|
994
|
+
query.set("pagination_token", paginationToken);
|
|
420
995
|
}
|
|
421
|
-
}
|
|
422
996
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
997
|
+
const payload = yield* runOAuth2JsonCommandEffect({
|
|
998
|
+
args: [
|
|
999
|
+
`/2/users/${resolvedUserId}/timelines/reverse_chronological?${query.toString()}`,
|
|
1000
|
+
],
|
|
1001
|
+
username,
|
|
1002
|
+
options: { timeoutMs },
|
|
1003
|
+
});
|
|
1004
|
+
return toXurlMentionsResponse(payload);
|
|
430
1005
|
});
|
|
431
|
-
|
|
432
|
-
query.set("pagination_token", paginationToken);
|
|
433
|
-
}
|
|
434
|
-
if (sinceId) {
|
|
435
|
-
query.set("since_id", sinceId);
|
|
436
|
-
}
|
|
437
|
-
if (startTime) {
|
|
438
|
-
query.set("start_time", startTime);
|
|
439
|
-
}
|
|
1006
|
+
}
|
|
440
1007
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
1008
|
+
export function listHomeTimelineViaXurl(options: {
|
|
1009
|
+
maxResults: number;
|
|
1010
|
+
username?: string;
|
|
1011
|
+
userId?: string;
|
|
1012
|
+
paginationToken?: string;
|
|
1013
|
+
timeoutMs?: number;
|
|
1014
|
+
}): Promise<XurlMentionsResponse> {
|
|
1015
|
+
return runEffectPromise(listHomeTimelineViaXurlEffect(options));
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function toXurlMentionsResponse(
|
|
1019
|
+
payload: Record<string, unknown>,
|
|
1020
|
+
): XurlMentionsResponse {
|
|
444
1021
|
return {
|
|
445
1022
|
data: Array.isArray(payload.data)
|
|
446
1023
|
? (payload.data as XurlMentionsResponse["data"])
|
|
@@ -456,7 +1033,7 @@ export async function listMentionsViaXurl({
|
|
|
456
1033
|
};
|
|
457
1034
|
}
|
|
458
1035
|
|
|
459
|
-
|
|
1036
|
+
function listTimelineCollectionViaXurlEffect({
|
|
460
1037
|
collection,
|
|
461
1038
|
maxResults,
|
|
462
1039
|
username,
|
|
@@ -470,88 +1047,128 @@ async function listTimelineCollectionViaXurl({
|
|
|
470
1047
|
userId?: string;
|
|
471
1048
|
isPaginatedWalk?: boolean;
|
|
472
1049
|
paginationToken?: string;
|
|
473
|
-
}):
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
1050
|
+
}): Effect.Effect<XurlMentionsResponse, Error> {
|
|
1051
|
+
return Effect.gen(function* () {
|
|
1052
|
+
const resolvedUserId = yield* resolveUserIdEffect({ username, userId });
|
|
1053
|
+
const requestMaxResults = capTimelineCollectionMaxResults(
|
|
1054
|
+
collection,
|
|
1055
|
+
maxResults,
|
|
1056
|
+
isPaginatedWalk,
|
|
1057
|
+
);
|
|
1058
|
+
const query = new URLSearchParams({
|
|
1059
|
+
max_results: String(requestMaxResults),
|
|
1060
|
+
expansions: AUTHOR_MEDIA_EXPANSIONS,
|
|
1061
|
+
"tweet.fields":
|
|
1062
|
+
"created_at,conversation_id,entities,public_metrics,referenced_tweets",
|
|
1063
|
+
"media.fields": MEDIA_FIELDS,
|
|
1064
|
+
"user.fields":
|
|
1065
|
+
"description,entities,location,public_metrics,profile_image_url,url,created_at,verified,verified_type",
|
|
1066
|
+
});
|
|
1067
|
+
if (paginationToken) {
|
|
1068
|
+
query.set("pagination_token", paginationToken);
|
|
488
1069
|
}
|
|
489
|
-
}
|
|
490
1070
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const query = new URLSearchParams({
|
|
497
|
-
max_results: String(requestMaxResults),
|
|
498
|
-
expansions: AUTHOR_MEDIA_EXPANSIONS,
|
|
499
|
-
"tweet.fields":
|
|
500
|
-
"created_at,conversation_id,entities,public_metrics,referenced_tweets",
|
|
501
|
-
"media.fields": MEDIA_FIELDS,
|
|
502
|
-
"user.fields":
|
|
503
|
-
"description,entities,location,public_metrics,profile_image_url,url,created_at,verified,verified_type",
|
|
1071
|
+
const payload = yield* runOAuth2JsonCommandEffect({
|
|
1072
|
+
args: [`/2/users/${resolvedUserId}/${collection}?${query.toString()}`],
|
|
1073
|
+
username,
|
|
1074
|
+
});
|
|
1075
|
+
return toXurlMentionsResponse(payload);
|
|
504
1076
|
});
|
|
505
|
-
if (paginationToken) {
|
|
506
|
-
query.set("pagination_token", paginationToken);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const payload = await runJsonCommand([
|
|
510
|
-
"--auth",
|
|
511
|
-
"oauth2",
|
|
512
|
-
`/2/users/${resolvedUserId}/${collection}?${query.toString()}`,
|
|
513
|
-
]);
|
|
514
|
-
return {
|
|
515
|
-
data: Array.isArray(payload.data)
|
|
516
|
-
? (payload.data as XurlMentionsResponse["data"])
|
|
517
|
-
: [],
|
|
518
|
-
includes:
|
|
519
|
-
payload.includes && typeof payload.includes === "object"
|
|
520
|
-
? (payload.includes as XurlMentionsResponse["includes"])
|
|
521
|
-
: undefined,
|
|
522
|
-
meta:
|
|
523
|
-
payload.meta && typeof payload.meta === "object"
|
|
524
|
-
? (payload.meta as XurlMentionsResponse["meta"])
|
|
525
|
-
: undefined,
|
|
526
|
-
};
|
|
527
1077
|
}
|
|
528
1078
|
|
|
529
|
-
export
|
|
1079
|
+
export function listLikedTweetsViaXurlEffect(options: {
|
|
530
1080
|
maxResults: number;
|
|
531
1081
|
username?: string;
|
|
532
1082
|
userId?: string;
|
|
533
1083
|
paginationToken?: string;
|
|
534
|
-
})
|
|
535
|
-
return
|
|
1084
|
+
}) {
|
|
1085
|
+
return listTimelineCollectionViaXurlEffect({
|
|
536
1086
|
...options,
|
|
537
1087
|
collection: "liked_tweets",
|
|
538
1088
|
});
|
|
539
1089
|
}
|
|
540
1090
|
|
|
541
|
-
export
|
|
1091
|
+
export function listLikedTweetsViaXurl(options: {
|
|
542
1092
|
maxResults: number;
|
|
543
1093
|
username?: string;
|
|
544
1094
|
userId?: string;
|
|
545
|
-
isPaginatedWalk?: boolean;
|
|
546
1095
|
paginationToken?: string;
|
|
547
1096
|
}): Promise<XurlMentionsResponse> {
|
|
548
|
-
return
|
|
1097
|
+
return runEffectPromise(listLikedTweetsViaXurlEffect(options));
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
export function listBookmarkedTweetsViaXurlEffect(options: {
|
|
1101
|
+
maxResults: number;
|
|
1102
|
+
username?: string;
|
|
1103
|
+
userId?: string;
|
|
1104
|
+
isPaginatedWalk?: boolean;
|
|
1105
|
+
paginationToken?: string;
|
|
1106
|
+
}) {
|
|
1107
|
+
return listTimelineCollectionViaXurlEffect({
|
|
549
1108
|
...options,
|
|
550
1109
|
collection: "bookmarks",
|
|
551
1110
|
});
|
|
552
1111
|
}
|
|
553
1112
|
|
|
554
|
-
export
|
|
1113
|
+
export function listBookmarkedTweetsViaXurl(options: {
|
|
1114
|
+
maxResults: number;
|
|
1115
|
+
username?: string;
|
|
1116
|
+
userId?: string;
|
|
1117
|
+
isPaginatedWalk?: boolean;
|
|
1118
|
+
paginationToken?: string;
|
|
1119
|
+
}): Promise<XurlMentionsResponse> {
|
|
1120
|
+
return runEffectPromise(listBookmarkedTweetsViaXurlEffect(options));
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
export function listDirectMessageEventsViaXurlEffect({
|
|
1124
|
+
maxResults,
|
|
1125
|
+
username,
|
|
1126
|
+
paginationToken,
|
|
1127
|
+
}: {
|
|
1128
|
+
maxResults: number;
|
|
1129
|
+
username?: string;
|
|
1130
|
+
paginationToken?: string;
|
|
1131
|
+
}): Effect.Effect<XurlDmEventsResponse, Error> {
|
|
1132
|
+
const query = new URLSearchParams({
|
|
1133
|
+
max_results: String(maxResults),
|
|
1134
|
+
event_types: "MessageCreate",
|
|
1135
|
+
"dm_event.fields": DM_EVENT_FIELDS,
|
|
1136
|
+
expansions: "sender_id,participant_ids",
|
|
1137
|
+
"user.fields": RICH_USER_FIELDS,
|
|
1138
|
+
});
|
|
1139
|
+
if (paginationToken) {
|
|
1140
|
+
query.set("pagination_token", paginationToken);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
return runOAuth2JsonCommandEffect({
|
|
1144
|
+
args: [`/2/dm_events?${query.toString()}`],
|
|
1145
|
+
username,
|
|
1146
|
+
}).pipe(
|
|
1147
|
+
Effect.map((payload) => ({
|
|
1148
|
+
data: Array.isArray(payload.data)
|
|
1149
|
+
? (payload.data as XurlDmEventsResponse["data"])
|
|
1150
|
+
: [],
|
|
1151
|
+
includes:
|
|
1152
|
+
payload.includes && typeof payload.includes === "object"
|
|
1153
|
+
? (payload.includes as XurlDmEventsResponse["includes"])
|
|
1154
|
+
: undefined,
|
|
1155
|
+
meta:
|
|
1156
|
+
payload.meta && typeof payload.meta === "object"
|
|
1157
|
+
? (payload.meta as Record<string, unknown>)
|
|
1158
|
+
: undefined,
|
|
1159
|
+
})),
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
export function listDirectMessageEventsViaXurl(options: {
|
|
1164
|
+
maxResults: number;
|
|
1165
|
+
username?: string;
|
|
1166
|
+
paginationToken?: string;
|
|
1167
|
+
}): Promise<XurlDmEventsResponse> {
|
|
1168
|
+
return runEffectPromise(listDirectMessageEventsViaXurlEffect(options));
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
export function listFollowUsersViaXurlEffect({
|
|
555
1172
|
direction,
|
|
556
1173
|
maxResults,
|
|
557
1174
|
username,
|
|
@@ -563,50 +1180,45 @@ export async function listFollowUsersViaXurl({
|
|
|
563
1180
|
username?: string;
|
|
564
1181
|
userId?: string;
|
|
565
1182
|
paginationToken?: string;
|
|
566
|
-
}):
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const user = await lookupAuthenticatedUser();
|
|
577
|
-
if (!user?.id) {
|
|
578
|
-
throw new Error("Could not resolve authenticated Twitter user id");
|
|
579
|
-
}
|
|
580
|
-
resolvedUserId = String(user.id);
|
|
1183
|
+
}): Effect.Effect<XurlFollowUsersResponse, Error> {
|
|
1184
|
+
return Effect.gen(function* () {
|
|
1185
|
+
const resolvedUserId = yield* resolveUserIdEffect({ username, userId });
|
|
1186
|
+
const query = new URLSearchParams({
|
|
1187
|
+
max_results: String(maxResults),
|
|
1188
|
+
"user.fields":
|
|
1189
|
+
"id,username,name,description,verified,protected,public_metrics,profile_image_url,created_at",
|
|
1190
|
+
});
|
|
1191
|
+
if (paginationToken) {
|
|
1192
|
+
query.set("pagination_token", paginationToken);
|
|
581
1193
|
}
|
|
582
|
-
}
|
|
583
1194
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
1195
|
+
const payload = yield* runOAuth2JsonCommandEffect({
|
|
1196
|
+
args: [`/2/users/${resolvedUserId}/${direction}?${query.toString()}`],
|
|
1197
|
+
username,
|
|
1198
|
+
});
|
|
1199
|
+
return {
|
|
1200
|
+
data: Array.isArray(payload.data)
|
|
1201
|
+
? (payload.data as XurlMentionUser[])
|
|
1202
|
+
: [],
|
|
1203
|
+
meta:
|
|
1204
|
+
payload.meta && typeof payload.meta === "object"
|
|
1205
|
+
? (payload.meta as Record<string, unknown>)
|
|
1206
|
+
: undefined,
|
|
1207
|
+
};
|
|
588
1208
|
});
|
|
589
|
-
|
|
590
|
-
query.set("pagination_token", paginationToken);
|
|
591
|
-
}
|
|
1209
|
+
}
|
|
592
1210
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
: [],
|
|
602
|
-
meta:
|
|
603
|
-
payload.meta && typeof payload.meta === "object"
|
|
604
|
-
? (payload.meta as Record<string, unknown>)
|
|
605
|
-
: undefined,
|
|
606
|
-
};
|
|
1211
|
+
export function listFollowUsersViaXurl(options: {
|
|
1212
|
+
direction: FollowDirection;
|
|
1213
|
+
maxResults: number;
|
|
1214
|
+
username?: string;
|
|
1215
|
+
userId?: string;
|
|
1216
|
+
paginationToken?: string;
|
|
1217
|
+
}): Promise<XurlFollowUsersResponse> {
|
|
1218
|
+
return runEffectPromise(listFollowUsersViaXurlEffect(options));
|
|
607
1219
|
}
|
|
608
1220
|
|
|
609
|
-
export
|
|
1221
|
+
export function listBlockedUsersEffect(
|
|
610
1222
|
userId: string,
|
|
611
1223
|
paginationToken?: string,
|
|
612
1224
|
) {
|
|
@@ -619,25 +1231,30 @@ export async function listBlockedUsers(
|
|
|
619
1231
|
query.set("pagination_token", paginationToken);
|
|
620
1232
|
}
|
|
621
1233
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
: null;
|
|
1234
|
+
return runJsonCommandEffect([`/2/users/${userId}/blocking?${query}`]).pipe(
|
|
1235
|
+
Effect.map((payload) => {
|
|
1236
|
+
const data = Array.isArray(payload.data)
|
|
1237
|
+
? (payload.data as XurlMentionUser[])
|
|
1238
|
+
: [];
|
|
1239
|
+
const meta =
|
|
1240
|
+
payload.meta && typeof payload.meta === "object"
|
|
1241
|
+
? (payload.meta as Record<string, unknown>)
|
|
1242
|
+
: null;
|
|
632
1243
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
1244
|
+
return {
|
|
1245
|
+
items: data,
|
|
1246
|
+
nextToken:
|
|
1247
|
+
typeof meta?.next_token === "string" ? String(meta.next_token) : null,
|
|
1248
|
+
};
|
|
1249
|
+
}),
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
export function listBlockedUsers(userId: string, paginationToken?: string) {
|
|
1254
|
+
return runEffectPromise(listBlockedUsersEffect(userId, paginationToken));
|
|
638
1255
|
}
|
|
639
1256
|
|
|
640
|
-
export
|
|
1257
|
+
export function listUserTweetsEffect(
|
|
641
1258
|
userId: string,
|
|
642
1259
|
{
|
|
643
1260
|
maxResults,
|
|
@@ -650,6 +1267,10 @@ export async function listUserTweets(
|
|
|
650
1267
|
userFields,
|
|
651
1268
|
mediaFields,
|
|
652
1269
|
auth,
|
|
1270
|
+
username,
|
|
1271
|
+
signal,
|
|
1272
|
+
onAttempt,
|
|
1273
|
+
useConfiguredCandidate,
|
|
653
1274
|
}: {
|
|
654
1275
|
maxResults: number;
|
|
655
1276
|
paginationToken?: string;
|
|
@@ -661,8 +1282,12 @@ export async function listUserTweets(
|
|
|
661
1282
|
userFields?: string[];
|
|
662
1283
|
mediaFields?: string[];
|
|
663
1284
|
auth?: "oauth2";
|
|
1285
|
+
username?: string;
|
|
1286
|
+
signal?: AbortSignal;
|
|
1287
|
+
onAttempt?: JsonCommandOptions["onAttempt"];
|
|
1288
|
+
useConfiguredCandidate?: boolean;
|
|
664
1289
|
},
|
|
665
|
-
):
|
|
1290
|
+
): Effect.Effect<XurlUserTweetsResponse, Error> {
|
|
666
1291
|
const query = new URLSearchParams({
|
|
667
1292
|
max_results: String(maxResults),
|
|
668
1293
|
expansions: MEDIA_EXPANSION,
|
|
@@ -694,34 +1319,84 @@ export async function listUserTweets(
|
|
|
694
1319
|
}
|
|
695
1320
|
|
|
696
1321
|
const endpoint = `/2/users/${userId}/tweets?${query}`;
|
|
697
|
-
const
|
|
698
|
-
auth === "oauth2"
|
|
1322
|
+
const command =
|
|
1323
|
+
auth === "oauth2"
|
|
1324
|
+
? runOAuth2JsonCommandEffect({
|
|
1325
|
+
args: [endpoint],
|
|
1326
|
+
username,
|
|
1327
|
+
options: { signal, onAttempt },
|
|
1328
|
+
useConfiguredCandidate,
|
|
1329
|
+
})
|
|
1330
|
+
: runJsonCommandEffect([endpoint], { signal, onAttempt });
|
|
1331
|
+
return command.pipe(
|
|
1332
|
+
Effect.map((payload) => {
|
|
1333
|
+
const data = Array.isArray(payload.data)
|
|
1334
|
+
? (payload.data as XurlUserTweet[])
|
|
1335
|
+
: [];
|
|
1336
|
+
const meta =
|
|
1337
|
+
payload.meta && typeof payload.meta === "object"
|
|
1338
|
+
? (payload.meta as Record<string, unknown>)
|
|
1339
|
+
: null;
|
|
1340
|
+
const includes =
|
|
1341
|
+
payload.includes && typeof payload.includes === "object"
|
|
1342
|
+
? (payload.includes as XurlUserTweetsResponse["includes"])
|
|
1343
|
+
: undefined;
|
|
1344
|
+
|
|
1345
|
+
return {
|
|
1346
|
+
items: data,
|
|
1347
|
+
nextToken:
|
|
1348
|
+
typeof meta?.next_token === "string" ? String(meta.next_token) : null,
|
|
1349
|
+
...(includes ? { includes } : {}),
|
|
1350
|
+
};
|
|
1351
|
+
}),
|
|
699
1352
|
);
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
export function listUserTweets(
|
|
1356
|
+
userId: string,
|
|
1357
|
+
options: {
|
|
1358
|
+
maxResults: number;
|
|
1359
|
+
paginationToken?: string;
|
|
1360
|
+
excludeRetweets?: boolean;
|
|
1361
|
+
sinceId?: string;
|
|
1362
|
+
untilId?: string;
|
|
1363
|
+
tweetFields?: string[];
|
|
1364
|
+
expansions?: string[];
|
|
1365
|
+
userFields?: string[];
|
|
1366
|
+
mediaFields?: string[];
|
|
1367
|
+
auth?: "oauth2";
|
|
1368
|
+
username?: string;
|
|
1369
|
+
signal?: AbortSignal;
|
|
1370
|
+
onAttempt?: JsonCommandOptions["onAttempt"];
|
|
1371
|
+
useConfiguredCandidate?: boolean;
|
|
1372
|
+
},
|
|
1373
|
+
): Promise<XurlUserTweetsResponse> {
|
|
1374
|
+
return runEffectPromise(listUserTweetsEffect(userId, options));
|
|
1375
|
+
}
|
|
711
1376
|
|
|
1377
|
+
function toXurlTweetsResponse(
|
|
1378
|
+
payload: Record<string, unknown>,
|
|
1379
|
+
): XurlTweetsResponse {
|
|
712
1380
|
return {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1381
|
+
data: Array.isArray(payload.data)
|
|
1382
|
+
? (payload.data as XurlTweetsResponse["data"])
|
|
1383
|
+
: [],
|
|
1384
|
+
includes:
|
|
1385
|
+
payload.includes && typeof payload.includes === "object"
|
|
1386
|
+
? (payload.includes as XurlTweetsResponse["includes"])
|
|
1387
|
+
: undefined,
|
|
1388
|
+
meta:
|
|
1389
|
+
payload.meta && typeof payload.meta === "object"
|
|
1390
|
+
? (payload.meta as XurlTweetsResponse["meta"])
|
|
1391
|
+
: undefined,
|
|
717
1392
|
};
|
|
718
1393
|
}
|
|
719
1394
|
|
|
720
|
-
export
|
|
1395
|
+
export function lookupTweetsByIdsEffect(
|
|
721
1396
|
ids: string[],
|
|
722
|
-
):
|
|
1397
|
+
): Effect.Effect<XurlTweetsResponse, Error> {
|
|
723
1398
|
if (ids.length === 0) {
|
|
724
|
-
return { data: [] };
|
|
1399
|
+
return Effect.succeed({ data: [] });
|
|
725
1400
|
}
|
|
726
1401
|
|
|
727
1402
|
const query = new URLSearchParams({
|
|
@@ -734,34 +1409,35 @@ export async function lookupTweetsByIds(
|
|
|
734
1409
|
"description,entities,location,public_metrics,profile_image_url,url,created_at,verified,verified_type",
|
|
735
1410
|
});
|
|
736
1411
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
? (payload.includes as XurlTweetsResponse["includes"])
|
|
745
|
-
: undefined,
|
|
746
|
-
meta:
|
|
747
|
-
payload.meta && typeof payload.meta === "object"
|
|
748
|
-
? (payload.meta as XurlTweetsResponse["meta"])
|
|
749
|
-
: undefined,
|
|
750
|
-
};
|
|
1412
|
+
return runJsonCommandEffect([`/2/tweets?${query.toString()}`]).pipe(
|
|
1413
|
+
Effect.map(toXurlTweetsResponse),
|
|
1414
|
+
);
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
export function lookupTweetsByIds(ids: string[]): Promise<XurlTweetsResponse> {
|
|
1418
|
+
return runEffectPromise(lookupTweetsByIdsEffect(ids));
|
|
751
1419
|
}
|
|
752
1420
|
|
|
753
|
-
export
|
|
1421
|
+
export function searchRecentByConversationIdEffect(
|
|
754
1422
|
conversationId: string,
|
|
755
1423
|
{
|
|
756
1424
|
maxResults,
|
|
757
1425
|
paginationToken,
|
|
758
1426
|
timeoutMs,
|
|
1427
|
+
auth,
|
|
1428
|
+
username,
|
|
1429
|
+
signal,
|
|
1430
|
+
onAttempt,
|
|
759
1431
|
}: {
|
|
760
1432
|
maxResults: number;
|
|
761
1433
|
paginationToken?: string;
|
|
762
1434
|
timeoutMs?: number;
|
|
1435
|
+
auth?: "oauth2";
|
|
1436
|
+
username?: string;
|
|
1437
|
+
signal?: AbortSignal;
|
|
1438
|
+
onAttempt?: JsonCommandOptions["onAttempt"];
|
|
763
1439
|
},
|
|
764
|
-
):
|
|
1440
|
+
): Effect.Effect<XurlTweetsResponse, Error> {
|
|
765
1441
|
const query = new URLSearchParams({
|
|
766
1442
|
query: `conversation_id:${conversationId}`,
|
|
767
1443
|
max_results: String(maxResults),
|
|
@@ -774,29 +1450,98 @@ export async function searchRecentByConversationId(
|
|
|
774
1450
|
query.set("pagination_token", paginationToken);
|
|
775
1451
|
}
|
|
776
1452
|
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
1453
|
+
const args = [`/2/tweets/search/recent?${query.toString()}`];
|
|
1454
|
+
const command =
|
|
1455
|
+
auth === "oauth2"
|
|
1456
|
+
? runOAuth2JsonCommandEffect({
|
|
1457
|
+
args,
|
|
1458
|
+
username,
|
|
1459
|
+
options: { timeoutMs, signal, onAttempt },
|
|
1460
|
+
useConfiguredCandidate: false,
|
|
1461
|
+
})
|
|
1462
|
+
: runJsonCommandEffect(args, { timeoutMs, signal, onAttempt });
|
|
1463
|
+
return command.pipe(Effect.map(toXurlTweetsResponse));
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
export function searchRecentByConversationId(
|
|
1467
|
+
conversationId: string,
|
|
1468
|
+
options: {
|
|
1469
|
+
maxResults: number;
|
|
1470
|
+
paginationToken?: string;
|
|
1471
|
+
timeoutMs?: number;
|
|
1472
|
+
auth?: "oauth2";
|
|
1473
|
+
username?: string;
|
|
1474
|
+
signal?: AbortSignal;
|
|
1475
|
+
onAttempt?: JsonCommandOptions["onAttempt"];
|
|
1476
|
+
},
|
|
1477
|
+
): Promise<XurlTweetsResponse> {
|
|
1478
|
+
return runEffectPromise(
|
|
1479
|
+
searchRecentByConversationIdEffect(conversationId, options),
|
|
780
1480
|
);
|
|
781
|
-
return {
|
|
782
|
-
data: Array.isArray(payload.data)
|
|
783
|
-
? (payload.data as XurlTweetsResponse["data"])
|
|
784
|
-
: [],
|
|
785
|
-
includes:
|
|
786
|
-
payload.includes && typeof payload.includes === "object"
|
|
787
|
-
? (payload.includes as XurlTweetsResponse["includes"])
|
|
788
|
-
: undefined,
|
|
789
|
-
meta:
|
|
790
|
-
payload.meta && typeof payload.meta === "object"
|
|
791
|
-
? (payload.meta as XurlTweetsResponse["meta"])
|
|
792
|
-
: undefined,
|
|
793
|
-
};
|
|
794
1481
|
}
|
|
795
1482
|
|
|
796
|
-
export
|
|
1483
|
+
export function searchRecentTweetsEffect(
|
|
1484
|
+
searchQuery: string,
|
|
1485
|
+
{
|
|
1486
|
+
maxResults,
|
|
1487
|
+
paginationToken,
|
|
1488
|
+
startTime,
|
|
1489
|
+
endTime,
|
|
1490
|
+
username,
|
|
1491
|
+
timeoutMs,
|
|
1492
|
+
}: {
|
|
1493
|
+
maxResults: number;
|
|
1494
|
+
paginationToken?: string;
|
|
1495
|
+
startTime?: string;
|
|
1496
|
+
endTime?: string;
|
|
1497
|
+
username?: string;
|
|
1498
|
+
timeoutMs?: number;
|
|
1499
|
+
},
|
|
1500
|
+
): Effect.Effect<XurlTweetsResponse, Error> {
|
|
1501
|
+
const query = new URLSearchParams({
|
|
1502
|
+
query: searchQuery,
|
|
1503
|
+
max_results: String(maxResults),
|
|
1504
|
+
expansions: AUTHOR_MEDIA_EXPANSIONS,
|
|
1505
|
+
"tweet.fields": THREAD_TWEET_FIELDS,
|
|
1506
|
+
"media.fields": MEDIA_FIELDS,
|
|
1507
|
+
"user.fields": RICH_USER_FIELDS,
|
|
1508
|
+
});
|
|
1509
|
+
if (paginationToken) {
|
|
1510
|
+
query.set("pagination_token", paginationToken);
|
|
1511
|
+
}
|
|
1512
|
+
if (startTime) {
|
|
1513
|
+
query.set("start_time", startTime);
|
|
1514
|
+
}
|
|
1515
|
+
if (endTime) {
|
|
1516
|
+
query.set("end_time", endTime);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
return runOAuth2JsonCommandEffect({
|
|
1520
|
+
args: [`/2/tweets/search/recent?${query.toString()}`],
|
|
1521
|
+
username,
|
|
1522
|
+
options: { timeoutMs },
|
|
1523
|
+
useConfiguredCandidate: false,
|
|
1524
|
+
}).pipe(Effect.map(toXurlTweetsResponse));
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
export function searchRecentTweets(
|
|
1528
|
+
searchQuery: string,
|
|
1529
|
+
options: {
|
|
1530
|
+
maxResults: number;
|
|
1531
|
+
paginationToken?: string;
|
|
1532
|
+
startTime?: string;
|
|
1533
|
+
endTime?: string;
|
|
1534
|
+
username?: string;
|
|
1535
|
+
timeoutMs?: number;
|
|
1536
|
+
},
|
|
1537
|
+
): Promise<XurlTweetsResponse> {
|
|
1538
|
+
return runEffectPromise(searchRecentTweetsEffect(searchQuery, options));
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
export function getTweetByIdEffect(
|
|
797
1542
|
id: string,
|
|
798
1543
|
{ timeoutMs }: { timeoutMs?: number } = {},
|
|
799
|
-
):
|
|
1544
|
+
): Effect.Effect<XurlTweetsResponse, Error> {
|
|
800
1545
|
const query = new URLSearchParams({
|
|
801
1546
|
expansions: AUTHOR_MEDIA_EXPANSIONS,
|
|
802
1547
|
"tweet.fields": THREAD_TWEET_FIELDS,
|
|
@@ -804,55 +1549,74 @@ export async function getTweetById(
|
|
|
804
1549
|
"user.fields": RICH_USER_FIELDS,
|
|
805
1550
|
});
|
|
806
1551
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1552
|
+
return runJsonCommandEffect([`/2/tweets/${id}?${query.toString()}`], {
|
|
1553
|
+
timeoutMs,
|
|
1554
|
+
}).pipe(
|
|
1555
|
+
Effect.map((payload) => {
|
|
1556
|
+
const data =
|
|
1557
|
+
payload.data &&
|
|
1558
|
+
typeof payload.data === "object" &&
|
|
1559
|
+
!Array.isArray(payload.data)
|
|
1560
|
+
? [payload.data as XurlTweetsResponse["data"][number]]
|
|
1561
|
+
: Array.isArray(payload.data)
|
|
1562
|
+
? (payload.data as XurlTweetsResponse["data"])
|
|
1563
|
+
: [];
|
|
1564
|
+
|
|
1565
|
+
return {
|
|
1566
|
+
data,
|
|
1567
|
+
includes:
|
|
1568
|
+
payload.includes && typeof payload.includes === "object"
|
|
1569
|
+
? (payload.includes as XurlTweetsResponse["includes"])
|
|
1570
|
+
: undefined,
|
|
1571
|
+
meta:
|
|
1572
|
+
payload.meta && typeof payload.meta === "object"
|
|
1573
|
+
? (payload.meta as XurlTweetsResponse["meta"])
|
|
1574
|
+
: undefined,
|
|
1575
|
+
};
|
|
1576
|
+
}),
|
|
812
1577
|
);
|
|
813
|
-
|
|
814
|
-
payload.data &&
|
|
815
|
-
typeof payload.data === "object" &&
|
|
816
|
-
!Array.isArray(payload.data)
|
|
817
|
-
? [payload.data as XurlTweetsResponse["data"][number]]
|
|
818
|
-
: Array.isArray(payload.data)
|
|
819
|
-
? (payload.data as XurlTweetsResponse["data"])
|
|
820
|
-
: [];
|
|
1578
|
+
}
|
|
821
1579
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1580
|
+
export function getTweetById(
|
|
1581
|
+
id: string,
|
|
1582
|
+
options: { timeoutMs?: number } = {},
|
|
1583
|
+
): Promise<XurlTweetsResponse> {
|
|
1584
|
+
return runEffectPromise(getTweetByIdEffect(id, options));
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
export function postViaXurlEffect(text: string) {
|
|
1588
|
+
return runShortcutEffect(["post", text]);
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
export function postViaXurl(text: string) {
|
|
1592
|
+
return runEffectPromise(postViaXurlEffect(text));
|
|
833
1593
|
}
|
|
834
1594
|
|
|
835
|
-
export
|
|
836
|
-
return
|
|
1595
|
+
export function replyViaXurlEffect(tweetId: string, text: string) {
|
|
1596
|
+
return runShortcutEffect(["reply", tweetId, text]);
|
|
837
1597
|
}
|
|
838
1598
|
|
|
839
|
-
export
|
|
840
|
-
return
|
|
1599
|
+
export function replyViaXurl(tweetId: string, text: string) {
|
|
1600
|
+
return runEffectPromise(replyViaXurlEffect(tweetId, text));
|
|
841
1601
|
}
|
|
842
1602
|
|
|
843
|
-
export
|
|
844
|
-
return
|
|
1603
|
+
export function dmViaXurlEffect(handle: string, text: string) {
|
|
1604
|
+
return runShortcutEffect([
|
|
845
1605
|
"dm",
|
|
846
1606
|
handle.startsWith("@") ? handle : `@${handle}`,
|
|
847
1607
|
text,
|
|
848
1608
|
]);
|
|
849
1609
|
}
|
|
850
1610
|
|
|
851
|
-
export
|
|
1611
|
+
export function dmViaXurl(handle: string, text: string) {
|
|
1612
|
+
return runEffectPromise(dmViaXurlEffect(handle, text));
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
export function blockUserViaXurlEffect(
|
|
852
1616
|
sourceUserId: string,
|
|
853
1617
|
targetUserId: string,
|
|
854
1618
|
) {
|
|
855
|
-
return
|
|
1619
|
+
return runMutationCommandEffect([
|
|
856
1620
|
"-X",
|
|
857
1621
|
"POST",
|
|
858
1622
|
`/2/users/${sourceUserId}/blocking`,
|
|
@@ -861,22 +1625,30 @@ export async function blockUserViaXurl(
|
|
|
861
1625
|
]);
|
|
862
1626
|
}
|
|
863
1627
|
|
|
864
|
-
export
|
|
1628
|
+
export function blockUserViaXurl(sourceUserId: string, targetUserId: string) {
|
|
1629
|
+
return runEffectPromise(blockUserViaXurlEffect(sourceUserId, targetUserId));
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
export function unblockUserViaXurlEffect(
|
|
865
1633
|
sourceUserId: string,
|
|
866
1634
|
targetUserId: string,
|
|
867
1635
|
) {
|
|
868
|
-
return
|
|
1636
|
+
return runMutationCommandEffect([
|
|
869
1637
|
"-X",
|
|
870
1638
|
"DELETE",
|
|
871
1639
|
`/2/users/${sourceUserId}/blocking/${targetUserId}`,
|
|
872
1640
|
]);
|
|
873
1641
|
}
|
|
874
1642
|
|
|
875
|
-
export
|
|
1643
|
+
export function unblockUserViaXurl(sourceUserId: string, targetUserId: string) {
|
|
1644
|
+
return runEffectPromise(unblockUserViaXurlEffect(sourceUserId, targetUserId));
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
export function muteUserViaXurlEffect(
|
|
876
1648
|
sourceUserId: string,
|
|
877
1649
|
targetUserId: string,
|
|
878
1650
|
) {
|
|
879
|
-
return
|
|
1651
|
+
return runMutationCommandEffect([
|
|
880
1652
|
"-X",
|
|
881
1653
|
"POST",
|
|
882
1654
|
`/2/users/${sourceUserId}/muting`,
|
|
@@ -885,13 +1657,21 @@ export async function muteUserViaXurl(
|
|
|
885
1657
|
]);
|
|
886
1658
|
}
|
|
887
1659
|
|
|
888
|
-
export
|
|
1660
|
+
export function muteUserViaXurl(sourceUserId: string, targetUserId: string) {
|
|
1661
|
+
return runEffectPromise(muteUserViaXurlEffect(sourceUserId, targetUserId));
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
export function unmuteUserViaXurlEffect(
|
|
889
1665
|
sourceUserId: string,
|
|
890
1666
|
targetUserId: string,
|
|
891
1667
|
) {
|
|
892
|
-
return
|
|
1668
|
+
return runMutationCommandEffect([
|
|
893
1669
|
"-X",
|
|
894
1670
|
"DELETE",
|
|
895
1671
|
`/2/users/${sourceUserId}/muting/${targetUserId}`,
|
|
896
1672
|
]);
|
|
897
1673
|
}
|
|
1674
|
+
|
|
1675
|
+
export function unmuteUserViaXurl(sourceUserId: string, targetUserId: string) {
|
|
1676
|
+
return runEffectPromise(unmuteUserViaXurlEffect(sourceUserId, targetUserId));
|
|
1677
|
+
}
|