birdclaw 0.5.0 → 0.6.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 +63 -0
- package/README.md +55 -5
- package/bin/birdclaw.mjs +50 -11
- package/package.json +9 -7
- package/public/birdclaw-mark.png +0 -0
- package/scripts/browser-perf.mjs +400 -0
- package/scripts/build-docs-site.mjs +940 -0
- package/scripts/docs-site-assets.mjs +311 -0
- package/scripts/run-vitest.mjs +21 -0
- package/scripts/sanitize-node-options.mjs +23 -0
- package/scripts/start-test-server.mjs +42 -0
- package/src/cli.ts +422 -14
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +29 -9
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/BrandMark.tsx +67 -0
- package/src/components/ConversationThread.tsx +11 -10
- package/src/components/DmWorkspace.tsx +39 -14
- package/src/components/FeedState.tsx +147 -0
- package/src/components/InboxCard.tsx +14 -2
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +452 -0
- package/src/components/SavedTimelineView.tsx +64 -56
- package/src/components/SyncNowButton.tsx +137 -0
- package/src/components/ThemeSlider.tsx +49 -93
- package/src/components/TimelineCard.tsx +364 -136
- package/src/components/TimelineRouteFrame.tsx +170 -0
- package/src/components/TweetMediaGrid.tsx +170 -24
- package/src/components/TweetRichText.tsx +28 -13
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +153 -0
- package/src/lib/account-sync-job.ts +654 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +304 -0
- package/src/lib/archive-finder.ts +72 -53
- package/src/lib/archive-import.ts +1377 -1298
- package/src/lib/authored-live.ts +261 -204
- package/src/lib/avatar-cache.ts +159 -44
- package/src/lib/backup.ts +1532 -951
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +549 -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/conversation-surface.ts +205 -0
- package/src/lib/db.ts +35 -3
- 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/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/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +352 -288
- package/src/lib/mentions-live.ts +390 -342
- 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/openai.ts +84 -53
- package/src/lib/period-digest.ts +953 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- 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 +1024 -189
- package/src/lib/research.ts +165 -120
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +60 -39
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/types.ts +38 -1
- package/src/lib/ui.ts +41 -10
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +511 -0
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl.ts +681 -411
- package/src/routeTree.gen.ts +63 -0
- package/src/routes/__root.tsx +25 -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/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/period-digest.tsx +123 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +79 -55
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +105 -0
- package/src/routes/dms.tsx +195 -55
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/index.tsx +21 -127
- package/src/routes/links.tsx +50 -5
- package/src/routes/mentions.tsx +21 -127
- package/src/routes/today.tsx +441 -0
- package/src/styles.css +74 -11
- package/vite.config.ts +8 -0
|
@@ -6,15 +6,16 @@ import {
|
|
|
6
6
|
unmuteUserViaBird,
|
|
7
7
|
} from "./bird-actions";
|
|
8
8
|
import { type ActionsTransport, resolveActionsTransport } from "./config";
|
|
9
|
+
import { Effect } from "effect";
|
|
10
|
+
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
9
11
|
import type {
|
|
10
12
|
ModerationAction,
|
|
11
13
|
ModerationActionTransportResult,
|
|
12
14
|
ModerationTransportKind,
|
|
13
15
|
} from "./types";
|
|
14
|
-
import { blockUserViaXWeb, unblockUserViaXWeb } from "./x-web";
|
|
15
16
|
import {
|
|
16
17
|
blockUserViaXurl,
|
|
17
|
-
|
|
18
|
+
lookupAuthenticatedUserFresh,
|
|
18
19
|
muteUserViaXurl,
|
|
19
20
|
unblockUserViaXurl,
|
|
20
21
|
unmuteUserViaXurl,
|
|
@@ -27,29 +28,96 @@ interface RunActionParams {
|
|
|
27
28
|
query: string;
|
|
28
29
|
targetUserId?: string;
|
|
29
30
|
transport?: string;
|
|
31
|
+
expectedAccount?: ExpectedActionAccount;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ExpectedActionAccount {
|
|
35
|
+
id: string;
|
|
36
|
+
handle: string;
|
|
37
|
+
externalUserId?: string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function toError(error: unknown) {
|
|
41
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function trySync<T>(try_: () => T) {
|
|
45
|
+
return Effect.try({
|
|
46
|
+
try: try_,
|
|
47
|
+
catch: toError,
|
|
48
|
+
});
|
|
30
49
|
}
|
|
31
50
|
|
|
32
51
|
function normalizeFailure(transport: ModerationTransportKind, output: string) {
|
|
33
52
|
return `${transport}: ${output}`;
|
|
34
53
|
}
|
|
35
54
|
|
|
36
|
-
|
|
55
|
+
function liveWritesDisabled() {
|
|
56
|
+
return process.env.BIRDCLAW_DISABLE_LIVE_WRITES === "1";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeHandle(value: string | null | undefined) {
|
|
60
|
+
return value?.replace(/^@/, "").toLowerCase() ?? "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function verifyExpectedAccountEffect(
|
|
64
|
+
expectedAccount: ExpectedActionAccount | undefined,
|
|
65
|
+
) {
|
|
66
|
+
return Effect.gen(function* () {
|
|
67
|
+
if (!expectedAccount) return null;
|
|
68
|
+
if (liveWritesDisabled()) return null;
|
|
69
|
+
|
|
70
|
+
const sourceUser = yield* tryPromise(() => lookupAuthenticatedUserFresh());
|
|
71
|
+
const sourceUserId =
|
|
72
|
+
sourceUser && typeof sourceUser.id === "string" ? sourceUser.id : "";
|
|
73
|
+
const sourceUsername =
|
|
74
|
+
sourceUser && typeof sourceUser.username === "string"
|
|
75
|
+
? normalizeHandle(sourceUser.username)
|
|
76
|
+
: "";
|
|
77
|
+
const expectedExternalUserId = expectedAccount.externalUserId?.trim() ?? "";
|
|
78
|
+
const expectedHandle = normalizeHandle(expectedAccount.handle);
|
|
79
|
+
|
|
80
|
+
if (expectedExternalUserId) {
|
|
81
|
+
if (sourceUserId === expectedExternalUserId) return sourceUserId;
|
|
82
|
+
return yield* Effect.fail(
|
|
83
|
+
new Error(
|
|
84
|
+
`xurl is authenticated as user ${sourceUserId || "unknown"}, not account ${expectedAccount.id}`,
|
|
85
|
+
),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (expectedHandle && sourceUsername === expectedHandle)
|
|
90
|
+
return sourceUserId;
|
|
91
|
+
return yield* Effect.fail(
|
|
92
|
+
new Error(
|
|
93
|
+
sourceUsername
|
|
94
|
+
? `xurl is authenticated as @${sourceUsername}, not @${expectedHandle}`
|
|
95
|
+
: "xurl authenticated user unavailable",
|
|
96
|
+
),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runBirdActionEffect(
|
|
37
102
|
action: ModerationAction,
|
|
38
103
|
query: string,
|
|
39
|
-
):
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
104
|
+
): Effect.Effect<ActionTransportResult, unknown> {
|
|
105
|
+
return Effect.gen(function* () {
|
|
106
|
+
const result = yield* tryPromise(() =>
|
|
107
|
+
action === "block"
|
|
108
|
+
? blockUserViaBird(query)
|
|
109
|
+
: action === "unblock"
|
|
110
|
+
? unblockUserViaBird(query)
|
|
111
|
+
: action === "mute"
|
|
112
|
+
? muteUserViaBird(query)
|
|
113
|
+
: unmuteUserViaBird(query),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...result,
|
|
118
|
+
transport: "bird",
|
|
119
|
+
};
|
|
120
|
+
});
|
|
53
121
|
}
|
|
54
122
|
|
|
55
123
|
function getVerifyExpectation(action: ModerationAction) {
|
|
@@ -64,145 +132,151 @@ function getVerifyExpectation(action: ModerationAction) {
|
|
|
64
132
|
};
|
|
65
133
|
}
|
|
66
134
|
|
|
67
|
-
|
|
135
|
+
function runXurlActionEffect(
|
|
68
136
|
action: ModerationAction,
|
|
69
137
|
query: string,
|
|
70
138
|
targetUserId?: string,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
139
|
+
verifiedSourceUserId?: string | null,
|
|
140
|
+
): Effect.Effect<ActionTransportResult, unknown> {
|
|
141
|
+
return Effect.gen(function* () {
|
|
142
|
+
if (!targetUserId) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
output: "missing target user id for xurl transport",
|
|
146
|
+
transport: "xurl",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
79
149
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
? await unblockUserViaXurl(sourceUserId, targetUserId)
|
|
96
|
-
: action === "mute"
|
|
97
|
-
? await muteUserViaXurl(sourceUserId, targetUserId)
|
|
98
|
-
: await unmuteUserViaXurl(sourceUserId, targetUserId);
|
|
99
|
-
|
|
100
|
-
if (!result.ok) {
|
|
101
|
-
return {
|
|
102
|
-
...result,
|
|
103
|
-
transport: "xurl",
|
|
104
|
-
};
|
|
105
|
-
}
|
|
150
|
+
let sourceUserId = verifiedSourceUserId ?? "";
|
|
151
|
+
if (!sourceUserId) {
|
|
152
|
+
const sourceUser = yield* tryPromise(() =>
|
|
153
|
+
lookupAuthenticatedUserFresh(),
|
|
154
|
+
);
|
|
155
|
+
sourceUserId =
|
|
156
|
+
sourceUser && typeof sourceUser.id === "string" ? sourceUser.id : "";
|
|
157
|
+
}
|
|
158
|
+
if (!sourceUserId) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
output: "xurl authenticated user unavailable",
|
|
162
|
+
transport: "xurl",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
106
165
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
166
|
+
const result = yield* tryPromise(() =>
|
|
167
|
+
action === "block"
|
|
168
|
+
? blockUserViaXurl(sourceUserId, targetUserId)
|
|
169
|
+
: action === "unblock"
|
|
170
|
+
? unblockUserViaXurl(sourceUserId, targetUserId)
|
|
171
|
+
: action === "mute"
|
|
172
|
+
? muteUserViaXurl(sourceUserId, targetUserId)
|
|
173
|
+
: unmuteUserViaXurl(sourceUserId, targetUserId),
|
|
174
|
+
);
|
|
114
175
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
}
|
|
176
|
+
if (!result.ok) {
|
|
177
|
+
return {
|
|
178
|
+
...result,
|
|
179
|
+
transport: "xurl",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
122
182
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
183
|
+
const status = yield* tryPromise(() => readBirdStatusViaBird(query));
|
|
184
|
+
const { field: verifyField, expected: expectedValue } =
|
|
185
|
+
getVerifyExpectation(action);
|
|
186
|
+
const actualValue =
|
|
187
|
+
status && typeof status[verifyField] === "boolean"
|
|
188
|
+
? Boolean(status[verifyField])
|
|
189
|
+
: null;
|
|
130
190
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
191
|
+
if (actualValue === null) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
output: `${result.output}\nxurl verify unavailable from bird status`,
|
|
195
|
+
transport: "xurl",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
137
198
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
output: `x-web does not support ${action}`,
|
|
146
|
-
transport: "x-web",
|
|
147
|
-
};
|
|
148
|
-
}
|
|
199
|
+
if (actualValue !== expectedValue) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
output: `${result.output}\nxurl verify mismatch ${verifyField}=${String(actualValue)}`,
|
|
203
|
+
transport: "xurl",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
149
206
|
|
|
150
|
-
if (!targetUserId) {
|
|
151
207
|
return {
|
|
152
|
-
ok:
|
|
153
|
-
output:
|
|
154
|
-
transport: "
|
|
208
|
+
ok: true,
|
|
209
|
+
output: `${result.output}\nverified ${verifyField}=${String(actualValue)}`,
|
|
210
|
+
transport: "xurl",
|
|
155
211
|
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const result =
|
|
159
|
-
action === "block"
|
|
160
|
-
? await blockUserViaXWeb(targetUserId)
|
|
161
|
-
: await unblockUserViaXWeb(targetUserId);
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
...result,
|
|
165
|
-
transport: "x-web",
|
|
166
|
-
};
|
|
212
|
+
});
|
|
167
213
|
}
|
|
168
214
|
|
|
169
|
-
export
|
|
215
|
+
export function runModerationActionEffect({
|
|
170
216
|
action,
|
|
171
217
|
query,
|
|
172
218
|
targetUserId,
|
|
173
219
|
transport,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
...xurlResult,
|
|
192
|
-
output: `${xurlResult.output}\nfalling back after ${normalizeFailure("bird", birdResult.output)}`,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
220
|
+
expectedAccount,
|
|
221
|
+
}: RunActionParams): Effect.Effect<ActionTransportResult, unknown> {
|
|
222
|
+
return Effect.gen(function* () {
|
|
223
|
+
const requestedTransport = yield* trySync(() =>
|
|
224
|
+
resolveActionsTransport(transport),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
const verifyXurlAccount = () =>
|
|
228
|
+
verifyExpectedAccountEffect(expectedAccount).pipe(
|
|
229
|
+
Effect.catchAll((error) =>
|
|
230
|
+
Effect.succeed({
|
|
231
|
+
ok: false as const,
|
|
232
|
+
output: error instanceof Error ? error.message : String(error),
|
|
233
|
+
transport: "xurl" as const,
|
|
234
|
+
}),
|
|
235
|
+
),
|
|
236
|
+
);
|
|
195
237
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
238
|
+
if (requestedTransport === "bird") {
|
|
239
|
+
return yield* runBirdActionEffect(action, query);
|
|
240
|
+
}
|
|
241
|
+
if (requestedTransport === "xurl") {
|
|
242
|
+
const accountCheck = yield* verifyXurlAccount();
|
|
243
|
+
if (accountCheck && typeof accountCheck === "object") {
|
|
244
|
+
return accountCheck;
|
|
245
|
+
}
|
|
246
|
+
return yield* runXurlActionEffect(
|
|
247
|
+
action,
|
|
248
|
+
query,
|
|
249
|
+
targetUserId,
|
|
250
|
+
typeof accountCheck === "string" ? accountCheck : null,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const birdResult = yield* runBirdActionEffect(action, query);
|
|
255
|
+
if (birdResult.ok) {
|
|
256
|
+
return birdResult;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const accountCheck = yield* verifyXurlAccount();
|
|
260
|
+
if (accountCheck && typeof accountCheck === "object") {
|
|
199
261
|
return {
|
|
200
|
-
|
|
262
|
+
ok: false,
|
|
201
263
|
output: [
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
`falling back after ${normalizeFailure("xurl", xurlResult.output)}`,
|
|
264
|
+
normalizeFailure("bird", birdResult.output),
|
|
265
|
+
normalizeFailure("xurl", accountCheck.output),
|
|
205
266
|
].join("\n"),
|
|
267
|
+
transport: "xurl",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const xurlResult = yield* runXurlActionEffect(
|
|
271
|
+
action,
|
|
272
|
+
query,
|
|
273
|
+
targetUserId,
|
|
274
|
+
typeof accountCheck === "string" ? accountCheck : null,
|
|
275
|
+
);
|
|
276
|
+
if (xurlResult.ok) {
|
|
277
|
+
return {
|
|
278
|
+
...xurlResult,
|
|
279
|
+
output: `${xurlResult.output}\nfalling back after ${normalizeFailure("bird", birdResult.output)}`,
|
|
206
280
|
};
|
|
207
281
|
}
|
|
208
282
|
|
|
@@ -211,20 +285,16 @@ export async function runModerationAction({
|
|
|
211
285
|
output: [
|
|
212
286
|
normalizeFailure("bird", birdResult.output),
|
|
213
287
|
normalizeFailure("xurl", xurlResult.output),
|
|
214
|
-
normalizeFailure("x-web", xWebResult.output),
|
|
215
288
|
].join("\n"),
|
|
216
|
-
transport:
|
|
289
|
+
transport: xurlResult.transport,
|
|
217
290
|
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
].join("\n"),
|
|
226
|
-
transport: xurlResult.transport,
|
|
227
|
-
};
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function runModerationAction(
|
|
295
|
+
params: RunActionParams,
|
|
296
|
+
): Promise<ActionTransportResult> {
|
|
297
|
+
return runEffectPromise(runModerationActionEffect(params));
|
|
228
298
|
}
|
|
229
299
|
|
|
230
300
|
export type { ActionsTransport };
|