birdclaw 0.8.2 → 0.8.3
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 +16 -0
- package/package.json +2 -1
- package/src/cli/command-context.ts +17 -0
- package/src/cli/register-analysis.ts +500 -0
- package/src/cli/register-compose.ts +40 -0
- package/src/cli/register-graph.ts +132 -0
- package/src/cli/register-inbox.ts +41 -0
- package/src/cli/register-storage.ts +106 -0
- package/src/cli.ts +30 -750
- package/src/components/AccountSwitcher.tsx +7 -15
- package/src/components/AvatarChip.tsx +1 -1
- package/src/components/AvatarPreload.ts +149 -0
- package/src/components/MarkdownCitations.tsx +680 -0
- package/src/components/MarkdownViewer.tsx +8 -674
- package/src/components/ProfileAnalysisClient.ts +191 -0
- package/src/components/ProfileAnalysisStream.tsx +16 -185
- package/src/components/ProfilePreview.tsx +2 -0
- package/src/components/links-controller.ts +162 -0
- package/src/components/links-model.ts +198 -0
- package/src/components/network-map-controller.ts +84 -0
- package/src/components/network-map-model.ts +255 -0
- package/src/components/useTimelineRouteData.ts +105 -235
- package/src/lib/analysis-runtime.ts +238 -0
- package/src/lib/api-client.ts +16 -215
- package/src/lib/api-contracts.ts +328 -0
- package/src/lib/archive-import-plan.ts +102 -0
- package/src/lib/archive-import.ts +170 -239
- package/src/lib/authored-live.ts +75 -120
- package/src/lib/backup.ts +335 -424
- package/src/lib/blocks-write.ts +30 -26
- package/src/lib/blocks.ts +18 -20
- package/src/lib/database-metrics.ts +88 -0
- package/src/lib/database-migrations.ts +34 -0
- package/src/lib/database-schema.ts +312 -0
- package/src/lib/database-writer.ts +69 -0
- package/src/lib/db.ts +84 -330
- package/src/lib/dm-read-model.ts +533 -0
- package/src/lib/dms-live.ts +34 -97
- package/src/lib/follow-graph.ts +17 -27
- package/src/lib/import-repository.ts +138 -0
- package/src/lib/inbox.ts +2 -1
- package/src/lib/live-sync-engine.ts +209 -0
- package/src/lib/live-transport-gateway.ts +128 -0
- package/src/lib/mention-threads-live.ts +90 -177
- package/src/lib/mentions-export.ts +1 -1
- package/src/lib/mentions-live.ts +57 -181
- package/src/lib/moderation-target.ts +15 -4
- package/src/lib/moderation-write.ts +1 -1
- package/src/lib/mutes-write.ts +30 -26
- package/src/lib/openai-response-runtime.ts +251 -0
- package/src/lib/paginated-sync.ts +93 -0
- package/src/lib/period-digest.ts +116 -304
- package/src/lib/profile-analysis.ts +36 -110
- package/src/lib/queries.ts +6 -2381
- package/src/lib/query-actions.ts +437 -0
- package/src/lib/query-client.tsx +47 -0
- package/src/lib/query-read-model-shared.ts +52 -0
- package/src/lib/query-read-models.ts +5 -0
- package/src/lib/query-resource.ts +41 -0
- package/src/lib/query-status.ts +164 -0
- package/src/lib/research.ts +1 -1
- package/src/lib/runtime-services.ts +20 -0
- package/src/lib/search-discussion.ts +75 -279
- package/src/lib/server-runtime-services.ts +30 -0
- package/src/lib/sqlite.ts +48 -12
- package/src/lib/streaming-ingestion.ts +240 -0
- package/src/lib/sync-cache.ts +6 -1
- package/src/lib/sync-plan.ts +175 -0
- package/src/lib/timeline-collections-live.ts +83 -257
- package/src/lib/timeline-live.ts +86 -236
- package/src/lib/timeline-read-model.ts +1191 -0
- package/src/lib/tweet-repository.ts +156 -0
- package/src/lib/tweet-search-live.ts +63 -167
- package/src/lib/web-sync.ts +67 -50
- package/src/lib/whois.ts +2 -1
- package/src/routes/__root.tsx +11 -8
- package/src/routes/api/action.tsx +1 -1
- package/src/routes/api/conversation.tsx +1 -1
- package/src/routes/api/query.tsx +32 -26
- package/src/routes/api/status.tsx +6 -4
- package/src/routes/api/sync.tsx +5 -2
- package/src/routes/blocks.tsx +97 -131
- package/src/routes/data-sources.tsx +17 -25
- package/src/routes/dms.tsx +167 -184
- package/src/routes/inbox.tsx +63 -57
- package/src/routes/links.tsx +31 -394
- package/src/routes/network-map.tsx +41 -344
- package/src/routes/rate-limits.tsx +17 -21
- package/src/lib/client-cache.ts +0 -109
package/src/lib/api-client.ts
CHANGED
|
@@ -1,125 +1,22 @@
|
|
|
1
1
|
import { Data, Effect } from "effect";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "./
|
|
4
|
+
actionResponseSchema,
|
|
5
|
+
queryEnvelopeSchema,
|
|
6
|
+
queryResponseSchema,
|
|
7
|
+
webSyncJobSchema,
|
|
8
|
+
} from "./api-contracts";
|
|
9
9
|
import { runEffectPromise } from "./effect-runtime";
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
QueryResponse,
|
|
15
|
-
TimelineItem,
|
|
16
|
-
} from "./types";
|
|
10
|
+
import {
|
|
11
|
+
defaultRuntimeServices,
|
|
12
|
+
type RuntimeServices,
|
|
13
|
+
} from "./runtime-services";
|
|
17
14
|
import type {
|
|
18
15
|
WebSyncJobSnapshot,
|
|
19
16
|
WebSyncKind,
|
|
20
17
|
WebSyncOptions,
|
|
21
|
-
WebSyncResponse,
|
|
22
18
|
} from "./web-sync";
|
|
23
|
-
|
|
24
|
-
const jsonRecordSchema = z.object({}).passthrough();
|
|
25
|
-
const resourceKindSchema = z.enum(["home", "mentions", "authored", "dms"]);
|
|
26
|
-
const webSyncKindSchema = z.enum([
|
|
27
|
-
"timeline",
|
|
28
|
-
"mentions",
|
|
29
|
-
"likes",
|
|
30
|
-
"bookmarks",
|
|
31
|
-
"dms",
|
|
32
|
-
]);
|
|
33
|
-
|
|
34
|
-
const queryEnvelopeSchema = z
|
|
35
|
-
.object({
|
|
36
|
-
accounts: z.array(jsonRecordSchema),
|
|
37
|
-
archives: z.array(jsonRecordSchema),
|
|
38
|
-
transport: z
|
|
39
|
-
.object({
|
|
40
|
-
statusText: z.string(),
|
|
41
|
-
})
|
|
42
|
-
.passthrough(),
|
|
43
|
-
stats: z.object({
|
|
44
|
-
home: z.number(),
|
|
45
|
-
mentions: z.number(),
|
|
46
|
-
dms: z.number(),
|
|
47
|
-
needsReply: z.number(),
|
|
48
|
-
inbox: z.number(),
|
|
49
|
-
}),
|
|
50
|
-
})
|
|
51
|
-
.transform((value) => value as unknown as QueryEnvelope);
|
|
52
|
-
|
|
53
|
-
const queryResponseSchema = z
|
|
54
|
-
.object({
|
|
55
|
-
resource: resourceKindSchema,
|
|
56
|
-
items: z.array(jsonRecordSchema),
|
|
57
|
-
selectedConversation: z
|
|
58
|
-
.object({
|
|
59
|
-
conversation: jsonRecordSchema,
|
|
60
|
-
messages: z.array(jsonRecordSchema),
|
|
61
|
-
})
|
|
62
|
-
.nullish(),
|
|
63
|
-
})
|
|
64
|
-
.transform(
|
|
65
|
-
(value) =>
|
|
66
|
-
({
|
|
67
|
-
...value,
|
|
68
|
-
items: value.items as unknown as Array<
|
|
69
|
-
TimelineItem | DmConversationItem
|
|
70
|
-
>,
|
|
71
|
-
selectedConversation: value.selectedConversation
|
|
72
|
-
? {
|
|
73
|
-
conversation: value.selectedConversation
|
|
74
|
-
.conversation as unknown as DmConversationItem,
|
|
75
|
-
messages: value.selectedConversation
|
|
76
|
-
.messages as unknown as DmMessageItem[],
|
|
77
|
-
}
|
|
78
|
-
: value.selectedConversation,
|
|
79
|
-
}) as QueryResponse,
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
const webSyncResponseSchema = z
|
|
83
|
-
.object({
|
|
84
|
-
ok: z.boolean(),
|
|
85
|
-
kind: webSyncKindSchema,
|
|
86
|
-
accountId: z.string().optional(),
|
|
87
|
-
summary: z.string(),
|
|
88
|
-
steps: z.array(jsonRecordSchema),
|
|
89
|
-
startedAt: z.string().optional(),
|
|
90
|
-
finishedAt: z.string().optional(),
|
|
91
|
-
inProgress: z.boolean().optional(),
|
|
92
|
-
backup: z.unknown().optional(),
|
|
93
|
-
error: z.string().optional(),
|
|
94
|
-
})
|
|
95
|
-
.transform((value) => value as unknown as WebSyncResponse);
|
|
96
|
-
|
|
97
|
-
const webSyncJobSchema = z
|
|
98
|
-
.object({
|
|
99
|
-
id: z.string(),
|
|
100
|
-
kind: webSyncKindSchema,
|
|
101
|
-
accountId: z.string().optional(),
|
|
102
|
-
status: z.enum(["running", "succeeded", "failed"]),
|
|
103
|
-
startedAt: z.string(),
|
|
104
|
-
finishedAt: z.string().optional(),
|
|
105
|
-
summary: z.string(),
|
|
106
|
-
inProgress: z.boolean(),
|
|
107
|
-
result: webSyncResponseSchema.optional(),
|
|
108
|
-
error: z.string().optional(),
|
|
109
|
-
})
|
|
110
|
-
.transform((value) => value as unknown as WebSyncJobSnapshot);
|
|
111
|
-
|
|
112
|
-
const actionResponseSchema = jsonRecordSchema;
|
|
113
19
|
const SYNC_POLL_INTERVAL_MS = 500;
|
|
114
|
-
const STATUS_CACHE_KEY = "api:/status";
|
|
115
|
-
const QUERY_CACHE_PREFIX = "api:/query:";
|
|
116
|
-
const STATUS_CACHE_MAX_AGE_MS = 60_000;
|
|
117
|
-
const QUERY_CACHE_MAX_AGE_MS = 5 * 60_000;
|
|
118
|
-
|
|
119
|
-
interface ClientFetchCacheOptions {
|
|
120
|
-
force?: boolean;
|
|
121
|
-
maxAgeMs?: number;
|
|
122
|
-
}
|
|
123
20
|
|
|
124
21
|
export class ApiFetchError extends Data.TaggedError("ApiFetchError")<{
|
|
125
22
|
readonly message: string;
|
|
@@ -163,64 +60,16 @@ function runApiEffect<T, E>(effect: Effect.Effect<T, E>) {
|
|
|
163
60
|
return runEffectPromise(effect);
|
|
164
61
|
}
|
|
165
62
|
|
|
166
|
-
function splitSignal(init?: RequestInit) {
|
|
167
|
-
if (!init) return { requestInit: undefined, signal: undefined };
|
|
168
|
-
const { signal, ...requestInit } = init;
|
|
169
|
-
return { requestInit, signal: signal ?? undefined };
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function waitForSignal<T>(promise: Promise<T>, signal?: AbortSignal) {
|
|
173
|
-
if (!signal) return promise;
|
|
174
|
-
if (signal.aborted) {
|
|
175
|
-
return Promise.reject(
|
|
176
|
-
new DOMException("The operation was aborted.", "AbortError"),
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return new Promise<T>((resolve, reject) => {
|
|
181
|
-
const onAbort = () => {
|
|
182
|
-
reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
183
|
-
};
|
|
184
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
185
|
-
promise.then(
|
|
186
|
-
(value) => {
|
|
187
|
-
signal.removeEventListener("abort", onAbort);
|
|
188
|
-
resolve(value);
|
|
189
|
-
},
|
|
190
|
-
(error: unknown) => {
|
|
191
|
-
signal.removeEventListener("abort", onAbort);
|
|
192
|
-
reject(error);
|
|
193
|
-
},
|
|
194
|
-
);
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function queryCacheKey(input: RequestInfo | URL) {
|
|
199
|
-
const raw =
|
|
200
|
-
typeof input === "string"
|
|
201
|
-
? input
|
|
202
|
-
: input instanceof URL
|
|
203
|
-
? input.toString()
|
|
204
|
-
: input.url;
|
|
205
|
-
const base =
|
|
206
|
-
typeof window === "undefined"
|
|
207
|
-
? "http://birdclaw.local"
|
|
208
|
-
: window.location.origin;
|
|
209
|
-
const url = new URL(raw, base);
|
|
210
|
-
url.searchParams.delete("refresh");
|
|
211
|
-
url.searchParams.sort();
|
|
212
|
-
return `${QUERY_CACHE_PREFIX}${url.pathname}?${url.searchParams.toString()}`;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
63
|
export function fetchJsonEffect<T>(
|
|
216
64
|
input: RequestInfo | URL,
|
|
217
65
|
init: RequestInit | undefined,
|
|
218
66
|
schema: z.ZodType<T>,
|
|
219
67
|
fallbackMessage: string,
|
|
68
|
+
runtime: RuntimeServices = defaultRuntimeServices,
|
|
220
69
|
) {
|
|
221
70
|
return Effect.gen(function* () {
|
|
222
71
|
const response = yield* Effect.tryPromise({
|
|
223
|
-
try: () => fetch(input, init),
|
|
72
|
+
try: () => runtime.fetch(input, init),
|
|
224
73
|
catch: (cause) => apiFetchErrorFromCause(cause, fallbackMessage),
|
|
225
74
|
});
|
|
226
75
|
const data = yield* readJsonEffect(response);
|
|
@@ -251,31 +100,15 @@ export function fetchJson<T>(
|
|
|
251
100
|
init: RequestInit | undefined,
|
|
252
101
|
schema: z.ZodType<T>,
|
|
253
102
|
fallbackMessage: string,
|
|
103
|
+
runtime: RuntimeServices = defaultRuntimeServices,
|
|
254
104
|
): Promise<T> {
|
|
255
|
-
return runApiEffect(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
export function readCachedQueryEnvelope() {
|
|
259
|
-
return readClientCache<QueryEnvelope>(
|
|
260
|
-
STATUS_CACHE_KEY,
|
|
261
|
-
STATUS_CACHE_MAX_AGE_MS,
|
|
105
|
+
return runApiEffect(
|
|
106
|
+
fetchJsonEffect(input, init, schema, fallbackMessage, runtime),
|
|
262
107
|
);
|
|
263
108
|
}
|
|
264
109
|
|
|
265
|
-
export function fetchQueryEnvelope(
|
|
266
|
-
init
|
|
267
|
-
{
|
|
268
|
-
force = false,
|
|
269
|
-
maxAgeMs = STATUS_CACHE_MAX_AGE_MS,
|
|
270
|
-
}: ClientFetchCacheOptions = {},
|
|
271
|
-
) {
|
|
272
|
-
const { requestInit, signal } = splitSignal(init);
|
|
273
|
-
const request = loadClientCache(
|
|
274
|
-
STATUS_CACHE_KEY,
|
|
275
|
-
() => runApiEffect(fetchQueryEnvelopeEffect(requestInit)),
|
|
276
|
-
{ force, maxAgeMs },
|
|
277
|
-
);
|
|
278
|
-
return waitForSignal(request, signal);
|
|
110
|
+
export function fetchQueryEnvelope(init?: RequestInit) {
|
|
111
|
+
return runApiEffect(fetchQueryEnvelopeEffect(init));
|
|
279
112
|
}
|
|
280
113
|
|
|
281
114
|
export function fetchQueryEnvelopeEffect(init?: RequestInit) {
|
|
@@ -294,38 +127,6 @@ export function fetchQueryResponse(
|
|
|
294
127
|
return runApiEffect(fetchQueryResponseEffect(input, init));
|
|
295
128
|
}
|
|
296
129
|
|
|
297
|
-
export function readCachedQueryResponse(input: RequestInfo | URL) {
|
|
298
|
-
return readClientCache<QueryResponse>(
|
|
299
|
-
queryCacheKey(input),
|
|
300
|
-
QUERY_CACHE_MAX_AGE_MS,
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export function fetchCachedQueryResponse(
|
|
305
|
-
input: RequestInfo | URL,
|
|
306
|
-
init?: RequestInit,
|
|
307
|
-
{
|
|
308
|
-
force = false,
|
|
309
|
-
maxAgeMs = QUERY_CACHE_MAX_AGE_MS,
|
|
310
|
-
}: ClientFetchCacheOptions = {},
|
|
311
|
-
) {
|
|
312
|
-
const { requestInit, signal } = splitSignal(init);
|
|
313
|
-
const request = loadClientCache(
|
|
314
|
-
queryCacheKey(input),
|
|
315
|
-
() => runApiEffect(fetchQueryResponseEffect(input, requestInit)),
|
|
316
|
-
{ force, maxAgeMs },
|
|
317
|
-
);
|
|
318
|
-
return waitForSignal(request, signal);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export function invalidateCachedQueryResponse(input: RequestInfo | URL) {
|
|
322
|
-
deleteClientCache(queryCacheKey(input));
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
export function invalidateCachedQueryResponses() {
|
|
326
|
-
deleteClientCacheByPrefix(QUERY_CACHE_PREFIX);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
130
|
export function fetchQueryResponseEffect(
|
|
330
131
|
input: RequestInfo | URL,
|
|
331
132
|
init?: RequestInit,
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
AccountRecord,
|
|
4
|
+
ArchiveCandidate,
|
|
5
|
+
DmConversationItem,
|
|
6
|
+
DmMessageItem,
|
|
7
|
+
DmSearchMatchItem,
|
|
8
|
+
EmbeddedTweet,
|
|
9
|
+
ProfileAffiliation,
|
|
10
|
+
ProfileRecord,
|
|
11
|
+
QueryEnvelope,
|
|
12
|
+
QueryResponse,
|
|
13
|
+
TimelineItem,
|
|
14
|
+
TransportStatus,
|
|
15
|
+
TweetEntities,
|
|
16
|
+
TweetMediaItem,
|
|
17
|
+
UrlExpansionItem,
|
|
18
|
+
} from "./types";
|
|
19
|
+
import type {
|
|
20
|
+
WebSyncJobSnapshot,
|
|
21
|
+
WebSyncResponse,
|
|
22
|
+
WebSyncStep,
|
|
23
|
+
} from "./web-sync";
|
|
24
|
+
|
|
25
|
+
const jsonRecordSchema = z.record(z.string(), z.unknown());
|
|
26
|
+
const unknownProfile: ProfileRecord = {
|
|
27
|
+
id: "profile_unknown",
|
|
28
|
+
handle: "unknown",
|
|
29
|
+
displayName: "Unknown",
|
|
30
|
+
bio: "",
|
|
31
|
+
followersCount: 0,
|
|
32
|
+
avatarHue: 0,
|
|
33
|
+
createdAt: "",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const profileAffiliationSchema: z.ZodType<ProfileAffiliation> = z.object({
|
|
37
|
+
organizationProfileId: z.string(),
|
|
38
|
+
organizationName: z.string().optional(),
|
|
39
|
+
organizationHandle: z.string().optional(),
|
|
40
|
+
badgeUrl: z.string().nullable().optional(),
|
|
41
|
+
url: z.string().nullable().optional(),
|
|
42
|
+
label: z.string().nullable().optional(),
|
|
43
|
+
source: z.string(),
|
|
44
|
+
firstSeenAt: z.string(),
|
|
45
|
+
lastSeenAt: z.string(),
|
|
46
|
+
isActive: z.boolean(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const profileRecordSchema = z.object({
|
|
50
|
+
id: z.string().default("profile_unknown"),
|
|
51
|
+
handle: z.string().default("unknown"),
|
|
52
|
+
displayName: z.string().default("Unknown"),
|
|
53
|
+
bio: z.string().default(""),
|
|
54
|
+
followersCount: z.number().default(0),
|
|
55
|
+
followingCount: z.number().optional(),
|
|
56
|
+
avatarHue: z.number().default(0),
|
|
57
|
+
avatarUrl: z.string().optional(),
|
|
58
|
+
location: z.string().optional(),
|
|
59
|
+
url: z.string().optional(),
|
|
60
|
+
verifiedType: z.string().optional(),
|
|
61
|
+
entities: jsonRecordSchema.optional(),
|
|
62
|
+
affiliations: z.array(profileAffiliationSchema).optional(),
|
|
63
|
+
primaryAffiliation: profileAffiliationSchema.optional(),
|
|
64
|
+
createdAt: z.string().default(""),
|
|
65
|
+
}) satisfies z.ZodType<ProfileRecord>;
|
|
66
|
+
|
|
67
|
+
const tweetEntitiesSchema: z.ZodType<TweetEntities> = z.object({
|
|
68
|
+
mentions: z
|
|
69
|
+
.array(
|
|
70
|
+
z.object({
|
|
71
|
+
username: z.string(),
|
|
72
|
+
id: z.string().optional(),
|
|
73
|
+
start: z.number(),
|
|
74
|
+
end: z.number(),
|
|
75
|
+
profile: profileRecordSchema.optional(),
|
|
76
|
+
}),
|
|
77
|
+
)
|
|
78
|
+
.optional(),
|
|
79
|
+
urls: z
|
|
80
|
+
.array(
|
|
81
|
+
z.object({
|
|
82
|
+
url: z.string(),
|
|
83
|
+
expandedUrl: z.string(),
|
|
84
|
+
displayUrl: z.string(),
|
|
85
|
+
start: z.number(),
|
|
86
|
+
end: z.number(),
|
|
87
|
+
title: z.string().optional(),
|
|
88
|
+
description: z.string().nullable().optional(),
|
|
89
|
+
imageUrl: z.string().nullable().optional(),
|
|
90
|
+
siteName: z.string().nullable().optional(),
|
|
91
|
+
}),
|
|
92
|
+
)
|
|
93
|
+
.optional(),
|
|
94
|
+
hashtags: z
|
|
95
|
+
.array(
|
|
96
|
+
z.object({
|
|
97
|
+
tag: z.string(),
|
|
98
|
+
start: z.number(),
|
|
99
|
+
end: z.number(),
|
|
100
|
+
}),
|
|
101
|
+
)
|
|
102
|
+
.optional(),
|
|
103
|
+
article: z
|
|
104
|
+
.object({
|
|
105
|
+
title: z.string(),
|
|
106
|
+
previewText: z.string().optional(),
|
|
107
|
+
url: z.string(),
|
|
108
|
+
coverImageUrl: z.string().optional(),
|
|
109
|
+
})
|
|
110
|
+
.optional(),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const tweetMediaSchema: z.ZodType<TweetMediaItem> = z.object({
|
|
114
|
+
url: z.string(),
|
|
115
|
+
type: z.enum(["image", "video", "gif", "unknown"]),
|
|
116
|
+
altText: z.string().optional(),
|
|
117
|
+
width: z.number().optional(),
|
|
118
|
+
height: z.number().optional(),
|
|
119
|
+
thumbnailUrl: z.string().optional(),
|
|
120
|
+
durationMs: z.number().optional(),
|
|
121
|
+
variants: z
|
|
122
|
+
.array(
|
|
123
|
+
z.object({
|
|
124
|
+
url: z.string(),
|
|
125
|
+
contentType: z.string().optional(),
|
|
126
|
+
bitRate: z.number().optional(),
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
.optional(),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const embeddedTweetSchema: z.ZodType<EmbeddedTweet> = z.object({
|
|
133
|
+
id: z.string(),
|
|
134
|
+
text: z.string(),
|
|
135
|
+
createdAt: z.string().default(""),
|
|
136
|
+
replyToId: z.string().nullable().optional(),
|
|
137
|
+
isReplied: z.boolean().optional(),
|
|
138
|
+
likeCount: z.number().optional(),
|
|
139
|
+
mediaCount: z.number().optional(),
|
|
140
|
+
bookmarked: z.boolean().optional(),
|
|
141
|
+
liked: z.boolean().optional(),
|
|
142
|
+
author: profileRecordSchema.default(unknownProfile),
|
|
143
|
+
entities: tweetEntitiesSchema.default({}),
|
|
144
|
+
media: z.array(tweetMediaSchema).default([]),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const timelineItemSchema: z.ZodType<TimelineItem> = z.object({
|
|
148
|
+
id: z.string(),
|
|
149
|
+
accountId: z.string().default("acct_primary"),
|
|
150
|
+
accountHandle: z.string().default(""),
|
|
151
|
+
kind: z
|
|
152
|
+
.enum(["home", "mention", "authored", "search", "like", "bookmark"])
|
|
153
|
+
.default("home"),
|
|
154
|
+
text: z.string(),
|
|
155
|
+
searchSnippet: z.string().optional(),
|
|
156
|
+
createdAt: z.string().default(""),
|
|
157
|
+
replyToId: z.string().nullable().optional(),
|
|
158
|
+
isReplied: z.boolean().default(false),
|
|
159
|
+
likeCount: z.number().default(0),
|
|
160
|
+
mediaCount: z.number().default(0),
|
|
161
|
+
bookmarked: z.boolean().default(false),
|
|
162
|
+
liked: z.boolean().default(false),
|
|
163
|
+
author: profileRecordSchema.default(unknownProfile),
|
|
164
|
+
entities: tweetEntitiesSchema.default({}),
|
|
165
|
+
media: z.array(tweetMediaSchema).default([]),
|
|
166
|
+
replyToTweet: embeddedTweetSchema.nullable().optional(),
|
|
167
|
+
quotedTweet: embeddedTweetSchema.nullable().optional(),
|
|
168
|
+
retweetedTweet: embeddedTweetSchema.nullable().optional(),
|
|
169
|
+
qualityReason: z.string().nullable().optional(),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const dmMessageSchema: z.ZodType<DmMessageItem> = z.object({
|
|
173
|
+
id: z.string(),
|
|
174
|
+
conversationId: z.string().default(""),
|
|
175
|
+
text: z.string(),
|
|
176
|
+
createdAt: z.string().default(""),
|
|
177
|
+
direction: z.enum(["inbound", "outbound"]).default("inbound"),
|
|
178
|
+
isReplied: z.boolean().default(false),
|
|
179
|
+
mediaCount: z.number().default(0),
|
|
180
|
+
sender: profileRecordSchema.default(unknownProfile),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const urlExpansionSchema: z.ZodType<UrlExpansionItem> = z.object({
|
|
184
|
+
url: z.string(),
|
|
185
|
+
expandedUrl: z.string(),
|
|
186
|
+
finalUrl: z.string(),
|
|
187
|
+
status: z.enum(["hit", "miss", "error"]),
|
|
188
|
+
source: z.enum(["cache", "network"]),
|
|
189
|
+
title: z.string().optional(),
|
|
190
|
+
description: z.string().nullable().optional(),
|
|
191
|
+
error: z.string().optional(),
|
|
192
|
+
updatedAt: z.string(),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const dmSearchMatchSchema: z.ZodType<DmSearchMatchItem> = z.object({
|
|
196
|
+
message: dmMessageSchema,
|
|
197
|
+
before: z.array(dmMessageSchema),
|
|
198
|
+
after: z.array(dmMessageSchema),
|
|
199
|
+
urlExpansions: z.array(urlExpansionSchema).optional(),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const dmConversationSchema: z.ZodType<DmConversationItem> = z.object({
|
|
203
|
+
id: z.string(),
|
|
204
|
+
accountId: z.string(),
|
|
205
|
+
accountHandle: z.string().default(""),
|
|
206
|
+
title: z.string(),
|
|
207
|
+
searchSnippet: z.string().optional(),
|
|
208
|
+
inboxKind: z.enum(["accepted", "request"]).optional(),
|
|
209
|
+
isMessageRequest: z.boolean().optional(),
|
|
210
|
+
lastMessageAt: z.string().default(""),
|
|
211
|
+
lastMessagePreview: z.string().default(""),
|
|
212
|
+
unreadCount: z.number().default(0),
|
|
213
|
+
needsReply: z.boolean().default(false),
|
|
214
|
+
influenceScore: z.number().default(0),
|
|
215
|
+
influenceLabel: z.string().default(""),
|
|
216
|
+
participant: profileRecordSchema.default(unknownProfile),
|
|
217
|
+
matches: z.array(dmSearchMatchSchema).optional(),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const accountRecordSchema: z.ZodType<AccountRecord> = z.object({
|
|
221
|
+
id: z.string(),
|
|
222
|
+
name: z.string().default(""),
|
|
223
|
+
handle: z.string().default(""),
|
|
224
|
+
externalUserId: z.string().nullable().optional(),
|
|
225
|
+
profileId: z.string().optional(),
|
|
226
|
+
avatarHue: z.number().optional(),
|
|
227
|
+
avatarUrl: z.string().optional(),
|
|
228
|
+
transport: z.string().default("local"),
|
|
229
|
+
isDefault: z.coerce.number().default(0),
|
|
230
|
+
createdAt: z.string().default(""),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const archiveCandidateSchema: z.ZodType<ArchiveCandidate> = z.object({
|
|
234
|
+
path: z.string(),
|
|
235
|
+
name: z.string().default(""),
|
|
236
|
+
size: z.number().default(0),
|
|
237
|
+
sizeFormatted: z.string().default(""),
|
|
238
|
+
modifiedTime: z.string().default(""),
|
|
239
|
+
dateFormatted: z.string().default(""),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
const transportStatusSchema: z.ZodType<TransportStatus> = z.object({
|
|
243
|
+
installed: z.boolean().default(false),
|
|
244
|
+
availableTransport: z.enum(["xurl", "local"]).default("local"),
|
|
245
|
+
statusText: z.string(),
|
|
246
|
+
rawStatus: z.string().optional(),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
export const queryEnvelopeSchema: z.ZodType<QueryEnvelope> = z.object({
|
|
250
|
+
accounts: z.array(accountRecordSchema),
|
|
251
|
+
archives: z.array(archiveCandidateSchema),
|
|
252
|
+
transport: transportStatusSchema,
|
|
253
|
+
stats: z.object({
|
|
254
|
+
home: z.number(),
|
|
255
|
+
mentions: z.number(),
|
|
256
|
+
dms: z.number(),
|
|
257
|
+
needsReply: z.number(),
|
|
258
|
+
inbox: z.number(),
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const timelineQueryResponseSchema = z.object({
|
|
263
|
+
resource: z.enum(["home", "mentions", "authored", "search"]),
|
|
264
|
+
items: z.array(timelineItemSchema),
|
|
265
|
+
selectedConversation: z.undefined().optional(),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const dmQueryResponseSchema = z.object({
|
|
269
|
+
resource: z.literal("dms"),
|
|
270
|
+
items: z.array(dmConversationSchema),
|
|
271
|
+
selectedConversation: z
|
|
272
|
+
.object({
|
|
273
|
+
conversation: dmConversationSchema,
|
|
274
|
+
messages: z.array(dmMessageSchema),
|
|
275
|
+
})
|
|
276
|
+
.nullable()
|
|
277
|
+
.optional(),
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
export const queryResponseSchema: z.ZodType<QueryResponse> = z.union([
|
|
281
|
+
timelineQueryResponseSchema,
|
|
282
|
+
dmQueryResponseSchema,
|
|
283
|
+
]);
|
|
284
|
+
|
|
285
|
+
export const webSyncKindSchema = z.enum([
|
|
286
|
+
"timeline",
|
|
287
|
+
"mentions",
|
|
288
|
+
"likes",
|
|
289
|
+
"bookmarks",
|
|
290
|
+
"dms",
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
const webSyncStepSchema: z.ZodType<WebSyncStep> = z.object({
|
|
294
|
+
kind: z.union([webSyncKindSchema, z.literal("mention-threads")]),
|
|
295
|
+
label: z.string(),
|
|
296
|
+
count: z.number(),
|
|
297
|
+
source: z.string().optional(),
|
|
298
|
+
partial: z.boolean().optional(),
|
|
299
|
+
warnings: z.array(z.string()).optional(),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
export const webSyncResponseSchema: z.ZodType<WebSyncResponse> = z.object({
|
|
303
|
+
ok: z.boolean(),
|
|
304
|
+
kind: webSyncKindSchema,
|
|
305
|
+
accountId: z.string().optional(),
|
|
306
|
+
startedAt: z.string().default(""),
|
|
307
|
+
finishedAt: z.string().optional(),
|
|
308
|
+
summary: z.string(),
|
|
309
|
+
steps: z.array(webSyncStepSchema).default([]),
|
|
310
|
+
inProgress: z.boolean().optional(),
|
|
311
|
+
backup: z.custom<WebSyncResponse["backup"]>().optional(),
|
|
312
|
+
error: z.string().optional(),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
export const webSyncJobSchema: z.ZodType<WebSyncJobSnapshot> = z.object({
|
|
316
|
+
id: z.string(),
|
|
317
|
+
kind: webSyncKindSchema,
|
|
318
|
+
accountId: z.string().optional(),
|
|
319
|
+
status: z.enum(["running", "succeeded", "failed"]),
|
|
320
|
+
startedAt: z.string(),
|
|
321
|
+
finishedAt: z.string().optional(),
|
|
322
|
+
summary: z.string(),
|
|
323
|
+
inProgress: z.boolean(),
|
|
324
|
+
result: webSyncResponseSchema.optional(),
|
|
325
|
+
error: z.string().optional(),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
export const actionResponseSchema = jsonRecordSchema;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export type ArchiveTweetRow = {
|
|
2
|
+
id: string;
|
|
3
|
+
kind: "home" | "like" | "bookmark";
|
|
4
|
+
authorProfileId: string;
|
|
5
|
+
text: string;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
isReplied: number;
|
|
8
|
+
replyToId: string | null;
|
|
9
|
+
likeCount: number;
|
|
10
|
+
mediaCount: number;
|
|
11
|
+
bookmarked: number;
|
|
12
|
+
liked: number;
|
|
13
|
+
entitiesJson: string;
|
|
14
|
+
mediaJson: string;
|
|
15
|
+
quotedTweetId: string | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ArchiveCollectionRow = {
|
|
19
|
+
tweetId: string;
|
|
20
|
+
kind: "likes" | "bookmarks";
|
|
21
|
+
collectedAt: string | null;
|
|
22
|
+
source: string;
|
|
23
|
+
rawJson: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ArchiveProfileRow = {
|
|
27
|
+
id: string;
|
|
28
|
+
handle: string;
|
|
29
|
+
displayName: string;
|
|
30
|
+
bio: string;
|
|
31
|
+
followersCount: number;
|
|
32
|
+
followingCount: number;
|
|
33
|
+
publicMetricsJson: string;
|
|
34
|
+
avatarHue: number;
|
|
35
|
+
avatarUrl: string | null;
|
|
36
|
+
location: string | null;
|
|
37
|
+
url: string | null;
|
|
38
|
+
verifiedType: string | null;
|
|
39
|
+
entitiesJson: string;
|
|
40
|
+
rawJson: string;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ArchiveConversationRow = {
|
|
45
|
+
id: string;
|
|
46
|
+
title: string;
|
|
47
|
+
accountId: string;
|
|
48
|
+
participantProfileId: string;
|
|
49
|
+
lastMessageAt: string;
|
|
50
|
+
unreadCount: number;
|
|
51
|
+
needsReply: number;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type ArchiveMessageRow = {
|
|
55
|
+
id: string;
|
|
56
|
+
conversationId: string;
|
|
57
|
+
senderProfileId: string;
|
|
58
|
+
text: string;
|
|
59
|
+
createdAt: string;
|
|
60
|
+
direction: "inbound" | "outbound";
|
|
61
|
+
mediaCount: number;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type ArchiveFollowRow = {
|
|
65
|
+
profileId: string;
|
|
66
|
+
externalUserId: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export class ArchiveImportPlan {
|
|
70
|
+
readonly mentionDirectory = new Map<
|
|
71
|
+
string,
|
|
72
|
+
{ handle?: string; displayName?: string }
|
|
73
|
+
>();
|
|
74
|
+
readonly tweets: ArchiveTweetRow[] = [];
|
|
75
|
+
readonly collections: ArchiveCollectionRow[] = [];
|
|
76
|
+
readonly profiles = new Map<string, ArchiveProfileRow>();
|
|
77
|
+
readonly conversations = new Map<string, ArchiveConversationRow>();
|
|
78
|
+
readonly dmMessages: ArchiveMessageRow[] = [];
|
|
79
|
+
readonly followers: ArchiveFollowRow[] = [];
|
|
80
|
+
readonly following: ArchiveFollowRow[] = [];
|
|
81
|
+
readonly followerIds = new Set<string>();
|
|
82
|
+
readonly followingIds = new Set<string>();
|
|
83
|
+
|
|
84
|
+
private readonly tweetsById = new Map<string, ArchiveTweetRow>();
|
|
85
|
+
|
|
86
|
+
addTweet(row: ArchiveTweetRow) {
|
|
87
|
+
const existing = this.tweetsById.get(row.id);
|
|
88
|
+
if (existing) {
|
|
89
|
+
existing.bookmarked = Math.max(existing.bookmarked, row.bookmarked);
|
|
90
|
+
existing.liked = Math.max(existing.liked, row.liked);
|
|
91
|
+
if (!existing.text && row.text) existing.text = row.text;
|
|
92
|
+
return existing;
|
|
93
|
+
}
|
|
94
|
+
this.tweets.push(row);
|
|
95
|
+
this.tweetsById.set(row.id, row);
|
|
96
|
+
return row;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getTweet(id: string) {
|
|
100
|
+
return this.tweetsById.get(id);
|
|
101
|
+
}
|
|
102
|
+
}
|