birdclaw 0.8.2 → 0.8.4
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 +33 -0
- package/README.md +10 -8
- package/bin/birdclaw.mjs +4 -66
- package/dist/cli/birdclaw.js +28165 -0
- package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
- package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
- package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
- package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
- package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
- package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
- package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
- package/dist/client/assets/chunk-C0phanGV.js +1 -0
- package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
- package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
- package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
- package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
- package/dist/client/assets/discuss-DTjaOugw.js +1 -0
- package/dist/client/assets/dms-CGd6Eogq.js +1 -0
- package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
- package/dist/client/assets/index-CEIG7S6s.js +31 -0
- package/dist/client/assets/index-bpFrQ5yG.css +1 -0
- package/dist/client/assets/likes-cLrcAQem.js +1 -0
- package/dist/client/assets/links-CM9QO1LC.js +1 -0
- package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
- package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
- package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
- package/dist/client/assets/network-map-B5a3whqj.js +2 -0
- package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
- package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
- package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
- package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
- package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
- package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
- package/dist/client/assets/react-nBylq1U5.js +1 -0
- package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
- package/dist/client/assets/routes-q7ZaW4En.js +1 -0
- package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
- package/dist/client/assets/search-Dq5kYsxA.js +1 -0
- package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
- package/dist/client/assets/styles-B8s3tt3r.css +2 -0
- package/dist/client/assets/today-CeG1O7Py.js +1 -0
- package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
- package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
- package/dist/client/assets/users-CHbKlqUr.js +1 -0
- package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
- package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
- package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
- package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
- package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
- package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
- package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
- package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
- package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
- package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
- package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
- package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
- package/dist/server/assets/blocks-DSgQghqP.js +17 -0
- package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
- package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
- package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
- package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
- package/dist/server/assets/discuss-QTo08NCN.js +338 -0
- package/dist/server/assets/dms-BuLj4bp8.js +368 -0
- package/dist/server/assets/dms-DAyQrVM2.js +240 -0
- package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
- package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
- package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
- package/dist/server/assets/likes-DBOAtsJU.js +14 -0
- package/dist/server/assets/links-DAhSjynu.js +240 -0
- package/dist/server/assets/links-DmH5lxHO.js +468 -0
- package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
- package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
- package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
- package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
- package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
- package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
- package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
- package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
- package/dist/server/assets/query-client-BdauHBgd.js +923 -0
- package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
- package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
- package/dist/server/assets/router-DqkwSPiM.js +17026 -0
- package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
- package/dist/server/assets/start-FaDOTmto.js +4 -0
- package/dist/server/assets/today-B5hVM57Z.js +17 -0
- package/dist/server/assets/today-fvGiZ7ud.js +303 -0
- package/dist/server/assets/ui-qbmcMSSX.js +137 -0
- package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
- package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
- package/dist/server/server.js +5520 -0
- package/package.json +27 -34
- package/playwright.config.ts +0 -35
- package/scripts/browser-perf.mjs +0 -427
- package/scripts/build-docs-site.mjs +0 -967
- package/scripts/docs-site-assets.mjs +0 -311
- package/scripts/run-vitest.mjs +0 -21
- package/scripts/sanitize-node-options.mjs +0 -23
- package/scripts/start-test-server.mjs +0 -42
- package/src/cli-moderation.ts +0 -210
- package/src/cli.ts +0 -2336
- package/src/components/AccountSwitcher.tsx +0 -186
- package/src/components/AppNav.tsx +0 -118
- package/src/components/AvatarChip.tsx +0 -58
- package/src/components/BrandMark.tsx +0 -67
- package/src/components/ConversationThread.tsx +0 -131
- package/src/components/DmWorkspace.tsx +0 -286
- package/src/components/EmbeddedTweetCard.tsx +0 -55
- package/src/components/FeedState.tsx +0 -147
- package/src/components/FloatingPreview.tsx +0 -382
- package/src/components/InboxCard.tsx +0 -152
- package/src/components/LinkPreviewCard.tsx +0 -292
- package/src/components/MarkdownViewer.tsx +0 -777
- package/src/components/ProfileAnalysisStream.tsx +0 -428
- package/src/components/ProfilePreview.tsx +0 -129
- package/src/components/SavedTimelineView.tsx +0 -155
- package/src/components/SmartTimestamp.tsx +0 -72
- package/src/components/SyncNowButton.tsx +0 -140
- package/src/components/ThemeSlider.tsx +0 -80
- package/src/components/TimelineCard.tsx +0 -533
- package/src/components/TimelineRouteFrame.tsx +0 -186
- package/src/components/TweetArticleCard.tsx +0 -66
- package/src/components/TweetMediaGrid.tsx +0 -185
- package/src/components/TweetRichText.tsx +0 -159
- package/src/components/account-selection.ts +0 -64
- package/src/components/useTimelineRouteData.ts +0 -323
- package/src/lib/account-sync-job.ts +0 -666
- package/src/lib/actions-transport.ts +0 -300
- package/src/lib/api-client.ts +0 -419
- package/src/lib/archive-finder.ts +0 -185
- package/src/lib/archive-import.ts +0 -2764
- package/src/lib/authored-live.ts +0 -1072
- package/src/lib/avatar-cache.ts +0 -349
- package/src/lib/backup.ts +0 -2741
- package/src/lib/bird-actions.ts +0 -294
- package/src/lib/bird-command.ts +0 -130
- package/src/lib/bird.ts +0 -1140
- package/src/lib/blocklist.ts +0 -136
- package/src/lib/blocks-write.ts +0 -167
- package/src/lib/blocks.ts +0 -389
- package/src/lib/bookmark-sync-job.ts +0 -536
- package/src/lib/client-cache.ts +0 -109
- package/src/lib/config.ts +0 -224
- package/src/lib/conversation-surface.ts +0 -205
- package/src/lib/data-sources.ts +0 -219
- package/src/lib/db.ts +0 -1135
- package/src/lib/dms-live.ts +0 -933
- package/src/lib/effect-runtime.ts +0 -45
- package/src/lib/follow-graph.ts +0 -1097
- package/src/lib/geocoding.ts +0 -296
- package/src/lib/http-effect.ts +0 -222
- package/src/lib/identity-search-index.ts +0 -369
- package/src/lib/inbox.ts +0 -241
- package/src/lib/link-index.ts +0 -641
- package/src/lib/link-insights.ts +0 -860
- package/src/lib/link-preview-metadata.ts +0 -754
- package/src/lib/location.ts +0 -137
- package/src/lib/media-fetch.ts +0 -860
- package/src/lib/media-includes.ts +0 -165
- package/src/lib/mention-threads-live.ts +0 -917
- package/src/lib/mentions-export.ts +0 -147
- package/src/lib/mentions-live.ts +0 -1238
- package/src/lib/moderation-target.ts +0 -216
- package/src/lib/moderation-write.ts +0 -131
- package/src/lib/mutes-write.ts +0 -167
- package/src/lib/mutes.ts +0 -84
- package/src/lib/ndjson-stream.ts +0 -106
- package/src/lib/network-map.ts +0 -382
- package/src/lib/openai.ts +0 -117
- package/src/lib/period-digest.ts +0 -1552
- package/src/lib/present.ts +0 -118
- package/src/lib/profile-affiliation-hydration.ts +0 -228
- package/src/lib/profile-affiliations.ts +0 -262
- package/src/lib/profile-analysis.ts +0 -1272
- package/src/lib/profile-bio-entities.ts +0 -318
- package/src/lib/profile-history.ts +0 -164
- package/src/lib/profile-hydration.ts +0 -180
- package/src/lib/profile-replies.ts +0 -77
- package/src/lib/profile-resolver.ts +0 -650
- package/src/lib/queries.ts +0 -2381
- package/src/lib/research.ts +0 -652
- package/src/lib/search-discussion.ts +0 -1016
- package/src/lib/seed.ts +0 -613
- package/src/lib/sqlite.ts +0 -147
- package/src/lib/sync-cache.ts +0 -65
- package/src/lib/theme-transition.ts +0 -113
- package/src/lib/theme.tsx +0 -165
- package/src/lib/timeline-collections-live.ts +0 -581
- package/src/lib/timeline-live.ts +0 -452
- package/src/lib/tweet-account-edges.ts +0 -48
- package/src/lib/tweet-lookup.ts +0 -46
- package/src/lib/tweet-render.ts +0 -430
- package/src/lib/tweet-search-live.ts +0 -566
- package/src/lib/types.ts +0 -723
- package/src/lib/ui.ts +0 -529
- package/src/lib/url-expansion-store.ts +0 -110
- package/src/lib/url-expansion.ts +0 -343
- package/src/lib/url-safety.ts +0 -220
- package/src/lib/web-sync.ts +0 -516
- package/src/lib/whois.ts +0 -1079
- package/src/lib/x-profile.ts +0 -365
- package/src/lib/x-web.ts +0 -199
- package/src/lib/xurl-rate-limits.ts +0 -267
- package/src/lib/xurl.ts +0 -1677
- package/src/routeTree.gen.ts +0 -723
- package/src/router.tsx +0 -20
- package/src/routes/__root.tsx +0 -79
- package/src/routes/api/action.tsx +0 -147
- package/src/routes/api/avatar.tsx +0 -50
- package/src/routes/api/blocks.tsx +0 -35
- package/src/routes/api/conversation.tsx +0 -45
- package/src/routes/api/data-sources.tsx +0 -24
- package/src/routes/api/inbox.tsx +0 -43
- package/src/routes/api/link-insights.tsx +0 -85
- package/src/routes/api/link-preview.tsx +0 -47
- package/src/routes/api/network-map.tsx +0 -55
- package/src/routes/api/period-digest.tsx +0 -101
- package/src/routes/api/profile-analysis.tsx +0 -97
- package/src/routes/api/profile-hydrate.tsx +0 -54
- package/src/routes/api/query.tsx +0 -115
- package/src/routes/api/search-discussion.tsx +0 -113
- package/src/routes/api/status.tsx +0 -28
- package/src/routes/api/sync.tsx +0 -105
- package/src/routes/api/xurl-rate-limits.tsx +0 -24
- package/src/routes/blocks.tsx +0 -377
- package/src/routes/bookmarks.tsx +0 -18
- package/src/routes/data-sources.tsx +0 -257
- package/src/routes/discuss.tsx +0 -419
- package/src/routes/dms.tsx +0 -432
- package/src/routes/inbox.tsx +0 -226
- package/src/routes/index.tsx +0 -32
- package/src/routes/likes.tsx +0 -18
- package/src/routes/links.tsx +0 -994
- package/src/routes/mentions.tsx +0 -32
- package/src/routes/network-map.tsx +0 -1035
- package/src/routes/profile-analyze.tsx +0 -112
- package/src/routes/profiles.$handle.tsx +0 -228
- package/src/routes/rate-limits.tsx +0 -309
- package/src/routes/today.tsx +0 -511
- package/src/styles.css +0 -257
- package/tsconfig.json +0 -29
- package/vite.config.ts +0 -29
- /package/{public → dist/client}/birdclaw-mark.png +0 -0
- /package/{public → dist/client}/favicon.ico +0 -0
- /package/{public → dist/client}/logo192.png +0 -0
- /package/{public → dist/client}/logo512.png +0 -0
- /package/{public → dist/client}/manifest.json +0 -0
- /package/{public → dist/client}/robots.txt +0 -0
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import type { Database } from "./sqlite";
|
|
2
|
-
import { Effect } from "effect";
|
|
3
|
-
import { lookupProfileViaBird } from "./bird-actions";
|
|
4
|
-
import { getNativeDb } from "./db";
|
|
5
|
-
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
6
|
-
import type { ProfileRecord, XurlMentionUser } from "./types";
|
|
7
|
-
import { getExternalUserId, upsertProfileFromXUser } from "./x-profile";
|
|
8
|
-
import {
|
|
9
|
-
lookupAuthenticatedUser,
|
|
10
|
-
lookupUsersByHandles,
|
|
11
|
-
lookupUsersByIds,
|
|
12
|
-
} from "./xurl";
|
|
13
|
-
|
|
14
|
-
export interface ResolvedModerationProfile {
|
|
15
|
-
profile: ProfileRecord;
|
|
16
|
-
externalUserId: string | null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function toError(error: unknown) {
|
|
20
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function trySync<T>(try_: () => T) {
|
|
24
|
-
return Effect.try({
|
|
25
|
-
try: try_,
|
|
26
|
-
catch: toError,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function toProfile(row: Record<string, unknown>): ProfileRecord {
|
|
31
|
-
const followingCount = Number(row.following_count ?? 0);
|
|
32
|
-
return {
|
|
33
|
-
id: String(row.id),
|
|
34
|
-
handle: String(row.handle),
|
|
35
|
-
displayName: String(row.display_name),
|
|
36
|
-
bio: String(row.bio),
|
|
37
|
-
followersCount: Number(row.followers_count),
|
|
38
|
-
...(Number.isFinite(followingCount) ? { followingCount } : {}),
|
|
39
|
-
avatarHue: Number(row.avatar_hue),
|
|
40
|
-
avatarUrl:
|
|
41
|
-
typeof row.avatar_url === "string" ? String(row.avatar_url) : undefined,
|
|
42
|
-
createdAt: String(row.created_at),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function normalizeProfileQuery(value: string) {
|
|
47
|
-
const trimmed = value.trim();
|
|
48
|
-
if (!trimmed) return "";
|
|
49
|
-
|
|
50
|
-
const withoutPrefix = trimmed.replace(/^@/, "");
|
|
51
|
-
const urlMatch = withoutPrefix.match(
|
|
52
|
-
/^(?:https?:\/\/)?(?:www\.)?(?:x|twitter)\.com\/([^/?#]+)/i,
|
|
53
|
-
);
|
|
54
|
-
return (urlMatch?.[1] ?? withoutPrefix).replace(/^@/, "").trim();
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function getDefaultAccountId(db: Database) {
|
|
58
|
-
const row = db
|
|
59
|
-
.prepare(
|
|
60
|
-
`
|
|
61
|
-
select id
|
|
62
|
-
from accounts
|
|
63
|
-
order by is_default desc, created_at asc
|
|
64
|
-
limit 1
|
|
65
|
-
`,
|
|
66
|
-
)
|
|
67
|
-
.get() as { id: string } | undefined;
|
|
68
|
-
return row?.id ?? "acct_primary";
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function getAccountHandle(db: Database, accountId: string) {
|
|
72
|
-
const row = db
|
|
73
|
-
.prepare("select handle from accounts where id = ?")
|
|
74
|
-
.get(accountId) as { handle: string } | undefined;
|
|
75
|
-
return row?.handle.replace(/^@/, "") ?? "";
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function resolveLocalProfile(
|
|
79
|
-
db: Database,
|
|
80
|
-
normalizedQuery: string,
|
|
81
|
-
): ResolvedModerationProfile | null {
|
|
82
|
-
const row = db
|
|
83
|
-
.prepare(
|
|
84
|
-
`
|
|
85
|
-
select id, handle, display_name, bio, followers_count, following_count, avatar_hue, avatar_url, created_at
|
|
86
|
-
from profiles
|
|
87
|
-
where id = ? or handle = ?
|
|
88
|
-
limit 1
|
|
89
|
-
`,
|
|
90
|
-
)
|
|
91
|
-
.get(normalizedQuery, normalizedQuery) as
|
|
92
|
-
| Record<string, unknown>
|
|
93
|
-
| undefined;
|
|
94
|
-
|
|
95
|
-
if (!row) {
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const profile = toProfile(row);
|
|
100
|
-
return {
|
|
101
|
-
profile,
|
|
102
|
-
externalUserId: getExternalUserId(profile.id),
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function resolveProfileEffect(
|
|
107
|
-
query: string,
|
|
108
|
-
): Effect.Effect<ResolvedModerationProfile, unknown> {
|
|
109
|
-
return Effect.gen(function* () {
|
|
110
|
-
const db = yield* trySync(() => getNativeDb());
|
|
111
|
-
const normalizedQuery = normalizeProfileQuery(query);
|
|
112
|
-
if (!normalizedQuery) {
|
|
113
|
-
return yield* Effect.fail(new Error("Missing profile handle or id"));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const local = yield* trySync(() =>
|
|
117
|
-
resolveLocalProfile(db, normalizedQuery),
|
|
118
|
-
);
|
|
119
|
-
if (process.env.BIRDCLAW_DISABLE_LIVE_PROFILE_LOOKUP === "1") {
|
|
120
|
-
if (local) {
|
|
121
|
-
return local;
|
|
122
|
-
}
|
|
123
|
-
return yield* Effect.fail(
|
|
124
|
-
new Error(`Profile not found locally: ${query}`),
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
if (
|
|
128
|
-
local &&
|
|
129
|
-
!local.profile.id.startsWith("profile_group_") &&
|
|
130
|
-
local.externalUserId
|
|
131
|
-
) {
|
|
132
|
-
return local;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
let user: XurlMentionUser | undefined;
|
|
136
|
-
let lastError: unknown;
|
|
137
|
-
|
|
138
|
-
const birdResult = yield* tryPromise(() =>
|
|
139
|
-
lookupProfileViaBird(local?.profile.handle ?? normalizedQuery),
|
|
140
|
-
).pipe(
|
|
141
|
-
Effect.map((value) => ({ ok: true as const, value })),
|
|
142
|
-
Effect.catchAll((error) => Effect.succeed({ ok: false as const, error })),
|
|
143
|
-
);
|
|
144
|
-
if (birdResult.ok) {
|
|
145
|
-
user = birdResult.value ?? undefined;
|
|
146
|
-
} else {
|
|
147
|
-
lastError = birdResult.error;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (!user) {
|
|
151
|
-
const xurlResult = yield* tryPromise(() =>
|
|
152
|
-
/^\d+$/.test(normalizedQuery)
|
|
153
|
-
? lookupUsersByIds([normalizedQuery])
|
|
154
|
-
: lookupUsersByHandles([local?.profile.handle ?? normalizedQuery]),
|
|
155
|
-
).pipe(
|
|
156
|
-
Effect.map((value) => ({ ok: true as const, value })),
|
|
157
|
-
Effect.catchAll((error) =>
|
|
158
|
-
Effect.succeed({ ok: false as const, error }),
|
|
159
|
-
),
|
|
160
|
-
);
|
|
161
|
-
if (xurlResult.ok) {
|
|
162
|
-
[user] = xurlResult.value;
|
|
163
|
-
} else {
|
|
164
|
-
lastError = xurlResult.error;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (!user && lastError) {
|
|
169
|
-
if (local) {
|
|
170
|
-
return local;
|
|
171
|
-
}
|
|
172
|
-
return yield* Effect.fail(lastError);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (!user) {
|
|
176
|
-
if (local) {
|
|
177
|
-
return local;
|
|
178
|
-
}
|
|
179
|
-
return yield* Effect.fail(new Error(`Profile not found: ${query}`));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (local) {
|
|
183
|
-
return yield* trySync(() => upsertProfileFromXUser(db, user));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const username = String(user.username ?? "").replace(/^@/, "");
|
|
187
|
-
if (username) {
|
|
188
|
-
const localByHandle = yield* trySync(() =>
|
|
189
|
-
resolveLocalProfile(db, username),
|
|
190
|
-
);
|
|
191
|
-
if (localByHandle) {
|
|
192
|
-
return yield* trySync(() => upsertProfileFromXUser(db, user));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return yield* trySync(() => upsertProfileFromXUser(db, user));
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function resolveProfile(
|
|
201
|
-
query: string,
|
|
202
|
-
): Promise<ResolvedModerationProfile> {
|
|
203
|
-
return runEffectPromise(resolveProfileEffect(query));
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function getAuthenticatedUserIdEffect() {
|
|
207
|
-
return Effect.gen(function* () {
|
|
208
|
-
const me = yield* tryPromise(() => lookupAuthenticatedUser());
|
|
209
|
-
const id = me?.id;
|
|
210
|
-
return typeof id === "string" && id.length > 0 ? id : null;
|
|
211
|
-
}).pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function getAuthenticatedUserId() {
|
|
215
|
-
return runEffectPromise(getAuthenticatedUserIdEffect());
|
|
216
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import type { Database } from "./sqlite";
|
|
2
|
-
import { Effect } from "effect";
|
|
3
|
-
import type { ActionsTransport } from "./config";
|
|
4
|
-
import { getNativeDb } from "./db";
|
|
5
|
-
import { runEffectPromise } from "./effect-runtime";
|
|
6
|
-
import {
|
|
7
|
-
getAccountHandle,
|
|
8
|
-
getDefaultAccountId,
|
|
9
|
-
normalizeProfileQuery,
|
|
10
|
-
resolveProfileEffect,
|
|
11
|
-
} from "./moderation-target";
|
|
12
|
-
import { getExternalUserId } from "./x-profile";
|
|
13
|
-
|
|
14
|
-
export interface ModerationActionOptions {
|
|
15
|
-
transport?: ActionsTransport;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface ResolveModerationTargetParams {
|
|
19
|
-
accountId: string;
|
|
20
|
-
query: string;
|
|
21
|
-
selfActionError: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function toError(error: unknown) {
|
|
25
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function trySync<T>(try_: () => T) {
|
|
29
|
-
return Effect.try({
|
|
30
|
-
try: try_,
|
|
31
|
-
catch: toError,
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function resolveModerationTargetEffect({
|
|
36
|
-
accountId,
|
|
37
|
-
query,
|
|
38
|
-
selfActionError,
|
|
39
|
-
}: ResolveModerationTargetParams) {
|
|
40
|
-
return Effect.gen(function* () {
|
|
41
|
-
const db = yield* trySync(() => getNativeDb());
|
|
42
|
-
const resolvedAccountId = accountId || getDefaultAccountId(db);
|
|
43
|
-
const accountHandle = yield* trySync(() =>
|
|
44
|
-
getAccountHandle(db, resolvedAccountId),
|
|
45
|
-
);
|
|
46
|
-
if (!accountHandle) {
|
|
47
|
-
return yield* Effect.fail(
|
|
48
|
-
new Error(`Unknown account: ${resolvedAccountId}`),
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
const normalizedQuery = normalizeProfileQuery(query);
|
|
52
|
-
if (normalizedQuery.toLowerCase() === accountHandle.toLowerCase()) {
|
|
53
|
-
return yield* Effect.fail(new Error(selfActionError));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const resolved = yield* resolveProfileEffect(query);
|
|
57
|
-
const account = yield* trySync(
|
|
58
|
-
() =>
|
|
59
|
-
db
|
|
60
|
-
.prepare("select handle, external_user_id from accounts where id = ?")
|
|
61
|
-
.get(resolvedAccountId) as
|
|
62
|
-
| { handle: string; external_user_id: string | null }
|
|
63
|
-
| undefined,
|
|
64
|
-
);
|
|
65
|
-
const accountProfile = yield* trySync(() =>
|
|
66
|
-
account
|
|
67
|
-
? (db
|
|
68
|
-
.prepare("select id from profiles where handle = ? limit 1")
|
|
69
|
-
.get(account.handle.replace(/^@/, "")) as
|
|
70
|
-
| { id: string }
|
|
71
|
-
| undefined)
|
|
72
|
-
: undefined,
|
|
73
|
-
);
|
|
74
|
-
const accountExternalUserId =
|
|
75
|
-
account?.external_user_id ?? getExternalUserId(accountProfile?.id ?? "");
|
|
76
|
-
if (
|
|
77
|
-
resolved.profile.handle.toLowerCase() === accountHandle.toLowerCase() ||
|
|
78
|
-
(accountProfile?.id && resolved.profile.id === accountProfile.id) ||
|
|
79
|
-
(accountExternalUserId &&
|
|
80
|
-
resolved.externalUserId === accountExternalUserId)
|
|
81
|
-
) {
|
|
82
|
-
return yield* Effect.fail(new Error(selfActionError));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
db,
|
|
87
|
-
resolved,
|
|
88
|
-
resolvedAccountId,
|
|
89
|
-
accountIdentity: {
|
|
90
|
-
id: resolvedAccountId,
|
|
91
|
-
handle: account?.handle ?? accountHandle,
|
|
92
|
-
externalUserId: accountExternalUserId || null,
|
|
93
|
-
},
|
|
94
|
-
actionQuery:
|
|
95
|
-
resolved.externalUserId ?? resolved.profile.handle ?? normalizedQuery,
|
|
96
|
-
};
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export function resolveModerationTarget(params: ResolveModerationTargetParams) {
|
|
101
|
-
return runEffectPromise(resolveModerationTargetEffect(params));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function writeModerationRow(
|
|
105
|
-
db: Database,
|
|
106
|
-
table: "blocks" | "mutes",
|
|
107
|
-
accountId: string,
|
|
108
|
-
profileId: string,
|
|
109
|
-
createdAt: string,
|
|
110
|
-
) {
|
|
111
|
-
db.prepare(
|
|
112
|
-
`
|
|
113
|
-
insert into ${table} (account_id, profile_id, source, created_at)
|
|
114
|
-
values (?, ?, 'manual', ?)
|
|
115
|
-
on conflict(account_id, profile_id) do update set
|
|
116
|
-
source = excluded.source,
|
|
117
|
-
created_at = excluded.created_at
|
|
118
|
-
`,
|
|
119
|
-
).run(accountId, profileId, createdAt);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function deleteModerationRow(
|
|
123
|
-
db: Database,
|
|
124
|
-
table: "blocks" | "mutes",
|
|
125
|
-
accountId: string,
|
|
126
|
-
profileId: string,
|
|
127
|
-
) {
|
|
128
|
-
db.prepare(
|
|
129
|
-
`delete from ${table} where account_id = ? and profile_id = ?`,
|
|
130
|
-
).run(accountId, profileId);
|
|
131
|
-
}
|
package/src/lib/mutes-write.ts
DELETED
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { runModerationAction } from "./actions-transport";
|
|
3
|
-
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
4
|
-
import {
|
|
5
|
-
deleteModerationRow,
|
|
6
|
-
type ModerationActionOptions,
|
|
7
|
-
resolveModerationTargetEffect,
|
|
8
|
-
writeModerationRow,
|
|
9
|
-
} from "./moderation-write";
|
|
10
|
-
|
|
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 addMuteEffect(
|
|
20
|
-
accountId: string,
|
|
21
|
-
query: string,
|
|
22
|
-
options: ModerationActionOptions = {},
|
|
23
|
-
) {
|
|
24
|
-
return Effect.gen(function* () {
|
|
25
|
-
const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } =
|
|
26
|
-
yield* resolveModerationTargetEffect({
|
|
27
|
-
accountId,
|
|
28
|
-
query,
|
|
29
|
-
selfActionError: "Cannot mute the current account",
|
|
30
|
-
});
|
|
31
|
-
const transport = yield* tryPromise(() =>
|
|
32
|
-
runModerationAction({
|
|
33
|
-
action: "mute",
|
|
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: "mute",
|
|
45
|
-
accountId: resolvedAccountId,
|
|
46
|
-
profile: resolved.profile,
|
|
47
|
-
transport,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const mutedAt = new Date().toISOString();
|
|
52
|
-
yield* trySync(() =>
|
|
53
|
-
writeModerationRow(
|
|
54
|
-
db,
|
|
55
|
-
"mutes",
|
|
56
|
-
resolvedAccountId,
|
|
57
|
-
resolved.profile.id,
|
|
58
|
-
mutedAt,
|
|
59
|
-
),
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
ok: true,
|
|
64
|
-
action: "mute",
|
|
65
|
-
accountId: resolvedAccountId,
|
|
66
|
-
mutedAt,
|
|
67
|
-
profile: resolved.profile,
|
|
68
|
-
transport,
|
|
69
|
-
};
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function addMute(
|
|
74
|
-
accountId: string,
|
|
75
|
-
query: string,
|
|
76
|
-
options: ModerationActionOptions = {},
|
|
77
|
-
) {
|
|
78
|
-
return runEffectPromise(addMuteEffect(accountId, query, options));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function recordMuteEffect(accountId: string, query: string) {
|
|
82
|
-
return Effect.gen(function* () {
|
|
83
|
-
const { db, resolved, resolvedAccountId } =
|
|
84
|
-
yield* resolveModerationTargetEffect({
|
|
85
|
-
accountId,
|
|
86
|
-
query,
|
|
87
|
-
selfActionError: "Cannot mute the current account",
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
const mutedAt = new Date().toISOString();
|
|
91
|
-
yield* trySync(() =>
|
|
92
|
-
writeModerationRow(
|
|
93
|
-
db,
|
|
94
|
-
"mutes",
|
|
95
|
-
resolvedAccountId,
|
|
96
|
-
resolved.profile.id,
|
|
97
|
-
mutedAt,
|
|
98
|
-
),
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
ok: true,
|
|
103
|
-
action: "record-mute",
|
|
104
|
-
accountId: resolvedAccountId,
|
|
105
|
-
mutedAt,
|
|
106
|
-
profile: resolved.profile,
|
|
107
|
-
};
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function recordMute(accountId: string, query: string) {
|
|
112
|
-
return runEffectPromise(recordMuteEffect(accountId, query));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function removeMuteEffect(
|
|
116
|
-
accountId: string,
|
|
117
|
-
query: string,
|
|
118
|
-
options: ModerationActionOptions = {},
|
|
119
|
-
) {
|
|
120
|
-
return Effect.gen(function* () {
|
|
121
|
-
const { db, resolved, resolvedAccountId, accountIdentity, actionQuery } =
|
|
122
|
-
yield* resolveModerationTargetEffect({
|
|
123
|
-
accountId,
|
|
124
|
-
query,
|
|
125
|
-
selfActionError: "Cannot mute the current account",
|
|
126
|
-
});
|
|
127
|
-
const transport = yield* tryPromise(() =>
|
|
128
|
-
runModerationAction({
|
|
129
|
-
action: "unmute",
|
|
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: "unmute",
|
|
141
|
-
accountId: resolvedAccountId,
|
|
142
|
-
profile: resolved.profile,
|
|
143
|
-
transport,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
yield* trySync(() =>
|
|
148
|
-
deleteModerationRow(db, "mutes", resolvedAccountId, resolved.profile.id),
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
ok: true,
|
|
153
|
-
action: "unmute",
|
|
154
|
-
accountId: resolvedAccountId,
|
|
155
|
-
profile: resolved.profile,
|
|
156
|
-
transport,
|
|
157
|
-
};
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function removeMute(
|
|
162
|
-
accountId: string,
|
|
163
|
-
query: string,
|
|
164
|
-
options: ModerationActionOptions = {},
|
|
165
|
-
) {
|
|
166
|
-
return runEffectPromise(removeMuteEffect(accountId, query, options));
|
|
167
|
-
}
|
package/src/lib/mutes.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import { getNativeDb } from "./db";
|
|
2
|
-
import { toProfile } from "./moderation-target";
|
|
3
|
-
import type { BlockItem } from "./types";
|
|
4
|
-
|
|
5
|
-
export {
|
|
6
|
-
addMute,
|
|
7
|
-
addMuteEffect,
|
|
8
|
-
recordMute,
|
|
9
|
-
recordMuteEffect,
|
|
10
|
-
removeMute,
|
|
11
|
-
removeMuteEffect,
|
|
12
|
-
} from "./mutes-write";
|
|
13
|
-
|
|
14
|
-
export interface MuteItem {
|
|
15
|
-
accountId: string;
|
|
16
|
-
accountHandle: string;
|
|
17
|
-
source: string;
|
|
18
|
-
mutedAt: string;
|
|
19
|
-
profile: BlockItem["profile"];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function listMutes({
|
|
23
|
-
account,
|
|
24
|
-
search,
|
|
25
|
-
limit = 50,
|
|
26
|
-
}: {
|
|
27
|
-
account?: string;
|
|
28
|
-
search?: string;
|
|
29
|
-
limit?: number;
|
|
30
|
-
} = {}): MuteItem[] {
|
|
31
|
-
const db = getNativeDb();
|
|
32
|
-
const params: Array<string | number> = [];
|
|
33
|
-
let where = "where 1 = 1";
|
|
34
|
-
|
|
35
|
-
if (account && account !== "all") {
|
|
36
|
-
where += " and m.account_id = ?";
|
|
37
|
-
params.push(account);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (search?.trim()) {
|
|
41
|
-
where += " and (p.handle like ? or p.display_name like ? or p.bio like ?)";
|
|
42
|
-
params.push(
|
|
43
|
-
`%${search.trim()}%`,
|
|
44
|
-
`%${search.trim()}%`,
|
|
45
|
-
`%${search.trim()}%`,
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
params.push(limit);
|
|
50
|
-
|
|
51
|
-
const rows = db
|
|
52
|
-
.prepare(
|
|
53
|
-
`
|
|
54
|
-
select
|
|
55
|
-
m.account_id,
|
|
56
|
-
a.handle as account_handle,
|
|
57
|
-
m.source,
|
|
58
|
-
m.created_at as muted_at,
|
|
59
|
-
p.id,
|
|
60
|
-
p.handle,
|
|
61
|
-
p.display_name,
|
|
62
|
-
p.bio,
|
|
63
|
-
p.followers_count,
|
|
64
|
-
p.avatar_hue,
|
|
65
|
-
p.avatar_url,
|
|
66
|
-
p.created_at
|
|
67
|
-
from mutes m
|
|
68
|
-
join accounts a on a.id = m.account_id
|
|
69
|
-
join profiles p on p.id = m.profile_id
|
|
70
|
-
${where}
|
|
71
|
-
order by m.created_at desc
|
|
72
|
-
limit ?
|
|
73
|
-
`,
|
|
74
|
-
)
|
|
75
|
-
.all(...params) as Array<Record<string, unknown>>;
|
|
76
|
-
|
|
77
|
-
return rows.map((row) => ({
|
|
78
|
-
accountId: String(row.account_id),
|
|
79
|
-
accountHandle: String(row.account_handle),
|
|
80
|
-
source: String(row.source),
|
|
81
|
-
mutedAt: String(row.muted_at),
|
|
82
|
-
profile: toProfile(row),
|
|
83
|
-
}));
|
|
84
|
-
}
|
package/src/lib/ndjson-stream.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { runEffectBackground } from "./effect-runtime";
|
|
3
|
-
|
|
4
|
-
const encoder = new TextEncoder();
|
|
5
|
-
const HEARTBEAT_INTERVAL_MS = 15_000;
|
|
6
|
-
const STREAM_START_DELAY_MS = 25;
|
|
7
|
-
const FLUSH_PADDING = `${" ".repeat(16_384)}\n`;
|
|
8
|
-
|
|
9
|
-
export function createEffectNdjsonResponse<Event>({
|
|
10
|
-
request,
|
|
11
|
-
initialEvents = [],
|
|
12
|
-
run,
|
|
13
|
-
errorEvent,
|
|
14
|
-
}: {
|
|
15
|
-
request: Request;
|
|
16
|
-
initialEvents?: Event[];
|
|
17
|
-
run: (context: {
|
|
18
|
-
signal: AbortSignal;
|
|
19
|
-
emit: (event: Event) => void;
|
|
20
|
-
}) => Effect.Effect<unknown, unknown>;
|
|
21
|
-
errorEvent: (error: unknown) => Event;
|
|
22
|
-
}) {
|
|
23
|
-
let abortStream: (() => void) | undefined;
|
|
24
|
-
|
|
25
|
-
return new Response(
|
|
26
|
-
new ReadableStream<Uint8Array>({
|
|
27
|
-
cancel() {
|
|
28
|
-
abortStream?.();
|
|
29
|
-
},
|
|
30
|
-
start(controller) {
|
|
31
|
-
const abortController = new AbortController();
|
|
32
|
-
let closed = false;
|
|
33
|
-
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
34
|
-
let startTimer: ReturnType<typeof setTimeout> | undefined;
|
|
35
|
-
|
|
36
|
-
const cleanup = () => {
|
|
37
|
-
request.signal.removeEventListener("abort", abort);
|
|
38
|
-
if (heartbeat !== undefined) clearInterval(heartbeat);
|
|
39
|
-
if (startTimer !== undefined) clearTimeout(startTimer);
|
|
40
|
-
};
|
|
41
|
-
const abort = () => {
|
|
42
|
-
if (closed) return;
|
|
43
|
-
closed = true;
|
|
44
|
-
cleanup();
|
|
45
|
-
abortController.abort();
|
|
46
|
-
};
|
|
47
|
-
const close = () => {
|
|
48
|
-
if (closed) return;
|
|
49
|
-
closed = true;
|
|
50
|
-
cleanup();
|
|
51
|
-
abortController.abort();
|
|
52
|
-
controller.close();
|
|
53
|
-
};
|
|
54
|
-
const enqueue = (value: string) => {
|
|
55
|
-
if (closed) return;
|
|
56
|
-
try {
|
|
57
|
-
controller.enqueue(encoder.encode(value));
|
|
58
|
-
} catch {
|
|
59
|
-
abort();
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
const emit = (event: Event) => {
|
|
63
|
-
enqueue(`${JSON.stringify(event)}\n`);
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
request.signal.addEventListener("abort", abort, { once: true });
|
|
67
|
-
abortStream = abort;
|
|
68
|
-
if (request.signal.aborted) {
|
|
69
|
-
abort();
|
|
70
|
-
controller.close();
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
for (const event of initialEvents) emit(event);
|
|
74
|
-
enqueue(FLUSH_PADDING);
|
|
75
|
-
heartbeat = setInterval(
|
|
76
|
-
() => enqueue(FLUSH_PADDING),
|
|
77
|
-
HEARTBEAT_INTERVAL_MS,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
startTimer = setTimeout(() => {
|
|
81
|
-
if (closed) return;
|
|
82
|
-
try {
|
|
83
|
-
runEffectBackground(run({ signal: abortController.signal, emit }), {
|
|
84
|
-
onSuccess: close,
|
|
85
|
-
onFailure: (error) => {
|
|
86
|
-
emit(errorEvent(error));
|
|
87
|
-
close();
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
} catch (error) {
|
|
91
|
-
emit(errorEvent(error));
|
|
92
|
-
close();
|
|
93
|
-
}
|
|
94
|
-
}, STREAM_START_DELAY_MS);
|
|
95
|
-
},
|
|
96
|
-
}),
|
|
97
|
-
{
|
|
98
|
-
headers: {
|
|
99
|
-
"cache-control": "no-store, no-transform",
|
|
100
|
-
"content-encoding": "identity",
|
|
101
|
-
"content-type": "application/x-ndjson; charset=utf-8",
|
|
102
|
-
"x-accel-buffering": "no",
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
);
|
|
106
|
-
}
|