birdclaw 0.5.1 → 0.7.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 (116) hide show
  1. package/CHANGELOG.md +92 -1
  2. package/README.md +75 -5
  3. package/package.json +8 -2
  4. package/scripts/browser-perf.mjs +1 -0
  5. package/scripts/start-test-server.mjs +16 -3
  6. package/src/cli.ts +812 -37
  7. package/src/components/AccountSwitcher.tsx +186 -0
  8. package/src/components/AppNav.tsx +37 -7
  9. package/src/components/AvatarChip.tsx +9 -3
  10. package/src/components/DmWorkspace.tsx +18 -8
  11. package/src/components/LinkPreviewCard.tsx +40 -18
  12. package/src/components/MarkdownViewer.tsx +818 -0
  13. package/src/components/ProfileAnalysisStream.tsx +428 -0
  14. package/src/components/ProfilePreview.tsx +120 -9
  15. package/src/components/SavedTimelineView.tsx +30 -8
  16. package/src/components/SyncNowButton.tsx +60 -25
  17. package/src/components/ThemeSlider.tsx +55 -50
  18. package/src/components/TimelineCard.tsx +240 -92
  19. package/src/components/TimelineRouteFrame.tsx +38 -8
  20. package/src/components/TweetMediaGrid.tsx +87 -38
  21. package/src/components/TweetRichText.tsx +45 -17
  22. package/src/components/account-selection.ts +64 -0
  23. package/src/components/useTimelineRouteData.ts +97 -13
  24. package/src/lib/account-sync-job.ts +666 -0
  25. package/src/lib/actions-transport.ts +216 -146
  26. package/src/lib/api-client.ts +128 -53
  27. package/src/lib/archive-finder.ts +78 -63
  28. package/src/lib/archive-import.ts +1593 -1291
  29. package/src/lib/authored-live.ts +262 -204
  30. package/src/lib/avatar-cache.ts +208 -43
  31. package/src/lib/backup.ts +1536 -954
  32. package/src/lib/bird-actions.ts +139 -57
  33. package/src/lib/bird-command.ts +101 -28
  34. package/src/lib/bird.ts +582 -194
  35. package/src/lib/blocklist.ts +40 -23
  36. package/src/lib/blocks-write.ts +129 -80
  37. package/src/lib/blocks.ts +165 -97
  38. package/src/lib/bookmark-sync-job.ts +250 -160
  39. package/src/lib/config.ts +35 -2
  40. package/src/lib/conversation-surface.ts +79 -48
  41. package/src/lib/data-sources.ts +219 -0
  42. package/src/lib/db.ts +95 -4
  43. package/src/lib/dms-live.ts +720 -66
  44. package/src/lib/effect-runtime.ts +45 -0
  45. package/src/lib/follow-graph.ts +224 -180
  46. package/src/lib/geocoding.ts +296 -0
  47. package/src/lib/http-effect.ts +222 -0
  48. package/src/lib/inbox.ts +74 -43
  49. package/src/lib/link-index.ts +88 -76
  50. package/src/lib/link-insights.ts +24 -0
  51. package/src/lib/link-preview-metadata.ts +472 -52
  52. package/src/lib/location.ts +137 -0
  53. package/src/lib/media-fetch.ts +286 -213
  54. package/src/lib/mention-threads-live.ts +445 -288
  55. package/src/lib/mentions-live.ts +549 -354
  56. package/src/lib/moderation-target.ts +102 -65
  57. package/src/lib/moderation-write.ts +77 -18
  58. package/src/lib/mutes-write.ts +129 -80
  59. package/src/lib/mutes.ts +8 -1
  60. package/src/lib/network-map.ts +382 -0
  61. package/src/lib/openai.ts +84 -53
  62. package/src/lib/period-digest.ts +1317 -0
  63. package/src/lib/profile-affiliation-hydration.ts +93 -54
  64. package/src/lib/profile-analysis.ts +1272 -0
  65. package/src/lib/profile-bio-entities.ts +1 -1
  66. package/src/lib/profile-hydration.ts +124 -72
  67. package/src/lib/profile-replies.ts +60 -43
  68. package/src/lib/profile-resolver.ts +402 -294
  69. package/src/lib/queries.ts +983 -203
  70. package/src/lib/research.ts +165 -120
  71. package/src/lib/search-discussion.ts +1016 -0
  72. package/src/lib/sqlite.ts +1 -0
  73. package/src/lib/timeline-collections-live.ts +204 -167
  74. package/src/lib/timeline-live.ts +325 -51
  75. package/src/lib/tweet-account-edges.ts +2 -0
  76. package/src/lib/tweet-lookup.ts +30 -19
  77. package/src/lib/tweet-render.ts +141 -1
  78. package/src/lib/tweet-search-live.ts +565 -0
  79. package/src/lib/types.ts +75 -3
  80. package/src/lib/ui.ts +31 -8
  81. package/src/lib/url-expansion.ts +226 -55
  82. package/src/lib/url-safety.ts +220 -0
  83. package/src/lib/web-sync.ts +222 -149
  84. package/src/lib/whois.ts +166 -147
  85. package/src/lib/x-web.ts +102 -71
  86. package/src/lib/xurl-rate-limits.ts +267 -0
  87. package/src/lib/xurl.ts +1185 -405
  88. package/src/routeTree.gen.ts +273 -0
  89. package/src/routes/__root.tsx +24 -5
  90. package/src/routes/api/action.tsx +127 -78
  91. package/src/routes/api/avatar.tsx +39 -30
  92. package/src/routes/api/blocks.tsx +26 -23
  93. package/src/routes/api/conversation.tsx +25 -14
  94. package/src/routes/api/data-sources.tsx +24 -0
  95. package/src/routes/api/inbox.tsx +27 -21
  96. package/src/routes/api/link-insights.tsx +31 -25
  97. package/src/routes/api/link-preview.tsx +25 -21
  98. package/src/routes/api/network-map.tsx +55 -0
  99. package/src/routes/api/period-digest.tsx +133 -0
  100. package/src/routes/api/profile-analysis.tsx +152 -0
  101. package/src/routes/api/profile-hydrate.tsx +31 -28
  102. package/src/routes/api/query.tsx +80 -55
  103. package/src/routes/api/search-discussion.tsx +169 -0
  104. package/src/routes/api/status.tsx +18 -10
  105. package/src/routes/api/sync.tsx +75 -29
  106. package/src/routes/api/xurl-rate-limits.tsx +24 -0
  107. package/src/routes/data-sources.tsx +255 -0
  108. package/src/routes/discuss.tsx +419 -0
  109. package/src/routes/dms.tsx +95 -28
  110. package/src/routes/inbox.tsx +32 -19
  111. package/src/routes/network-map.tsx +1035 -0
  112. package/src/routes/profile-analyze.tsx +112 -0
  113. package/src/routes/profiles.$handle.tsx +228 -0
  114. package/src/routes/rate-limits.tsx +309 -0
  115. package/src/routes/today.tsx +455 -0
  116. package/src/styles.css +22 -0
