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
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
+
import { Effect } from "effect";
|
|
3
4
|
import type { EmbeddedTweet } from "#/lib/types";
|
|
5
|
+
import { runEffectPromise } from "./effect-runtime";
|
|
4
6
|
|
|
5
7
|
type ConversationStatus = "idle" | "loading" | "ready" | "error";
|
|
6
8
|
|
|
@@ -60,53 +62,81 @@ function getSnapshot() {
|
|
|
60
62
|
return snapshot;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
status: "loading",
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
const response = await fetch(
|
|
79
|
-
`/api/conversation?tweetId=${encodeURIComponent(tweetId)}`,
|
|
80
|
-
);
|
|
81
|
-
const data = (await response.json()) as {
|
|
65
|
+
function fetchConversationItemsEffect(tweetId: string) {
|
|
66
|
+
return Effect.gen(function* () {
|
|
67
|
+
const response = yield* Effect.tryPromise({
|
|
68
|
+
try: () =>
|
|
69
|
+
fetch(`/api/conversation?tweetId=${encodeURIComponent(tweetId)}`),
|
|
70
|
+
catch: (error) => error,
|
|
71
|
+
});
|
|
72
|
+
const data = (yield* Effect.tryPromise({
|
|
73
|
+
try: () => response.json(),
|
|
74
|
+
catch: (error) => error,
|
|
75
|
+
})) as {
|
|
82
76
|
error?: string;
|
|
83
77
|
items?: EmbeddedTweet[];
|
|
84
78
|
ok?: boolean;
|
|
85
79
|
};
|
|
86
80
|
if (!response.ok || data.ok === false) {
|
|
87
|
-
|
|
81
|
+
return yield* Effect.fail(
|
|
82
|
+
new Error(data.error ?? "Conversation unavailable"),
|
|
83
|
+
);
|
|
88
84
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
return (data.items ?? []).filter(Boolean);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function loadConversationEffect(
|
|
90
|
+
surfaceId: string,
|
|
91
|
+
tweetId = surfaceId,
|
|
92
|
+
): Effect.Effect<void, never> {
|
|
93
|
+
return Effect.gen(function* () {
|
|
94
|
+
const current = snapshot.records.get(surfaceId);
|
|
95
|
+
if (current?.status === "ready" || inFlight.has(surfaceId)) {
|
|
99
96
|
return;
|
|
100
97
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
98
|
+
|
|
99
|
+
const loadGeneration = generation;
|
|
100
|
+
inFlight.add(surfaceId);
|
|
101
|
+
yield* Effect.gen(function* () {
|
|
102
|
+
updateRecord(surfaceId, {
|
|
103
|
+
error: null,
|
|
104
|
+
items: current?.items ?? [],
|
|
105
|
+
status: "loading",
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const result = yield* fetchConversationItemsEffect(tweetId).pipe(
|
|
109
|
+
Effect.match({
|
|
110
|
+
onFailure: (error) => ({ error, ok: false as const }),
|
|
111
|
+
onSuccess: (items) => ({ items, ok: true as const }),
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (loadGeneration !== generation) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (result.ok) {
|
|
119
|
+
updateRecord(surfaceId, {
|
|
120
|
+
error: null,
|
|
121
|
+
items: result.items,
|
|
122
|
+
status: "ready",
|
|
123
|
+
});
|
|
124
|
+
} else {
|
|
125
|
+
updateRecord(surfaceId, {
|
|
126
|
+
error:
|
|
127
|
+
result.error instanceof Error
|
|
128
|
+
? result.error.message
|
|
129
|
+
: "Conversation unavailable",
|
|
130
|
+
items: [],
|
|
131
|
+
status: "error",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}).pipe(Effect.ensuring(Effect.sync(() => inFlight.delete(surfaceId))));
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function loadConversation(surfaceId: string, tweetId = surfaceId) {
|
|
139
|
+
return runEffectPromise(loadConversationEffect(surfaceId, tweetId));
|
|
110
140
|
}
|
|
111
141
|
|
|
112
142
|
export function retainConversationSurfaceScope() {
|
|
@@ -137,30 +167,31 @@ export function ConversationSurfaceScope({
|
|
|
137
167
|
return children;
|
|
138
168
|
}
|
|
139
169
|
|
|
140
|
-
export function useConversationSurface(
|
|
170
|
+
export function useConversationSurface(surfaceId: string, tweetId = surfaceId) {
|
|
141
171
|
const currentSnapshot = useSyncExternalStore(
|
|
142
172
|
subscribe,
|
|
143
173
|
getSnapshot,
|
|
144
174
|
getSnapshot,
|
|
145
175
|
);
|
|
146
|
-
const record = currentSnapshot.records.get(
|
|
147
|
-
const isOpen = currentSnapshot.expandedTweetId ===
|
|
176
|
+
const record = currentSnapshot.records.get(surfaceId) ?? emptyRecord;
|
|
177
|
+
const isOpen = currentSnapshot.expandedTweetId === surfaceId;
|
|
148
178
|
|
|
149
179
|
const toggle = useCallback(() => {
|
|
150
|
-
const nextExpanded =
|
|
180
|
+
const nextExpanded =
|
|
181
|
+
snapshot.expandedTweetId === surfaceId ? null : surfaceId;
|
|
151
182
|
setSnapshot({ ...snapshot, expandedTweetId: nextExpanded });
|
|
152
183
|
if (nextExpanded) {
|
|
153
|
-
void loadConversation(tweetId);
|
|
184
|
+
void loadConversation(surfaceId, tweetId);
|
|
154
185
|
}
|
|
155
|
-
}, [tweetId]);
|
|
186
|
+
}, [surfaceId, tweetId]);
|
|
156
187
|
|
|
157
188
|
const prefetch = useCallback(() => {
|
|
158
|
-
const current = snapshot.records.get(
|
|
189
|
+
const current = snapshot.records.get(surfaceId);
|
|
159
190
|
if (current?.status === "ready" || current?.status === "loading") {
|
|
160
191
|
return;
|
|
161
192
|
}
|
|
162
|
-
void loadConversation(tweetId);
|
|
163
|
-
}, [tweetId]);
|
|
193
|
+
void loadConversation(surfaceId, tweetId);
|
|
194
|
+
}, [surfaceId, tweetId]);
|
|
164
195
|
|
|
165
196
|
return {
|
|
166
197
|
error: record.error,
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { getAuthenticatedBirdAccountEffect } from "./bird";
|
|
3
|
+
import { getNativeDb } from "./db";
|
|
4
|
+
import { runEffectPromise } from "./effect-runtime";
|
|
5
|
+
import type {
|
|
6
|
+
LiveDataSourceAccount,
|
|
7
|
+
LiveDataSourceCapability,
|
|
8
|
+
LiveDataSourcesResponse,
|
|
9
|
+
LiveDataSourceStatus,
|
|
10
|
+
} from "./types";
|
|
11
|
+
import {
|
|
12
|
+
getTransportStatusEffect,
|
|
13
|
+
lookupAuthenticatedOAuth2UserEffect,
|
|
14
|
+
readXurlOAuth2AccountsEffect,
|
|
15
|
+
} from "./xurl";
|
|
16
|
+
|
|
17
|
+
function errorMessage(error: unknown) {
|
|
18
|
+
return error instanceof Error ? error.message : String(error);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readLocalAccounts(): LiveDataSourceAccount[] {
|
|
22
|
+
const db = getNativeDb();
|
|
23
|
+
const rows = db
|
|
24
|
+
.prepare(
|
|
25
|
+
`
|
|
26
|
+
select id, handle, external_user_id, is_default
|
|
27
|
+
from accounts
|
|
28
|
+
order by is_default desc, lower(handle) asc
|
|
29
|
+
`,
|
|
30
|
+
)
|
|
31
|
+
.all() as Array<{
|
|
32
|
+
id: string;
|
|
33
|
+
handle: string;
|
|
34
|
+
external_user_id: string | null;
|
|
35
|
+
is_default: number;
|
|
36
|
+
}>;
|
|
37
|
+
return rows.map((row) => ({
|
|
38
|
+
id: row.external_user_id ?? row.id,
|
|
39
|
+
handle: row.handle,
|
|
40
|
+
username: row.handle.replace(/^@/, ""),
|
|
41
|
+
isDefault: row.is_default === 1,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getBirdclawStatusEffect(): Effect.Effect<LiveDataSourceStatus, never> {
|
|
46
|
+
return Effect.try({
|
|
47
|
+
try: () => readLocalAccounts(),
|
|
48
|
+
catch: (error) => error,
|
|
49
|
+
}).pipe(
|
|
50
|
+
Effect.map((accounts) => ({
|
|
51
|
+
source: "birdclaw" as const,
|
|
52
|
+
label: "Birdclaw local",
|
|
53
|
+
works: true,
|
|
54
|
+
installed: true,
|
|
55
|
+
status: "ok" as const,
|
|
56
|
+
detail:
|
|
57
|
+
accounts.length > 0
|
|
58
|
+
? `${accounts.length.toString()} local account${accounts.length === 1 ? "" : "s"}`
|
|
59
|
+
: "local database ready; no accounts imported yet",
|
|
60
|
+
accounts,
|
|
61
|
+
})),
|
|
62
|
+
Effect.catchAll((error) =>
|
|
63
|
+
Effect.succeed({
|
|
64
|
+
source: "birdclaw" as const,
|
|
65
|
+
label: "Birdclaw local",
|
|
66
|
+
works: false,
|
|
67
|
+
installed: true,
|
|
68
|
+
status: "error" as const,
|
|
69
|
+
detail: errorMessage(error),
|
|
70
|
+
accounts: [],
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getBirdStatusEffect(): Effect.Effect<LiveDataSourceStatus, never> {
|
|
77
|
+
return getAuthenticatedBirdAccountEffect().pipe(
|
|
78
|
+
Effect.map((account) => ({
|
|
79
|
+
source: "bird" as const,
|
|
80
|
+
label: "bird",
|
|
81
|
+
works: true,
|
|
82
|
+
installed: true,
|
|
83
|
+
status: "ok" as const,
|
|
84
|
+
detail: `authenticated as @${account.username}`,
|
|
85
|
+
accounts: [
|
|
86
|
+
{
|
|
87
|
+
...(account.id ? { id: account.id } : {}),
|
|
88
|
+
username: account.username,
|
|
89
|
+
handle: `@${account.username}`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
})),
|
|
93
|
+
Effect.catchAll((error) =>
|
|
94
|
+
Effect.succeed({
|
|
95
|
+
source: "bird" as const,
|
|
96
|
+
label: "bird",
|
|
97
|
+
works: false,
|
|
98
|
+
status: "error" as const,
|
|
99
|
+
detail: errorMessage(error),
|
|
100
|
+
accounts: [],
|
|
101
|
+
}),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getXurlStatusEffect(): Effect.Effect<LiveDataSourceStatus, never> {
|
|
107
|
+
return Effect.gen(function* () {
|
|
108
|
+
const transport = yield* getTransportStatusEffect();
|
|
109
|
+
const oauth2Accounts = yield* readXurlOAuth2AccountsEffect();
|
|
110
|
+
const authenticated = yield* lookupAuthenticatedOAuth2UserEffect().pipe(
|
|
111
|
+
Effect.catchAll(() => Effect.succeed(null)),
|
|
112
|
+
);
|
|
113
|
+
const authenticatedAccount =
|
|
114
|
+
authenticated && typeof authenticated === "object"
|
|
115
|
+
? ({
|
|
116
|
+
...(typeof authenticated.id === "string"
|
|
117
|
+
? { id: authenticated.id }
|
|
118
|
+
: {}),
|
|
119
|
+
...(typeof authenticated.username === "string"
|
|
120
|
+
? {
|
|
121
|
+
username: authenticated.username,
|
|
122
|
+
handle: `@${authenticated.username}`,
|
|
123
|
+
}
|
|
124
|
+
: {}),
|
|
125
|
+
} satisfies LiveDataSourceAccount)
|
|
126
|
+
: undefined;
|
|
127
|
+
const accounts: LiveDataSourceAccount[] = [
|
|
128
|
+
...(authenticatedAccount ? [authenticatedAccount] : []),
|
|
129
|
+
...oauth2Accounts,
|
|
130
|
+
];
|
|
131
|
+
const deduped = accounts.filter(
|
|
132
|
+
(account, index) =>
|
|
133
|
+
accounts.findIndex(
|
|
134
|
+
(candidate) =>
|
|
135
|
+
(candidate.app ?? "") === (account.app ?? "") &&
|
|
136
|
+
(candidate.username ?? candidate.handle ?? "") ===
|
|
137
|
+
(account.username ?? account.handle ?? ""),
|
|
138
|
+
) === index,
|
|
139
|
+
);
|
|
140
|
+
const works = transport.availableTransport === "xurl";
|
|
141
|
+
return {
|
|
142
|
+
source: "xurl" as const,
|
|
143
|
+
label: "xurl",
|
|
144
|
+
works,
|
|
145
|
+
installed: transport.installed,
|
|
146
|
+
status: works ? ("ok" as const) : ("warning" as const),
|
|
147
|
+
detail: transport.statusText,
|
|
148
|
+
accounts: deduped,
|
|
149
|
+
};
|
|
150
|
+
}).pipe(
|
|
151
|
+
Effect.catchAll((error) =>
|
|
152
|
+
Effect.succeed({
|
|
153
|
+
source: "xurl" as const,
|
|
154
|
+
label: "xurl",
|
|
155
|
+
works: false,
|
|
156
|
+
status: "error" as const,
|
|
157
|
+
detail: errorMessage(error),
|
|
158
|
+
accounts: [],
|
|
159
|
+
}),
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const capabilities: LiveDataSourceCapability[] = [
|
|
165
|
+
{
|
|
166
|
+
key: "timeline",
|
|
167
|
+
label: "Home timeline",
|
|
168
|
+
primary: "xurl",
|
|
169
|
+
fallbacks: ["bird"],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
key: "mentions",
|
|
173
|
+
label: "Mentions",
|
|
174
|
+
primary: "xurl",
|
|
175
|
+
fallbacks: ["bird", "birdclaw"],
|
|
176
|
+
notes: "bird fallback is skipped when a since/start cursor requires xurl.",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
key: "search",
|
|
180
|
+
label: "Fresh search",
|
|
181
|
+
primary: "bird",
|
|
182
|
+
fallbacks: ["xurl", "birdclaw"],
|
|
183
|
+
notes: "dated searches require xurl.",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
key: "dms",
|
|
187
|
+
label: "DMs",
|
|
188
|
+
primary: "xurl",
|
|
189
|
+
fallbacks: ["bird", "birdclaw"],
|
|
190
|
+
notes: "message requests require bird.",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
key: "follow-graph",
|
|
194
|
+
label: "Followers / following",
|
|
195
|
+
primary: "bird",
|
|
196
|
+
fallbacks: ["xurl", "birdclaw"],
|
|
197
|
+
},
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
export function getLiveDataSourcesEffect(): Effect.Effect<
|
|
201
|
+
LiveDataSourcesResponse,
|
|
202
|
+
never
|
|
203
|
+
> {
|
|
204
|
+
return Effect.gen(function* () {
|
|
205
|
+
const sources = yield* Effect.all(
|
|
206
|
+
[getBirdclawStatusEffect(), getBirdStatusEffect(), getXurlStatusEffect()],
|
|
207
|
+
{ concurrency: "unbounded" },
|
|
208
|
+
);
|
|
209
|
+
return {
|
|
210
|
+
generatedAt: new Date().toISOString(),
|
|
211
|
+
sources,
|
|
212
|
+
capabilities,
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function getLiveDataSources(): Promise<LiveDataSourcesResponse> {
|
|
218
|
+
return runEffectPromise(getLiveDataSourcesEffect());
|
|
219
|
+
}
|
package/src/lib/db.ts
CHANGED
|
@@ -117,7 +117,13 @@ export interface TweetCollectionsTable {
|
|
|
117
117
|
export interface TweetAccountEdgesTable {
|
|
118
118
|
account_id: string;
|
|
119
119
|
tweet_id: string;
|
|
120
|
-
kind:
|
|
120
|
+
kind:
|
|
121
|
+
| "home"
|
|
122
|
+
| "mention"
|
|
123
|
+
| "authored"
|
|
124
|
+
| "search"
|
|
125
|
+
| "profile"
|
|
126
|
+
| "thread_context";
|
|
121
127
|
first_seen_at: string;
|
|
122
128
|
last_seen_at: string;
|
|
123
129
|
seen_count: number;
|
|
@@ -131,6 +137,7 @@ export interface DmConversationsTable {
|
|
|
131
137
|
account_id: string;
|
|
132
138
|
participant_profile_id: string;
|
|
133
139
|
title: string;
|
|
140
|
+
inbox_kind: "accepted" | "request";
|
|
134
141
|
last_message_at: string;
|
|
135
142
|
unread_count: number;
|
|
136
143
|
needs_reply: number;
|
|
@@ -257,6 +264,31 @@ export interface FollowEventsTable {
|
|
|
257
264
|
snapshot_id: string;
|
|
258
265
|
}
|
|
259
266
|
|
|
267
|
+
export interface GeocodedLocationsTable {
|
|
268
|
+
normalized_key: string;
|
|
269
|
+
original: string;
|
|
270
|
+
lat: number;
|
|
271
|
+
lng: number;
|
|
272
|
+
formatted: string | null;
|
|
273
|
+
country_code: string | null;
|
|
274
|
+
confidence: number | null;
|
|
275
|
+
provider: string;
|
|
276
|
+
approx_radius_m: number | null;
|
|
277
|
+
bounds_json: string;
|
|
278
|
+
components_json: string;
|
|
279
|
+
hits: number;
|
|
280
|
+
created_at: string;
|
|
281
|
+
last_used_at: string;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface GeocodedLocationsUnresolvedTable {
|
|
285
|
+
normalized_key: string;
|
|
286
|
+
original: string;
|
|
287
|
+
reason: string;
|
|
288
|
+
last_attempted_at: string;
|
|
289
|
+
ttl_until: string | null;
|
|
290
|
+
}
|
|
291
|
+
|
|
260
292
|
export interface BirdclawDatabase {
|
|
261
293
|
accounts: AccountsTable;
|
|
262
294
|
profiles: ProfilesTable;
|
|
@@ -280,6 +312,8 @@ export interface BirdclawDatabase {
|
|
|
280
312
|
follow_snapshots: FollowSnapshotsTable;
|
|
281
313
|
follow_snapshot_members: FollowSnapshotMembersTable;
|
|
282
314
|
follow_events: FollowEventsTable;
|
|
315
|
+
geocoded_locations: GeocodedLocationsTable;
|
|
316
|
+
geocoded_locations_unresolved: GeocodedLocationsUnresolvedTable;
|
|
283
317
|
}
|
|
284
318
|
|
|
285
319
|
let nativeDb: Database | undefined;
|
|
@@ -429,6 +463,7 @@ const BASE_SCHEMA_SQL = `
|
|
|
429
463
|
account_id text not null,
|
|
430
464
|
participant_profile_id text not null,
|
|
431
465
|
title text not null,
|
|
466
|
+
inbox_kind text not null default 'accepted',
|
|
432
467
|
last_message_at text not null,
|
|
433
468
|
unread_count integer not null default 0,
|
|
434
469
|
needs_reply integer not null default 0
|
|
@@ -561,6 +596,31 @@ const BASE_SCHEMA_SQL = `
|
|
|
561
596
|
snapshot_id text not null
|
|
562
597
|
);
|
|
563
598
|
|
|
599
|
+
create table if not exists geocoded_locations (
|
|
600
|
+
normalized_key text primary key,
|
|
601
|
+
original text not null,
|
|
602
|
+
lat real not null,
|
|
603
|
+
lng real not null,
|
|
604
|
+
formatted text,
|
|
605
|
+
country_code text,
|
|
606
|
+
confidence integer,
|
|
607
|
+
provider text not null,
|
|
608
|
+
approx_radius_m real,
|
|
609
|
+
bounds_json text not null default '{}',
|
|
610
|
+
components_json text not null default '{}',
|
|
611
|
+
hits integer not null default 1,
|
|
612
|
+
created_at text not null,
|
|
613
|
+
last_used_at text not null
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
create table if not exists geocoded_locations_unresolved (
|
|
617
|
+
normalized_key text primary key,
|
|
618
|
+
original text not null,
|
|
619
|
+
reason text not null,
|
|
620
|
+
last_attempted_at text not null,
|
|
621
|
+
ttl_until text
|
|
622
|
+
);
|
|
623
|
+
|
|
564
624
|
create virtual table if not exists tweets_fts using fts5(
|
|
565
625
|
tweet_id unindexed,
|
|
566
626
|
text
|
|
@@ -609,6 +669,9 @@ const INDEX_SQL = `
|
|
|
609
669
|
create index if not exists idx_follow_edges_profile on follow_edges(profile_id, current);
|
|
610
670
|
create index if not exists idx_follow_snapshots_account on follow_snapshots(account_id, direction, completed_at desc);
|
|
611
671
|
create index if not exists idx_follow_events_account on follow_events(account_id, direction, kind, event_at desc);
|
|
672
|
+
create index if not exists idx_geocoded_locations_country on geocoded_locations(country_code);
|
|
673
|
+
create index if not exists idx_geocoded_locations_last_used on geocoded_locations(last_used_at desc);
|
|
674
|
+
create index if not exists idx_geocoded_unresolved_ttl on geocoded_locations_unresolved(ttl_until desc);
|
|
612
675
|
`;
|
|
613
676
|
|
|
614
677
|
function getColumnNames(db: Database, tableName: string): Set<string> {
|
|
@@ -678,6 +741,15 @@ function ensureAccountExternalUserIdColumn(db: Database) {
|
|
|
678
741
|
}
|
|
679
742
|
}
|
|
680
743
|
|
|
744
|
+
function ensureDmConversationInboxColumns(db: Database) {
|
|
745
|
+
const columnNames = getColumnNames(db, "dm_conversations");
|
|
746
|
+
if (!columnNames.has("inbox_kind")) {
|
|
747
|
+
db.exec(
|
|
748
|
+
"alter table dm_conversations add column inbox_kind text not null default 'accepted'",
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
681
753
|
function ensureTweetCollectionsTable(db: Database) {
|
|
682
754
|
db.exec(`
|
|
683
755
|
create table if not exists tweet_collections (
|
|
@@ -941,6 +1013,7 @@ function initDatabase(options: InitDatabaseOptions = {}) {
|
|
|
941
1013
|
nativeDb = new NativeSqliteDatabase(dbPath);
|
|
942
1014
|
nativeDb.exec(BASE_SCHEMA_SQL);
|
|
943
1015
|
ensureAccountExternalUserIdColumn(nativeDb);
|
|
1016
|
+
ensureDmConversationInboxColumns(nativeDb);
|
|
944
1017
|
ensureTweetMetadataColumns(nativeDb);
|
|
945
1018
|
ensureProfileAvatarColumns(nativeDb);
|
|
946
1019
|
ensureTweetCollectionsTable(nativeDb);
|
|
@@ -981,11 +1054,29 @@ export function getDb() {
|
|
|
981
1054
|
return kyselyDb as Kysely<BirdclawDatabase>;
|
|
982
1055
|
}
|
|
983
1056
|
|
|
984
|
-
export function
|
|
985
|
-
kyselyDb
|
|
1057
|
+
export async function closeDatabase() {
|
|
1058
|
+
const db = kyselyDb;
|
|
1059
|
+
const native = nativeDb;
|
|
986
1060
|
kyselyDb = undefined;
|
|
1061
|
+
nativeDb = undefined;
|
|
1062
|
+
demoSeedAttempted = false;
|
|
987
1063
|
|
|
988
|
-
|
|
1064
|
+
if (db) {
|
|
1065
|
+
await db.destroy();
|
|
1066
|
+
} else {
|
|
1067
|
+
native?.close();
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
export function resetDatabaseForTests() {
|
|
1072
|
+
const db = kyselyDb;
|
|
1073
|
+
const native = nativeDb;
|
|
1074
|
+
kyselyDb = undefined;
|
|
989
1075
|
nativeDb = undefined;
|
|
990
1076
|
demoSeedAttempted = false;
|
|
1077
|
+
if (db) {
|
|
1078
|
+
void db.destroy();
|
|
1079
|
+
} else {
|
|
1080
|
+
native?.close();
|
|
1081
|
+
}
|
|
991
1082
|
}
|