birdclaw 0.1.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +21 -0
  3. package/README.md +389 -0
  4. package/bin/birdclaw.mjs +28 -0
  5. package/package.json +100 -0
  6. package/playwright.config.ts +31 -0
  7. package/public/favicon.ico +0 -0
  8. package/public/logo192.png +0 -0
  9. package/public/logo512.png +0 -0
  10. package/public/manifest.json +25 -0
  11. package/public/robots.txt +3 -0
  12. package/src/cli-moderation.ts +210 -0
  13. package/src/cli.ts +450 -0
  14. package/src/components/AppNav.tsx +49 -0
  15. package/src/components/AvatarChip.tsx +47 -0
  16. package/src/components/DmWorkspace.tsx +246 -0
  17. package/src/components/EmbeddedTweetCard.tsx +44 -0
  18. package/src/components/InboxCard.tsx +136 -0
  19. package/src/components/ProfilePreview.tsx +55 -0
  20. package/src/components/ThemeSlider.tsx +124 -0
  21. package/src/components/TimelineCard.tsx +118 -0
  22. package/src/components/TweetMediaGrid.tsx +39 -0
  23. package/src/components/TweetRichText.tsx +85 -0
  24. package/src/lib/actions-transport.ts +173 -0
  25. package/src/lib/archive-finder.ts +128 -0
  26. package/src/lib/archive-import.ts +736 -0
  27. package/src/lib/avatar-cache.ts +184 -0
  28. package/src/lib/bird-actions.ts +200 -0
  29. package/src/lib/bird.ts +183 -0
  30. package/src/lib/blocklist.ts +119 -0
  31. package/src/lib/blocks-write.ts +118 -0
  32. package/src/lib/blocks.ts +326 -0
  33. package/src/lib/config.ts +152 -0
  34. package/src/lib/db.ts +326 -0
  35. package/src/lib/dms-live.ts +279 -0
  36. package/src/lib/inbox.ts +210 -0
  37. package/src/lib/mentions-export.ts +147 -0
  38. package/src/lib/mentions-live.ts +475 -0
  39. package/src/lib/moderation-target.ts +171 -0
  40. package/src/lib/moderation-write.ts +72 -0
  41. package/src/lib/mutes-write.ts +118 -0
  42. package/src/lib/mutes.ts +77 -0
  43. package/src/lib/openai.ts +86 -0
  44. package/src/lib/present.ts +20 -0
  45. package/src/lib/profile-hydration.ts +144 -0
  46. package/src/lib/profile-replies.ts +60 -0
  47. package/src/lib/queries.ts +725 -0
  48. package/src/lib/seed.ts +486 -0
  49. package/src/lib/sync-cache.ts +65 -0
  50. package/src/lib/theme-transition.ts +117 -0
  51. package/src/lib/theme.tsx +157 -0
  52. package/src/lib/tweet-render.ts +115 -0
  53. package/src/lib/types.ts +316 -0
  54. package/src/lib/ui.ts +270 -0
  55. package/src/lib/x-profile.ts +203 -0
  56. package/src/lib/x-web.ts +168 -0
  57. package/src/lib/xurl.ts +492 -0
  58. package/src/routeTree.gen.ts +282 -0
  59. package/src/router.tsx +20 -0
  60. package/src/routes/__root.tsx +64 -0
  61. package/src/routes/api/action.tsx +88 -0
  62. package/src/routes/api/avatar.tsx +41 -0
  63. package/src/routes/api/blocks.tsx +32 -0
  64. package/src/routes/api/inbox.tsx +35 -0
  65. package/src/routes/api/query.tsx +72 -0
  66. package/src/routes/api/status.tsx +15 -0
  67. package/src/routes/blocks.tsx +352 -0
  68. package/src/routes/dms.tsx +262 -0
  69. package/src/routes/inbox.tsx +201 -0
  70. package/src/routes/index.tsx +125 -0
  71. package/src/routes/mentions.tsx +125 -0
  72. package/src/styles.css +109 -0
  73. package/tsconfig.json +30 -0
  74. package/vite.config.ts +23 -0