@@ -0,0 +1,55 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { Effect } from "effect";
3
+ import { maybeAutoUpdateBackupEffect } from "#/lib/backup";
4
+ import {
5
+ jsonResponse,
6
+ parseBoundedInteger,
7
+ runRouteEffect,
8
+ sensitiveRequestErrorResponse,
9
+ } from "#/lib/http-effect";
10
+ import { getNetworkMap, type NetworkMapKind } from "#/lib/network-map";
11
+
12
+ function parseType(value: string | null): NetworkMapKind {
13
+ if (
14
+ value === "followers" ||
15
+ value === "following" ||
16
+ value === "mutual" ||
17
+ value === "all"
18
+ ) {
19
+ return value;
20
+ }
21
+ return "all";
22
+ }
23
+
24
+ export const Route = createFileRoute("/api/network-map")({
25
+ server: {
26
+ handlers: {
27
+ GET: ({ request }) =>
28
+ runRouteEffect(
29
+ Effect.gen(function* () {
30
+ const denied = sensitiveRequestErrorResponse(request);
31
+ if (denied) return denied;
32
+
33
+ yield* maybeAutoUpdateBackupEffect();
34
+ const url = new URL(request.url);
35
+ const response = yield* Effect.promise(() =>
36
+ getNetworkMap({
37
+ account: url.searchParams.get("account") ?? undefined,
38
+ type: parseType(url.searchParams.get("type")),
39
+ limit: parseBoundedInteger(url.searchParams.get("limit"), {
40
+ max: 50_000,
41
+ }),
42
+ geocodeLimit: parseBoundedInteger(
43
+ url.searchParams.get("geocodeLimit"),
44
+ { max: 500, min: 0 },
45
+ ),
46
+ refresh: url.searchParams.get("refresh") === "true",
47
+ signal: request.signal,
48
+ }),
49
+ );
50
+ return jsonResponse(response);
51
+ }),
52
+ ),
53
+ },
54
+ },
55
+ });
@@ -0,0 +1,133 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { Effect } from "effect";
3
+ import { maybeAutoUpdateBackupEffect } from "#/lib/backup";
4
+ import { runEffectBackground } from "#/lib/effect-runtime";
5
+ import {
6
+ parseBoundedInteger,
7
+ runRouteEffect,
8
+ sensitiveRequestErrorResponse,
9
+ } from "#/lib/http-effect";
10
+ import {
11
+ streamPeriodDigestEffect,
12
+ type PeriodDigestOptions,
13
+ type PeriodDigestStreamEvent,
14
+ } from "#/lib/period-digest";
15
+
16
+ const encoder = new TextEncoder();
17
+
18
+ function parseBoolean(value: string | null) {
19
+ return value === "true" || value === "1" || value === "yes";
20
+ }
21
+
22
+ function parseOptions(url: URL): PeriodDigestOptions {
23
+ return {
24
+ period: url.searchParams.get("period") ?? undefined,
25
+ since: url.searchParams.get("since") ?? undefined,
26
+ until: url.searchParams.get("until") ?? undefined,
27
+ account: url.searchParams.get("account") ?? undefined,
28
+ includeDms: parseBoolean(url.searchParams.get("includeDms")),
29
+ refresh: parseBoolean(url.searchParams.get("refresh")),
30
+ model: url.searchParams.get("model") === "gpt-5.5" ? "gpt-5.5" : undefined,
31
+ maxTweets: parseBoundedInteger(url.searchParams.get("maxTweets"), {
32
+ max: 5_000,
33
+ }),
34
+ maxLinks: parseBoundedInteger(url.searchParams.get("maxLinks"), {
35
+ max: 25,
36
+ }),
37
+ liveSync: url.searchParams.get("liveSync") !== "false",
38
+ liveSyncMode: "xurl",
39
+ liveTimelineLimit: parseBoundedInteger(
40
+ url.searchParams.get("liveTimelineLimit"),
41
+ { max: 100_000 },
42
+ ),
43
+ liveTimelineMaxPages: parseBoundedInteger(
44
+ url.searchParams.get("liveTimelineMaxPages"),
45
+ { max: 1_000 },
46
+ ),
47
+ };
48
+ }
49
+
50
+ function encodeEvent(event: PeriodDigestStreamEvent) {
51
+ return encoder.encode(`${JSON.stringify(event)}\n`);
52
+ }
53
+
54
+ export const Route = createFileRoute("/api/period-digest")({
55
+ server: {
56
+ handlers: {
57
+ GET: ({ request }) =>
58
+ runRouteEffect(
59
+ Effect.gen(function* () {
60
+ const denied = sensitiveRequestErrorResponse(request);
61
+ if (denied) return denied;
62
+
63
+ yield* maybeAutoUpdateBackupEffect();
64
+ const url = new URL(request.url);
65
+ const options = parseOptions(url);
66
+ let abortDigest: (() => void) | undefined;
67
+
68
+ return new Response(
69
+ new ReadableStream({
70
+ cancel() {
71
+ abortDigest?.();
72
+ },
73
+ start(controller) {
74
+ const abortController = new AbortController();
75
+ let closed = false;
76
+ const close = () => {
77
+ closed = true;
78
+ abortController.abort();
79
+ };
80
+ const closeController = () => {
81
+ request.signal.removeEventListener("abort", onAbort);
82
+ if (!closed) {
83
+ closed = true;
84
+ controller.close();
85
+ }
86
+ };
87
+ const onAbort = () => close();
88
+ request.signal.addEventListener("abort", onAbort, {
89
+ once: true,
90
+ });
91
+ abortDigest = close;
92
+ const enqueue = (event: PeriodDigestStreamEvent) => {
93
+ if (closed) return;
94
+ try {
95
+ controller.enqueue(encodeEvent(event));
96
+ } catch {
97
+ close();
98
+ }
99
+ };
100
+
101
+ runEffectBackground(
102
+ streamPeriodDigestEffect(
103
+ { ...options, signal: abortController.signal },
104
+ { onEvent: enqueue },
105
+ ),
106
+ {
107
+ onSuccess: closeController,
108
+ onFailure: (error) => {
109
+ enqueue({
110
+ type: "error",
111
+ error:
112
+ error instanceof Error
113
+ ? error.message
114
+ : "Digest failed",
115
+ });
116
+ closeController();
117
+ },
118
+ },
119
+ );
120
+ },
121
+ }),
122
+ {
123
+ headers: {
124
+ "cache-control": "no-store",
125
+ "content-type": "application/x-ndjson; charset=utf-8",
126
+ },
127
+ },
128
+ );
129
+ }),
130
+ ),
131
+ },
132
+ },
133
+ });
@@ -0,0 +1,152 @@
1
+ import { createFileRoute } from "@tanstack/react-router";
2
+ import { Effect } from "effect";
3
+ import { maybeAutoUpdateBackupEffect } from "#/lib/backup";
4
+ import { runEffectBackground } from "#/lib/effect-runtime";
5
+ import {
6
+ parseBoundedInteger,
7
+ runRouteEffect,
8
+ sensitiveRequestErrorResponse,
9
+ } from "#/lib/http-effect";
10
+ import {
11
+ streamProfileAnalysisEffect,
12
+ type ProfileAnalysisOptions,
13
+ type ProfileAnalysisStreamEvent,
14
+ } from "#/lib/profile-analysis";
15
+
16
+ const encoder = new TextEncoder();
17
+
18
+ function parseBoolean(value: string | null) {
19
+ return value === "true" || value === "1" || value === "yes";
20
+ }
21
+
22
+ function parseOptions(url: URL): ProfileAnalysisOptions {
23
+ const conversationDelayMs = parseBoundedInteger(
24
+ url.searchParams.get("conversationDelayMs"),
25
+ { min: 0, max: 60_000 },
26
+ );
27
+ const rateLimitRetryMs = parseBoundedInteger(
28
+ url.searchParams.get("rateLimitRetryMs"),
29
+ { min: 0, max: 900_000 },
30
+ );
31
+ const rateLimitMaxRetries = parseBoundedInteger(
32
+ url.searchParams.get("rateLimitRetries"),
33
+ { min: 0, max: 10 },
34
+ );
35
+ return {
36
+ handle: url.searchParams.get("handle") ?? "",
37
+ account: url.searchParams.get("account") ?? undefined,
38
+ refresh: parseBoolean(url.searchParams.get("refresh")),
39
+ model: url.searchParams.get("model") === "gpt-5.5" ? "gpt-5.5" : undefined,
40
+ maxTweets: parseBoundedInteger(url.searchParams.get("maxTweets"), {
41
+ max: 20_000,
42
+ }),
43
+ maxPages: parseBoundedInteger(url.searchParams.get("maxPages"), {
44
+ max: 500,
45
+ }),
46
+ maxConversations: parseBoundedInteger(
47
+ url.searchParams.get("maxConversations"),
48
+ { max: 500 },
49
+ ),
50
+ maxConversationPages: parseBoundedInteger(
51
+ url.searchParams.get("maxConversationPages"),
52
+ { max: 50 },
53
+ ),
54
+ ...(conversationDelayMs !== undefined ? { conversationDelayMs } : {}),
55
+ ...(rateLimitRetryMs !== undefined ? { rateLimitRetryMs } : {}),
56
+ ...(rateLimitMaxRetries !== undefined ? { rateLimitMaxRetries } : {}),
57
+ };
58
+ }
59
+
60
+ function encodeEvent(event: ProfileAnalysisStreamEvent) {
61
+ return encoder.encode(`${JSON.stringify(event)}\n`);
62
+ }
63
+
64
+ export const Route = createFileRoute("/api/profile-analysis")({
65
+ server: {
66
+ handlers: {
67
+ GET: ({ request }) =>
68
+ runRouteEffect(
69
+ Effect.sync(() => {
70
+ const denied = sensitiveRequestErrorResponse(request);
71
+ if (denied) return denied;
72
+
73
+ const url = new URL(request.url);
74
+ const options = parseOptions(url);
75
+ let abortAnalysis: (() => void) | undefined;
76
+
77
+ return new Response(
78
+ new ReadableStream({
79
+ cancel() {
80
+ abortAnalysis?.();
81
+ },
82
+ start(controller) {
83
+ const abortController = new AbortController();
84
+ let closed = false;
85
+ const close = () => {
86
+ closed = true;
87
+ abortController.abort();
88
+ };
89
+ const closeController = () => {
90
+ request.signal.removeEventListener("abort", onAbort);
91
+ if (!closed) {
92
+ closed = true;
93
+ controller.close();
94
+ }
95
+ };
96
+ const onAbort = () => close();
97
+ request.signal.addEventListener("abort", onAbort, {
98
+ once: true,
99
+ });
100
+ abortAnalysis = close;
101
+ const enqueue = (event: ProfileAnalysisStreamEvent) => {
102
+ if (closed) return;
103
+ try {
104
+ controller.enqueue(encodeEvent(event));
105
+ } catch {
106
+ close();
107
+ }
108
+ };
109
+ enqueue({
110
+ type: "status",
111
+ label: "Starting profile analysis",
112
+ });
113
+
114
+ runEffectBackground(
115
+ Effect.gen(function* () {
116
+ yield* maybeAutoUpdateBackupEffect();
117
+ return yield* streamProfileAnalysisEffect(
118
+ {
119
+ ...options,
120
+ signal: abortController.signal,
121
+ },
122
+ { onEvent: enqueue },
123
+ );
124
+ }),
125
+ {
126
+ onSuccess: closeController,
127
+ onFailure: (error) => {
128
+ enqueue({
129
+ type: "error",
130
+ error:
131
+ error instanceof Error
132
+ ? error.message
133
+ : "Profile analysis failed",
134
+ });
135
+ closeController();
136
+ },
137
+ },
138
+ );
139
+ },
140
+ }),
141
+ {
142
+ headers: {
143
+ "cache-control": "no-store",
144
+ "content-type": "application/x-ndjson; charset=utf-8",
145
+ },
146
+ },
147
+ );
148
+ }),
149
+ ),
150
+ },
151
+ },
152
+ });
@@ -1,15 +1,11 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { resolveProfilesForHandles } from "#/lib/profile-resolver";
3
-
4
- function json(data: unknown, init?: ResponseInit) {
5
- return new Response(JSON.stringify(data), {
6
- ...init,
7
- headers: {
8
- "content-type": "application/json",
9
- ...init?.headers,
10
- },
11
- });
12
- }
2
+ import { Effect } from "effect";
3
+ import {
4
+ jsonResponse,
5
+ runRouteEffect,
6
+ sensitiveRequestErrorResponse,
7
+ } from "#/lib/http-effect";
8
+ import { resolveProfilesForHandlesEffect } from "#/lib/profile-resolver";
13
9
 
