birdclaw 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -1
- package/README.md +50 -5
- package/package.json +3 -2
- package/scripts/browser-perf.mjs +1 -0
- package/scripts/start-test-server.mjs +16 -3
- package/src/cli.ts +376 -13
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +27 -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 +452 -0
- package/src/components/SyncNowButton.tsx +57 -25
- package/src/components/ThemeSlider.tsx +55 -50
- package/src/components/TimelineCard.tsx +225 -93
- package/src/components/TimelineRouteFrame.tsx +22 -8
- package/src/components/TweetMediaGrid.tsx +87 -38
- package/src/components/TweetRichText.tsx +15 -11
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +23 -7
- package/src/lib/account-sync-job.ts +654 -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 +1364 -1300
- package/src/lib/authored-live.ts +261 -204
- package/src/lib/avatar-cache.ts +159 -44
- package/src/lib/backup.ts +1532 -951
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +549 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/conversation-surface.ts +79 -48
- package/src/lib/db.ts +33 -3
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +352 -288
- package/src/lib/mentions-live.ts +390 -342
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +953 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +969 -199
- package/src/lib/research.ts +165 -120
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +60 -39
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/types.ts +38 -1
- package/src/lib/ui.ts +30 -7
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +216 -148
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl.ts +681 -411
- package/src/routeTree.gen.ts +42 -0
- package/src/routes/__root.tsx +25 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/period-digest.tsx +123 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +79 -55
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +75 -29
- package/src/routes/dms.tsx +95 -28
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/today.tsx +441 -0
package/src/routeTree.gen.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
10
10
|
|
|
11
11
|
import { Route as rootRouteImport } from './routes/__root'
|
|
12
|
+
import { Route as TodayRouteImport } from './routes/today'
|
|
12
13
|
import { Route as MentionsRouteImport } from './routes/mentions'
|
|
13
14
|
import { Route as LinksRouteImport } from './routes/links'
|
|
14
15
|
import { Route as LikesRouteImport } from './routes/likes'
|
|
@@ -21,6 +22,7 @@ import { Route as ApiSyncRouteImport } from './routes/api/sync'
|
|
|
21
22
|
import { Route as ApiStatusRouteImport } from './routes/api/status'
|
|
22
23
|
import { Route as ApiQueryRouteImport } from './routes/api/query'
|
|
23
24
|
import { Route as ApiProfileHydrateRouteImport } from './routes/api/profile-hydrate'
|
|
25
|
+
import { Route as ApiPeriodDigestRouteImport } from './routes/api/period-digest'
|
|
24
26
|
import { Route as ApiLinkPreviewRouteImport } from './routes/api/link-preview'
|
|
25
27
|
import { Route as ApiLinkInsightsRouteImport } from './routes/api/link-insights'
|
|
26
28
|
import { Route as ApiInboxRouteImport } from './routes/api/inbox'
|
|
@@ -29,6 +31,11 @@ import { Route as ApiBlocksRouteImport } from './routes/api/blocks'
|
|
|
29
31
|
import { Route as ApiAvatarRouteImport } from './routes/api/avatar'
|
|
30
32
|
import { Route as ApiActionRouteImport } from './routes/api/action'
|
|
31
33
|
|
|
34
|
+
const TodayRoute = TodayRouteImport.update({
|
|
35
|
+
id: '/today',
|
|
36
|
+
path: '/today',
|
|
37
|
+
getParentRoute: () => rootRouteImport,
|
|
38
|
+
} as any)
|
|
32
39
|
const MentionsRoute = MentionsRouteImport.update({
|
|
33
40
|
id: '/mentions',
|
|
34
41
|
path: '/mentions',
|
|
@@ -89,6 +96,11 @@ const ApiProfileHydrateRoute = ApiProfileHydrateRouteImport.update({
|
|
|
89
96
|
path: '/api/profile-hydrate',
|
|
90
97
|
getParentRoute: () => rootRouteImport,
|
|
91
98
|
} as any)
|
|
99
|
+
const ApiPeriodDigestRoute = ApiPeriodDigestRouteImport.update({
|
|
100
|
+
id: '/api/period-digest',
|
|
101
|
+
path: '/api/period-digest',
|
|
102
|
+
getParentRoute: () => rootRouteImport,
|
|
103
|
+
} as any)
|
|
92
104
|
const ApiLinkPreviewRoute = ApiLinkPreviewRouteImport.update({
|
|
93
105
|
id: '/api/link-preview',
|
|
94
106
|
path: '/api/link-preview',
|
|
@@ -134,6 +146,7 @@ export interface FileRoutesByFullPath {
|
|
|
134
146
|
'/likes': typeof LikesRoute
|
|
135
147
|
'/links': typeof LinksRoute
|
|
136
148
|
'/mentions': typeof MentionsRoute
|
|
149
|
+
'/today': typeof TodayRoute
|
|
137
150
|
'/api/action': typeof ApiActionRoute
|
|
138
151
|
'/api/avatar': typeof ApiAvatarRoute
|
|
139
152
|
'/api/blocks': typeof ApiBlocksRoute
|
|
@@ -141,6 +154,7 @@ export interface FileRoutesByFullPath {
|
|
|
141
154
|
'/api/inbox': typeof ApiInboxRoute
|
|
142
155
|
'/api/link-insights': typeof ApiLinkInsightsRoute
|
|
143
156
|
'/api/link-preview': typeof ApiLinkPreviewRoute
|
|
157
|
+
'/api/period-digest': typeof ApiPeriodDigestRoute
|
|
144
158
|
'/api/profile-hydrate': typeof ApiProfileHydrateRoute
|
|
145
159
|
'/api/query': typeof ApiQueryRoute
|
|
146
160
|
'/api/status': typeof ApiStatusRoute
|
|
@@ -155,6 +169,7 @@ export interface FileRoutesByTo {
|
|
|
155
169
|
'/likes': typeof LikesRoute
|
|
156
170
|
'/links': typeof LinksRoute
|
|
157
171
|
'/mentions': typeof MentionsRoute
|
|
172
|
+
'/today': typeof TodayRoute
|
|
158
173
|
'/api/action': typeof ApiActionRoute
|
|
159
174
|
'/api/avatar': typeof ApiAvatarRoute
|
|
160
175
|
'/api/blocks': typeof ApiBlocksRoute
|
|
@@ -162,6 +177,7 @@ export interface FileRoutesByTo {
|
|
|
162
177
|
'/api/inbox': typeof ApiInboxRoute
|
|
163
178
|
'/api/link-insights': typeof ApiLinkInsightsRoute
|
|
164
179
|
'/api/link-preview': typeof ApiLinkPreviewRoute
|
|
180
|
+
'/api/period-digest': typeof ApiPeriodDigestRoute
|
|
165
181
|
'/api/profile-hydrate': typeof ApiProfileHydrateRoute
|
|
166
182
|
'/api/query': typeof ApiQueryRoute
|
|
167
183
|
'/api/status': typeof ApiStatusRoute
|
|
@@ -177,6 +193,7 @@ export interface FileRoutesById {
|
|
|
177
193
|
'/likes': typeof LikesRoute
|
|
178
194
|
'/links': typeof LinksRoute
|
|
179
195
|
'/mentions': typeof MentionsRoute
|
|
196
|
+
'/today': typeof TodayRoute
|
|
180
197
|
'/api/action': typeof ApiActionRoute
|
|
181
198
|
'/api/avatar': typeof ApiAvatarRoute
|
|
182
199
|
'/api/blocks': typeof ApiBlocksRoute
|
|
@@ -184,6 +201,7 @@ export interface FileRoutesById {
|
|
|
184
201
|
'/api/inbox': typeof ApiInboxRoute
|
|
185
202
|
'/api/link-insights': typeof ApiLinkInsightsRoute
|
|
186
203
|
'/api/link-preview': typeof ApiLinkPreviewRoute
|
|
204
|
+
'/api/period-digest': typeof ApiPeriodDigestRoute
|
|
187
205
|
'/api/profile-hydrate': typeof ApiProfileHydrateRoute
|
|
188
206
|
'/api/query': typeof ApiQueryRoute
|
|
189
207
|
'/api/status': typeof ApiStatusRoute
|
|
@@ -200,6 +218,7 @@ export interface FileRouteTypes {
|
|
|
200
218
|
| '/likes'
|
|
201
219
|
| '/links'
|
|
202
220
|
| '/mentions'
|
|
221
|
+
| '/today'
|
|
203
222
|
| '/api/action'
|
|
204
223
|
| '/api/avatar'
|
|
205
224
|
| '/api/blocks'
|
|
@@ -207,6 +226,7 @@ export interface FileRouteTypes {
|
|
|
207
226
|
| '/api/inbox'
|
|
208
227
|
| '/api/link-insights'
|
|
209
228
|
| '/api/link-preview'
|
|
229
|
+
| '/api/period-digest'
|
|
210
230
|
| '/api/profile-hydrate'
|
|
211
231
|
| '/api/query'
|
|
212
232
|
| '/api/status'
|
|
@@ -221,6 +241,7 @@ export interface FileRouteTypes {
|
|
|
221
241
|
| '/likes'
|
|
222
242
|
| '/links'
|
|
223
243
|
| '/mentions'
|
|
244
|
+
| '/today'
|
|
224
245
|
| '/api/action'
|
|
225
246
|
| '/api/avatar'
|
|
226
247
|
| '/api/blocks'
|
|
@@ -228,6 +249,7 @@ export interface FileRouteTypes {
|
|
|
228
249
|
| '/api/inbox'
|
|
229
250
|
| '/api/link-insights'
|
|
230
251
|
| '/api/link-preview'
|
|
252
|
+
| '/api/period-digest'
|
|
231
253
|
| '/api/profile-hydrate'
|
|
232
254
|
| '/api/query'
|
|
233
255
|
| '/api/status'
|
|
@@ -242,6 +264,7 @@ export interface FileRouteTypes {
|
|
|
242
264
|
| '/likes'
|
|
243
265
|
| '/links'
|
|
244
266
|
| '/mentions'
|
|
267
|
+
| '/today'
|
|
245
268
|
| '/api/action'
|
|
246
269
|
| '/api/avatar'
|
|
247
270
|
| '/api/blocks'
|
|
@@ -249,6 +272,7 @@ export interface FileRouteTypes {
|
|
|
249
272
|
| '/api/inbox'
|
|
250
273
|
| '/api/link-insights'
|
|
251
274
|
| '/api/link-preview'
|
|
275
|
+
| '/api/period-digest'
|
|
252
276
|
| '/api/profile-hydrate'
|
|
253
277
|
| '/api/query'
|
|
254
278
|
| '/api/status'
|
|
@@ -264,6 +288,7 @@ export interface RootRouteChildren {
|
|
|
264
288
|
LikesRoute: typeof LikesRoute
|
|
265
289
|
LinksRoute: typeof LinksRoute
|
|
266
290
|
MentionsRoute: typeof MentionsRoute
|
|
291
|
+
TodayRoute: typeof TodayRoute
|
|
267
292
|
ApiActionRoute: typeof ApiActionRoute
|
|
268
293
|
ApiAvatarRoute: typeof ApiAvatarRoute
|
|
269
294
|
ApiBlocksRoute: typeof ApiBlocksRoute
|
|
@@ -271,6 +296,7 @@ export interface RootRouteChildren {
|
|
|
271
296
|
ApiInboxRoute: typeof ApiInboxRoute
|
|
272
297
|
ApiLinkInsightsRoute: typeof ApiLinkInsightsRoute
|
|
273
298
|
ApiLinkPreviewRoute: typeof ApiLinkPreviewRoute
|
|
299
|
+
ApiPeriodDigestRoute: typeof ApiPeriodDigestRoute
|
|
274
300
|
ApiProfileHydrateRoute: typeof ApiProfileHydrateRoute
|
|
275
301
|
ApiQueryRoute: typeof ApiQueryRoute
|
|
276
302
|
ApiStatusRoute: typeof ApiStatusRoute
|
|
@@ -279,6 +305,13 @@ export interface RootRouteChildren {
|
|
|
279
305
|
|
|
280
306
|
declare module '@tanstack/react-router' {
|
|
281
307
|
interface FileRoutesByPath {
|
|
308
|
+
'/today': {
|
|
309
|
+
id: '/today'
|
|
310
|
+
path: '/today'
|
|
311
|
+
fullPath: '/today'
|
|
312
|
+
preLoaderRoute: typeof TodayRouteImport
|
|
313
|
+
parentRoute: typeof rootRouteImport
|
|
314
|
+
}
|
|
282
315
|
'/mentions': {
|
|
283
316
|
id: '/mentions'
|
|
284
317
|
path: '/mentions'
|
|
@@ -363,6 +396,13 @@ declare module '@tanstack/react-router' {
|
|
|
363
396
|
preLoaderRoute: typeof ApiProfileHydrateRouteImport
|
|
364
397
|
parentRoute: typeof rootRouteImport
|
|
365
398
|
}
|
|
399
|
+
'/api/period-digest': {
|
|
400
|
+
id: '/api/period-digest'
|
|
401
|
+
path: '/api/period-digest'
|
|
402
|
+
fullPath: '/api/period-digest'
|
|
403
|
+
preLoaderRoute: typeof ApiPeriodDigestRouteImport
|
|
404
|
+
parentRoute: typeof rootRouteImport
|
|
405
|
+
}
|
|
366
406
|
'/api/link-preview': {
|
|
367
407
|
id: '/api/link-preview'
|
|
368
408
|
path: '/api/link-preview'
|
|
@@ -424,6 +464,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|
|
424
464
|
LikesRoute: LikesRoute,
|
|
425
465
|
LinksRoute: LinksRoute,
|
|
426
466
|
MentionsRoute: MentionsRoute,
|
|
467
|
+
TodayRoute: TodayRoute,
|
|
427
468
|
ApiActionRoute: ApiActionRoute,
|
|
428
469
|
ApiAvatarRoute: ApiAvatarRoute,
|
|
429
470
|
ApiBlocksRoute: ApiBlocksRoute,
|
|
@@ -431,6 +472,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|
|
431
472
|
ApiInboxRoute: ApiInboxRoute,
|
|
432
473
|
ApiLinkInsightsRoute: ApiLinkInsightsRoute,
|
|
433
474
|
ApiLinkPreviewRoute: ApiLinkPreviewRoute,
|
|
475
|
+
ApiPeriodDigestRoute: ApiPeriodDigestRoute,
|
|
434
476
|
ApiProfileHydrateRoute: ApiProfileHydrateRoute,
|
|
435
477
|
ApiQueryRoute: ApiQueryRoute,
|
|
436
478
|
ApiStatusRoute: ApiStatusRoute,
|
package/src/routes/__root.tsx
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import { TanStackDevtools } from "@tanstack/react-devtools";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
createRootRoute,
|
|
4
|
+
HeadContent,
|
|
5
|
+
Scripts,
|
|
6
|
+
useRouterState,
|
|
7
|
+
} from "@tanstack/react-router";
|
|
3
8
|
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
|
4
9
|
import type { ReactNode } from "react";
|
|
5
10
|
import { AppNav } from "#/components/AppNav";
|
|
6
11
|
import { ThemeProvider, themeScript } from "#/lib/theme";
|
|
7
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
bodyClass,
|
|
14
|
+
mainColumnClass,
|
|
15
|
+
mainColumnDmClass,
|
|
16
|
+
siteShellClass,
|
|
17
|
+
siteShellDmClass,
|
|
18
|
+
} from "#/lib/ui";
|
|
8
19
|
|
|
9
20
|
import appCss from "../styles.css?url";
|
|
10
21
|
|
|
@@ -42,6 +53,11 @@ function NotFoundView() {
|
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
function RootDocument({ children }: { children: ReactNode }) {
|
|
56
|
+
const pathname = useRouterState({
|
|
57
|
+
select: (state) => state.location.pathname,
|
|
58
|
+
});
|
|
59
|
+
const messagesMode = pathname.startsWith("/dms");
|
|
60
|
+
|
|
45
61
|
return (
|
|
46
62
|
<html lang="en" suppressHydrationWarning>
|
|
47
63
|
<head>
|
|
@@ -50,9 +66,13 @@ function RootDocument({ children }: { children: ReactNode }) {
|
|
|
50
66
|
</head>
|
|
51
67
|
<body className={bodyClass}>
|
|
52
68
|
<ThemeProvider>
|
|
53
|
-
<div className={siteShellClass}>
|
|
54
|
-
<AppNav />
|
|
55
|
-
<main
|
|
69
|
+
<div className={messagesMode ? siteShellDmClass : siteShellClass}>
|
|
70
|
+
<AppNav compact={messagesMode} />
|
|
71
|
+
<main
|
|
72
|
+
className={messagesMode ? mainColumnDmClass : mainColumnClass}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</main>
|
|
56
76
|
</div>
|
|
57
77
|
</ThemeProvider>
|
|
58
78
|
<TanStackDevtools
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import {
|
|
4
|
+
addBlockEffect,
|
|
5
|
+
removeBlockEffect,
|
|
6
|
+
syncBlocksEffect,
|
|
7
|
+
} from "#/lib/blocks";
|
|
8
|
+
import {
|
|
9
|
+
jsonResponse,
|
|
10
|
+
parseBoundedInteger,
|
|
11
|
+
requestJsonEffect,
|
|
12
|
+
runRouteEffect,
|
|
13
|
+
sensitiveRequestErrorResponse,
|
|
14
|
+
} from "#/lib/http-effect";
|
|
15
|
+
import { scoreInboxEffect } from "#/lib/inbox";
|
|
16
|
+
import { addMuteEffect, removeMuteEffect } from "#/lib/mutes";
|
|
17
|
+
import {
|
|
18
|
+
createDmReplyEffect,
|
|
19
|
+
createPostEffect,
|
|
20
|
+
createTweetReplyEffect,
|
|
21
|
+
} from "#/lib/queries";
|
|
6
22
|
import type { ActionsTransport } from "#/lib/config";
|
|
7
23
|
import type { InboxKind } from "#/lib/types";
|
|
8
24
|
|
|
@@ -14,85 +30,118 @@ function parseActionsTransport(
|
|
|
14
30
|
: undefined;
|
|
15
31
|
}
|
|
16
32
|
|
|
33
|
+
function actionErrorMessage(error: unknown) {
|
|
34
|
+
if (
|
|
35
|
+
typeof error === "object" &&
|
|
36
|
+
error !== null &&
|
|
37
|
+
"cause" in error &&
|
|
38
|
+
error.cause instanceof Error
|
|
39
|
+
) {
|
|
40
|
+
return error.cause.message;
|
|
41
|
+
}
|
|
42
|
+
if (error instanceof Error) {
|
|
43
|
+
return error.message;
|
|
44
|
+
}
|
|
45
|
+
return String(error);
|
|
46
|
+
}
|
|
47
|
+
|
|
17
48
|
export const Route = createFileRoute("/api/action")({
|
|
18
49
|
server: {
|
|
19
50
|
handlers: {
|
|
20
|
-
POST:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
51
|
+
POST: ({ request }) =>
|
|
52
|
+
runRouteEffect(
|
|
53
|
+
Effect.gen(function* () {
|
|
54
|
+
const denied = sensitiveRequestErrorResponse(request);
|
|
55
|
+
if (denied) return denied;
|
|
56
|
+
|
|
57
|
+
const body =
|
|
58
|
+
yield* requestJsonEffect<Record<string, string>>(request);
|
|
59
|
+
const transport = parseActionsTransport(body.transport);
|
|
60
|
+
let result: unknown;
|
|
24
61
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
{
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
62
|
+
if (body.kind === "post") {
|
|
63
|
+
result = yield* createPostEffect(
|
|
64
|
+
body.accountId || "acct_primary",
|
|
65
|
+
body.text || "",
|
|
66
|
+
);
|
|
67
|
+
} else if (body.kind === "replyTweet") {
|
|
68
|
+
result = yield* createTweetReplyEffect(
|
|
69
|
+
body.accountId || "acct_primary",
|
|
70
|
+
body.tweetId || "",
|
|
71
|
+
body.text || "",
|
|
72
|
+
);
|
|
73
|
+
} else if (body.kind === "replyDm") {
|
|
74
|
+
result = yield* createDmReplyEffect(
|
|
75
|
+
body.conversationId || "",
|
|
76
|
+
body.text || "",
|
|
77
|
+
);
|
|
78
|
+
} else if (body.kind === "scoreInbox") {
|
|
79
|
+
result = yield* scoreInboxEffect({
|
|
80
|
+
kind: ((body.scoreKind as InboxKind) || "mixed") as InboxKind,
|
|
81
|
+
account: body.account,
|
|
82
|
+
limit: parseBoundedInteger(body.limit, {
|
|
83
|
+
defaultValue: 8,
|
|
84
|
+
max: 20,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
} else if (body.kind === "blockProfile") {
|
|
88
|
+
result = yield* addBlockEffect(
|
|
89
|
+
body.accountId || "acct_primary",
|
|
90
|
+
body.query || "",
|
|
91
|
+
{
|
|
92
|
+
transport,
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
} else if (body.kind === "unblockProfile") {
|
|
96
|
+
result = yield* removeBlockEffect(
|
|
97
|
+
body.accountId || "acct_primary",
|
|
98
|
+
body.query || "",
|
|
99
|
+
{
|
|
100
|
+
transport,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
} else if (body.kind === "muteProfile") {
|
|
104
|
+
result = yield* addMuteEffect(
|
|
105
|
+
body.accountId || "acct_primary",
|
|
106
|
+
body.query || "",
|
|
107
|
+
{
|
|
108
|
+
transport,
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
} else if (body.kind === "unmuteProfile") {
|
|
112
|
+
result = yield* removeMuteEffect(
|
|
113
|
+
body.accountId || "acct_primary",
|
|
114
|
+
body.query || "",
|
|
115
|
+
{
|
|
116
|
+
transport,
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
} else if (body.kind === "syncBlocks") {
|
|
120
|
+
result = yield* syncBlocksEffect(
|
|
121
|
+
body.accountId || "acct_primary",
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
return jsonResponse(
|
|
125
|
+
{ ok: false, message: "Unknown action kind" },
|
|
126
|
+
{ status: 400 },
|
|
127
|
+
);
|
|
128
|
+
}
|
|
89
129
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
130
|
+
return jsonResponse(result);
|
|
131
|
+
}).pipe(
|
|
132
|
+
Effect.catchAll((error) =>
|
|
133
|
+
Effect.succeed(
|
|
134
|
+
jsonResponse(
|
|
135
|
+
{
|
|
136
|
+
ok: false,
|
|
137
|
+
message: actionErrorMessage(error),
|
|
138
|
+
},
|
|
139
|
+
{ status: 500 },
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
96
145
|
},
|
|
97
146
|
},
|
|
98
147
|
});
|
|
@@ -1,41 +1,50 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import {
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { readCachedAvatarEffect } from "#/lib/avatar-cache";
|
|
4
|
+
import {
|
|
5
|
+
jsonResponse,
|
|
6
|
+
runRouteEffect,
|
|
7
|
+
sensitiveRequestErrorResponse,
|
|
8
|
+
} from "#/lib/http-effect";
|
|
3
9
|
|
|
4
10
|
export const Route = createFileRoute("/api/avatar")({
|
|
5
11
|
server: {
|
|
6
12
|
handlers: {
|
|
7
|
-
GET:
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
GET: ({ request }) =>
|
|
14
|
+
runRouteEffect(
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const sensitiveError = sensitiveRequestErrorResponse(request);
|
|
17
|
+
if (sensitiveError) return sensitiveError;
|
|
10
18
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
JSON.stringify({ ok: false, message: "Missing profileId" }),
|
|
14
|
-
{
|
|
15
|
-
status: 400,
|
|
16
|
-
headers: { "content-type": "application/json" },
|
|
17
|
-
},
|
|
18
|
-
);
|
|
19
|
-
}
|
|
19
|
+
const url = new URL(request.url);
|
|
20
|
+
const profileId = url.searchParams.get("profileId")?.trim();
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
headers: { "content-type": "application/json" },
|
|
28
|
-
},
|
|
29
|
-
);
|
|
30
|
-
}
|
|
22
|
+
if (!profileId) {
|
|
23
|
+
return jsonResponse(
|
|
24
|
+
{ ok: false, message: "Missing profileId" },
|
|
25
|
+
{ status: 400 },
|
|
26
|
+
);
|
|
27
|
+
}
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
const avatar = yield* readCachedAvatarEffect(profileId).pipe(
|
|
30
|
+
Effect.catchAll(() => Effect.succeed(null)),
|
|
31
|
+
);
|
|
32
|
+
if (!avatar) {
|
|
33
|
+
return jsonResponse(
|
|
34
|
+
{ ok: false, message: "Avatar not found" },
|
|
35
|
+
{ status: 404 },
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return new Response(new Uint8Array(avatar.buffer), {
|
|
40
|
+
headers: {
|
|
41
|
+
"cache-control": "public, max-age=86400, immutable",
|
|
42
|
+
"content-type": avatar.contentType,
|
|
43
|
+
"x-content-type-options": "nosniff",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}),
|
|
47
|
+
),
|
|
39
48
|
},
|
|
40
49
|
},
|
|
41
50
|
});
|
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { Effect } from "effect";
|
|
2
3
|
import { getBlocksResponse } from "#/lib/blocks";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
4
|
+
import {
|
|
5
|
+
jsonResponse,
|
|
6
|
+
parseBoundedInteger,
|
|
7
|
+
runRouteEffect,
|
|
8
|
+
sensitiveRequestErrorResponse,
|
|
9
|
+
} from "#/lib/http-effect";
|
|
9
10
|
|
|
10
11
|
export const Route = createFileRoute("/api/blocks")({
|
|
11
12
|
server: {
|
|
12
13
|
handlers: {
|
|
13
|
-
GET: ({ request }) =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
14
|
+
GET: ({ request }) =>
|
|
15
|
+
runRouteEffect(
|
|
16
|
+
Effect.sync(() => {
|
|
17
|
+
const denied = sensitiveRequestErrorResponse(request);
|
|
18
|
+
if (denied) return denied;
|
|
19
|
+
|
|
20
|
+
const url = new URL(request.url);
|
|
21
|
+
return jsonResponse(
|
|
22
|
+
getBlocksResponse({
|
|
23
|
+
accountId: url.searchParams.get("account") ?? undefined,
|
|
24
|
+
search: url.searchParams.get("search") ?? undefined,
|
|
25
|
+
limit: parseBoundedInteger(url.searchParams.get("limit"), {
|
|
26
|
+
defaultValue: 12,
|
|
27
|
+
max: 50,
|
|
28
|
+
}),
|
|
29
|
+
}),
|
|
30
|
+
);
|
|
31
|
+
}),
|
|
32
|
+
),
|
|
30
33
|
},
|
|
31
34
|
},
|
|
32
35
|
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import {
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { maybeAutoUpdateBackupEffect } from "#/lib/backup";
|
|
4
|
+
import {
|
|
5
|
+
runRouteEffect,
|
|
6
|
+
sensitiveRequestErrorResponse,
|
|
7
|
+
} from "#/lib/http-effect";
|
|
3
8
|
import { getTweetConversation } from "#/lib/queries";
|
|
4
9
|
|
|
5
10
|
function json(data: unknown, status = 200) {
|
|
@@ -14,21 +19,27 @@ function json(data: unknown, status = 200) {
|
|
|
14
19
|
export const Route = createFileRoute("/api/conversation")({
|
|
15
20
|
server: {
|
|
16
21
|
handlers: {
|
|
17
|
-
GET:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return json({ ok: false, error: "Missing tweetId" }, 400);
|
|
23
|
-
}
|
|
22
|
+
GET: ({ request }) =>
|
|
23
|
+
runRouteEffect(
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const denied = sensitiveRequestErrorResponse(request);
|
|
26
|
+
if (denied) return denied;
|
|
24
27
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
yield* maybeAutoUpdateBackupEffect();
|
|
29
|
+
const url = new URL(request.url);
|
|
30
|
+
const tweetId = url.searchParams.get("tweetId")?.trim();
|
|
31
|
+
if (!tweetId) {
|
|
32
|
+
return json({ ok: false, error: "Missing tweetId" }, 400);
|
|
33
|
+
}
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
const conversation = getTweetConversation(tweetId);
|
|
36
|
+
if (!conversation) {
|
|
37
|
+
return json({ ok: false, error: "Tweet not found" }, 404);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return json({ ok: true, ...conversation });
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
32
43
|
},
|
|
33
44
|
},
|
|
34
45
|
});
|