birdclaw 0.8.2 → 0.8.4
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 +33 -0
- package/README.md +10 -8
- package/bin/birdclaw.mjs +4 -66
- package/dist/cli/birdclaw.js +28165 -0
- package/dist/client/assets/AvatarChip-DLoqM5xD.js +1 -0
- package/dist/client/assets/ProfilePreview-BejAuYsJ.js +1 -0
- package/dist/client/assets/SavedTimelineView-BIS7HKpp.js +1 -0
- package/dist/client/assets/SmartTimestamp-BR0-9se2.js +1 -0
- package/dist/client/assets/TimelineRouteFrame-CyGPj0eM.js +1 -0
- package/dist/client/assets/blocks-7UDKQp8H.js +1 -0
- package/dist/client/assets/bookmarks-CbBkMvXk.js +1 -0
- package/dist/client/assets/chunk-C0phanGV.js +1 -0
- package/dist/client/assets/circle-check-C_Vq3ElD.js +1 -0
- package/dist/client/assets/client-stream-contracts-ys3XhWG4.js +5 -0
- package/dist/client/assets/createLucideIcon-JV8isFFR.js +1 -0
- package/dist/client/assets/data-sources-Cm2bpNvg.js +1 -0
- package/dist/client/assets/discuss-DTjaOugw.js +1 -0
- package/dist/client/assets/dms-CGd6Eogq.js +1 -0
- package/dist/client/assets/inbox-BHlGuOy2.js +1 -0
- package/dist/client/assets/index-CEIG7S6s.js +31 -0
- package/dist/client/assets/index-bpFrQ5yG.css +1 -0
- package/dist/client/assets/likes-cLrcAQem.js +1 -0
- package/dist/client/assets/links-CM9QO1LC.js +1 -0
- package/dist/client/assets/mapbox-BxTlqXt9.js +2 -0
- package/dist/client/assets/mapbox-gl-dgf_hX3a.js +3215 -0
- package/dist/client/assets/mentions-CxKCgzIf.js +1 -0
- package/dist/client/assets/network-map-B5a3whqj.js +2 -0
- package/dist/client/assets/profile-analyze-CRtNEwlb.js +1 -0
- package/dist/client/assets/profile-hydration-client-YEtJsNTv.js +1 -0
- package/dist/client/assets/profiles._handle-C82_HoKX.js +1 -0
- package/dist/client/assets/query-client-fYu2rzRC.js +17 -0
- package/dist/client/assets/rate-limits-ScaEwdVK.js +1 -0
- package/dist/client/assets/react-dom-DHl6Ug7F.js +1 -0
- package/dist/client/assets/react-nBylq1U5.js +1 -0
- package/dist/client/assets/repeat-2-xCwHM9hD.js +1 -0
- package/dist/client/assets/routes-q7ZaW4En.js +1 -0
- package/dist/client/assets/schemas-I9r6F6ya.js +64 -0
- package/dist/client/assets/search-Dq5kYsxA.js +1 -0
- package/dist/client/assets/sparkles-CQVlvHsZ.js +1 -0
- package/dist/client/assets/styles-B8s3tt3r.css +2 -0
- package/dist/client/assets/today-CeG1O7Py.js +1 -0
- package/dist/client/assets/triangle-alert-CBL7s2pf.js +1 -0
- package/dist/client/assets/useTimelineRouteData-1HsqgvUi.js +1 -0
- package/dist/client/assets/users-CHbKlqUr.js +1 -0
- package/dist/server/assets/AvatarChip-zUDAEmjL.js +105 -0
- package/dist/server/assets/FeedState-ntW0Q2GB.js +214 -0
- package/dist/server/assets/ProfileAnalysisStream-BFvhVdhn.js +198 -0
- package/dist/server/assets/ProfilePreview-C6gAor_j.js +752 -0
- package/dist/server/assets/SavedTimelineView-BG1jnBp1.js +64 -0
- package/dist/server/assets/SmartTimestamp-DfwE1NGf.js +45 -0
- package/dist/server/assets/SyncNowButton-BOn_IBXS.js +76 -0
- package/dist/server/assets/TimelineRouteFrame-CU7ilM7U.js +85 -0
- package/dist/server/assets/TweetRichText-DhCVv1-6.js +74 -0
- package/dist/server/assets/_tanstack-start-manifest_v-C-ZRnG_n.js +148 -0
- package/dist/server/assets/account-selection-CTdFGWRk.js +44 -0
- package/dist/server/assets/blocks-C1mgGTB3.js +299 -0
- package/dist/server/assets/blocks-DSgQghqP.js +17 -0
- package/dist/server/assets/bookmarks-BJN8tykY.js +14 -0
- package/dist/server/assets/client-stream-contracts-C2xbWVjm.js +769 -0
- package/dist/server/assets/data-sources-BnA1Q94p.js +180 -0
- package/dist/server/assets/discuss-CJ1dau1j.js +15 -0
- package/dist/server/assets/discuss-QTo08NCN.js +338 -0
- package/dist/server/assets/dms-BuLj4bp8.js +368 -0
- package/dist/server/assets/dms-DAyQrVM2.js +240 -0
- package/dist/server/assets/empty-plugin-adapters-DzrEkdBF.js +5 -0
- package/dist/server/assets/inbox-CkGOaW1q.js +214 -0
- package/dist/server/assets/inbox-DpWd1EzC.js +146 -0
- package/dist/server/assets/likes-DBOAtsJU.js +14 -0
- package/dist/server/assets/links-DAhSjynu.js +240 -0
- package/dist/server/assets/links-DmH5lxHO.js +468 -0
- package/dist/server/assets/mentions-BH2MzKtB.js +26 -0
- package/dist/server/assets/network-map-D3CzRcC4.js +224 -0
- package/dist/server/assets/network-map-D5DoLOwR.js +582 -0
- package/dist/server/assets/profile-analyze-BP65PED0.js +90 -0
- package/dist/server/assets/profile-analyze-C1NoM0tV.js +9 -0
- package/dist/server/assets/profile-hydration-client-GJ4kwJS8.js +133 -0
- package/dist/server/assets/profiles._handle-DjwlN_ac.js +184 -0
- package/dist/server/assets/profiles._handle-DsLNj2Ca.js +12 -0
- package/dist/server/assets/query-client-BdauHBgd.js +923 -0
- package/dist/server/assets/rate-limits-VAr2qzYD.js +262 -0
- package/dist/server/assets/route-search-BUlQ5lGM.js +117 -0
- package/dist/server/assets/router-DqkwSPiM.js +17026 -0
- package/dist/server/assets/routes-A9c9ZhwN.js +26 -0
- package/dist/server/assets/start-FaDOTmto.js +4 -0
- package/dist/server/assets/today-B5hVM57Z.js +17 -0
- package/dist/server/assets/today-fvGiZ7ud.js +303 -0
- package/dist/server/assets/ui-qbmcMSSX.js +137 -0
- package/{src/components/useDebouncedValue.ts → dist/server/assets/useDebouncedValue-DOnA6Xcy.js} +4 -4
- package/dist/server/assets/useTimelineRouteData-27cxynxa.js +1101 -0
- package/dist/server/server.js +5520 -0
- package/package.json +27 -34
- package/playwright.config.ts +0 -35
- package/scripts/browser-perf.mjs +0 -427
- package/scripts/build-docs-site.mjs +0 -967
- package/scripts/docs-site-assets.mjs +0 -311
- package/scripts/run-vitest.mjs +0 -21
- package/scripts/sanitize-node-options.mjs +0 -23
- package/scripts/start-test-server.mjs +0 -42
- package/src/cli-moderation.ts +0 -210
- package/src/cli.ts +0 -2336
- package/src/components/AccountSwitcher.tsx +0 -186
- package/src/components/AppNav.tsx +0 -118
- package/src/components/AvatarChip.tsx +0 -58
- package/src/components/BrandMark.tsx +0 -67
- package/src/components/ConversationThread.tsx +0 -131
- package/src/components/DmWorkspace.tsx +0 -286
- package/src/components/EmbeddedTweetCard.tsx +0 -55
- package/src/components/FeedState.tsx +0 -147
- package/src/components/FloatingPreview.tsx +0 -382
- package/src/components/InboxCard.tsx +0 -152
- package/src/components/LinkPreviewCard.tsx +0 -292
- package/src/components/MarkdownViewer.tsx +0 -777
- package/src/components/ProfileAnalysisStream.tsx +0 -428
- package/src/components/ProfilePreview.tsx +0 -129
- package/src/components/SavedTimelineView.tsx +0 -155
- package/src/components/SmartTimestamp.tsx +0 -72
- package/src/components/SyncNowButton.tsx +0 -140
- package/src/components/ThemeSlider.tsx +0 -80
- package/src/components/TimelineCard.tsx +0 -533
- package/src/components/TimelineRouteFrame.tsx +0 -186
- package/src/components/TweetArticleCard.tsx +0 -66
- package/src/components/TweetMediaGrid.tsx +0 -185
- package/src/components/TweetRichText.tsx +0 -159
- package/src/components/account-selection.ts +0 -64
- package/src/components/useTimelineRouteData.ts +0 -323
- package/src/lib/account-sync-job.ts +0 -666
- package/src/lib/actions-transport.ts +0 -300
- package/src/lib/api-client.ts +0 -419
- package/src/lib/archive-finder.ts +0 -185
- package/src/lib/archive-import.ts +0 -2764
- package/src/lib/authored-live.ts +0 -1072
- package/src/lib/avatar-cache.ts +0 -349
- package/src/lib/backup.ts +0 -2741
- package/src/lib/bird-actions.ts +0 -294
- package/src/lib/bird-command.ts +0 -130
- package/src/lib/bird.ts +0 -1140
- package/src/lib/blocklist.ts +0 -136
- package/src/lib/blocks-write.ts +0 -167
- package/src/lib/blocks.ts +0 -389
- package/src/lib/bookmark-sync-job.ts +0 -536
- package/src/lib/client-cache.ts +0 -109
- package/src/lib/config.ts +0 -224
- package/src/lib/conversation-surface.ts +0 -205
- package/src/lib/data-sources.ts +0 -219
- package/src/lib/db.ts +0 -1135
- package/src/lib/dms-live.ts +0 -933
- package/src/lib/effect-runtime.ts +0 -45
- package/src/lib/follow-graph.ts +0 -1097
- package/src/lib/geocoding.ts +0 -296
- package/src/lib/http-effect.ts +0 -222
- package/src/lib/identity-search-index.ts +0 -369
- package/src/lib/inbox.ts +0 -241
- package/src/lib/link-index.ts +0 -641
- package/src/lib/link-insights.ts +0 -860
- package/src/lib/link-preview-metadata.ts +0 -754
- package/src/lib/location.ts +0 -137
- package/src/lib/media-fetch.ts +0 -860
- package/src/lib/media-includes.ts +0 -165
- package/src/lib/mention-threads-live.ts +0 -917
- package/src/lib/mentions-export.ts +0 -147
- package/src/lib/mentions-live.ts +0 -1238
- package/src/lib/moderation-target.ts +0 -216
- package/src/lib/moderation-write.ts +0 -131
- package/src/lib/mutes-write.ts +0 -167
- package/src/lib/mutes.ts +0 -84
- package/src/lib/ndjson-stream.ts +0 -106
- package/src/lib/network-map.ts +0 -382
- package/src/lib/openai.ts +0 -117
- package/src/lib/period-digest.ts +0 -1552
- package/src/lib/present.ts +0 -118
- package/src/lib/profile-affiliation-hydration.ts +0 -228
- package/src/lib/profile-affiliations.ts +0 -262
- package/src/lib/profile-analysis.ts +0 -1272
- package/src/lib/profile-bio-entities.ts +0 -318
- package/src/lib/profile-history.ts +0 -164
- package/src/lib/profile-hydration.ts +0 -180
- package/src/lib/profile-replies.ts +0 -77
- package/src/lib/profile-resolver.ts +0 -650
- package/src/lib/queries.ts +0 -2381
- package/src/lib/research.ts +0 -652
- package/src/lib/search-discussion.ts +0 -1016
- package/src/lib/seed.ts +0 -613
- package/src/lib/sqlite.ts +0 -147
- package/src/lib/sync-cache.ts +0 -65
- package/src/lib/theme-transition.ts +0 -113
- package/src/lib/theme.tsx +0 -165
- package/src/lib/timeline-collections-live.ts +0 -581
- package/src/lib/timeline-live.ts +0 -452
- package/src/lib/tweet-account-edges.ts +0 -48
- package/src/lib/tweet-lookup.ts +0 -46
- package/src/lib/tweet-render.ts +0 -430
- package/src/lib/tweet-search-live.ts +0 -566
- package/src/lib/types.ts +0 -723
- package/src/lib/ui.ts +0 -529
- package/src/lib/url-expansion-store.ts +0 -110
- package/src/lib/url-expansion.ts +0 -343
- package/src/lib/url-safety.ts +0 -220
- package/src/lib/web-sync.ts +0 -516
- package/src/lib/whois.ts +0 -1079
- package/src/lib/x-profile.ts +0 -365
- package/src/lib/x-web.ts +0 -199
- package/src/lib/xurl-rate-limits.ts +0 -267
- package/src/lib/xurl.ts +0 -1677
- package/src/routeTree.gen.ts +0 -723
- package/src/router.tsx +0 -20
- package/src/routes/__root.tsx +0 -79
- package/src/routes/api/action.tsx +0 -147
- package/src/routes/api/avatar.tsx +0 -50
- package/src/routes/api/blocks.tsx +0 -35
- package/src/routes/api/conversation.tsx +0 -45
- package/src/routes/api/data-sources.tsx +0 -24
- package/src/routes/api/inbox.tsx +0 -43
- package/src/routes/api/link-insights.tsx +0 -85
- package/src/routes/api/link-preview.tsx +0 -47
- package/src/routes/api/network-map.tsx +0 -55
- package/src/routes/api/period-digest.tsx +0 -101
- package/src/routes/api/profile-analysis.tsx +0 -97
- package/src/routes/api/profile-hydrate.tsx +0 -54
- package/src/routes/api/query.tsx +0 -115
- package/src/routes/api/search-discussion.tsx +0 -113
- package/src/routes/api/status.tsx +0 -28
- package/src/routes/api/sync.tsx +0 -105
- package/src/routes/api/xurl-rate-limits.tsx +0 -24
- package/src/routes/blocks.tsx +0 -377
- package/src/routes/bookmarks.tsx +0 -18
- package/src/routes/data-sources.tsx +0 -257
- package/src/routes/discuss.tsx +0 -419
- package/src/routes/dms.tsx +0 -432
- package/src/routes/inbox.tsx +0 -226
- package/src/routes/index.tsx +0 -32
- package/src/routes/likes.tsx +0 -18
- package/src/routes/links.tsx +0 -994
- package/src/routes/mentions.tsx +0 -32
- package/src/routes/network-map.tsx +0 -1035
- package/src/routes/profile-analyze.tsx +0 -112
- package/src/routes/profiles.$handle.tsx +0 -228
- package/src/routes/rate-limits.tsx +0 -309
- package/src/routes/today.tsx +0 -511
- package/src/styles.css +0 -257
- package/tsconfig.json +0 -29
- package/vite.config.ts +0 -29
- /package/{public → dist/client}/birdclaw-mark.png +0 -0
- /package/{public → dist/client}/favicon.ico +0 -0
- /package/{public → dist/client}/logo192.png +0 -0
- /package/{public → dist/client}/logo512.png +0 -0
- /package/{public → dist/client}/manifest.json +0 -0
- /package/{public → dist/client}/robots.txt +0 -0
package/src/lib/network-map.ts
DELETED
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
import { getNativeDb } from "./db";
|
|
2
|
-
import {
|
|
3
|
-
geocodeLocation,
|
|
4
|
-
GeocodeRateLimitError,
|
|
5
|
-
getOpenCageApiKey,
|
|
6
|
-
readCachedGeocodes,
|
|
7
|
-
readSuppressedGeocodeKeys,
|
|
8
|
-
type GeocodeResult,
|
|
9
|
-
} from "./geocoding";
|
|
10
|
-
import { isMeaningfulLocation, normalizeLocationKey } from "./location";
|
|
11
|
-
import type { Database } from "./sqlite";
|
|
12
|
-
|
|
13
|
-
export type NetworkMapKind = "all" | "followers" | "following" | "mutual";
|
|
14
|
-
|
|
15
|
-
export interface NetworkMapProfileProperties {
|
|
16
|
-
profileId: string;
|
|
17
|
-
handle: string;
|
|
18
|
-
name: string;
|
|
19
|
-
avatarUrl: string | null;
|
|
20
|
-
location: string;
|
|
21
|
-
resolvedLocation: string | null;
|
|
22
|
-
followersCount: number;
|
|
23
|
-
followingCount: number;
|
|
24
|
-
verified: boolean | null;
|
|
25
|
-
relationship: "followers" | "following" | "mutual";
|
|
26
|
-
approxRadiusM: number | null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface NetworkMapFeature {
|
|
30
|
-
type: "Feature";
|
|
31
|
-
geometry: { type: "Point"; coordinates: [number, number] };
|
|
32
|
-
properties: NetworkMapProfileProperties;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface NetworkMapResponse {
|
|
36
|
-
type: "FeatureCollection";
|
|
37
|
-
features: NetworkMapFeature[];
|
|
38
|
-
meta: {
|
|
39
|
-
accountId: string;
|
|
40
|
-
type: NetworkMapKind;
|
|
41
|
-
totalProfiles: number;
|
|
42
|
-
profilesWithLocation: number;
|
|
43
|
-
meaningfulProfiles: number;
|
|
44
|
-
locatedProfiles: number;
|
|
45
|
-
missingGeocodes: number;
|
|
46
|
-
geocodedThisRun: number;
|
|
47
|
-
suppressedGeocodes: number;
|
|
48
|
-
opencageConfigured: boolean;
|
|
49
|
-
mapboxTokenConfigured: boolean;
|
|
50
|
-
};
|
|
51
|
-
config: {
|
|
52
|
-
mapboxToken: string | null;
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
interface ProfileLocationRow {
|
|
57
|
-
id: string;
|
|
58
|
-
handle: string;
|
|
59
|
-
display_name: string;
|
|
60
|
-
followers_count: number;
|
|
61
|
-
following_count: number;
|
|
62
|
-
avatar_url: string | null;
|
|
63
|
-
location: string | null;
|
|
64
|
-
verified_type: string | null;
|
|
65
|
-
in_followers: number;
|
|
66
|
-
in_following: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
interface NetworkMapOptions {
|
|
70
|
-
account?: string;
|
|
71
|
-
type?: NetworkMapKind;
|
|
72
|
-
limit?: number;
|
|
73
|
-
geocodeLimit?: number;
|
|
74
|
-
refresh?: boolean;
|
|
75
|
-
signal?: AbortSignal;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const DEFAULT_LIMIT = 10_000;
|
|
79
|
-
const MAX_LIMIT = 50_000;
|
|
80
|
-
const DEFAULT_GEOCODE_LIMIT = 80;
|
|
81
|
-
const MAX_GEOCODE_LIMIT = 500;
|
|
82
|
-
const OPENCAGE_REQUEST_DELAY_MS = 1100;
|
|
83
|
-
|
|
84
|
-
function abortableDelay(ms: number, signal?: AbortSignal) {
|
|
85
|
-
if (signal?.aborted) return Promise.reject(new Error("geocode aborted"));
|
|
86
|
-
return new Promise<void>((resolve, reject) => {
|
|
87
|
-
const timeout = setTimeout(() => {
|
|
88
|
-
signal?.removeEventListener("abort", onAbort);
|
|
89
|
-
resolve();
|
|
90
|
-
}, ms);
|
|
91
|
-
function onAbort() {
|
|
92
|
-
clearTimeout(timeout);
|
|
93
|
-
reject(new Error("geocode aborted"));
|
|
94
|
-
}
|
|
95
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function getPublicMapboxToken() {
|
|
100
|
-
const token =
|
|
101
|
-
process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN?.trim() ||
|
|
102
|
-
process.env.BIRDCLAW_MAPBOX_ACCESS_TOKEN?.trim() ||
|
|
103
|
-
null;
|
|
104
|
-
return token?.startsWith("pk.") ? token : null;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function parseLimit(
|
|
108
|
-
value: number | undefined,
|
|
109
|
-
fallback: number,
|
|
110
|
-
max: number,
|
|
111
|
-
min = 1,
|
|
112
|
-
) {
|
|
113
|
-
if (!Number.isFinite(value) || value === undefined || value < min)
|
|
114
|
-
return fallback;
|
|
115
|
-
return Math.min(max, Math.floor(value));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function resolveAccountId(db: Database, accountId?: string) {
|
|
119
|
-
const row = accountId
|
|
120
|
-
? (db
|
|
121
|
-
.prepare("select id from accounts where id = ? or handle = ? limit 1")
|
|
122
|
-
.get(accountId, accountId.replace(/^@/, "")) as
|
|
123
|
-
| { id: string }
|
|
124
|
-
| undefined)
|
|
125
|
-
: (db
|
|
126
|
-
.prepare(
|
|
127
|
-
"select id from accounts order by is_default desc, created_at asc limit 1",
|
|
128
|
-
)
|
|
129
|
-
.get() as { id: string } | undefined);
|
|
130
|
-
if (!row) throw new Error(`Unknown account: ${accountId ?? "default"}`);
|
|
131
|
-
return row.id;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function relationshipForRow(
|
|
135
|
-
row: Pick<ProfileLocationRow, "in_followers" | "in_following">,
|
|
136
|
-
) {
|
|
137
|
-
if (row.in_followers && row.in_following) return "mutual";
|
|
138
|
-
if (row.in_followers) return "followers";
|
|
139
|
-
return "following";
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function fetchNetworkRows({
|
|
143
|
-
db,
|
|
144
|
-
accountId,
|
|
145
|
-
type,
|
|
146
|
-
limit,
|
|
147
|
-
}: {
|
|
148
|
-
db: Database;
|
|
149
|
-
accountId: string;
|
|
150
|
-
type: NetworkMapKind;
|
|
151
|
-
limit: number;
|
|
152
|
-
}) {
|
|
153
|
-
const having =
|
|
154
|
-
type === "mutual"
|
|
155
|
-
? "having max(case when fe.direction = 'followers' then 1 else 0 end) = 1 and max(case when fe.direction = 'following' then 1 else 0 end) = 1"
|
|
156
|
-
: type === "followers"
|
|
157
|
-
? "having max(case when fe.direction = 'followers' then 1 else 0 end) = 1"
|
|
158
|
-
: type === "following"
|
|
159
|
-
? "having max(case when fe.direction = 'following' then 1 else 0 end) = 1"
|
|
160
|
-
: "";
|
|
161
|
-
const rows = db
|
|
162
|
-
.prepare(
|
|
163
|
-
`
|
|
164
|
-
select
|
|
165
|
-
p.id,
|
|
166
|
-
p.handle,
|
|
167
|
-
p.display_name,
|
|
168
|
-
p.followers_count,
|
|
169
|
-
p.following_count,
|
|
170
|
-
p.avatar_url,
|
|
171
|
-
p.location,
|
|
172
|
-
p.verified_type,
|
|
173
|
-
max(case when fe.direction = 'followers' then 1 else 0 end) as in_followers,
|
|
174
|
-
max(case when fe.direction = 'following' then 1 else 0 end) as in_following
|
|
175
|
-
from follow_edges fe
|
|
176
|
-
join profiles p on p.id = fe.profile_id
|
|
177
|
-
where fe.account_id = ?
|
|
178
|
-
and fe.current = 1
|
|
179
|
-
group by p.id
|
|
180
|
-
${having}
|
|
181
|
-
order by p.followers_count desc, p.handle asc
|
|
182
|
-
limit ?
|
|
183
|
-
`,
|
|
184
|
-
)
|
|
185
|
-
.all(accountId, limit) as ProfileLocationRow[];
|
|
186
|
-
return rows;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function collectKeys(rows: ProfileLocationRow[]) {
|
|
190
|
-
const originalByKey = new Map<string, string>();
|
|
191
|
-
const keyByProfile = new Map<string, string>();
|
|
192
|
-
const seen = new Set<string>();
|
|
193
|
-
const keys: string[] = [];
|
|
194
|
-
for (const row of rows) {
|
|
195
|
-
const location = row.location;
|
|
196
|
-
if (!location || !isMeaningfulLocation(location)) continue;
|
|
197
|
-
const key = normalizeLocationKey(location);
|
|
198
|
-
if (!key) continue;
|
|
199
|
-
keyByProfile.set(row.id, key);
|
|
200
|
-
if (!originalByKey.has(key)) originalByKey.set(key, location);
|
|
201
|
-
if (!seen.has(key)) {
|
|
202
|
-
seen.add(key);
|
|
203
|
-
keys.push(key);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return { keys, keyByProfile, originalByKey };
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
async function fillMissingGeocodes({
|
|
210
|
-
db,
|
|
211
|
-
keys,
|
|
212
|
-
originalByKey,
|
|
213
|
-
refresh,
|
|
214
|
-
geocodeLimit,
|
|
215
|
-
signal,
|
|
216
|
-
}: {
|
|
217
|
-
db: Database;
|
|
218
|
-
keys: string[];
|
|
219
|
-
originalByKey: Map<string, string>;
|
|
220
|
-
refresh: boolean;
|
|
221
|
-
geocodeLimit: number;
|
|
222
|
-
signal?: AbortSignal;
|
|
223
|
-
}) {
|
|
224
|
-
const cache = readCachedGeocodes(keys, db);
|
|
225
|
-
const suppressed = readSuppressedGeocodeKeys(keys, db);
|
|
226
|
-
const coordinateKeys = keys.filter(
|
|
227
|
-
(key) => key.startsWith("coords:") && (refresh || !cache.has(key)),
|
|
228
|
-
);
|
|
229
|
-
const uncachedKeys = keys.filter(
|
|
230
|
-
(key) =>
|
|
231
|
-
!key.startsWith("coords:") &&
|
|
232
|
-
!cache.has(key) &&
|
|
233
|
-
(refresh || !suppressed.has(key)),
|
|
234
|
-
);
|
|
235
|
-
const refreshKeys = refresh
|
|
236
|
-
? keys.filter((key) => !key.startsWith("coords:") && cache.has(key))
|
|
237
|
-
: [];
|
|
238
|
-
const openCageKeys = getOpenCageApiKey()
|
|
239
|
-
? [...uncachedKeys, ...refreshKeys].slice(0, geocodeLimit)
|
|
240
|
-
: [];
|
|
241
|
-
let geocoded = 0;
|
|
242
|
-
for (const key of coordinateKeys) {
|
|
243
|
-
if (signal?.aborted) break;
|
|
244
|
-
const original = originalByKey.get(key);
|
|
245
|
-
if (!original) continue;
|
|
246
|
-
const result = await geocodeLocation(original, db, signal).catch(
|
|
247
|
-
() => null,
|
|
248
|
-
);
|
|
249
|
-
if (result) geocoded += 1;
|
|
250
|
-
}
|
|
251
|
-
for (let index = 0; index < openCageKeys.length; index += 1) {
|
|
252
|
-
if (signal?.aborted) break;
|
|
253
|
-
const key = openCageKeys[index];
|
|
254
|
-
if (!key) continue;
|
|
255
|
-
if (index > 0) {
|
|
256
|
-
await abortableDelay(OPENCAGE_REQUEST_DELAY_MS, signal).catch(() => null);
|
|
257
|
-
if (signal?.aborted) break;
|
|
258
|
-
}
|
|
259
|
-
const original = originalByKey.get(key);
|
|
260
|
-
if (!original) continue;
|
|
261
|
-
try {
|
|
262
|
-
const result = await geocodeLocation(original, db, signal);
|
|
263
|
-
if (result) geocoded += 1;
|
|
264
|
-
} catch (error) {
|
|
265
|
-
if (signal?.aborted) break;
|
|
266
|
-
if (error instanceof GeocodeRateLimitError) break;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
const updatedCache = readCachedGeocodes(keys, db);
|
|
270
|
-
const updatedSuppressed = readSuppressedGeocodeKeys(keys, db);
|
|
271
|
-
return {
|
|
272
|
-
cache: updatedCache,
|
|
273
|
-
missingCount: keys.filter(
|
|
274
|
-
(key) => !updatedCache.has(key) && !updatedSuppressed.has(key),
|
|
275
|
-
).length,
|
|
276
|
-
suppressedCount: updatedSuppressed.size,
|
|
277
|
-
geocoded,
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function buildFeatures({
|
|
282
|
-
rows,
|
|
283
|
-
keyByProfile,
|
|
284
|
-
cache,
|
|
285
|
-
}: {
|
|
286
|
-
rows: ProfileLocationRow[];
|
|
287
|
-
keyByProfile: Map<string, string>;
|
|
288
|
-
cache: Map<string, GeocodeResult>;
|
|
289
|
-
}) {
|
|
290
|
-
const byKey = new Map<string, ProfileLocationRow[]>();
|
|
291
|
-
for (const row of rows) {
|
|
292
|
-
const key = keyByProfile.get(row.id);
|
|
293
|
-
if (!key || !cache.has(key)) continue;
|
|
294
|
-
const group = byKey.get(key);
|
|
295
|
-
if (group) group.push(row);
|
|
296
|
-
else byKey.set(key, [row]);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const features: NetworkMapFeature[] = [];
|
|
300
|
-
for (const [key, members] of byKey) {
|
|
301
|
-
const geo = cache.get(key);
|
|
302
|
-
if (!geo) continue;
|
|
303
|
-
for (let index = 0; index < members.length; index += 1) {
|
|
304
|
-
const row = members[index];
|
|
305
|
-
if (!row?.location) continue;
|
|
306
|
-
features.push({
|
|
307
|
-
type: "Feature",
|
|
308
|
-
geometry: { type: "Point", coordinates: [geo.lng, geo.lat] },
|
|
309
|
-
properties: {
|
|
310
|
-
profileId: row.id,
|
|
311
|
-
handle: row.handle,
|
|
312
|
-
name: row.display_name,
|
|
313
|
-
avatarUrl: row.avatar_url,
|
|
314
|
-
location: row.location,
|
|
315
|
-
resolvedLocation: geo.formatted ?? null,
|
|
316
|
-
followersCount: Number(row.followers_count ?? 0),
|
|
317
|
-
followingCount: Number(row.following_count ?? 0),
|
|
318
|
-
verified:
|
|
319
|
-
row.verified_type && row.verified_type !== "none" ? true : null,
|
|
320
|
-
relationship: relationshipForRow(row),
|
|
321
|
-
approxRadiusM: geo.approxRadiusM ?? null,
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return features;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
export async function getNetworkMap(
|
|
330
|
-
options: NetworkMapOptions = {},
|
|
331
|
-
db = getNativeDb(),
|
|
332
|
-
): Promise<NetworkMapResponse> {
|
|
333
|
-
const accountId = resolveAccountId(db, options.account);
|
|
334
|
-
const type = options.type ?? "all";
|
|
335
|
-
const limit = parseLimit(options.limit, DEFAULT_LIMIT, MAX_LIMIT);
|
|
336
|
-
const geocodeLimit = parseLimit(
|
|
337
|
-
options.geocodeLimit,
|
|
338
|
-
DEFAULT_GEOCODE_LIMIT,
|
|
339
|
-
MAX_GEOCODE_LIMIT,
|
|
340
|
-
0,
|
|
341
|
-
);
|
|
342
|
-
const rows = fetchNetworkRows({ db, accountId, type, limit });
|
|
343
|
-
const rowsWithLocation = rows.filter((row) => row.location);
|
|
344
|
-
const meaningfulRows = rowsWithLocation.filter((row) =>
|
|
345
|
-
row.location ? isMeaningfulLocation(row.location) : false,
|
|
346
|
-
);
|
|
347
|
-
const { keys, keyByProfile, originalByKey } = collectKeys(meaningfulRows);
|
|
348
|
-
const geocodes = await fillMissingGeocodes({
|
|
349
|
-
db,
|
|
350
|
-
keys,
|
|
351
|
-
originalByKey,
|
|
352
|
-
refresh: options.refresh === true,
|
|
353
|
-
geocodeLimit,
|
|
354
|
-
signal: options.signal,
|
|
355
|
-
});
|
|
356
|
-
const features = buildFeatures({
|
|
357
|
-
rows: meaningfulRows,
|
|
358
|
-
keyByProfile,
|
|
359
|
-
cache: geocodes.cache,
|
|
360
|
-
});
|
|
361
|
-
const token = getPublicMapboxToken();
|
|
362
|
-
return {
|
|
363
|
-
type: "FeatureCollection",
|
|
364
|
-
features,
|
|
365
|
-
meta: {
|
|
366
|
-
accountId,
|
|
367
|
-
type,
|
|
368
|
-
totalProfiles: rows.length,
|
|
369
|
-
profilesWithLocation: rowsWithLocation.length,
|
|
370
|
-
meaningfulProfiles: meaningfulRows.length,
|
|
371
|
-
locatedProfiles: features.length,
|
|
372
|
-
missingGeocodes: geocodes.missingCount,
|
|
373
|
-
geocodedThisRun: geocodes.geocoded,
|
|
374
|
-
suppressedGeocodes: geocodes.suppressedCount,
|
|
375
|
-
opencageConfigured: Boolean(getOpenCageApiKey()),
|
|
376
|
-
mapboxTokenConfigured: Boolean(token),
|
|
377
|
-
},
|
|
378
|
-
config: {
|
|
379
|
-
mapboxToken: token,
|
|
380
|
-
},
|
|
381
|
-
};
|
|
382
|
-
}
|
package/src/lib/openai.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
|
-
import { runEffectPromise, tryPromise } from "./effect-runtime";
|
|
3
|
-
|
|
4
|
-
export interface OpenAIInboxScore {
|
|
5
|
-
score: number;
|
|
6
|
-
summary: string;
|
|
7
|
-
reasoning: string;
|
|
8
|
-
model: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface OpenAIInboxInput {
|
|
12
|
-
entityKind: "mention" | "dm";
|
|
13
|
-
title: string;
|
|
14
|
-
text: string;
|
|
15
|
-
participant: {
|
|
16
|
-
handle: string;
|
|
17
|
-
displayName: string;
|
|
18
|
-
bio: string;
|
|
19
|
-
followersCount: number;
|
|
20
|
-
};
|
|
21
|
-
influenceScore: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function clampScore(value: number) {
|
|
25
|
-
return Math.max(0, Math.min(100, Math.round(value)));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function toError(error: unknown) {
|
|
29
|
-
return error instanceof Error ? error : new Error(String(error));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function trySync<T>(try_: () => T) {
|
|
33
|
-
return Effect.try({
|
|
34
|
-
try: try_,
|
|
35
|
-
catch: toError,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function scoreInboxItemWithOpenAIEffect(
|
|
40
|
-
input: OpenAIInboxInput,
|
|
41
|
-
): Effect.Effect<OpenAIInboxScore, Error> {
|
|
42
|
-
return Effect.gen(function* () {
|
|
43
|
-
const apiKey = process.env.OPENAI_API_KEY;
|
|
44
|
-
if (!apiKey) {
|
|
45
|
-
return yield* Effect.fail(new Error("OPENAI_API_KEY is not set"));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const model = process.env.BIRDCLAW_OPENAI_MODEL || "gpt-5.2";
|
|
49
|
-
const response = yield* tryPromise(() =>
|
|
50
|
-
fetch("https://api.openai.com/v1/chat/completions", {
|
|
51
|
-
method: "POST",
|
|
52
|
-
headers: {
|
|
53
|
-
authorization: `Bearer ${apiKey}`,
|
|
54
|
-
"content-type": "application/json",
|
|
55
|
-
},
|
|
56
|
-
body: JSON.stringify({
|
|
57
|
-
model,
|
|
58
|
-
response_format: { type: "json_object" },
|
|
59
|
-
messages: [
|
|
60
|
-
{
|
|
61
|
-
role: "system",
|
|
62
|
-
content:
|
|
63
|
-
"You rank inbound Twitter mentions and DMs for Peter Steinberger. Return JSON only with keys score, summary, reasoning. Score 0-100. High score means worth replying soon. Prefer specific, actionable, novel, high-signal items. Penalize generic praise, low-context asks, and low-signal chatter. summary max 18 words. reasoning max 28 words.",
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
role: "user",
|
|
67
|
-
content: JSON.stringify(input),
|
|
68
|
-
},
|
|
69
|
-
],
|
|
70
|
-
}),
|
|
71
|
-
}),
|
|
72
|
-
).pipe(Effect.mapError(toError));
|
|
73
|
-
|
|
74
|
-
if (!response.ok) {
|
|
75
|
-
return yield* Effect.fail(
|
|
76
|
-
new Error(`OpenAI request failed: ${response.status}`),
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const payload = (yield* tryPromise(() => response.json()).pipe(
|
|
81
|
-
Effect.mapError(toError),
|
|
82
|
-
)) as {
|
|
83
|
-
choices?: Array<{
|
|
84
|
-
message?: {
|
|
85
|
-
content?: string;
|
|
86
|
-
};
|
|
87
|
-
}>;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const content = payload.choices?.[0]?.message?.content;
|
|
91
|
-
if (!content) {
|
|
92
|
-
return yield* Effect.fail(new Error("OpenAI returned no content"));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const parsed = yield* trySync(
|
|
96
|
-
() =>
|
|
97
|
-
JSON.parse(content) as {
|
|
98
|
-
score?: number;
|
|
99
|
-
summary?: string;
|
|
100
|
-
reasoning?: string;
|
|
101
|
-
},
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
model,
|
|
106
|
-
score: clampScore(parsed.score ?? 0),
|
|
107
|
-
summary: String(parsed.summary ?? "No summary"),
|
|
108
|
-
reasoning: String(parsed.reasoning ?? "No reasoning"),
|
|
109
|
-
};
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function scoreInboxItemWithOpenAI(
|
|
114
|
-
input: OpenAIInboxInput,
|
|
115
|
-
): Promise<OpenAIInboxScore> {
|
|
116
|
-
return runEffectPromise(scoreInboxItemWithOpenAIEffect(input));
|
|
117
|
-
}
|