birdclaw 0.6.0 → 0.8.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 +64 -0
- package/README.md +32 -2
- package/package.json +29 -30
- package/scripts/build-docs-site.mjs +39 -12
- package/src/cli.ts +457 -26
- package/src/components/AppNav.tsx +10 -0
- package/src/components/MarkdownViewer.tsx +438 -72
- package/src/components/ProfileAnalysisStream.tsx +428 -0
- package/src/components/ProfilePreview.tsx +120 -9
- package/src/components/SavedTimelineView.tsx +30 -8
- package/src/components/SyncNowButton.tsx +5 -2
- package/src/components/TimelineCard.tsx +20 -4
- package/src/components/TimelineRouteFrame.tsx +16 -0
- package/src/components/TweetRichText.tsx +36 -12
- package/src/components/useTimelineRouteData.ts +74 -6
- package/src/lib/account-sync-job.ts +15 -3
- package/src/lib/archive-finder.ts +1 -1
- package/src/lib/archive-import.ts +245 -7
- package/src/lib/authored-live.ts +1 -0
- package/src/lib/avatar-cache.ts +50 -0
- package/src/lib/backup.ts +4 -3
- package/src/lib/bird.ts +33 -0
- package/src/lib/config.ts +35 -2
- package/src/lib/data-sources.ts +219 -0
- package/src/lib/db.ts +62 -1
- package/src/lib/geocoding.ts +296 -0
- package/src/lib/link-insights.ts +2 -0
- package/src/lib/location.ts +137 -0
- package/src/lib/mention-threads-live.ts +94 -1
- package/src/lib/mentions-live.ts +187 -40
- package/src/lib/network-map.ts +382 -0
- package/src/lib/period-digest.ts +468 -29
- package/src/lib/profile-analysis.ts +1272 -0
- package/src/lib/profile-bio-entities.ts +1 -1
- package/src/lib/queries.ts +14 -4
- package/src/lib/search-discussion.ts +1016 -0
- package/src/lib/timeline-live.ts +272 -19
- package/src/lib/tweet-account-edges.ts +2 -0
- package/src/lib/tweet-render.ts +141 -1
- package/src/lib/tweet-search-live.ts +565 -0
- package/src/lib/types.ts +37 -2
- package/src/lib/ui.ts +1 -1
- package/src/lib/web-sync.ts +7 -2
- package/src/lib/xurl-rate-limits.ts +267 -0
- package/src/lib/xurl.ts +551 -41
- package/src/routeTree.gen.ts +231 -0
- package/src/routes/__root.tsx +5 -6
- package/src/routes/api/data-sources.tsx +24 -0
- package/src/routes/api/network-map.tsx +55 -0
- package/src/routes/api/period-digest.tsx +29 -3
- package/src/routes/api/profile-analysis.tsx +152 -0
- package/src/routes/api/query.tsx +1 -0
- package/src/routes/api/search-discussion.tsx +169 -0
- package/src/routes/api/xurl-rate-limits.tsx +24 -0
- package/src/routes/data-sources.tsx +257 -0
- package/src/routes/discuss.tsx +419 -0
- package/src/routes/network-map.tsx +1035 -0
- package/src/routes/profile-analyze.tsx +112 -0
- package/src/routes/profiles.$handle.tsx +228 -0
- package/src/routes/rate-limits.tsx +309 -0
- package/src/routes/today.tsx +22 -8
- package/src/styles.css +22 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const COMBINING_DIACRITIC_PATTERN = /[\u0300-\u036f]/g;
|
|
2
|
+
const COLLAPSED_WHITESPACE_PATTERN = /\s+/g;
|
|
3
|
+
const COORDINATE_CAPTURE_PATTERN =
|
|
4
|
+
/^\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*$/;
|
|
5
|
+
const LETTER_PATTERN = /[a-z]/i;
|
|
6
|
+
const URL_OR_HANDLE_PATTERN = /(https?:\/\/|@|[A-Z0-9._%+-]+@[A-Z0-9.-]+)/i;
|
|
7
|
+
const MONEY_PATTERN = /\$\s?\d/;
|
|
8
|
+
const EMOJI_BLOCK_PATTERN =
|
|
9
|
+
/[\u{1f1e6}-\u{1f1ff}\u{1f300}-\u{1faff}\u{2600}-\u{27bf}]/gu;
|
|
10
|
+
|
|
11
|
+
const GENERIC_LOCATIONS = new Set([
|
|
12
|
+
"earth",
|
|
13
|
+
"everywhere",
|
|
14
|
+
"internet",
|
|
15
|
+
"milky way",
|
|
16
|
+
"n/a",
|
|
17
|
+
"na",
|
|
18
|
+
"none",
|
|
19
|
+
"null",
|
|
20
|
+
"online",
|
|
21
|
+
"somewhere",
|
|
22
|
+
"space",
|
|
23
|
+
"the internet",
|
|
24
|
+
"the world",
|
|
25
|
+
"undefined",
|
|
26
|
+
"world",
|
|
27
|
+
"world wide web",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const DROP_WORDS = new Set([
|
|
31
|
+
"a",
|
|
32
|
+
"an",
|
|
33
|
+
"and",
|
|
34
|
+
"anywhere",
|
|
35
|
+
"around",
|
|
36
|
+
"at",
|
|
37
|
+
"between",
|
|
38
|
+
"by",
|
|
39
|
+
"everywhere",
|
|
40
|
+
"global",
|
|
41
|
+
"here",
|
|
42
|
+
"in",
|
|
43
|
+
"inside",
|
|
44
|
+
"my",
|
|
45
|
+
"near",
|
|
46
|
+
"nearby",
|
|
47
|
+
"of",
|
|
48
|
+
"on",
|
|
49
|
+
"remote",
|
|
50
|
+
"somewhere",
|
|
51
|
+
"the",
|
|
52
|
+
"there",
|
|
53
|
+
"where",
|
|
54
|
+
"worldwide",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const NON_LOCATION_PHRASES = [
|
|
58
|
+
/\b(check(out)? my|follow me|newsletter|subscribe|readers)\b/i,
|
|
59
|
+
/\b(in my (own )?head|right behind you|on my computer)\b/i,
|
|
60
|
+
/\b(dev\/null|dev null|xcode|vim|screen|framebuffer)\b/i,
|
|
61
|
+
/\b(blockchain|metaverse|omniverse|hyperamerica)\b/i,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function parseCoordinatesFromString(input: string) {
|
|
65
|
+
const match = input.match(COORDINATE_CAPTURE_PATTERN);
|
|
66
|
+
if (!match) return null;
|
|
67
|
+
const lat = Number(match[1]);
|
|
68
|
+
const lng = Number(match[2]);
|
|
69
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;
|
|
70
|
+
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) return null;
|
|
71
|
+
return { lat, lng };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stripJunkWords(part: string) {
|
|
75
|
+
const words = part
|
|
76
|
+
.split(" ")
|
|
77
|
+
.map((word) => word.trim())
|
|
78
|
+
.filter((word) => word.length > 0);
|
|
79
|
+
const kept = words.filter((word) => !DROP_WORDS.has(word));
|
|
80
|
+
return kept.join(" ").replace(COLLAPSED_WHITESPACE_PATTERN, " ").trim();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeLocationKey(input: string): string {
|
|
84
|
+
const raw = String(input || "").trim();
|
|
85
|
+
if (!raw) return "";
|
|
86
|
+
|
|
87
|
+
if (URL_OR_HANDLE_PATTERN.test(raw) || MONEY_PATTERN.test(raw)) return "";
|
|
88
|
+
for (const pattern of NON_LOCATION_PHRASES) {
|
|
89
|
+
if (pattern.test(raw)) return "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const coords = parseCoordinatesFromString(raw);
|
|
93
|
+
if (coords) {
|
|
94
|
+
return `coords:${Number(coords.lat.toFixed(6))},${Number(
|
|
95
|
+
coords.lng.toFixed(6),
|
|
96
|
+
)}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let value = raw
|
|
100
|
+
.toLowerCase()
|
|
101
|
+
.normalize("NFKD")
|
|
102
|
+
.replace(COMBINING_DIACRITIC_PATTERN, "")
|
|
103
|
+
.replace(EMOJI_BLOCK_PATTERN, "")
|
|
104
|
+
.replace(/[|;]+/g, ",")
|
|
105
|
+
.replace(/\s+(?:and|or|\/|-)\s+/g, ",")
|
|
106
|
+
.replace(/[\\/]+/g, ",")
|
|
107
|
+
.replace(/[“”"']/g, "")
|
|
108
|
+
.replace(COLLAPSED_WHITESPACE_PATTERN, " ")
|
|
109
|
+
.replace(/\s*,\s*/g, ",")
|
|
110
|
+
.replace(/,+/g, ",")
|
|
111
|
+
.replace(/[^a-z0-9, .-]/g, "")
|
|
112
|
+
.replace(/^[,\s.-]+|[,\s.-]+$/g, "");
|
|
113
|
+
|
|
114
|
+
if (!value || GENERIC_LOCATIONS.has(value)) return "";
|
|
115
|
+
|
|
116
|
+
const parts = value
|
|
117
|
+
.split(",")
|
|
118
|
+
.map((part) => stripJunkWords(part.trim()))
|
|
119
|
+
.filter((part) => part.length > 0 && !GENERIC_LOCATIONS.has(part));
|
|
120
|
+
|
|
121
|
+
value = parts.join(",");
|
|
122
|
+
if (!value || GENERIC_LOCATIONS.has(value)) return "";
|
|
123
|
+
return value.length <= 100 ? value : "";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isMeaningfulLocation(input: string): boolean {
|
|
127
|
+
const key = normalizeLocationKey(input);
|
|
128
|
+
if (!key) return false;
|
|
129
|
+
if (key.startsWith("coords:")) return true;
|
|
130
|
+
return LETTER_PATTERN.test(key) && key.length >= 2 && key.length <= 100;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function coordinatesFromLocationKey(key: string) {
|
|
134
|
+
if (!key.startsWith("coords:")) return null;
|
|
135
|
+
const parsed = parseCoordinatesFromString(key.slice("coords:".length));
|
|
136
|
+
return parsed ? { ...parsed, provider: "coords" as const } : null;
|
|
137
|
+
}
|
|
@@ -23,14 +23,23 @@ const DEFAULT_FALLBACK_DEPTH = 12;
|
|
|
23
23
|
const MAX_XURL_SEARCH_RESULTS = 100;
|
|
24
24
|
|
|
25
25
|
export type MentionThreadsMode = "bird" | "xurl";
|
|
26
|
+
export interface MentionThreadsProgress {
|
|
27
|
+
source: MentionThreadsMode;
|
|
28
|
+
processed: number;
|
|
29
|
+
total: number;
|
|
30
|
+
fetched: number;
|
|
31
|
+
done: boolean;
|
|
32
|
+
}
|
|
26
33
|
export interface SyncMentionThreadsOptions {
|
|
27
34
|
account?: string;
|
|
28
35
|
mode?: string;
|
|
29
36
|
limit?: number;
|
|
37
|
+
tweetIds?: string[];
|
|
30
38
|
delayMs?: number;
|
|
31
39
|
timeoutMs?: number;
|
|
32
40
|
all?: boolean;
|
|
33
41
|
maxPages?: number;
|
|
42
|
+
onProgress?: (progress: MentionThreadsProgress) => void;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
interface LocalMention {
|
|
@@ -268,6 +277,62 @@ function listRecentMentions(
|
|
|
268
277
|
});
|
|
269
278
|
}
|
|
270
279
|
|
|
280
|
+
function listMentionsByIds(
|
|
281
|
+
db: Database,
|
|
282
|
+
accountId: string,
|
|
283
|
+
tweetIds: string[],
|
|
284
|
+
limit: number,
|
|
285
|
+
): LocalMention[] {
|
|
286
|
+
const ids = [...new Set(tweetIds.filter((id) => id.trim().length > 0))].slice(
|
|
287
|
+
0,
|
|
288
|
+
limit,
|
|
289
|
+
);
|
|
290
|
+
if (ids.length === 0) return [];
|
|
291
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
292
|
+
const rows = db
|
|
293
|
+
.prepare(
|
|
294
|
+
`
|
|
295
|
+
select
|
|
296
|
+
t.id,
|
|
297
|
+
t.created_at as createdAt,
|
|
298
|
+
t.reply_to_id as replyToId,
|
|
299
|
+
coalesce(edge.raw_json, '{}') as rawJson
|
|
300
|
+
from tweets t
|
|
301
|
+
left join tweet_account_edges edge
|
|
302
|
+
on edge.tweet_id = t.id
|
|
303
|
+
and edge.account_id = ?
|
|
304
|
+
and edge.kind = 'mention'
|
|
305
|
+
where t.id in (${placeholders})
|
|
306
|
+
and (
|
|
307
|
+
edge.tweet_id is not null
|
|
308
|
+
or (t.kind = 'mention' and t.account_id = ?)
|
|
309
|
+
)
|
|
310
|
+
order by t.created_at desc
|
|
311
|
+
limit ?
|
|
312
|
+
`,
|
|
313
|
+
)
|
|
314
|
+
.all(accountId, ...ids, accountId, limit) as Array<{
|
|
315
|
+
id: string;
|
|
316
|
+
createdAt: string;
|
|
317
|
+
replyToId: string | null;
|
|
318
|
+
rawJson: string | null;
|
|
319
|
+
}>;
|
|
320
|
+
|
|
321
|
+
return rows.map((row) => {
|
|
322
|
+
const rawTweet = parseRawTweet(row.rawJson);
|
|
323
|
+
return {
|
|
324
|
+
id: row.id,
|
|
325
|
+
replyToId:
|
|
326
|
+
row.replyToId ?? (rawTweet ? getReplyToId(rawTweet) : undefined),
|
|
327
|
+
conversationId:
|
|
328
|
+
typeof rawTweet?.conversation_id === "string"
|
|
329
|
+
? rawTweet.conversation_id
|
|
330
|
+
: undefined,
|
|
331
|
+
rawTweet,
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
271
336
|
function mergeMentionThreadIntoLocalStore({
|
|
272
337
|
db,
|
|
273
338
|
accountId,
|
|
@@ -655,10 +720,12 @@ export function syncMentionThreadsEffect({
|
|
|
655
720
|
account,
|
|
656
721
|
mode = DEFAULT_MODE,
|
|
657
722
|
limit = DEFAULT_LIMIT,
|
|
723
|
+
tweetIds,
|
|
658
724
|
delayMs = DEFAULT_DELAY_MS,
|
|
659
725
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
660
726
|
all = false,
|
|
661
727
|
maxPages,
|
|
728
|
+
onProgress,
|
|
662
729
|
}: SyncMentionThreadsOptions) {
|
|
663
730
|
return Effect.gen(function* () {
|
|
664
731
|
const parsedMode = yield* trySync(() => parseMode(mode));
|
|
@@ -677,7 +744,14 @@ export function syncMentionThreadsEffect({
|
|
|
677
744
|
const db = yield* trySync(() => getNativeDb());
|
|
678
745
|
const resolvedAccount = yield* trySync(() => resolveAccount(db, account));
|
|
679
746
|
const mentions = yield* trySync(() =>
|
|
680
|
-
|
|
747
|
+
tweetIds
|
|
748
|
+
? listMentionsByIds(
|
|
749
|
+
db,
|
|
750
|
+
resolvedAccount.accountId,
|
|
751
|
+
tweetIds,
|
|
752
|
+
parsedLimit,
|
|
753
|
+
)
|
|
754
|
+
: listRecentMentions(db, resolvedAccount.accountId, parsedLimit),
|
|
681
755
|
);
|
|
682
756
|
const mentionIds = mentions.map((mention) => mention.id);
|
|
683
757
|
const mentionIdSet = new Set(mentionIds);
|
|
@@ -759,6 +833,15 @@ export function syncMentionThreadsEffect({
|
|
|
759
833
|
? fetched.error.message
|
|
760
834
|
: String(fetched.error),
|
|
761
835
|
});
|
|
836
|
+
yield* Effect.sync(() =>
|
|
837
|
+
onProgress?.({
|
|
838
|
+
source: parsedMode,
|
|
839
|
+
processed: index + 1,
|
|
840
|
+
total: mentions.length,
|
|
841
|
+
fetched: uniqueTweetIds.size,
|
|
842
|
+
done: index + 1 === mentions.length,
|
|
843
|
+
}),
|
|
844
|
+
);
|
|
762
845
|
continue;
|
|
763
846
|
}
|
|
764
847
|
|
|
@@ -782,6 +865,15 @@ export function syncMentionThreadsEffect({
|
|
|
782
865
|
warnings:
|
|
783
866
|
fetchResult.warnings.length > 0 ? fetchResult.warnings : undefined,
|
|
784
867
|
});
|
|
868
|
+
yield* Effect.sync(() =>
|
|
869
|
+
onProgress?.({
|
|
870
|
+
source: parsedMode,
|
|
871
|
+
processed: index + 1,
|
|
872
|
+
total: mentions.length,
|
|
873
|
+
fetched: uniqueTweetIds.size,
|
|
874
|
+
done: index + 1 === mentions.length,
|
|
875
|
+
}),
|
|
876
|
+
);
|
|
785
877
|
}
|
|
786
878
|
|
|
787
879
|
const failures = results.filter((item) => !item.ok);
|
|
@@ -810,6 +902,7 @@ export function syncMentionThreadsEffect({
|
|
|
810
902
|
all,
|
|
811
903
|
maxPages: parsedMaxPages ?? null,
|
|
812
904
|
maxFallbackDepth: DEFAULT_FALLBACK_DEPTH,
|
|
905
|
+
tweetIds: tweetIds ?? null,
|
|
813
906
|
},
|
|
814
907
|
results,
|
|
815
908
|
failures,
|