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.
- package/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/bin/birdclaw.mjs +28 -0
- package/package.json +100 -0
- package/playwright.config.ts +31 -0
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +3 -0
- package/src/cli-moderation.ts +210 -0
- package/src/cli.ts +450 -0
- package/src/components/AppNav.tsx +49 -0
- package/src/components/AvatarChip.tsx +47 -0
- package/src/components/DmWorkspace.tsx +246 -0
- package/src/components/EmbeddedTweetCard.tsx +44 -0
- package/src/components/InboxCard.tsx +136 -0
- package/src/components/ProfilePreview.tsx +55 -0
- package/src/components/ThemeSlider.tsx +124 -0
- package/src/components/TimelineCard.tsx +118 -0
- package/src/components/TweetMediaGrid.tsx +39 -0
- package/src/components/TweetRichText.tsx +85 -0
- package/src/lib/actions-transport.ts +173 -0
- package/src/lib/archive-finder.ts +128 -0
- package/src/lib/archive-import.ts +736 -0
- package/src/lib/avatar-cache.ts +184 -0
- package/src/lib/bird-actions.ts +200 -0
- package/src/lib/bird.ts +183 -0
- package/src/lib/blocklist.ts +119 -0
- package/src/lib/blocks-write.ts +118 -0
- package/src/lib/blocks.ts +326 -0
- package/src/lib/config.ts +152 -0
- package/src/lib/db.ts +326 -0
- package/src/lib/dms-live.ts +279 -0
- package/src/lib/inbox.ts +210 -0
- package/src/lib/mentions-export.ts +147 -0
- package/src/lib/mentions-live.ts +475 -0
- package/src/lib/moderation-target.ts +171 -0
- package/src/lib/moderation-write.ts +72 -0
- package/src/lib/mutes-write.ts +118 -0
- package/src/lib/mutes.ts +77 -0
- package/src/lib/openai.ts +86 -0
- package/src/lib/present.ts +20 -0
- package/src/lib/profile-hydration.ts +144 -0
- package/src/lib/profile-replies.ts +60 -0
- package/src/lib/queries.ts +725 -0
- package/src/lib/seed.ts +486 -0
- package/src/lib/sync-cache.ts +65 -0
- package/src/lib/theme-transition.ts +117 -0
- package/src/lib/theme.tsx +157 -0
- package/src/lib/tweet-render.ts +115 -0
- package/src/lib/types.ts +316 -0
- package/src/lib/ui.ts +270 -0
- package/src/lib/x-profile.ts +203 -0
- package/src/lib/x-web.ts +168 -0
- package/src/lib/xurl.ts +492 -0
- package/src/routeTree.gen.ts +282 -0
- package/src/router.tsx +20 -0
- package/src/routes/__root.tsx +64 -0
- package/src/routes/api/action.tsx +88 -0
- package/src/routes/api/avatar.tsx +41 -0
- package/src/routes/api/blocks.tsx +32 -0
- package/src/routes/api/inbox.tsx +35 -0
- package/src/routes/api/query.tsx +72 -0
- package/src/routes/api/status.tsx +15 -0
- package/src/routes/blocks.tsx +352 -0
- package/src/routes/dms.tsx +262 -0
- package/src/routes/inbox.tsx +201 -0
- package/src/routes/index.tsx +125 -0
- package/src/routes/mentions.tsx +125 -0
- package/src/styles.css +109 -0
- package/tsconfig.json +30 -0
- 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
|
+
});
|