14
10
  function parseHandles(url: URL) {
15
11
  const rawValues = [
@@ -28,24 +24,31 @@ function parseHandles(url: URL) {
28
24
  export const Route = createFileRoute("/api/profile-hydrate")({
29
25
  server: {
30
26
  handlers: {
31
- GET: async ({ request }) => {
32
- const url = new URL(request.url);
33
- const handles = parseHandles(url);
34
- if (handles.length === 0) {
35
- return json(
36
- { ok: false, message: "Missing handles" },
37
- { status: 400 },
38
- );
39
- }
27
+ GET: ({ request }) =>
28
+ runRouteEffect(
29
+ Effect.gen(function* () {
30
+ const denied = sensitiveRequestErrorResponse(request);
31
+ if (denied) return denied;
32
+
33
+ const url = new URL(request.url);
34
+ const handles = parseHandles(url);
35
+ if (handles.length === 0) {
36
+ return jsonResponse(
37
+ { ok: false, message: "Missing handles" },
38
+ { status: 400 },
39
+ );
40
+ }
40
41
 
41
- const results = await resolveProfilesForHandles(handles);
42
- return json({
43
- ok: true,
44
- results,
45
- hydratedProfiles: results.filter((result) => result.status === "hit")
46
- .length,
47
- });
48
- },
42
+ const results = yield* resolveProfilesForHandlesEffect(handles);
43
+ return jsonResponse({
44
+ ok: true,
45
+ results,
46
+ hydratedProfiles: results.filter(
47
+ (result) => result.status === "hit",
48
+ ).length,
49
+ });
50
+ }),
51
+ ),
49
52
  },
50
53
  },
51
54
  });
@@ -1,20 +1,20 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { maybeAutoUpdateBackup } from "#/lib/backup";
2
+ import { Effect } from "effect";
3
+ import { maybeAutoUpdateBackupEffect } from "#/lib/backup";
4
+ import {
5
+ jsonResponse,
6
+ parseBoundedInteger,
7
+ runRouteEffect,
8
+ sensitiveRequestErrorResponse,
9
+ } from "#/lib/http-effect";
3
10
  import { queryResource } from "#/lib/queries";
4
11
  import type {
12
+ DmQuery,
5
13
  ReplyFilter,
6
14
  ResourceKind,
7
15
  TimelineQualityFilter,
8
16
  } from "#/lib/types";
9
17
 
10
- function json(data: unknown) {
11
- return new Response(JSON.stringify(data), {
12
- headers: {
13
- "content-type": "application/json",
14
- },
15
- });
16
- }
17
-
18
18
  function parseReplyFilter(value: string | null): ReplyFilter {
19
19
  if (value === "replied" || value === "unreplied") {
20
20
  return value;
@@ -22,69 +22,94 @@ function parseReplyFilter(value: string | null): ReplyFilter {
22
22
  return "all";
23
23
  }
24
24
 
25
- function parseNumber(value: string | null) {
25
+ function parseOptionalNumber(value: string | null) {
26
26
  if (!value) return undefined;
27
27
  const parsed = Number(value);
28
28
  return Number.isFinite(parsed) ? parsed : undefined;
29
29
  }
30
30
 
31
+ function parseDmSort(value: string | null) {
32
+ if (value === "followers" || value === "influence") {
33
+ return "followers";
34
+ }
35
+ return "recent";
36
+ }
37
+
31
38
  function parseQualityFilter(value: string | null): TimelineQualityFilter {
32
39
  return value === "summary" ? "summary" : "all";
33
40
  }
34
41
 
42
+ function parseDmInbox(value: string | null): NonNullable<DmQuery["inbox"]> {
43
+ if (value === "accepted" || value === "requests") return value;
44
+ return "all";
45
+ }
46
+
35
47
  export const Route = createFileRoute("/api/query")({
36
48
  server: {
37
49
  handlers: {
38
- GET: async ({ request }) => {
39
- await maybeAutoUpdateBackup();
40
- const url = new URL(request.url);
41
- const resource = (url.searchParams.get("resource") ??
42
- "home") as ResourceKind;
43
- const baseFilters = {
44
- account: url.searchParams.get("account") ?? undefined,
45
- search: url.searchParams.get("search") ?? undefined,
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",
55
- limit: parseNumber(url.searchParams.get("limit")) ?? undefined,
56
- };
50
+ GET: ({ request }) =>
51
+ runRouteEffect(
52
+ Effect.gen(function* () {
53
+ const denied = sensitiveRequestErrorResponse(request);
54
+ if (denied) return denied;
57
55
 
58
- if (resource === "dms") {
59
- return json(
60
- queryResource("dms", {
61
- ...baseFilters,
62
- participant: url.searchParams.get("participant") ?? undefined,
63
- minFollowers: parseNumber(url.searchParams.get("minFollowers")),
64
- maxFollowers: parseNumber(url.searchParams.get("maxFollowers")),
65
- minInfluenceScore: parseNumber(
66
- url.searchParams.get("minInfluenceScore"),
56
+ yield* maybeAutoUpdateBackupEffect();
57
+ const url = new URL(request.url);
58
+ const resource = (url.searchParams.get("resource") ??
59
+ "home") as ResourceKind;
60
+ const baseFilters = {
61
+ account: url.searchParams.get("account") ?? undefined,
62
+ search: url.searchParams.get("search") ?? undefined,
63
+ replyFilter: parseReplyFilter(
64
+ url.searchParams.get("replyFilter"),
67
65
  ),
68
- maxInfluenceScore: parseNumber(
69
- url.searchParams.get("maxInfluenceScore"),
66
+ since: url.searchParams.get("since") ?? undefined,
67
+ until: url.searchParams.get("until") ?? undefined,
68
+ includeReplies: url.searchParams.get("originalsOnly") !== "true",
69
+ qualityFilter: parseQualityFilter(
70
+ url.searchParams.get("qualityFilter"),
70
71
  ),
71
- sort:
72
- url.searchParams.get("sort") === "influence"
73
- ? "influence"
74
- : "recent",
75
- conversationId:
76
- url.searchParams.get("conversationId") ?? undefined,
77
- }),
78
- );
79
- }
72
+ likedOnly: url.searchParams.get("liked") === "true",
73
+ bookmarkedOnly: url.searchParams.get("bookmarked") === "true",
74
+ limit: parseBoundedInteger(url.searchParams.get("limit"), {
75
+ max: 200,
76
+ }),
77
+ };
78
+
79
+ if (resource === "dms") {
80
+ return jsonResponse(
81
+ queryResource("dms", {
82
+ ...baseFilters,
83
+ participant: url.searchParams.get("participant") ?? undefined,
84
+ minFollowers: parseOptionalNumber(
85
+ url.searchParams.get("minFollowers"),
86
+ ),
87
+ maxFollowers: parseOptionalNumber(
88
+ url.searchParams.get("maxFollowers"),
89
+ ),
90
+ minInfluenceScore: parseOptionalNumber(
91
+ url.searchParams.get("minInfluenceScore"),
92
+ ),
93
+ maxInfluenceScore: parseOptionalNumber(
94
+ url.searchParams.get("maxInfluenceScore"),
95
+ ),
96
+ sort: parseDmSort(url.searchParams.get("sort")),
97
+ inbox: parseDmInbox(url.searchParams.get("inbox")),
98
+ conversationId:
99
+ url.searchParams.get("conversationId") ?? undefined,
100
+ }),
101
+ );
102
+ }
80
103
 
81
- return json(
82
- queryResource(resource, {
83
- ...baseFilters,
84
- resource,
104
+ return jsonResponse(
105
+ queryResource(resource, {
106
+ ...baseFilters,
107
+ resource,
108
+ untilId: url.searchParams.get("untilId") ?? undefined,
109
+ }),
110
+ );
85
111
  }),
86
- );
87
- },
112
+ ),
88
113
  },
89
114
  },
90
115
  });