birdclaw 0.1.0 → 0.2.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/README.md +117 -10
- package/bin/birdclaw.mjs +3 -1
- package/package.json +4 -3
- package/src/cli.ts +237 -4
- package/src/components/AppNav.tsx +2 -0
- package/src/components/SavedTimelineView.tsx +115 -0
- package/src/components/TimelineCard.tsx +26 -17
- package/src/lib/archive-import.ts +193 -16
- package/src/lib/backup.ts +1617 -0
- package/src/lib/bird.ts +163 -24
- package/src/lib/blocks-write.ts +2 -2
- package/src/lib/blocks.ts +2 -7
- package/src/lib/bookmark-sync-job.ts +446 -0
- package/src/lib/config.ts +6 -0
- package/src/lib/db.ts +82 -4
- package/src/lib/mentions-live.ts +2 -5
- package/src/lib/moderation-target.ts +4 -4
- package/src/lib/mutes-write.ts +2 -2
- package/src/lib/queries.ts +70 -1
- package/src/lib/seed.ts +4 -2
- package/src/lib/theme-transition.ts +6 -10
- package/src/lib/theme.tsx +13 -5
- package/src/lib/timeline-collections-live.ts +406 -0
- package/src/lib/types.ts +9 -1
- package/src/lib/xurl.ts +90 -4
- package/src/routeTree.gen.ts +42 -0
- package/src/routes/__root.tsx +6 -1
- package/src/routes/api/action.tsx +14 -4
- package/src/routes/api/inbox.tsx +3 -1
- package/src/routes/api/query.tsx +20 -2
- package/src/routes/api/status.tsx +6 -3
- package/src/routes/bookmarks.tsx +18 -0
- package/src/routes/likes.tsx +18 -0
- package/tsconfig.json +0 -1
package/src/lib/xurl.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { promisify } from "node:util";
|
|
|
3
3
|
import type {
|
|
4
4
|
TransportStatus,
|
|
5
5
|
XurlMentionsResponse,
|
|
6
|
+
XurlMentionUser,
|
|
6
7
|
XurlUserTweet,
|
|
7
8
|
} from "./types";
|
|
8
9
|
|
|
@@ -131,7 +132,7 @@ export async function getTransportStatus(): Promise<TransportStatus> {
|
|
|
131
132
|
return transportStatusCache.pending;
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
const pending = (async () => {
|
|
135
|
+
const pending: Promise<TransportStatus> = (async () => {
|
|
135
136
|
const installed = await hasXurl();
|
|
136
137
|
if (!installed) {
|
|
137
138
|
return {
|
|
@@ -242,7 +243,7 @@ export async function lookupUsersByIds(ids: string[]) {
|
|
|
242
243
|
});
|
|
243
244
|
const payload = await runJsonCommand([`/2/users?${query.toString()}`]);
|
|
244
245
|
const data = payload.data;
|
|
245
|
-
return Array.isArray(data) ? (data as
|
|
246
|
+
return Array.isArray(data) ? (data as XurlMentionUser[]) : [];
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
export async function lookupUsersByHandles(handles: string[]) {
|
|
@@ -257,7 +258,7 @@ export async function lookupUsersByHandles(handles: string[]) {
|
|
|
257
258
|
});
|
|
258
259
|
const payload = await runJsonCommand([`/2/users/by?${query.toString()}`]);
|
|
259
260
|
const data = payload.data;
|
|
260
|
-
return Array.isArray(data) ? (data as
|
|
261
|
+
return Array.isArray(data) ? (data as XurlMentionUser[]) : [];
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
export async function lookupAuthenticatedUser() {
|
|
@@ -357,6 +358,91 @@ export async function listMentionsViaXurl({
|
|
|
357
358
|
};
|
|
358
359
|
}
|
|
359
360
|
|
|
361
|
+
async function listTimelineCollectionViaXurl({
|
|
362
|
+
collection,
|
|
363
|
+
maxResults,
|
|
364
|
+
username,
|
|
365
|
+
userId,
|
|
366
|
+
paginationToken,
|
|
367
|
+
}: {
|
|
368
|
+
collection: "liked_tweets" | "bookmarks";
|
|
369
|
+
maxResults: number;
|
|
370
|
+
username?: string;
|
|
371
|
+
userId?: string;
|
|
372
|
+
paginationToken?: string;
|
|
373
|
+
}): Promise<XurlMentionsResponse> {
|
|
374
|
+
let resolvedUserId = userId;
|
|
375
|
+
if (!resolvedUserId) {
|
|
376
|
+
if (username) {
|
|
377
|
+
const [user] = await lookupUsersByHandles([username]);
|
|
378
|
+
if (!user?.id) {
|
|
379
|
+
throw new Error(`Could not resolve X user id for @${username}`);
|
|
380
|
+
}
|
|
381
|
+
resolvedUserId = String(user.id);
|
|
382
|
+
} else {
|
|
383
|
+
const user = await lookupAuthenticatedUser();
|
|
384
|
+
if (!user?.id) {
|
|
385
|
+
throw new Error("Could not resolve authenticated X user id");
|
|
386
|
+
}
|
|
387
|
+
resolvedUserId = String(user.id);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const query = new URLSearchParams({
|
|
392
|
+
max_results: String(maxResults),
|
|
393
|
+
expansions: "author_id",
|
|
394
|
+
"tweet.fields": "created_at,conversation_id,entities,public_metrics",
|
|
395
|
+
"user.fields":
|
|
396
|
+
"description,public_metrics,profile_image_url,created_at,verified",
|
|
397
|
+
});
|
|
398
|
+
if (paginationToken) {
|
|
399
|
+
query.set("pagination_token", paginationToken);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const payload = await runJsonCommand([
|
|
403
|
+
"--auth",
|
|
404
|
+
"oauth2",
|
|
405
|
+
`/2/users/${resolvedUserId}/${collection}?${query.toString()}`,
|
|
406
|
+
]);
|
|
407
|
+
return {
|
|
408
|
+
data: Array.isArray(payload.data)
|
|
409
|
+
? (payload.data as XurlMentionsResponse["data"])
|
|
410
|
+
: [],
|
|
411
|
+
includes:
|
|
412
|
+
payload.includes && typeof payload.includes === "object"
|
|
413
|
+
? (payload.includes as XurlMentionsResponse["includes"])
|
|
414
|
+
: undefined,
|
|
415
|
+
meta:
|
|
416
|
+
payload.meta && typeof payload.meta === "object"
|
|
417
|
+
? (payload.meta as XurlMentionsResponse["meta"])
|
|
418
|
+
: undefined,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export async function listLikedTweetsViaXurl(options: {
|
|
423
|
+
maxResults: number;
|
|
424
|
+
username?: string;
|
|
425
|
+
userId?: string;
|
|
426
|
+
paginationToken?: string;
|
|
427
|
+
}): Promise<XurlMentionsResponse> {
|
|
428
|
+
return listTimelineCollectionViaXurl({
|
|
429
|
+
...options,
|
|
430
|
+
collection: "liked_tweets",
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export async function listBookmarkedTweetsViaXurl(options: {
|
|
435
|
+
maxResults: number;
|
|
436
|
+
username?: string;
|
|
437
|
+
userId?: string;
|
|
438
|
+
paginationToken?: string;
|
|
439
|
+
}): Promise<XurlMentionsResponse> {
|
|
440
|
+
return listTimelineCollectionViaXurl({
|
|
441
|
+
...options,
|
|
442
|
+
collection: "bookmarks",
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
360
446
|
export async function listBlockedUsers(
|
|
361
447
|
userId: string,
|
|
362
448
|
paginationToken?: string,
|
|
@@ -373,7 +459,7 @@ export async function listBlockedUsers(
|
|
|
373
459
|
`/2/users/${userId}/blocking?${query}`,
|
|
374
460
|
]);
|
|
375
461
|
const data = Array.isArray(payload.data)
|
|
376
|
-
? (payload.data as
|
|
462
|
+
? (payload.data as XurlMentionUser[])
|
|
377
463
|
: [];
|
|
378
464
|
const meta =
|
|
379
465
|
payload.meta && typeof payload.meta === "object"
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import { Route as rootRouteImport } from './routes/__root'
|
|
12
12
|
import { Route as MentionsRouteImport } from './routes/mentions'
|
|
13
|
+
import { Route as LikesRouteImport } from './routes/likes'
|
|
13
14
|
import { Route as InboxRouteImport } from './routes/inbox'
|
|
14
15
|
import { Route as DmsRouteImport } from './routes/dms'
|
|
16
|
+
import { Route as BookmarksRouteImport } from './routes/bookmarks'
|
|
15
17
|
import { Route as BlocksRouteImport } from './routes/blocks'
|
|
16
18
|
import { Route as IndexRouteImport } from './routes/index'
|
|
17
19
|
import { Route as ApiStatusRouteImport } from './routes/api/status'
|
|
@@ -26,6 +28,11 @@ const MentionsRoute = MentionsRouteImport.update({
|
|
|
26
28
|
path: '/mentions',
|
|
27
29
|
getParentRoute: () => rootRouteImport,
|
|
28
30
|
} as any)
|
|
31
|
+
const LikesRoute = LikesRouteImport.update({
|
|
32
|
+
id: '/likes',
|
|
33
|
+
path: '/likes',
|
|
34
|
+
getParentRoute: () => rootRouteImport,
|
|
35
|
+
} as any)
|
|
29
36
|
const InboxRoute = InboxRouteImport.update({
|
|
30
37
|
id: '/inbox',
|
|
31
38
|
path: '/inbox',
|
|
@@ -36,6 +43,11 @@ const DmsRoute = DmsRouteImport.update({
|
|
|
36
43
|
path: '/dms',
|
|
37
44
|
getParentRoute: () => rootRouteImport,
|
|
38
45
|
} as any)
|
|
46
|
+
const BookmarksRoute = BookmarksRouteImport.update({
|
|
47
|
+
id: '/bookmarks',
|
|
48
|
+
path: '/bookmarks',
|
|
49
|
+
getParentRoute: () => rootRouteImport,
|
|
50
|
+
} as any)
|
|
39
51
|
const BlocksRoute = BlocksRouteImport.update({
|
|
40
52
|
id: '/blocks',
|
|
41
53
|
path: '/blocks',
|
|
@@ -80,8 +92,10 @@ const ApiActionRoute = ApiActionRouteImport.update({
|
|
|
80
92
|
export interface FileRoutesByFullPath {
|
|
81
93
|
'/': typeof IndexRoute
|
|
82
94
|
'/blocks': typeof BlocksRoute
|
|
95
|
+
'/bookmarks': typeof BookmarksRoute
|
|
83
96
|
'/dms': typeof DmsRoute
|
|
84
97
|
'/inbox': typeof InboxRoute
|
|
98
|
+
'/likes': typeof LikesRoute
|
|
85
99
|
'/mentions': typeof MentionsRoute
|
|
86
100
|
'/api/action': typeof ApiActionRoute
|
|
87
101
|
'/api/avatar': typeof ApiAvatarRoute
|
|
@@ -93,8 +107,10 @@ export interface FileRoutesByFullPath {
|
|
|
93
107
|
export interface FileRoutesByTo {
|
|
94
108
|
'/': typeof IndexRoute
|
|
95
109
|
'/blocks': typeof BlocksRoute
|
|
110
|
+
'/bookmarks': typeof BookmarksRoute
|
|
96
111
|
'/dms': typeof DmsRoute
|
|
97
112
|
'/inbox': typeof InboxRoute
|
|
113
|
+
'/likes': typeof LikesRoute
|
|
98
114
|
'/mentions': typeof MentionsRoute
|
|
99
115
|
'/api/action': typeof ApiActionRoute
|
|
100
116
|
'/api/avatar': typeof ApiAvatarRoute
|
|
@@ -107,8 +123,10 @@ export interface FileRoutesById {
|
|
|
107
123
|
__root__: typeof rootRouteImport
|
|
108
124
|
'/': typeof IndexRoute
|
|
109
125
|
'/blocks': typeof BlocksRoute
|
|
126
|
+
'/bookmarks': typeof BookmarksRoute
|
|
110
127
|
'/dms': typeof DmsRoute
|
|
111
128
|
'/inbox': typeof InboxRoute
|
|
129
|
+
'/likes': typeof LikesRoute
|
|
112
130
|
'/mentions': typeof MentionsRoute
|
|
113
131
|
'/api/action': typeof ApiActionRoute
|
|
114
132
|
'/api/avatar': typeof ApiAvatarRoute
|
|
@@ -122,8 +140,10 @@ export interface FileRouteTypes {
|
|
|
122
140
|
fullPaths:
|
|
123
141
|
| '/'
|
|
124
142
|
| '/blocks'
|
|
143
|
+
| '/bookmarks'
|
|
125
144
|
| '/dms'
|
|
126
145
|
| '/inbox'
|
|
146
|
+
| '/likes'
|
|
127
147
|
| '/mentions'
|
|
128
148
|
| '/api/action'
|
|
129
149
|
| '/api/avatar'
|
|
@@ -135,8 +155,10 @@ export interface FileRouteTypes {
|
|
|
135
155
|
to:
|
|
136
156
|
| '/'
|
|
137
157
|
| '/blocks'
|
|
158
|
+
| '/bookmarks'
|
|
138
159
|
| '/dms'
|
|
139
160
|
| '/inbox'
|
|
161
|
+
| '/likes'
|
|
140
162
|
| '/mentions'
|
|
141
163
|
| '/api/action'
|
|
142
164
|
| '/api/avatar'
|
|
@@ -148,8 +170,10 @@ export interface FileRouteTypes {
|
|
|
148
170
|
| '__root__'
|
|
149
171
|
| '/'
|
|
150
172
|
| '/blocks'
|
|
173
|
+
| '/bookmarks'
|
|
151
174
|
| '/dms'
|
|
152
175
|
| '/inbox'
|
|
176
|
+
| '/likes'
|
|
153
177
|
| '/mentions'
|
|
154
178
|
| '/api/action'
|
|
155
179
|
| '/api/avatar'
|
|
@@ -162,8 +186,10 @@ export interface FileRouteTypes {
|
|
|
162
186
|
export interface RootRouteChildren {
|
|
163
187
|
IndexRoute: typeof IndexRoute
|
|
164
188
|
BlocksRoute: typeof BlocksRoute
|
|
189
|
+
BookmarksRoute: typeof BookmarksRoute
|
|
165
190
|
DmsRoute: typeof DmsRoute
|
|
166
191
|
InboxRoute: typeof InboxRoute
|
|
192
|
+
LikesRoute: typeof LikesRoute
|
|
167
193
|
MentionsRoute: typeof MentionsRoute
|
|
168
194
|
ApiActionRoute: typeof ApiActionRoute
|
|
169
195
|
ApiAvatarRoute: typeof ApiAvatarRoute
|
|
@@ -182,6 +208,13 @@ declare module '@tanstack/react-router' {
|
|
|
182
208
|
preLoaderRoute: typeof MentionsRouteImport
|
|
183
209
|
parentRoute: typeof rootRouteImport
|
|
184
210
|
}
|
|
211
|
+
'/likes': {
|
|
212
|
+
id: '/likes'
|
|
213
|
+
path: '/likes'
|
|
214
|
+
fullPath: '/likes'
|
|
215
|
+
preLoaderRoute: typeof LikesRouteImport
|
|
216
|
+
parentRoute: typeof rootRouteImport
|
|
217
|
+
}
|
|
185
218
|
'/inbox': {
|
|
186
219
|
id: '/inbox'
|
|
187
220
|
path: '/inbox'
|
|
@@ -196,6 +229,13 @@ declare module '@tanstack/react-router' {
|
|
|
196
229
|
preLoaderRoute: typeof DmsRouteImport
|
|
197
230
|
parentRoute: typeof rootRouteImport
|
|
198
231
|
}
|
|
232
|
+
'/bookmarks': {
|
|
233
|
+
id: '/bookmarks'
|
|
234
|
+
path: '/bookmarks'
|
|
235
|
+
fullPath: '/bookmarks'
|
|
236
|
+
preLoaderRoute: typeof BookmarksRouteImport
|
|
237
|
+
parentRoute: typeof rootRouteImport
|
|
238
|
+
}
|
|
199
239
|
'/blocks': {
|
|
200
240
|
id: '/blocks'
|
|
201
241
|
path: '/blocks'
|
|
@@ -258,8 +298,10 @@ declare module '@tanstack/react-router' {
|
|
|
258
298
|
const rootRouteChildren: RootRouteChildren = {
|
|
259
299
|
IndexRoute: IndexRoute,
|
|
260
300
|
BlocksRoute: BlocksRoute,
|
|
301
|
+
BookmarksRoute: BookmarksRoute,
|
|
261
302
|
DmsRoute: DmsRoute,
|
|
262
303
|
InboxRoute: InboxRoute,
|
|
304
|
+
LikesRoute: LikesRoute,
|
|
263
305
|
MentionsRoute: MentionsRoute,
|
|
264
306
|
ApiActionRoute: ApiActionRoute,
|
|
265
307
|
ApiAvatarRoute: ApiAvatarRoute,
|
package/src/routes/__root.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
import { AppNav } from "#/components/AppNav";
|
|
6
6
|
import { ThemeProvider, themeScript } from "#/lib/theme";
|
|
7
|
-
import { bodyClass, siteShellClass } from "#/lib/ui";
|
|
7
|
+
import { bodyClass, pageWrapClass, siteShellClass } from "#/lib/ui";
|
|
8
8
|
|
|
9
9
|
import appCss from "../styles.css?url";
|
|
10
10
|
|
|
@@ -29,9 +29,14 @@ export const Route = createRootRoute({
|
|
|
29
29
|
},
|
|
30
30
|
],
|
|
31
31
|
}),
|
|
32
|
+
notFoundComponent: NotFoundView,
|
|
32
33
|
shellComponent: RootDocument,
|
|
33
34
|
});
|
|
34
35
|
|
|
36
|
+
function NotFoundView() {
|
|
37
|
+
return <main className={pageWrapClass}>Not Found</main>;
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
function RootDocument({ children }: { children: ReactNode }) {
|
|
36
41
|
return (
|
|
37
42
|
<html lang="en" suppressHydrationWarning>
|
|
@@ -3,13 +3,23 @@ import { addBlock, removeBlock, syncBlocks } from "#/lib/blocks";
|
|
|
3
3
|
import { scoreInbox } from "#/lib/inbox";
|
|
4
4
|
import { addMute, removeMute } from "#/lib/mutes";
|
|
5
5
|
import { createDmReply, createPost, createTweetReply } from "#/lib/queries";
|
|
6
|
+
import type { ActionsTransport } from "#/lib/config";
|
|
6
7
|
import type { InboxKind } from "#/lib/types";
|
|
7
8
|
|
|
9
|
+
function parseActionsTransport(
|
|
10
|
+
value: string | undefined,
|
|
11
|
+
): ActionsTransport | undefined {
|
|
12
|
+
return value === "auto" || value === "bird" || value === "xurl"
|
|
13
|
+
? value
|
|
14
|
+
: undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
export const Route = createFileRoute("/api/action")({
|
|
9
18
|
server: {
|
|
10
19
|
handlers: {
|
|
11
20
|
POST: async ({ request }) => {
|
|
12
21
|
const body = (await request.json()) as Record<string, string>;
|
|
22
|
+
const transport = parseActionsTransport(body.transport);
|
|
13
23
|
let result: unknown;
|
|
14
24
|
|
|
15
25
|
if (body.kind === "post") {
|
|
@@ -38,7 +48,7 @@ export const Route = createFileRoute("/api/action")({
|
|
|
38
48
|
body.accountId || "acct_primary",
|
|
39
49
|
body.query || "",
|
|
40
50
|
{
|
|
41
|
-
transport
|
|
51
|
+
transport,
|
|
42
52
|
},
|
|
43
53
|
);
|
|
44
54
|
} else if (body.kind === "unblockProfile") {
|
|
@@ -46,7 +56,7 @@ export const Route = createFileRoute("/api/action")({
|
|
|
46
56
|
body.accountId || "acct_primary",
|
|
47
57
|
body.query || "",
|
|
48
58
|
{
|
|
49
|
-
transport
|
|
59
|
+
transport,
|
|
50
60
|
},
|
|
51
61
|
);
|
|
52
62
|
} else if (body.kind === "muteProfile") {
|
|
@@ -54,7 +64,7 @@ export const Route = createFileRoute("/api/action")({
|
|
|
54
64
|
body.accountId || "acct_primary",
|
|
55
65
|
body.query || "",
|
|
56
66
|
{
|
|
57
|
-
transport
|
|
67
|
+
transport,
|
|
58
68
|
},
|
|
59
69
|
);
|
|
60
70
|
} else if (body.kind === "unmuteProfile") {
|
|
@@ -62,7 +72,7 @@ export const Route = createFileRoute("/api/action")({
|
|
|
62
72
|
body.accountId || "acct_primary",
|
|
63
73
|
body.query || "",
|
|
64
74
|
{
|
|
65
|
-
transport
|
|
75
|
+
transport,
|
|
66
76
|
},
|
|
67
77
|
);
|
|
68
78
|
} else if (body.kind === "syncBlocks") {
|
package/src/routes/api/inbox.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { maybeAutoUpdateBackup } from "#/lib/backup";
|
|
2
3
|
import { listInboxItems } from "#/lib/inbox";
|
|
3
4
|
import type { InboxKind } from "#/lib/types";
|
|
4
5
|
|
|
@@ -11,7 +12,8 @@ function parseNumber(value: string | null) {
|
|
|
11
12
|
export const Route = createFileRoute("/api/inbox")({
|
|
12
13
|
server: {
|
|
13
14
|
handlers: {
|
|
14
|
-
GET: ({ request }) => {
|
|
15
|
+
GET: async ({ request }) => {
|
|
16
|
+
await maybeAutoUpdateBackup();
|
|
15
17
|
const url = new URL(request.url);
|
|
16
18
|
const kind = (url.searchParams.get("kind") ?? "mixed") as InboxKind;
|
|
17
19
|
return new Response(
|
package/src/routes/api/query.tsx
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { maybeAutoUpdateBackup } from "#/lib/backup";
|
|
2
3
|
import { queryResource } from "#/lib/queries";
|
|
3
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
ReplyFilter,
|
|
6
|
+
ResourceKind,
|
|
7
|
+
TimelineQualityFilter,
|
|
8
|
+
} from "#/lib/types";
|
|
4
9
|
|
|
5
10
|
function json(data: unknown) {
|
|
6
11
|
return new Response(JSON.stringify(data), {
|
|
@@ -23,10 +28,15 @@ function parseNumber(value: string | null) {
|
|
|
23
28
|
return Number.isFinite(parsed) ? parsed : undefined;
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
function parseQualityFilter(value: string | null): TimelineQualityFilter {
|
|
32
|
+
return value === "summary" ? "summary" : "all";
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
export const Route = createFileRoute("/api/query")({
|
|
27
36
|
server: {
|
|
28
37
|
handlers: {
|
|
29
|
-
GET: ({ request }) => {
|
|
38
|
+
GET: async ({ request }) => {
|
|
39
|
+
await maybeAutoUpdateBackup();
|
|
30
40
|
const url = new URL(request.url);
|
|
31
41
|
const resource = (url.searchParams.get("resource") ??
|
|
32
42
|
"home") as ResourceKind;
|
|
@@ -34,6 +44,14 @@ export const Route = createFileRoute("/api/query")({
|
|
|
34
44
|
account: url.searchParams.get("account") ?? undefined,
|
|
35
45
|
search: url.searchParams.get("search") ?? undefined,
|
|
36
46
|
replyFilter: parseReplyFilter(url.searchParams.get("replyFilter")),
|
|
47
|
+
since: url.searchParams.get("since") ?? undefined,
|
|
48
|
+
until: url.searchParams.get("until") ?? undefined,
|
|
49
|
+
includeReplies: url.searchParams.get("originalsOnly") !== "true",
|
|
50
|
+
qualityFilter: parseQualityFilter(
|
|
51
|
+
url.searchParams.get("qualityFilter"),
|
|
52
|
+
),
|
|
53
|
+
likedOnly: url.searchParams.get("liked") === "true",
|
|
54
|
+
bookmarkedOnly: url.searchParams.get("bookmarked") === "true",
|
|
37
55
|
limit: parseNumber(url.searchParams.get("limit")) ?? undefined,
|
|
38
56
|
};
|
|
39
57
|
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { maybeAutoUpdateBackup } from "#/lib/backup";
|
|
2
3
|
import { getQueryEnvelope } from "#/lib/queries";
|
|
3
4
|
|
|
4
5
|
export const Route = createFileRoute("/api/status")({
|
|
5
6
|
server: {
|
|
6
7
|
handlers: {
|
|
7
|
-
GET: async () =>
|
|
8
|
-
|
|
8
|
+
GET: async () => {
|
|
9
|
+
await maybeAutoUpdateBackup();
|
|
10
|
+
return new Response(JSON.stringify(await getQueryEnvelope()), {
|
|
9
11
|
headers: {
|
|
10
12
|
"content-type": "application/json",
|
|
11
13
|
},
|
|
12
|
-
})
|
|
14
|
+
});
|
|
15
|
+
},
|
|
13
16
|
},
|
|
14
17
|
},
|
|
15
18
|
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { SavedTimelineView } from "#/components/SavedTimelineView";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/bookmarks")({
|
|
5
|
+
component: BookmarksRoute,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
function BookmarksRoute() {
|
|
9
|
+
return (
|
|
10
|
+
<SavedTimelineView
|
|
11
|
+
eyebrow="bookmarks"
|
|
12
|
+
filter="bookmarked"
|
|
13
|
+
loadingLabel="Loading bookmarks..."
|
|
14
|
+
searchPlaceholder="Search bookmarks"
|
|
15
|
+
title="Saved threads, receipts, and future rabbit holes."
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
+
import { SavedTimelineView } from "#/components/SavedTimelineView";
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute("/likes")({
|
|
5
|
+
component: LikesRoute,
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
function LikesRoute() {
|
|
9
|
+
return (
|
|
10
|
+
<SavedTimelineView
|
|
11
|
+
eyebrow="liked posts"
|
|
12
|
+
filter="liked"
|
|
13
|
+
loadingLabel="Loading liked posts..."
|
|
14
|
+
searchPlaceholder="Search likes"
|
|
15
|
+
title="Things worth keeping in the orbit."
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|