@@ -0,0 +1,282 @@
1
+ /* eslint-disable */
2
+
3
+ // @ts-nocheck
4
+
5
+ // noinspection JSUnusedGlobalSymbols
6
+
7
+ // This file was automatically generated by TanStack Router.
8
+ // You should NOT make any changes in this file as it will be overwritten.
9
+ // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
10
+
11
+ import { Route as rootRouteImport } from './routes/__root'
12
+ import { Route as MentionsRouteImport } from './routes/mentions'
13
+ import { Route as InboxRouteImport } from './routes/inbox'
14
+ import { Route as DmsRouteImport } from './routes/dms'
15
+ import { Route as BlocksRouteImport } from './routes/blocks'
16
+ import { Route as IndexRouteImport } from './routes/index'
17
+ import { Route as ApiStatusRouteImport } from './routes/api/status'
18
+ import { Route as ApiQueryRouteImport } from './routes/api/query'
19
+ import { Route as ApiInboxRouteImport } from './routes/api/inbox'
20
+ import { Route as ApiBlocksRouteImport } from './routes/api/blocks'
21
+ import { Route as ApiAvatarRouteImport } from './routes/api/avatar'
22
+ import { Route as ApiActionRouteImport } from './routes/api/action'
23
+
24
+ const MentionsRoute = MentionsRouteImport.update({
25
+ id: '/mentions',
26
+ path: '/mentions',
27
+ getParentRoute: () => rootRouteImport,
28
+ } as any)
29
+ const InboxRoute = InboxRouteImport.update({
30
+ id: '/inbox',
31
+ path: '/inbox',
32
+ getParentRoute: () => rootRouteImport,
33
+ } as any)
34
+ const DmsRoute = DmsRouteImport.update({
35
+ id: '/dms',
36
+ path: '/dms',
37
+ getParentRoute: () => rootRouteImport,
38
+ } as any)
39
+ const BlocksRoute = BlocksRouteImport.update({
40
+ id: '/blocks',
41
+ path: '/blocks',
42
+ getParentRoute: () => rootRouteImport,
43
+ } as any)
44
+ const IndexRoute = IndexRouteImport.update({
45
+ id: '/',
46
+ path: '/',
47
+ getParentRoute: () => rootRouteImport,
48
+ } as any)
49
+ const ApiStatusRoute = ApiStatusRouteImport.update({
50
+ id: '/api/status',
51
+ path: '/api/status',
52
+ getParentRoute: () => rootRouteImport,
53
+ } as any)
54
+ const ApiQueryRoute = ApiQueryRouteImport.update({
55
+ id: '/api/query',
56
+ path: '/api/query',
57
+ getParentRoute: () => rootRouteImport,
58
+ } as any)
59
+ const ApiInboxRoute = ApiInboxRouteImport.update({
60
+ id: '/api/inbox',
61
+ path: '/api/inbox',
62
+ getParentRoute: () => rootRouteImport,
63
+ } as any)
64
+ const ApiBlocksRoute = ApiBlocksRouteImport.update({
65
+ id: '/api/blocks',
66
+ path: '/api/blocks',
67
+ getParentRoute: () => rootRouteImport,
68
+ } as any)
69
+ const ApiAvatarRoute = ApiAvatarRouteImport.update({
70
+ id: '/api/avatar',
71
+ path: '/api/avatar',
72
+ getParentRoute: () => rootRouteImport,
73
+ } as any)
74
+ const ApiActionRoute = ApiActionRouteImport.update({
75
+ id: '/api/action',
76
+ path: '/api/action',
77
+ getParentRoute: () => rootRouteImport,
78
+ } as any)
79
+
80
+ export interface FileRoutesByFullPath {
81
+ '/': typeof IndexRoute
82
+ '/blocks': typeof BlocksRoute
83
+ '/dms': typeof DmsRoute
84
+ '/inbox': typeof InboxRoute
85
+ '/mentions': typeof MentionsRoute
86
+ '/api/action': typeof ApiActionRoute
87
+ '/api/avatar': typeof ApiAvatarRoute
88
+ '/api/blocks': typeof ApiBlocksRoute
89
+ '/api/inbox': typeof ApiInboxRoute
90
+ '/api/query': typeof ApiQueryRoute
91
+ '/api/status': typeof ApiStatusRoute
92
+ }
93
+ export interface FileRoutesByTo {
94
+ '/': typeof IndexRoute
95
+ '/blocks': typeof BlocksRoute
96
+ '/dms': typeof DmsRoute
97
+ '/inbox': typeof InboxRoute
98
+ '/mentions': typeof MentionsRoute
99
+ '/api/action': typeof ApiActionRoute
100
+ '/api/avatar': typeof ApiAvatarRoute
101
+ '/api/blocks': typeof ApiBlocksRoute
102
+ '/api/inbox': typeof ApiInboxRoute
103
+ '/api/query': typeof ApiQueryRoute
104
+ '/api/status': typeof ApiStatusRoute
105
+ }
106
+ export interface FileRoutesById {
107
+ __root__: typeof rootRouteImport
108
+ '/': typeof IndexRoute
109
+ '/blocks': typeof BlocksRoute
110
+ '/dms': typeof DmsRoute
111
+ '/inbox': typeof InboxRoute
112
+ '/mentions': typeof MentionsRoute
113
+ '/api/action': typeof ApiActionRoute
114
+ '/api/avatar': typeof ApiAvatarRoute
115
+ '/api/blocks': typeof ApiBlocksRoute
116
+ '/api/inbox': typeof ApiInboxRoute
117
+ '/api/query': typeof ApiQueryRoute
118
+ '/api/status': typeof ApiStatusRoute
119
+ }
120
+ export interface FileRouteTypes {
121
+ fileRoutesByFullPath: FileRoutesByFullPath
122
+ fullPaths:
123
+ | '/'
124
+ | '/blocks'
125
+ | '/dms'
126
+ | '/inbox'
127
+ | '/mentions'
128
+ | '/api/action'
129
+ | '/api/avatar'
130
+ | '/api/blocks'
131
+ | '/api/inbox'
132
+ | '/api/query'
133
+ | '/api/status'
134
+ fileRoutesByTo: FileRoutesByTo
135
+ to:
136
+ | '/'
137
+ | '/blocks'
138
+ | '/dms'
139
+ | '/inbox'
140
+ | '/mentions'
141
+ | '/api/action'
142
+ | '/api/avatar'
143
+ | '/api/blocks'
144
+ | '/api/inbox'
145
+ | '/api/query'
146
+ | '/api/status'
147
+ id:
148
+ | '__root__'
149
+ | '/'
150
+ | '/blocks'
151
+ | '/dms'
152
+ | '/inbox'
153
+ | '/mentions'
154
+ | '/api/action'
155
+ | '/api/avatar'
156
+ | '/api/blocks'
157
+ | '/api/inbox'
158
+ | '/api/query'
159
+ | '/api/status'
160
+ fileRoutesById: FileRoutesById
161
+ }
162
+ export interface RootRouteChildren {
163
+ IndexRoute: typeof IndexRoute
164
+ BlocksRoute: typeof BlocksRoute
165
+ DmsRoute: typeof DmsRoute
166
+ InboxRoute: typeof InboxRoute
167
+ MentionsRoute: typeof MentionsRoute
168
+ ApiActionRoute: typeof ApiActionRoute
169
+ ApiAvatarRoute: typeof ApiAvatarRoute
170
+ ApiBlocksRoute: typeof ApiBlocksRoute
171
+ ApiInboxRoute: typeof ApiInboxRoute
172
+ ApiQueryRoute: typeof ApiQueryRoute
173
+ ApiStatusRoute: typeof ApiStatusRoute
174
+ }
175
+
176
+ declare module '@tanstack/react-router' {
177
+ interface FileRoutesByPath {
178
+ '/mentions': {
179
+ id: '/mentions'
180
+ path: '/mentions'
181
+ fullPath: '/mentions'
182
+ preLoaderRoute: typeof MentionsRouteImport
183
+ parentRoute: typeof rootRouteImport
184
+ }
185
+ '/inbox': {
186
+ id: '/inbox'
187
+ path: '/inbox'
188
+ fullPath: '/inbox'
189
+ preLoaderRoute: typeof InboxRouteImport
190
+ parentRoute: typeof rootRouteImport
191
+ }
192
+ '/dms': {
193
+ id: '/dms'
194
+ path: '/dms'
195
+ fullPath: '/dms'
196
+ preLoaderRoute: typeof DmsRouteImport
197
+ parentRoute: typeof rootRouteImport
198
+ }
199
+ '/blocks': {
200
+ id: '/blocks'
201
+ path: '/blocks'
202
+ fullPath: '/blocks'
203
+ preLoaderRoute: typeof BlocksRouteImport
204
+ parentRoute: typeof rootRouteImport
205
+ }
206
+ '/': {
207
+ id: '/'
208
+ path: '/'
209
+ fullPath: '/'
210
+ preLoaderRoute: typeof IndexRouteImport
211
+ parentRoute: typeof rootRouteImport
212
+ }
213
+ '/api/status': {
214
+ id: '/api/status'
215
+ path: '/api/status'
216
+ fullPath: '/api/status'
217
+ preLoaderRoute: typeof ApiStatusRouteImport
218
+ parentRoute: typeof rootRouteImport
219
+ }
220
+ '/api/query': {
221
+ id: '/api/query'
222
+ path: '/api/query'
223
+ fullPath: '/api/query'
224
+ preLoaderRoute: typeof ApiQueryRouteImport
225
+ parentRoute: typeof rootRouteImport
226
+ }
227
+ '/api/inbox': {
228
+ id: '/api/inbox'
229
+ path: '/api/inbox'
230
+ fullPath: '/api/inbox'
231
+ preLoaderRoute: typeof ApiInboxRouteImport
232
+ parentRoute: typeof rootRouteImport
233
+ }
234
+ '/api/blocks': {
235
+ id: '/api/blocks'
236
+ path: '/api/blocks'
237
+ fullPath: '/api/blocks'
238
+ preLoaderRoute: typeof ApiBlocksRouteImport
239
+ parentRoute: typeof rootRouteImport
240
+ }
241
+ '/api/avatar': {
242
+ id: '/api/avatar'
243
+ path: '/api/avatar'
244
+ fullPath: '/api/avatar'
245
+ preLoaderRoute: typeof ApiAvatarRouteImport
246
+ parentRoute: typeof rootRouteImport
247
+ }
248
+ '/api/action': {
249
+ id: '/api/action'
250
+ path: '/api/action'
251
+ fullPath: '/api/action'
252
+ preLoaderRoute: typeof ApiActionRouteImport
253
+ parentRoute: typeof rootRouteImport
254
+ }
255
+ }
256
+ }
257
+
258
+ const rootRouteChildren: RootRouteChildren = {
259
+ IndexRoute: IndexRoute,
260
+ BlocksRoute: BlocksRoute,
261
+ DmsRoute: DmsRoute,
262
+ InboxRoute: InboxRoute,
263
+ MentionsRoute: MentionsRoute,
264
+ ApiActionRoute: ApiActionRoute,
265
+ ApiAvatarRoute: ApiAvatarRoute,
266
+ ApiBlocksRoute: ApiBlocksRoute,
267
+ ApiInboxRoute: ApiInboxRoute,
268
+ ApiQueryRoute: ApiQueryRoute,
269
+ ApiStatusRoute: ApiStatusRoute,
270
+ }
271
+ export const routeTree = rootRouteImport
272
+ ._addFileChildren(rootRouteChildren)
273
+ ._addFileTypes<FileRouteTypes>()
274
+
275
+ import type { getRouter } from './router.tsx'
276
+ import type { createStart } from '@tanstack/react-start'
277
+ declare module '@tanstack/react-start' {
278
+ interface Register {
279
+ ssr: true
280
+ router: Awaited<ReturnType<typeof getRouter>>
281
+ }
282
+ }
package/src/router.tsx ADDED
@@ -0,0 +1,20 @@
1
+ import { createRouter as createTanStackRouter } from "@tanstack/react-router";
2
+ import { routeTree } from "./routeTree.gen";
3
+
4
+ export function getRouter() {
5
+ const router = createTanStackRouter({
6
+ routeTree,
7
+
8
+ scrollRestoration: true,
9
+ defaultPreload: "intent",
10
+ defaultPreloadStaleTime: 0,
11
+ });
12
+
13
+ return router;
14
+ }
15
+
16
+ declare module "@tanstack/react-router" {
17
+ interface Register {
18
+ router: ReturnType<typeof getRouter>;
19
+ }
20
+ }
@@ -0,0 +1,64 @@
1
+ import { TanStackDevtools } from "@tanstack/react-devtools";
2
+ import { createRootRoute, HeadContent, Scripts } from "@tanstack/react-router";
3
+ import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
4
+ import type { ReactNode } from "react";
5
+ import { AppNav } from "#/components/AppNav";
6
+ import { ThemeProvider, themeScript } from "#/lib/theme";
7
+ import { bodyClass, siteShellClass } from "#/lib/ui";
8
+
9
+ import appCss from "../styles.css?url";
10
+
11
+ export const Route = createRootRoute({
12
+ head: () => ({
13
+ meta: [
14
+ {
15
+ charSet: "utf-8",
16
+ },
17
+ {
18
+ name: "viewport",
19
+ content: "width=device-width, initial-scale=1",
20
+ },
21
+ {
22
+ title: "birdclaw",
23
+ },
24
+ ],
25
+ links: [
26
+ {
27
+ rel: "stylesheet",
28
+ href: appCss,
29
+ },
30
+ ],
31
+ }),
32
+ shellComponent: RootDocument,
33
+ });
34
+
35
+ function RootDocument({ children }: { children: ReactNode }) {
36
+ return (
37
+ <html lang="en" suppressHydrationWarning>
38
+ <head>
39
+ <HeadContent />
40
+ <script suppressHydrationWarning>{themeScript}</script>
41
+ </head>
42
+ <body className={bodyClass}>
43
+ <ThemeProvider>
44
+ <div className={siteShellClass}>
45
+ <AppNav />
46
+ {children}
47
+ </div>
48
+ </ThemeProvider>
49
+ <TanStackDevtools
50
+ config={{
51
+ position: "bottom-right",
52
+ }}
53
+ plugins={[
54
+ {
55
+ name: "Tanstack Router",
56
+ render: <TanStackRouterDevtoolsPanel />,
57
+ },
58
+ ]}
59
+ />
60
+ <Scripts />
61
+ </body>
62
+ </html>
63
+ );
64
+ }
@@ -0,0 +1,88 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { addBlock, removeBlock, syncBlocks } from "#/lib/blocks";
3
+ import { scoreInbox } from "#/lib/inbox";
4
+ import { addMute, removeMute } from "#/lib/mutes";
5
+ import { createDmReply, createPost, createTweetReply } from "#/lib/queries";
6
+ import type { InboxKind } from "#/lib/types";
7
+
8
+ export const Route = createFileRoute("/api/action")({
9
+ server: {
10
+ handlers: {
11
+ POST: async ({ request }) => {
12
+ const body = (await request.json()) as Record<string, string>;
13
+ let result: unknown;
14
+
15
+ if (body.kind === "post") {
16
+ result = await createPost(
17
+ body.accountId || "acct_primary",
18
+ body.text || "",
19
+ );
20
+ } else if (body.kind === "replyTweet") {
21
+ result = await createTweetReply(
22
+ body.accountId || "acct_primary",
23
+ body.tweetId || "",
24
+ body.text || "",
25
+ );
26
+ } else if (body.kind === "replyDm") {
27
+ result = await createDmReply(
28
+ body.conversationId || "",
29
+ body.text || "",
30
+ );
31
+ } else if (body.kind === "scoreInbox") {
32
+ result = await scoreInbox({
33
+ kind: ((body.scoreKind as InboxKind) || "mixed") as InboxKind,
34
+ limit: body.limit ? Number(body.limit) : 8,
35
+ });
36
+ } else if (body.kind === "blockProfile") {
37
+ result = await addBlock(
38
+ body.accountId || "acct_primary",
39
+ body.query || "",
40
+ {
41
+ transport: body.transport,
42
+ },
43
+ );
44
+ } else if (body.kind === "unblockProfile") {
45
+ result = await removeBlock(
46
+ body.accountId || "acct_primary",
47
+ body.query || "",
48
+ {
49
+ transport: body.transport,
50
+ },
51
+ );
52
+ } else if (body.kind === "muteProfile") {
53
+ result = await addMute(
54
+ body.accountId || "acct_primary",
55
+ body.query || "",
56
+ {
57
+ transport: body.transport,
58
+ },
59
+ );
60
+ } else if (body.kind === "unmuteProfile") {
61
+ result = await removeMute(
62
+ body.accountId || "acct_primary",
63
+ body.query || "",
64
+ {
65
+ transport: body.transport,
66
+ },
67
+ );
68
+ } else if (body.kind === "syncBlocks") {
69
+ result = await syncBlocks(body.accountId || "acct_primary");
70
+ } else {
71
+ return new Response(
72
+ JSON.stringify({ ok: false, message: "Unknown action kind" }),
73
+ {
74
+ status: 400,
75
+ headers: { "content-type": "application/json" },
76
+ },
77
+ );
78
+ }
79
+
80
+ return new Response(JSON.stringify(result), {
81
+ headers: {
82
+ "content-type": "application/json",
83
+ },
84
+ });
85
+ },
86
+ },
87
+ },
88
+ });
@@ -0,0 +1,41 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { readCachedAvatar } from "#/lib/avatar-cache";
3
+
4
+ export const Route = createFileRoute("/api/avatar")({
5
+ server: {
6
+ handlers: {
7
+ GET: async ({ request }) => {
8
+ const url = new URL(request.url);
9
+ const profileId = url.searchParams.get("profileId")?.trim();
10
+
11
+ if (!profileId) {
12
+ return new Response(
13
+ JSON.stringify({ ok: false, message: "Missing profileId" }),
14
+ {
15
+ status: 400,
16
+ headers: { "content-type": "application/json" },
17
+ },
18
+ );
19
+ }
20
+
21
+ const avatar = await readCachedAvatar(profileId);
22
+ if (!avatar) {
23
+ return new Response(
24
+ JSON.stringify({ ok: false, message: "Avatar not found" }),
25
+ {
26
+ status: 404,
27
+ headers: { "content-type": "application/json" },
28
+ },
29
+ );
30
+ }
31
+
32
+ return new Response(new Uint8Array(avatar.buffer), {
33
+ headers: {
34
+ "cache-control": "public, max-age=86400, immutable",
35
+ "content-type": avatar.contentType,
36
+ },
37
+ });
38
+ },
39
+ },
40
+ },
41
+ });
@@ -0,0 +1,32 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { getBlocksResponse } from "#/lib/blocks";
3
+
4
+ function parseNumber(value: string | null) {
5
+ if (!value) return undefined;
6
+ const parsed = Number(value);
7
+ return Number.isFinite(parsed) ? parsed : undefined;
8
+ }
9
+
10
+ export const Route = createFileRoute("/api/blocks")({
11
+ server: {
12
+ handlers: {
13
+ GET: ({ request }) => {
14
+ const url = new URL(request.url);
15
+ return new Response(
16
+ JSON.stringify(
17
+ getBlocksResponse({
18
+ accountId: url.searchParams.get("account") ?? undefined,
19
+ search: url.searchParams.get("search") ?? undefined,
20
+ limit: parseNumber(url.searchParams.get("limit")) ?? 12,
21
+ }),
22
+ ),
23
+ {
24
+ headers: {
25
+ "content-type": "application/json",
26
+ },
27
+ },
28
+ );
29
+ },
30
+ },
31
+ },
32
+ });
@@ -0,0 +1,35 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { listInboxItems } from "#/lib/inbox";
3
+ import type { InboxKind } from "#/lib/types";
4
+
5
+ function parseNumber(value: string | null) {
6
+ if (!value) return undefined;
7
+ const parsed = Number(value);
8
+ return Number.isFinite(parsed) ? parsed : undefined;
9
+ }
10
+
11
+ export const Route = createFileRoute("/api/inbox")({
12
+ server: {
13
+ handlers: {
14
+ GET: ({ request }) => {
15
+ const url = new URL(request.url);
16
+ const kind = (url.searchParams.get("kind") ?? "mixed") as InboxKind;
17
+ return new Response(
18
+ JSON.stringify(
19
+ listInboxItems({
20
+ kind: kind === "mentions" || kind === "dms" ? kind : "mixed",
21
+ minScore: parseNumber(url.searchParams.get("minScore")),
22
+ hideLowSignal: url.searchParams.get("hideLowSignal") === "1",
23
+ limit: parseNumber(url.searchParams.get("limit")) ?? 20,
24
+ }),
25
+ ),
26
+ {
27
+ headers: {
28
+ "content-type": "application/json",
29
+ },
30
+ },
31
+ );
32
+ },
33
+ },
34
+ },
35
+ });
@@ -0,0 +1,72 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { queryResource } from "#/lib/queries";
3
+ import type { ReplyFilter, ResourceKind } from "#/lib/types";
4
+
5
+ function json(data: unknown) {
6
+ return new Response(JSON.stringify(data), {
7
+ headers: {
8
+ "content-type": "application/json",
9
+ },
10
+ });
11
+ }
12
+
13
+ function parseReplyFilter(value: string | null): ReplyFilter {
14
+ if (value === "replied" || value === "unreplied") {
15
+ return value;
16
+ }
17
+ return "all";
18
+ }
19
+
20
+ function parseNumber(value: string | null) {
21
+ if (!value) return undefined;
22
+ const parsed = Number(value);
23
+ return Number.isFinite(parsed) ? parsed : undefined;
24
+ }
25
+
26
+ export const Route = createFileRoute("/api/query")({
27
+ server: {
28
+ handlers: {
29
+ GET: ({ request }) => {
30
+ const url = new URL(request.url);
31
+ const resource = (url.searchParams.get("resource") ??
32
+ "home") as ResourceKind;
33
+ const baseFilters = {
34
+ account: url.searchParams.get("account") ?? undefined,
35
+ search: url.searchParams.get("search") ?? undefined,
36
+ replyFilter: parseReplyFilter(url.searchParams.get("replyFilter")),
37
+ limit: parseNumber(url.searchParams.get("limit")) ?? undefined,
38
+ };
39
+
40
+ if (resource === "dms") {
41
+ return json(
42
+ queryResource("dms", {
43
+ ...baseFilters,
44
+ participant: url.searchParams.get("participant") ?? undefined,
45
+ minFollowers: parseNumber(url.searchParams.get("minFollowers")),
46
+ maxFollowers: parseNumber(url.searchParams.get("maxFollowers")),
47
+ minInfluenceScore: parseNumber(
48
+ url.searchParams.get("minInfluenceScore"),
49
+ ),
50
+ maxInfluenceScore: parseNumber(
51
+ url.searchParams.get("maxInfluenceScore"),
52
+ ),
53
+ sort:
54
+ url.searchParams.get("sort") === "influence"
55
+ ? "influence"
56
+ : "recent",
57
+ conversationId:
58
+ url.searchParams.get("conversationId") ?? undefined,
59
+ }),
60
+ );
61
+ }
62
+
63
+ return json(
64
+ queryResource(resource, {
65
+ ...baseFilters,
66
+ resource,
67
+ }),
68
+ );
69
+ },
70
+ },
71
+ },
72
+ });
@@ -0,0 +1,15 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { getQueryEnvelope } from "#/lib/queries";
3
+
4
+ export const Route = createFileRoute("/api/status")({
5
+ server: {
6
+ handlers: {
7
+ GET: async () =>
8
+ new Response(JSON.stringify(await getQueryEnvelope()), {
9
+ headers: {
10
+ "content-type": "application/json",
11
+ },
12
+ }),
13
+ },
14
+ },
15
+ });