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
package/src/lib/blocklist.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { Effect } from "effect";
|
|
2
3
|
import { addBlock } from "./blocks";
|
|
4
|
+
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
3
5
|
|
|
4
6
|
export interface BlocklistImportItem {
|
|
5
7
|
query: string;
|
|
@@ -76,14 +78,31 @@ export function parseBlocklistText(text: string) {
|
|
|
76
78
|
return items;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
|
-
export
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
export function importBlocklistEffect(accountId: string, filePath: string) {
|
|
82
|
+
return Effect.gen(function* () {
|
|
83
|
+
const text = yield* tryPromise(() => readFile(filePath, "utf8"));
|
|
84
|
+
const queries = parseBlocklistText(text);
|
|
85
|
+
const items: BlocklistImportItem[] = [];
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
for (const query of queries) {
|
|
88
|
+
const blocked = yield* tryPromise(() => addBlock(accountId, query)).pipe(
|
|
89
|
+
Effect.map((result) => ({ ok: true as const, result })),
|
|
90
|
+
Effect.catchAll((error) =>
|
|
91
|
+
Effect.succeed({ ok: false as const, error }),
|
|
92
|
+
),
|
|
93
|
+
);
|
|
94
|
+
if (!blocked.ok) {
|
|
95
|
+
items.push({
|
|
96
|
+
query,
|
|
97
|
+
ok: false,
|
|
98
|
+
error:
|
|
99
|
+
blocked.error instanceof Error
|
|
100
|
+
? blocked.error.message
|
|
101
|
+
: "block failed",
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const { result } = blocked;
|
|
87
106
|
if (!result.ok) {
|
|
88
107
|
items.push({
|
|
89
108
|
query,
|
|
@@ -98,22 +117,20 @@ export async function importBlocklist(accountId: string, filePath: string) {
|
|
|
98
117
|
blockedAt: result.blockedAt,
|
|
99
118
|
handle: result.profile.handle,
|
|
100
119
|
});
|
|
101
|
-
} catch (error) {
|
|
102
|
-
items.push({
|
|
103
|
-
query,
|
|
104
|
-
ok: false,
|
|
105
|
-
error: error instanceof Error ? error.message : "block failed",
|
|
106
|
-
});
|
|
107
120
|
}
|
|
108
|
-
}
|
|
109
121
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
122
|
+
return {
|
|
123
|
+
ok: items.every((item) => item.ok),
|
|
124
|
+
accountId,
|
|
125
|
+
path: filePath,
|
|
126
|
+
requestedCount: queries.length,
|
|
127
|
+
blockedCount: items.filter((item) => item.ok).length,
|
|
128
|
+
failedCount: items.filter((item) => !item.ok).length,
|
|
129
|
+
items,
|
|
130
|
+
} satisfies BlocklistImportResult;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function importBlocklist(accountId: string, filePath: string) {
|
|
135
|
+
return runEffectPromise(importBlocklistEffect(accountId, filePath));
|
|
119
136
|
}
|
package/src/lib/blocks-write.ts
CHANGED
|
@@ -1,118 +1,167 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import { runModerationAction } from "./actions-transport";
|
|
3
|
+
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
2
4
|
import {
|
|
3
5
|
deleteModerationRow,
|
|
4
6
|
type ModerationActionOptions,
|
|
5
|
-
|
|
7
|
+
resolveModerationTargetEffect,
|
|
6
8
|
writeModerationRow,
|
|
7
9
|
} from "./moderation-write";
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
function trySync<T>(try_: () => T) {
|
|
12
|
+
return Effect.try({
|
|
13
|
+
try: try_,
|
|
14
|
+
catch: (error) =>
|
|
15
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function addBlockEffect(
|
|
10
20
|
accountId: string,
|
|
11
21
|
query: string,
|
|
12
22
|
options: ModerationActionOptions = {},
|
|
13
23
|
) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
return Effect.gen(function* () {
|
|
25
|
+
const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } =
|
|
26
|
+
yield* resolveModerationTargetEffect({
|
|
27
|
+
accountId,
|
|
28
|
+
query,
|
|
29
|
+
selfActionError: "Cannot block the current account",
|
|
30
|
+
});
|
|
31
|
+
const transport = yield* tryPromise(() =>
|
|
32
|
+
runModerationAction({
|
|
33
|
+
action: "block",
|
|
34
|
+
query: actionQuery,
|
|
35
|
+
targetUserId: resolved.externalUserId ?? undefined,
|
|
36
|
+
transport: options.transport,
|
|
37
|
+
expectedAccount: accountIdentity,
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (!transport.ok) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
action: "block",
|
|
45
|
+
accountId: resolvedAccountId,
|
|
46
|
+
profile: resolved.profile,
|
|
47
|
+
transport,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const blockedAt = new Date().toISOString();
|
|
52
|
+
yield* trySync(() =>
|
|
53
|
+
writeModerationRow(
|
|
54
|
+
db,
|
|
55
|
+
"blocks",
|
|
56
|
+
resolvedAccountId,
|
|
57
|
+
resolved.profile.id,
|
|
58
|
+
blockedAt,
|
|
59
|
+
),
|
|
60
|
+
);
|
|
26
61
|
|
|
27
|
-
if (!transport.ok) {
|
|
28
62
|
return {
|
|
29
|
-
ok:
|
|
63
|
+
ok: true,
|
|
30
64
|
action: "block",
|
|
31
65
|
accountId: resolvedAccountId,
|
|
66
|
+
blockedAt,
|
|
32
67
|
profile: resolved.profile,
|
|
33
68
|
transport,
|
|
34
69
|
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const blockedAt = new Date().toISOString();
|
|
38
|
-
writeModerationRow(
|
|
39
|
-
db,
|
|
40
|
-
"blocks",
|
|
41
|
-
resolvedAccountId,
|
|
42
|
-
resolved.profile.id,
|
|
43
|
-
blockedAt,
|
|
44
|
-
);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
45
72
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
transport,
|
|
53
|
-
};
|
|
73
|
+
export function addBlock(
|
|
74
|
+
accountId: string,
|
|
75
|
+
query: string,
|
|
76
|
+
options: ModerationActionOptions = {},
|
|
77
|
+
) {
|
|
78
|
+
return runEffectPromise(addBlockEffect(accountId, query, options));
|
|
54
79
|
}
|
|
55
80
|
|
|
56
|
-
export
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
81
|
+
export function recordBlockEffect(accountId: string, query: string) {
|
|
82
|
+
return Effect.gen(function* () {
|
|
83
|
+
const { db, resolved, resolvedAccountId } =
|
|
84
|
+
yield* resolveModerationTargetEffect({
|
|
85
|
+
accountId,
|
|
86
|
+
query,
|
|
87
|
+
selfActionError: "Cannot block the current account",
|
|
88
|
+
});
|
|
62
89
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
90
|
+
const blockedAt = new Date().toISOString();
|
|
91
|
+
yield* trySync(() =>
|
|
92
|
+
writeModerationRow(
|
|
93
|
+
db,
|
|
94
|
+
"blocks",
|
|
95
|
+
resolvedAccountId,
|
|
96
|
+
resolved.profile.id,
|
|
97
|
+
blockedAt,
|
|
98
|
+
),
|
|
99
|
+
);
|
|
71
100
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
101
|
+
return {
|
|
102
|
+
ok: true,
|
|
103
|
+
action: "record-block",
|
|
104
|
+
accountId: resolvedAccountId,
|
|
105
|
+
blockedAt,
|
|
106
|
+
profile: resolved.profile,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function recordBlock(accountId: string, query: string) {
|
|
112
|
+
return runEffectPromise(recordBlockEffect(accountId, query));
|
|
79
113
|
}
|
|
80
114
|
|
|
81
|
-
export
|
|
115
|
+
export function removeBlockEffect(
|
|
82
116
|
accountId: string,
|
|
83
117
|
query: string,
|
|
84
118
|
options: ModerationActionOptions = {},
|
|
85
119
|
) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
return Effect.gen(function* () {
|
|
121
|
+
const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } =
|
|
122
|
+
yield* resolveModerationTargetEffect({
|
|
123
|
+
accountId,
|
|
124
|
+
query,
|
|
125
|
+
selfActionError: "Cannot block the current account",
|
|
126
|
+
});
|
|
127
|
+
const transport = yield* tryPromise(() =>
|
|
128
|
+
runModerationAction({
|
|
129
|
+
action: "unblock",
|
|
130
|
+
query: actionQuery,
|
|
131
|
+
targetUserId: resolved.externalUserId ?? undefined,
|
|
132
|
+
transport: options.transport,
|
|
133
|
+
expectedAccount: accountIdentity,
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (!transport.ok) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
action: "unblock",
|
|
141
|
+
accountId: resolvedAccountId,
|
|
142
|
+
profile: resolved.profile,
|
|
143
|
+
transport,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
yield* trySync(() =>
|
|
148
|
+
deleteModerationRow(db, "blocks", resolvedAccountId, resolved.profile.id),
|
|
149
|
+
);
|
|
98
150
|
|
|
99
|
-
if (!transport.ok) {
|
|
100
151
|
return {
|
|
101
|
-
ok:
|
|
152
|
+
ok: true,
|
|
102
153
|
action: "unblock",
|
|
103
154
|
accountId: resolvedAccountId,
|
|
104
155
|
profile: resolved.profile,
|
|
105
156
|
transport,
|
|
106
157
|
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
deleteModerationRow(db, "blocks", resolvedAccountId, resolved.profile.id);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
110
160
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
};
|
|
161
|
+
export function removeBlock(
|
|
162
|
+
accountId: string,
|
|
163
|
+
query: string,
|
|
164
|
+
options: ModerationActionOptions = {},
|
|
165
|
+
) {
|
|
166
|
+
return runEffectPromise(removeBlockEffect(accountId, query, options));
|
|
118
167
|
}
|
package/src/lib/blocks.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import type { Database } from "./sqlite";
|
|
2
3
|
import { getNativeDb } from "./db";
|
|
4
|
+
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
3
5
|
import {
|
|
4
6
|
getAccountHandle,
|
|
5
7
|
getDefaultAccountId,
|
|
@@ -7,14 +9,32 @@ import {
|
|
|
7
9
|
} from "./moderation-target";
|
|
8
10
|
import type { BlockItem, BlockListResponse, BlockSearchItem } from "./types";
|
|
9
11
|
import { upsertProfileFromXUser } from "./x-profile";
|
|
10
|
-
import { listBlockedUsers,
|
|
12
|
+
import { listBlockedUsers, lookupAuthenticatedUserFresh } from "./xurl";
|
|
11
13
|
|
|
12
|
-
export {
|
|
14
|
+
export {
|
|
15
|
+
addBlock,
|
|
16
|
+
addBlockEffect,
|
|
17
|
+
recordBlock,
|
|
18
|
+
recordBlockEffect,
|
|
19
|
+
removeBlock,
|
|
20
|
+
removeBlockEffect,
|
|
21
|
+
} from "./blocks-write";
|
|
13
22
|
|
|
14
23
|
function remoteBlockSyncDisabled() {
|
|
15
24
|
return process.env.BIRDCLAW_DISABLE_LIVE_WRITES === "1";
|
|
16
25
|
}
|
|
17
26
|
|
|
27
|
+
function toError(error: unknown) {
|
|
28
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function trySync<T>(try_: () => T) {
|
|
32
|
+
return Effect.try({
|
|
33
|
+
try: try_,
|
|
34
|
+
catch: toError,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
function upsertRemoteBlock(
|
|
19
39
|
db: Database,
|
|
20
40
|
accountId: string,
|
|
@@ -206,116 +226,164 @@ export function getBlocksResponse({
|
|
|
206
226
|
};
|
|
207
227
|
}
|
|
208
228
|
|
|
209
|
-
export
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const remoteProfileIds: string[] = [];
|
|
214
|
-
|
|
215
|
-
if (remoteBlockSyncDisabled()) {
|
|
216
|
-
return {
|
|
217
|
-
ok: true,
|
|
218
|
-
accountId: resolvedAccountId,
|
|
219
|
-
synced: false,
|
|
220
|
-
syncedCount: 0,
|
|
221
|
-
transport: {
|
|
222
|
-
ok: true,
|
|
223
|
-
output: "remote block sync disabled in test mode",
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
const me = await lookupAuthenticatedUser();
|
|
230
|
-
const sourceUserId =
|
|
231
|
-
typeof me?.id === "string" && me.id.length > 0 ? me.id : null;
|
|
232
|
-
const sourceUsername =
|
|
233
|
-
typeof me?.username === "string" ? me.username.replace(/^@/, "") : "";
|
|
229
|
+
export function syncBlocksEffect(accountId: string) {
|
|
230
|
+
return Effect.gen(function* () {
|
|
231
|
+
const db = yield* trySync(() => getNativeDb());
|
|
232
|
+
const resolvedAccountId = accountId || getDefaultAccountId(db);
|
|
234
233
|
const accountHandle = getAccountHandle(db, resolvedAccountId);
|
|
234
|
+
if (!accountHandle) {
|
|
235
|
+
return yield* Effect.fail(
|
|
236
|
+
new Error(`Unknown account: ${resolvedAccountId}`),
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const account = yield* trySync(
|
|
240
|
+
() =>
|
|
241
|
+
db
|
|
242
|
+
.prepare("select handle, external_user_id from accounts where id = ?")
|
|
243
|
+
.get(resolvedAccountId) as
|
|
244
|
+
| { handle: string; external_user_id: string | null }
|
|
245
|
+
| undefined,
|
|
246
|
+
);
|
|
247
|
+
const blockedAt = new Date().toISOString();
|
|
248
|
+
const remoteProfileIds: string[] = [];
|
|
235
249
|
|
|
236
|
-
if (
|
|
250
|
+
if (remoteBlockSyncDisabled()) {
|
|
237
251
|
return {
|
|
238
|
-
ok:
|
|
252
|
+
ok: true,
|
|
239
253
|
accountId: resolvedAccountId,
|
|
240
254
|
synced: false,
|
|
241
255
|
syncedCount: 0,
|
|
242
256
|
transport: {
|
|
243
|
-
ok:
|
|
244
|
-
output:
|
|
245
|
-
"xurl block sync unavailable without an authenticated account",
|
|
257
|
+
ok: true,
|
|
258
|
+
output: "remote block sync disabled in test mode",
|
|
246
259
|
},
|
|
247
260
|
};
|
|
248
261
|
}
|
|
249
262
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
263
|
+
return yield* Effect.gen(function* () {
|
|
264
|
+
const me = yield* tryPromise(() => lookupAuthenticatedUserFresh()).pipe(
|
|
265
|
+
Effect.mapError(toError),
|
|
266
|
+
);
|
|
267
|
+
const sourceUserId =
|
|
268
|
+
typeof me?.id === "string" && me.id.length > 0 ? me.id : null;
|
|
269
|
+
const sourceUsername =
|
|
270
|
+
typeof me?.username === "string" ? me.username.replace(/^@/, "") : "";
|
|
271
|
+
const accountExternalUserId = account?.external_user_id?.trim() ?? "";
|
|
272
|
+
if (!sourceUserId) {
|
|
273
|
+
return {
|
|
257
274
|
ok: false,
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
275
|
+
accountId: resolvedAccountId,
|
|
276
|
+
synced: false,
|
|
277
|
+
syncedCount: 0,
|
|
278
|
+
transport: {
|
|
279
|
+
ok: false,
|
|
280
|
+
output:
|
|
281
|
+
"xurl block sync unavailable without an authenticated account",
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (accountExternalUserId && sourceUserId !== accountExternalUserId) {
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
accountId: resolvedAccountId,
|
|
290
|
+
synced: false,
|
|
291
|
+
syncedCount: 0,
|
|
292
|
+
transport: {
|
|
293
|
+
ok: false,
|
|
294
|
+
output: `xurl is authenticated as user ${sourceUserId}, not account ${resolvedAccountId}`,
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
262
298
|
|
|
263
|
-
|
|
264
|
-
|
|
299
|
+
if (
|
|
300
|
+
!accountExternalUserId &&
|
|
301
|
+
(!sourceUsername || accountHandle !== sourceUsername)
|
|
302
|
+
) {
|
|
303
|
+
return {
|
|
304
|
+
ok: false,
|
|
305
|
+
accountId: resolvedAccountId,
|
|
306
|
+
synced: false,
|
|
307
|
+
syncedCount: 0,
|
|
308
|
+
transport: {
|
|
309
|
+
ok: false,
|
|
310
|
+
output: sourceUsername
|
|
311
|
+
? `xurl is authenticated as @${sourceUsername}, not @${accountHandle}`
|
|
312
|
+
: "xurl authenticated username unavailable",
|
|
313
|
+
},
|
|
314
|
+
};
|
|
315
|
+
}
|
|
265
316
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
for (const user of page.items) {
|
|
270
|
-
const resolved = upsertProfileFromXUser(db, user);
|
|
271
|
-
remoteProfileIds.push(resolved.profile.id);
|
|
272
|
-
upsertRemoteBlock(
|
|
273
|
-
db,
|
|
274
|
-
resolvedAccountId,
|
|
275
|
-
resolved.profile.id,
|
|
276
|
-
blockedAt,
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
mergeRemotePage();
|
|
281
|
-
nextToken = page.nextToken;
|
|
282
|
-
pageCount += 1;
|
|
283
|
-
} while (nextToken && pageCount < 20);
|
|
317
|
+
let nextToken: string | null = null;
|
|
318
|
+
let pageCount = 0;
|
|
319
|
+
let completed = false;
|
|
284
320
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
321
|
+
do {
|
|
322
|
+
const page = yield* tryPromise(() =>
|
|
323
|
+
listBlockedUsers(sourceUserId, nextToken ?? undefined),
|
|
324
|
+
).pipe(Effect.mapError(toError));
|
|
325
|
+
yield* trySync(() => {
|
|
326
|
+
const mergeRemotePage = db.transaction(() => {
|
|
327
|
+
for (const user of page.items) {
|
|
328
|
+
const resolved = upsertProfileFromXUser(db, user);
|
|
329
|
+
remoteProfileIds.push(resolved.profile.id);
|
|
330
|
+
upsertRemoteBlock(
|
|
331
|
+
db,
|
|
332
|
+
resolvedAccountId,
|
|
333
|
+
resolved.profile.id,
|
|
334
|
+
blockedAt,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
mergeRemotePage();
|
|
339
|
+
});
|
|
340
|
+
nextToken = page.nextToken;
|
|
341
|
+
pageCount += 1;
|
|
342
|
+
} while (nextToken && pageCount < 20);
|
|
289
343
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
344
|
+
completed = !nextToken;
|
|
345
|
+
if (completed) {
|
|
346
|
+
yield* trySync(() => {
|
|
347
|
+
const pruneMergedBlocks = db.transaction(() => {
|
|
348
|
+
pruneRemoteBlocks(db, resolvedAccountId, remoteProfileIds);
|
|
349
|
+
});
|
|
350
|
+
pruneMergedBlocks();
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
296
355
|
ok: true,
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
356
|
+
accountId: resolvedAccountId,
|
|
357
|
+
synced: true,
|
|
358
|
+
syncedCount: remoteProfileIds.length,
|
|
359
|
+
partial: !completed,
|
|
360
|
+
transport: {
|
|
361
|
+
ok: true,
|
|
362
|
+
output: completed
|
|
363
|
+
? `synced ${remoteProfileIds.length} remote blocks`
|
|
364
|
+
: `synced ${remoteProfileIds.length} remote blocks (partial; skipped pruning)`,
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
}).pipe(
|
|
368
|
+
Effect.catchAll((error) =>
|
|
369
|
+
Effect.succeed({
|
|
370
|
+
ok: false,
|
|
371
|
+
accountId: resolvedAccountId,
|
|
372
|
+
synced: remoteProfileIds.length > 0,
|
|
373
|
+
syncedCount: remoteProfileIds.length,
|
|
374
|
+
transport: {
|
|
375
|
+
ok: false,
|
|
376
|
+
output:
|
|
377
|
+
remoteProfileIds.length > 0
|
|
378
|
+
? `partial block sync after ${remoteProfileIds.length} profiles: ${error.message}`
|
|
379
|
+
: error.message,
|
|
380
|
+
},
|
|
381
|
+
}),
|
|
382
|
+
),
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export function syncBlocks(accountId: string) {
|
|
388
|
+
return runEffectPromise(syncBlocksEffect(accountId));
|
|
321
389
|
}
|