birdclaw 0.2.1 → 0.4.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 (39) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +54 -15
  3. package/package.json +14 -15
  4. package/playwright.config.ts +5 -2
  5. package/src/cli.ts +289 -20
  6. package/src/lib/actions-transport.ts +58 -1
  7. package/src/lib/archive-import.ts +34 -2
  8. package/src/lib/backup.ts +271 -33
  9. package/src/lib/bird-actions.ts +21 -0
  10. package/src/lib/bird.ts +332 -17
  11. package/src/lib/blocks.ts +3 -3
  12. package/src/lib/bookmark-sync-job.ts +3 -3
  13. package/src/lib/config.ts +34 -1
  14. package/src/lib/db.ts +321 -14
  15. package/src/lib/dms-live.ts +3 -3
  16. package/src/lib/identity-search-index.ts +369 -0
  17. package/src/lib/mention-threads-live.ts +267 -0
  18. package/src/lib/mentions-live.ts +18 -7
  19. package/src/lib/moderation-target.ts +7 -5
  20. package/src/lib/moderation-write.ts +3 -3
  21. package/src/lib/profile-affiliation-hydration.ts +189 -0
  22. package/src/lib/profile-affiliations.ts +262 -0
  23. package/src/lib/profile-bio-entities.ts +318 -0
  24. package/src/lib/profile-history.ts +164 -0
  25. package/src/lib/profile-hydration.ts +8 -24
  26. package/src/lib/profile-resolver.ts +428 -0
  27. package/src/lib/queries.ts +522 -68
  28. package/src/lib/research.ts +607 -0
  29. package/src/lib/seed.ts +10 -4
  30. package/src/lib/sqlite.ts +143 -0
  31. package/src/lib/timeline-collections-live.ts +22 -8
  32. package/src/lib/timeline-live.ts +182 -0
  33. package/src/lib/tweet-account-edges.ts +39 -0
  34. package/src/lib/tweet-lookup.ts +35 -0
  35. package/src/lib/types.ts +103 -1
  36. package/src/lib/url-expansion.ts +147 -0
  37. package/src/lib/whois.ts +1060 -0
  38. package/src/lib/x-profile.ts +174 -17
  39. package/src/lib/xurl.ts +41 -6
@@ -0,0 +1,147 @@
1
+ import { readSyncCache, writeSyncCache } from "./sync-cache";
2
+ import type { UrlExpansionItem } from "./types";
3
+
4
+ const SUCCESS_CACHE_TTL_MS = 365 * 24 * 60 * 60 * 1000;
5
+ const FAILURE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
6
+ const URL_REGEX = /https?:\/\/[^\s<>"')\]]+/g;
7
+
8
+ interface CachedUrlExpansion {
9
+ expandedUrl: string;
10
+ finalUrl: string;
11
+ status: UrlExpansionItem["status"];
12
+ title?: string;
13
+ description?: string | null;
14
+ error?: string;
15
+ }
16
+
17
+ export interface ExpandUrlsOptions {
18
+ refresh?: boolean;
19
+ successMaxAgeMs?: number;
20
+ failureMaxAgeMs?: number;
21
+ fetchImpl?: typeof fetch;
22
+ }
23
+
24
+ function cacheKeyForUrl(url: string) {
25
+ return `url:expand:${url}`;
26
+ }
27
+
28
+ function isFresh(updatedAt: string, maxAgeMs: number) {
29
+ return Date.now() - new Date(updatedAt).getTime() <= maxAgeMs;
30
+ }
31
+
32
+ function trimTrailingPunctuation(url: string) {
33
+ return url.replace(/[.,;:!?]+$/g, "");
34
+ }
35
+
36
+ export function extractUrls(text: string) {
37
+ return Array.from(
38
+ new Set(
39
+ Array.from(text.matchAll(URL_REGEX), (match) =>
40
+ trimTrailingPunctuation(match[0]),
41
+ ),
42
+ ),
43
+ ).filter((url) => url.length > 0);
44
+ }
45
+
46
+ function toExpansionItem(
47
+ url: string,
48
+ value: CachedUrlExpansion,
49
+ source: UrlExpansionItem["source"],
50
+ updatedAt: string,
51
+ ): UrlExpansionItem {
52
+ return {
53
+ url,
54
+ expandedUrl: value.expandedUrl,
55
+ finalUrl: value.finalUrl,
56
+ status: value.status,
57
+ source,
58
+ ...(value.title ? { title: value.title } : {}),
59
+ ...(value.description !== undefined
60
+ ? { description: value.description }
61
+ : {}),
62
+ ...(value.error ? { error: value.error } : {}),
63
+ updatedAt,
64
+ };
65
+ }
66
+
67
+ async function fetchExpansion(
68
+ url: string,
69
+ fetchImpl: typeof fetch,
70
+ ): Promise<CachedUrlExpansion> {
71
+ try {
72
+ let response = await fetchImpl(url, {
73
+ method: "HEAD",
74
+ redirect: "follow",
75
+ headers: { "user-agent": "birdclaw/0.3 url-expander" },
76
+ });
77
+
78
+ if (!response.url || response.url === url || response.status >= 400) {
79
+ response = await fetchImpl(url, {
80
+ method: "GET",
81
+ redirect: "follow",
82
+ headers: { "user-agent": "birdclaw/0.3 url-expander" },
83
+ });
84
+ }
85
+
86
+ const finalUrl = response.url || url;
87
+ return {
88
+ expandedUrl: finalUrl,
89
+ finalUrl,
90
+ status: response.ok || finalUrl !== url ? "hit" : "miss",
91
+ ...(response.ok ? {} : { error: `HTTP ${response.status}` }),
92
+ };
93
+ } catch (error) {
94
+ return {
95
+ expandedUrl: url,
96
+ finalUrl: url,
97
+ status: "error",
98
+ error: error instanceof Error ? error.message : String(error),
99
+ };
100
+ }
101
+ }
102
+
103
+ export async function expandUrls(
104
+ urls: string[],
105
+ options: ExpandUrlsOptions = {},
106
+ ): Promise<UrlExpansionItem[]> {
107
+ const uniqueUrls = Array.from(new Set(urls));
108
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
109
+ const results: UrlExpansionItem[] = [];
110
+
111
+ for (const url of uniqueUrls) {
112
+ const cached = readSyncCache<CachedUrlExpansion>(cacheKeyForUrl(url));
113
+ if (cached && !options.refresh) {
114
+ const maxAge =
115
+ cached.value.status === "hit"
116
+ ? (options.successMaxAgeMs ?? SUCCESS_CACHE_TTL_MS)
117
+ : (options.failureMaxAgeMs ?? FAILURE_CACHE_TTL_MS);
118
+ if (isFresh(cached.updatedAt, maxAge)) {
119
+ results.push(
120
+ toExpansionItem(url, cached.value, "cache", cached.updatedAt),
121
+ );
122
+ continue;
123
+ }
124
+ }
125
+
126
+ const value = await fetchExpansion(url, fetchImpl);
127
+ const updatedAt = writeSyncCache(cacheKeyForUrl(url), value);
128
+ results.push(toExpansionItem(url, value, "network", updatedAt));
129
+ }
130
+
131
+ return results;
132
+ }
133
+
134
+ export async function expandUrlsFromTexts(
135
+ texts: string[],
136
+ options: ExpandUrlsOptions = {},
137
+ ) {
138
+ return expandUrls(
139
+ texts.flatMap((text) => extractUrls(text)),
140
+ options,
141
+ );
142
+ }
143
+
144
+ export const __test__ = {
145
+ cacheKeyForUrl,
146
+ trimTrailingPunctuation,
147
+ };