birdclaw 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/README.md +55 -5
- package/bin/birdclaw.mjs +50 -11
- package/package.json +9 -7
- package/public/birdclaw-mark.png +0 -0
- package/scripts/browser-perf.mjs +400 -0
- package/scripts/build-docs-site.mjs +940 -0
- package/scripts/docs-site-assets.mjs +311 -0
- package/scripts/run-vitest.mjs +21 -0
- package/scripts/sanitize-node-options.mjs +23 -0
- package/scripts/start-test-server.mjs +42 -0
- package/src/cli.ts +422 -14
- package/src/components/AccountSwitcher.tsx +186 -0
- package/src/components/AppNav.tsx +29 -9
- package/src/components/AvatarChip.tsx +9 -3
- package/src/components/BrandMark.tsx +67 -0
- package/src/components/ConversationThread.tsx +11 -10
- package/src/components/DmWorkspace.tsx +39 -14
- package/src/components/FeedState.tsx +147 -0
- package/src/components/InboxCard.tsx +14 -2
- package/src/components/LinkPreviewCard.tsx +40 -18
- package/src/components/MarkdownViewer.tsx +452 -0
- package/src/components/SavedTimelineView.tsx +64 -56
- package/src/components/SyncNowButton.tsx +137 -0
- package/src/components/ThemeSlider.tsx +49 -93
- package/src/components/TimelineCard.tsx +364 -136
- package/src/components/TimelineRouteFrame.tsx +170 -0
- package/src/components/TweetMediaGrid.tsx +170 -24
- package/src/components/TweetRichText.tsx +28 -13
- package/src/components/account-selection.ts +64 -0
- package/src/components/useTimelineRouteData.ts +153 -0
- package/src/lib/account-sync-job.ts +654 -0
- package/src/lib/actions-transport.ts +216 -146
- package/src/lib/api-client.ts +304 -0
- package/src/lib/archive-finder.ts +72 -53
- package/src/lib/archive-import.ts +1377 -1298
- package/src/lib/authored-live.ts +261 -204
- package/src/lib/avatar-cache.ts +159 -44
- package/src/lib/backup.ts +1532 -951
- package/src/lib/bird-actions.ts +139 -57
- package/src/lib/bird-command.ts +101 -28
- package/src/lib/bird.ts +549 -194
- package/src/lib/blocklist.ts +40 -23
- package/src/lib/blocks-write.ts +129 -80
- package/src/lib/blocks.ts +165 -97
- package/src/lib/bookmark-sync-job.ts +250 -160
- package/src/lib/conversation-surface.ts +205 -0
- package/src/lib/db.ts +35 -3
- package/src/lib/dms-live.ts +720 -66
- package/src/lib/effect-runtime.ts +45 -0
- package/src/lib/follow-graph.ts +224 -180
- package/src/lib/http-effect.ts +222 -0
- package/src/lib/inbox.ts +74 -43
- package/src/lib/link-index.ts +88 -76
- package/src/lib/link-insights.ts +24 -0
- package/src/lib/link-preview-metadata.ts +472 -52
- package/src/lib/media-fetch.ts +286 -213
- package/src/lib/mention-threads-live.ts +352 -288
- package/src/lib/mentions-live.ts +390 -342
- package/src/lib/moderation-target.ts +102 -65
- package/src/lib/moderation-write.ts +77 -18
- package/src/lib/mutes-write.ts +129 -80
- package/src/lib/mutes.ts +8 -1
- package/src/lib/openai.ts +84 -53
- package/src/lib/period-digest.ts +953 -0
- package/src/lib/profile-affiliation-hydration.ts +93 -54
- package/src/lib/profile-hydration.ts +124 -72
- package/src/lib/profile-replies.ts +60 -43
- package/src/lib/profile-resolver.ts +402 -294
- package/src/lib/queries.ts +1024 -189
- package/src/lib/research.ts +165 -120
- package/src/lib/sqlite.ts +1 -0
- package/src/lib/timeline-collections-live.ts +204 -167
- package/src/lib/timeline-live.ts +60 -39
- package/src/lib/tweet-lookup.ts +30 -19
- package/src/lib/types.ts +38 -1
- package/src/lib/ui.ts +41 -10
- package/src/lib/url-expansion.ts +226 -55
- package/src/lib/url-safety.ts +220 -0
- package/src/lib/web-sync.ts +511 -0
- package/src/lib/whois.ts +166 -147
- package/src/lib/x-web.ts +102 -71
- package/src/lib/xurl.ts +681 -411
- package/src/routeTree.gen.ts +63 -0
- package/src/routes/__root.tsx +25 -5
- package/src/routes/api/action.tsx +127 -78
- package/src/routes/api/avatar.tsx +39 -30
- package/src/routes/api/blocks.tsx +26 -23
- package/src/routes/api/conversation.tsx +25 -14
- package/src/routes/api/inbox.tsx +27 -21
- package/src/routes/api/link-insights.tsx +31 -25
- package/src/routes/api/link-preview.tsx +25 -21
- package/src/routes/api/period-digest.tsx +123 -0
- package/src/routes/api/profile-hydrate.tsx +31 -28
- package/src/routes/api/query.tsx +79 -55
- package/src/routes/api/status.tsx +18 -10
- package/src/routes/api/sync.tsx +105 -0
- package/src/routes/dms.tsx +195 -55
- package/src/routes/inbox.tsx +32 -19
- package/src/routes/index.tsx +21 -127
- package/src/routes/links.tsx +50 -5
- package/src/routes/mentions.tsx +21 -127
- package/src/routes/today.tsx +441 -0
- package/src/styles.css +74 -11
- package/vite.config.ts +8 -0
package/src/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'
|
|
@@ -17,9 +18,11 @@ import { Route as DmsRouteImport } from './routes/dms'
|
|
|
17
18
|
import { Route as BookmarksRouteImport } from './routes/bookmarks'
|
|
18
19
|
import { Route as BlocksRouteImport } from './routes/blocks'
|
|
19
20
|
import { Route as IndexRouteImport } from './routes/index'
|
|
21
|
+
import { Route as ApiSyncRouteImport } from './routes/api/sync'
|
|
20
22
|
import { Route as ApiStatusRouteImport } from './routes/api/status'
|
|
21
23
|
import { Route as ApiQueryRouteImport } from './routes/api/query'
|
|
22
24
|
import { Route as ApiProfileHydrateRouteImport } from './routes/api/profile-hydrate'
|
|
25
|
+
import { Route as ApiPeriodDigestRouteImport } from './routes/api/period-digest'
|
|
23
26
|
import { Route as ApiLinkPreviewRouteImport } from './routes/api/link-preview'
|
|
24
27
|
import { Route as ApiLinkInsightsRouteImport } from './routes/api/link-insights'
|
|
25
28
|
import { Route as ApiInboxRouteImport } from './routes/api/inbox'
|
|
@@ -28,6 +31,11 @@ import { Route as ApiBlocksRouteImport } from './routes/api/blocks'
|
|
|
28
31
|
import { Route as ApiAvatarRouteImport } from './routes/api/avatar'
|
|
29
32
|
import { Route as ApiActionRouteImport } from './routes/api/action'
|
|
30
33
|
|
|
34
|
+
const TodayRoute = TodayRouteImport.update({
|
|
35
|
+
id: '/today',
|
|
36
|
+
path: '/today',
|
|
37
|
+
getParentRoute: () => rootRouteImport,
|
|
38
|
+
} as any)
|
|
31
39
|
const MentionsRoute = MentionsRouteImport.update({
|
|
32
40
|
id: '/mentions',
|
|
33
41
|
path: '/mentions',
|
|
@@ -68,6 +76,11 @@ const IndexRoute = IndexRouteImport.update({
|
|
|
68
76
|
path: '/',
|
|
69
77
|
getParentRoute: () => rootRouteImport,
|
|
70
78
|
} as any)
|
|
79
|
+
const ApiSyncRoute = ApiSyncRouteImport.update({
|
|
80
|
+
id: '/api/sync',
|
|
81
|
+
path: '/api/sync',
|
|
82
|
+
getParentRoute: () => rootRouteImport,
|
|
83
|
+
} as any)
|
|
71
84
|
const ApiStatusRoute = ApiStatusRouteImport.update({
|
|
72
85
|
id: '/api/status',
|
|
73
86
|
path: '/api/status',
|
|
@@ -83,6 +96,11 @@ const ApiProfileHydrateRoute = ApiProfileHydrateRouteImport.update({
|
|
|
83
96
|
path: '/api/profile-hydrate',
|
|
84
97
|
getParentRoute: () => rootRouteImport,
|
|
85
98
|
} as any)
|
|
99
|
+
const ApiPeriodDigestRoute = ApiPeriodDigestRouteImport.update({
|
|
100
|
+
id: '/api/period-digest',
|
|
101
|
+
path: '/api/period-digest',
|
|
102
|
+
getParentRoute: () => rootRouteImport,
|
|
103
|
+
} as any)
|
|
86
104
|
const ApiLinkPreviewRoute = ApiLinkPreviewRouteImport.update({
|
|
87
105
|
id: '/api/link-preview',
|
|
88
106
|
path: '/api/link-preview',
|
|
@@ -128,6 +146,7 @@ export interface FileRoutesByFullPath {
|
|
|
128
146
|
'/likes': typeof LikesRoute
|
|
129
147
|
'/links': typeof LinksRoute
|
|
130
148
|
'/mentions': typeof MentionsRoute
|
|
149
|
+
'/today': typeof TodayRoute
|
|
131
150
|
'/api/action': typeof ApiActionRoute
|
|
132
151
|
'/api/avatar': typeof ApiAvatarRoute
|
|
133
152
|
'/api/blocks': typeof ApiBlocksRoute
|
|
@@ -135,9 +154,11 @@ export interface FileRoutesByFullPath {
|
|
|
135
154
|
'/api/inbox': typeof ApiInboxRoute
|
|
136
155
|
'/api/link-insights': typeof ApiLinkInsightsRoute
|
|
137
156
|
'/api/link-preview': typeof ApiLinkPreviewRoute
|
|
157
|
+
'/api/period-digest': typeof ApiPeriodDigestRoute
|
|
138
158
|
'/api/profile-hydrate': typeof ApiProfileHydrateRoute
|
|
139
159
|
'/api/query': typeof ApiQueryRoute
|
|
140
160
|
'/api/status': typeof ApiStatusRoute
|
|
161
|
+
'/api/sync': typeof ApiSyncRoute
|
|
141
162
|
}
|
|
142
163
|
export interface FileRoutesByTo {
|
|
143
164
|
'/': typeof IndexRoute
|
|
@@ -148,6 +169,7 @@ export interface FileRoutesByTo {
|
|
|
148
169
|
'/likes': typeof LikesRoute
|
|
149
170
|
'/links': typeof LinksRoute
|
|
150
171
|
'/mentions': typeof MentionsRoute
|
|
172
|
+
'/today': typeof TodayRoute
|
|
151
173
|
'/api/action': typeof ApiActionRoute
|
|
152
174
|
'/api/avatar': typeof ApiAvatarRoute
|
|
153
175
|
'/api/blocks': typeof ApiBlocksRoute
|
|
@@ -155,9 +177,11 @@ export interface FileRoutesByTo {
|
|
|
155
177
|
'/api/inbox': typeof ApiInboxRoute
|
|
156
178
|
'/api/link-insights': typeof ApiLinkInsightsRoute
|
|
157
179
|
'/api/link-preview': typeof ApiLinkPreviewRoute
|
|
180
|
+
'/api/period-digest': typeof ApiPeriodDigestRoute
|
|
158
181
|
'/api/profile-hydrate': typeof ApiProfileHydrateRoute
|
|
159
182
|
'/api/query': typeof ApiQueryRoute
|
|
160
183
|
'/api/status': typeof ApiStatusRoute
|
|
184
|
+
'/api/sync': typeof ApiSyncRoute
|
|
161
185
|
}
|
|
162
186
|
export interface FileRoutesById {
|
|
163
187
|
__root__: typeof rootRouteImport
|
|
@@ -169,6 +193,7 @@ export interface FileRoutesById {
|
|
|
169
193
|
'/likes': typeof LikesRoute
|
|
170
194
|
'/links': typeof LinksRoute
|
|
171
195
|
'/mentions': typeof MentionsRoute
|
|
196
|
+
'/today': typeof TodayRoute
|
|
172
197
|
'/api/action': typeof ApiActionRoute
|
|
173
198
|
'/api/avatar': typeof ApiAvatarRoute
|
|
174
199
|
'/api/blocks': typeof ApiBlocksRoute
|
|
@@ -176,9 +201,11 @@ export interface FileRoutesById {
|
|
|
176
201
|
'/api/inbox': typeof ApiInboxRoute
|
|
177
202
|
'/api/link-insights': typeof ApiLinkInsightsRoute
|
|
178
203
|
'/api/link-preview': typeof ApiLinkPreviewRoute
|
|
204
|
+
'/api/period-digest': typeof ApiPeriodDigestRoute
|
|
179
205
|
'/api/profile-hydrate': typeof ApiProfileHydrateRoute
|
|
180
206
|
'/api/query': typeof ApiQueryRoute
|
|
181
207
|
'/api/status': typeof ApiStatusRoute
|
|
208
|
+
'/api/sync': typeof ApiSyncRoute
|
|
182
209
|
}
|
|
183
210
|
export interface FileRouteTypes {
|
|
184
211
|
fileRoutesByFullPath: FileRoutesByFullPath
|
|
@@ -191,6 +218,7 @@ export interface FileRouteTypes {
|
|
|
191
218
|
| '/likes'
|
|
192
219
|
| '/links'
|
|
193
220
|
| '/mentions'
|
|
221
|
+
| '/today'
|
|
194
222
|
| '/api/action'
|
|
195
223
|
| '/api/avatar'
|
|
196
224
|
| '/api/blocks'
|
|
@@ -198,9 +226,11 @@ export interface FileRouteTypes {
|
|
|
198
226
|
| '/api/inbox'
|
|
199
227
|
| '/api/link-insights'
|
|
200
228
|
| '/api/link-preview'
|
|
229
|
+
| '/api/period-digest'
|
|
201
230
|
| '/api/profile-hydrate'
|
|
202
231
|
| '/api/query'
|
|
203
232
|
| '/api/status'
|
|
233
|
+
| '/api/sync'
|
|
204
234
|
fileRoutesByTo: FileRoutesByTo
|
|
205
235
|
to:
|
|
206
236
|
| '/'
|
|
@@ -211,6 +241,7 @@ export interface FileRouteTypes {
|
|
|
211
241
|
| '/likes'
|
|
212
242
|
| '/links'
|
|
213
243
|
| '/mentions'
|
|
244
|
+
| '/today'
|
|
214
245
|
| '/api/action'
|
|
215
246
|
| '/api/avatar'
|
|
216
247
|
| '/api/blocks'
|
|
@@ -218,9 +249,11 @@ export interface FileRouteTypes {
|
|
|
218
249
|
| '/api/inbox'
|
|
219
250
|
| '/api/link-insights'
|
|
220
251
|
| '/api/link-preview'
|
|
252
|
+
| '/api/period-digest'
|
|
221
253
|
| '/api/profile-hydrate'
|
|
222
254
|
| '/api/query'
|
|
223
255
|
| '/api/status'
|
|
256
|
+
| '/api/sync'
|
|
224
257
|
id:
|
|
225
258
|
| '__root__'
|
|
226
259
|
| '/'
|
|
@@ -231,6 +264,7 @@ export interface FileRouteTypes {
|
|
|
231
264
|
| '/likes'
|
|
232
265
|
| '/links'
|
|
233
266
|
| '/mentions'
|
|
267
|
+
| '/today'
|
|
234
268
|
| '/api/action'
|
|
235
269
|
| '/api/avatar'
|
|
236
270
|
| '/api/blocks'
|
|
@@ -238,9 +272,11 @@ export interface FileRouteTypes {
|
|
|
238
272
|
| '/api/inbox'
|
|
239
273
|
| '/api/link-insights'
|
|
240
274
|
| '/api/link-preview'
|
|
275
|
+
| '/api/period-digest'
|
|
241
276
|
| '/api/profile-hydrate'
|
|
242
277
|
| '/api/query'
|
|
243
278
|
| '/api/status'
|
|
279
|
+
| '/api/sync'
|
|
244
280
|
fileRoutesById: FileRoutesById
|
|
245
281
|
}
|
|
246
282
|
export interface RootRouteChildren {
|
|
@@ -252,6 +288,7 @@ export interface RootRouteChildren {
|
|
|
252
288
|
LikesRoute: typeof LikesRoute
|
|
253
289
|
LinksRoute: typeof LinksRoute
|
|
254
290
|
MentionsRoute: typeof MentionsRoute
|
|
291
|
+
TodayRoute: typeof TodayRoute
|
|
255
292
|
ApiActionRoute: typeof ApiActionRoute
|
|
256
293
|
ApiAvatarRoute: typeof ApiAvatarRoute
|
|
257
294
|
ApiBlocksRoute: typeof ApiBlocksRoute
|
|
@@ -259,13 +296,22 @@ export interface RootRouteChildren {
|
|
|
259
296
|
ApiInboxRoute: typeof ApiInboxRoute
|
|
260
297
|
ApiLinkInsightsRoute: typeof ApiLinkInsightsRoute
|
|
261
298
|
ApiLinkPreviewRoute: typeof ApiLinkPreviewRoute
|
|
299
|
+
ApiPeriodDigestRoute: typeof ApiPeriodDigestRoute
|
|
262
300
|
ApiProfileHydrateRoute: typeof ApiProfileHydrateRoute
|
|
263
301
|
ApiQueryRoute: typeof ApiQueryRoute
|
|
264
302
|
ApiStatusRoute: typeof ApiStatusRoute
|
|
303
|
+
ApiSyncRoute: typeof ApiSyncRoute
|
|
265
304
|
}
|
|
266
305
|
|
|
267
306
|
declare module '@tanstack/react-router' {
|
|
268
307
|
interface FileRoutesByPath {
|
|
308
|
+
'/today': {
|
|
309
|
+
id: '/today'
|
|
310
|
+
path: '/today'
|
|
311
|
+
fullPath: '/today'
|
|
312
|
+
preLoaderRoute: typeof TodayRouteImport
|
|
313
|
+
parentRoute: typeof rootRouteImport
|
|
314
|
+
}
|
|
269
315
|
'/mentions': {
|
|
270
316
|
id: '/mentions'
|
|
271
317
|
path: '/mentions'
|
|
@@ -322,6 +368,13 @@ declare module '@tanstack/react-router' {
|
|
|
322
368
|
preLoaderRoute: typeof IndexRouteImport
|
|
323
369
|
parentRoute: typeof rootRouteImport
|
|
324
370
|
}
|
|
371
|
+
'/api/sync': {
|
|
372
|
+
id: '/api/sync'
|
|
373
|
+
path: '/api/sync'
|
|
374
|
+
fullPath: '/api/sync'
|
|
375
|
+
preLoaderRoute: typeof ApiSyncRouteImport
|
|
376
|
+
parentRoute: typeof rootRouteImport
|
|
377
|
+
}
|
|
325
378
|
'/api/status': {
|
|
326
379
|
id: '/api/status'
|
|
327
380
|
path: '/api/status'
|
|
@@ -343,6 +396,13 @@ declare module '@tanstack/react-router' {
|
|
|
343
396
|
preLoaderRoute: typeof ApiProfileHydrateRouteImport
|
|
344
397
|
parentRoute: typeof rootRouteImport
|
|
345
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
|
+
}
|
|
346
406
|
'/api/link-preview': {
|
|
347
407
|
id: '/api/link-preview'
|
|
348
408
|
path: '/api/link-preview'
|
|
@@ -404,6 +464,7 @@ const rootRouteChildren: RootRouteChildren = {
|
|
|
404
464
|
LikesRoute: LikesRoute,
|
|
405
465
|
LinksRoute: LinksRoute,
|
|
406
466
|
MentionsRoute: MentionsRoute,
|
|
467
|
+
TodayRoute: TodayRoute,
|
|
407
468
|
ApiActionRoute: ApiActionRoute,
|
|
408
469
|
ApiAvatarRoute: ApiAvatarRoute,
|
|
409
470
|
ApiBlocksRoute: ApiBlocksRoute,
|
|
@@ -411,9 +472,11 @@ const rootRouteChildren: RootRouteChildren = {
|
|
|
411
472
|
ApiInboxRoute: ApiInboxRoute,
|
|
412
473
|
ApiLinkInsightsRoute: ApiLinkInsightsRoute,
|
|
413
474
|
ApiLinkPreviewRoute: ApiLinkPreviewRoute,
|
|
475
|
+
ApiPeriodDigestRoute: ApiPeriodDigestRoute,
|
|
414
476
|
ApiProfileHydrateRoute: ApiProfileHydrateRoute,
|
|
415
477
|
ApiQueryRoute: ApiQueryRoute,
|
|
416
478
|
ApiStatusRoute: ApiStatusRoute,
|
|
479
|
+
ApiSyncRoute: ApiSyncRoute,
|
|
417
480
|
}
|
|
418
481
|
export const routeTree = rootRouteImport
|
|
419
482
|
._addFileChildren(rootRouteChildren)
|
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
|
});
|