-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcrawler.js
More file actions
109 lines (90 loc) · 2.68 KB
/
Copy pathcrawler.js
File metadata and controls
109 lines (90 loc) · 2.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
const { Pool } = require("pg");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
function log(emoji, ...args) {
const time = new Date().toISOString().replace("T", " ").slice(0, 19);
console.log(`[${time}]`, emoji, ...args);
}
let knownSites = [
"https://ternbook.neocities.org",
];
let visited = new Set();
async function fetchJSON(baseUrl) {
try {
const res = await fetch(baseUrl + "/.well-known/ternbook.json", {
headers: { Accept: "application/json" },
signal: AbortSignal.timeout(5000),
});
if (!res.ok) throw new Error("bad response");
const data = await res.json();
return data;
} catch (err) {
log("❌", "fetch failed:", baseUrl, "-", err.message);
return null;
}
}
function validate(data, baseUrl) {
if (!data) return false;
if (!data.name || !data.url) return false;
if (data.url !== baseUrl) return false;
if (data.tags && data.tags.length > 10) return false;
if (data.neighbors && data.neighbors.length > 20) return false;
return true;
}
async function save(data) {
await pool.query(
`INSERT INTO sites (url, name, description, tags, neighbors, ial, heartbeat, last_seen)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (url) DO UPDATE SET
name = EXCLUDED.name,
description = EXCLUDED.description,
tags = EXCLUDED.tags,
neighbors = EXCLUDED.neighbors,
ial = EXCLUDED.ial,
heartbeat = EXCLUDED.heartbeat,
last_seen = EXCLUDED.last_seen`,
[
data.url,
data.name,
data.description ?? null,
data.tags ?? null,
data.neighbors ?? null,
data.ial ?? null,
data.heartbeat ? new Date(data.heartbeat) : null,
new Date(data.lastSeen),
]
);
}
async function crawlSite(url) {
if (visited.has(url)) return;
visited.add(url);
log("🔍", "crawling:", url);
const data = await fetchJSON(url);
if (!validate(data, url)) {
log("⚠️", "invalid or unreachable:", url);
return;
}
data.lastSeen = new Date().toISOString();
await save(data);
log("💾", "saved:", url);
if (data.neighbors) {
for (let neighbor of data.neighbors) {
if (!visited.has(neighbor)) {
log("🔗", "discovered neighbor:", neighbor);
knownSites.push(neighbor);
}
}
}
}
async function main() {
log("🚀", "crawler started, seed sites:", knownSites.length);
let i = 0;
for (let site of knownSites) {
i++;
log("📋", `progress: ${i}/${knownSites.length}`, `| queue: ${knownSites.length - i} remaining`);
await crawlSite(site);
await new Promise(r => setTimeout(r, 2000));
}
await pool.end();
log("✅", `done — visited ${visited.size} site(s)`);
}
main();