six-degrees 0.4.0 → 0.4.2
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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +14 -14
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/legacy/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/export/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/import/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/restart/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js +1 -1
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-preview/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/settings/sector-suggestions/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/favicon.ico.body +0 -0
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/paths.html +1 -1
- package/.next/standalone/.next/server/app/paths.rsc +3 -3
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/profile.html +1 -1
- package/.next/standalone/.next/server/app/profile.rsc +3 -3
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/queue.html +1 -1
- package/.next/standalone/.next/server/app/queue.rsc +3 -3
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup.html +1 -1
- package/.next/standalone/.next/server/app/setup.rsc +3 -3
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e0fei3._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__03ydxxh._.js → [root-of-the-server]__20tmem7._.js} +1 -1
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +6 -6
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +1 -1
- package/.next/standalone/.next/server/chunks/_1mxn_lb._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_db-client_15le7cw.js +21 -7
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00d8snt._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00g61b8._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08lf3gy._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ho-u0w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ze473p._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1194jps._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cnxxc9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ll97ry._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1m0255u._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1w9z0zv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_15xuwkv._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1hgd5dp._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_20w06i-._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +4 -4
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +6 -2
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{3e8zg6ctvvb8p.js → 049vavjsmqgmn.js} +3 -3
- package/.next/standalone/.next/static/chunks/0ljxa-wk_iu__.js +18 -0
- package/.next/standalone/.next/static/chunks/0mp9b72fm-jp6.js +6 -0
- package/.next/standalone/.next/static/chunks/1pre1cotntk6-.js +1 -0
- package/.next/standalone/.next/static/chunks/{18xu0fbgo_qz-.js → 22x2_r768-u5i.js} +1 -1
- package/.next/standalone/.next/static/chunks/2p5yt1o9tnxjn.js +2 -0
- package/.next/standalone/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
- package/.next/standalone/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
- package/.next/standalone/CHANGELOG.md +86 -0
- package/.next/standalone/app/api/admin-delete/route.js +3 -0
- package/.next/standalone/app/api/ingest/route.js +13 -0
- package/.next/standalone/app/api/scraper/route.js +10 -2
- package/.next/standalone/app/components/ChainView.js +76 -15
- package/.next/standalone/app/components/FilterPanel.js +37 -1
- package/.next/standalone/app/components/ForceGraph.js +144 -12
- package/.next/standalone/app/components/LinkedInLimits.js +20 -3
- package/.next/standalone/app/components/OrbitGraph.js +92 -30
- package/.next/standalone/app/components/Sidebar.js +37 -0
- package/.next/standalone/app/components/views.js +7 -5
- package/.next/standalone/app/favicon.ico +0 -0
- package/.next/standalone/app/page.js +24 -7
- package/.next/standalone/app/paths/page.js +8 -1
- package/.next/standalone/db/schema.js +14 -0
- package/.next/standalone/lib/brokerage.js +94 -0
- package/.next/standalone/lib/chain-layout.js +89 -0
- package/.next/standalone/lib/circle.js +23 -4
- package/.next/standalone/lib/degrees.js +30 -0
- package/.next/standalone/lib/dot-rings.js +21 -0
- package/.next/standalone/lib/ingest.js +3 -3
- package/.next/standalone/lib/linkedin-limits.js +9 -4
- package/.next/standalone/lib/reach.js +22 -1
- package/.next/standalone/lib/search-risk.js +38 -0
- package/.next/standalone/lib/ties.js +23 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/scrape.py +50 -12
- package/.next/static/chunks/{3e8zg6ctvvb8p.js → 049vavjsmqgmn.js} +3 -3
- package/.next/static/chunks/0ljxa-wk_iu__.js +18 -0
- package/.next/static/chunks/0mp9b72fm-jp6.js +6 -0
- package/.next/static/chunks/1pre1cotntk6-.js +1 -0
- package/.next/static/chunks/{18xu0fbgo_qz-.js → 22x2_r768-u5i.js} +1 -1
- package/.next/static/chunks/2p5yt1o9tnxjn.js +2 -0
- package/.next/static/chunks/3zs_6-nn0dapb.js +1 -0
- package/.next/static/media/favicon.0zdzw5kurn8m-.ico +0 -0
- package/CHANGELOG.md +86 -0
- package/db/schema.js +14 -0
- package/lib/brokerage.js +94 -0
- package/lib/chain-layout.js +89 -0
- package/lib/circle.js +23 -4
- package/lib/degrees.js +30 -0
- package/lib/dot-rings.js +21 -0
- package/lib/ingest.js +3 -3
- package/lib/linkedin-limits.js +9 -4
- package/lib/reach.js +22 -1
- package/lib/search-risk.js +38 -0
- package/lib/ties.js +23 -0
- package/package.json +1 -1
- package/scripts/scrape.py +50 -12
- package/.next/standalone/.next/static/chunks/0w76iraw7ij3k.js +0 -18
- package/.next/standalone/.next/static/chunks/135n87ydxjrra.js +0 -2
- package/.next/standalone/.next/static/chunks/2pnbm1z6j7xab.js +0 -1
- package/.next/standalone/.next/static/chunks/3qh78g2aqhprd.js +0 -2
- package/.next/standalone/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- package/.next/static/chunks/0w76iraw7ij3k.js +0 -18
- package/.next/static/chunks/135n87ydxjrra.js +0 -2
- package/.next/static/chunks/2pnbm1z6j7xab.js +0 -1
- package/.next/static/chunks/3qh78g2aqhprd.js +0 -2
- package/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- /package/.next/standalone/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_ssgManifest.js +0 -0
- /package/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_buildManifest.js +0 -0
- /package/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{WseE_5HuU-1q0ywC9CP3U → wGVrXBkFLmEB4snfhtOcO}/_ssgManifest.js +0 -0
package/lib/linkedin-limits.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// changes here — the budget, and lifting a cooldown.
|
|
6
6
|
//
|
|
7
7
|
// The budget is searches (a day and a month) and profile views (a day). Profile
|
|
8
|
-
// views
|
|
9
|
-
//
|
|
8
|
+
// views got an account restricted (TRAPS §16), so their cap has no "no limit",
|
|
9
|
+
// and the scanner also keeps any two a minute apart. Searches did too, at 373 in
|
|
10
|
+
// 24 hours: the page asks before a risky search budget (lib/search-risk.js).
|
|
10
11
|
//
|
|
11
12
|
// They belong to the LinkedIn account, not to a profile in the app. TRAPS §35.
|
|
12
13
|
|
|
@@ -58,14 +59,18 @@ export function nextMonthStartPacific(ms = Date.now()) {
|
|
|
58
59
|
return monthStartPacific(start + 40 * DAY * 1000);
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
const profileChoice = (n) => PROFILE_CHOICES.filter((c) => c <= n).pop() ?? PROFILE_CHOICES[0];
|
|
63
|
+
|
|
61
64
|
export function readLimits(dir) {
|
|
62
65
|
const raw = readJson(path.join(dir, 'scan-limits.json')) || {};
|
|
63
66
|
const num = (v, d) => (Number.isInteger(v) && v >= 0 ? v : d);
|
|
64
67
|
return {
|
|
65
68
|
daily: num(raw.daily, DEFAULT_LIMITS.daily),
|
|
66
69
|
monthly: num(raw.monthly, DEFAULT_LIMITS.monthly),
|
|
67
|
-
// 0 is "no limit" for searches; for profile views the scanner reads it as the
|
|
68
|
-
|
|
70
|
+
// 0 is "no limit" for searches; for profile views the scanner reads it as the
|
|
71
|
+
// default. A number typed in by hand counts as the largest choice under it,
|
|
72
|
+
// as the scanner reads it (scrape.py search_limits).
|
|
73
|
+
profiles: profileChoice(num(raw.profiles, 0) || DEFAULT_LIMITS.profiles),
|
|
69
74
|
};
|
|
70
75
|
}
|
|
71
76
|
|
package/lib/reach.js
CHANGED
|
@@ -45,6 +45,10 @@ export function reachIndex(connections = [], degree2 = [], notes = {}) {
|
|
|
45
45
|
for (const url of notes?.read || []) {
|
|
46
46
|
if (url) read.add(keyFor({ profile_url: url }));
|
|
47
47
|
}
|
|
48
|
+
const lists = new Map();
|
|
49
|
+
for (const [url, list] of Object.entries(notes?.lists || {})) {
|
|
50
|
+
if (url && list) lists.set(keyFor({ profile_url: url }), list);
|
|
51
|
+
}
|
|
48
52
|
const reachedFrom = new Map();
|
|
49
53
|
for (const row of connections || []) {
|
|
50
54
|
const from = row?.unlocked_from_bridge_id;
|
|
@@ -53,7 +57,7 @@ export function reachIndex(connections = [], degree2 = [], notes = {}) {
|
|
|
53
57
|
if (!list) reachedFrom.set(from, (list = []));
|
|
54
58
|
list.push(row);
|
|
55
59
|
}
|
|
56
|
-
return { scanned, read, hidden, reachedFrom };
|
|
60
|
+
return { scanned, read, hidden, reachedFrom, lists };
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
/**
|
|
@@ -111,3 +115,20 @@ export function circleScanCost(pages = 100) {
|
|
|
111
115
|
minutes: searches >= 100 ? 55 : Math.max(5, Math.round(searches * 0.55)),
|
|
112
116
|
};
|
|
113
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* How much of someone's circle is scanned, as 0–5 bars for the ring round
|
|
121
|
+
* their dot; null for no ring (not scanned, or their list is hidden).
|
|
122
|
+
* A list read to the end is 5. Part-read, it's the pages read against
|
|
123
|
+
* LinkedIn's own count of the list (about 10 a page), 1 to 4; with no count
|
|
124
|
+
* (read before the scanner kept one), 2.
|
|
125
|
+
*/
|
|
126
|
+
export function scanBars(person, reach) {
|
|
127
|
+
const state = circleState(person, reach);
|
|
128
|
+
const list = person?.profile_url ? reach.lists?.get(keyFor(person)) : null;
|
|
129
|
+
if (state === 'hidden') return null;
|
|
130
|
+
if (!list) return state === 'scanned' ? 2 : null;
|
|
131
|
+
if (!list.more) return 5;
|
|
132
|
+
if (!list.total) return 2;
|
|
133
|
+
return Math.max(1, Math.min(4, Math.round((5 * Math.min(1, (list.pages * 10) / list.total)))));
|
|
134
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// How much searching is asking for trouble, for the Scan page's budget picker.
|
|
2
|
+
// No Node imports, so the page and the tests share it (lib/linkedin-limits.js
|
|
3
|
+
// reads files and can't load in the browser).
|
|
4
|
+
//
|
|
5
|
+
// The number behind it (TRAPS §16): on 2026-09-28 a real account was restricted
|
|
6
|
+
// after 373 searches in 24 hours, run back to back, with the daily budget raised
|
|
7
|
+
// to 500 and no monthly cap. It opened 7 profiles that day, so searches alone
|
|
8
|
+
// were enough.
|
|
9
|
+
|
|
10
|
+
export const RISKY_DAILY = 100; // above this, the picker asks first
|
|
11
|
+
export const RESTRICTED_AT = 373; // searches in 24 hours when that account was restricted
|
|
12
|
+
export const SAFE_LIMITS = { daily: 50, monthly: 250 };
|
|
13
|
+
|
|
14
|
+
export const riskyDaily = (n) => Number(n) > RISKY_DAILY;
|
|
15
|
+
export const riskyLimits = ({ daily, monthly } = {}) => riskyDaily(daily) || monthly === 0;
|
|
16
|
+
|
|
17
|
+
/** What the picker asks before a change that raises the risk; null when there's nothing to ask. */
|
|
18
|
+
export function limitQuestion(from = {}, to = {}) {
|
|
19
|
+
if (riskyDaily(to.daily) && Number(to.daily) > Number(from.daily)) {
|
|
20
|
+
return `${to.daily} searches a day is more than LinkedIn has put up with. A real account was `
|
|
21
|
+
+ `restricted after ${RESTRICTED_AT} searches in 24 hours, run back to back. The default is `
|
|
22
|
+
+ `${SAFE_LIMITS.daily} a day.\n\nSet ${to.daily} a day anyway?`;
|
|
23
|
+
}
|
|
24
|
+
if (to.monthly === 0 && from.monthly !== 0) {
|
|
25
|
+
return 'With no monthly cap, only the daily budget stops a scan. A real account was restricted '
|
|
26
|
+
+ `after ${RESTRICTED_AT} searches in 24 hours with no monthly cap set.\n\nTurn the monthly cap off anyway?`;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The note under the picker while the budget is risky; null when it isn't. */
|
|
32
|
+
export function limitNote({ daily, monthly } = {}) {
|
|
33
|
+
if (!riskyLimits({ daily, monthly })) return null;
|
|
34
|
+
const parts = [`A real account was restricted after ${RESTRICTED_AT} searches in 24 hours.`];
|
|
35
|
+
if (riskyDaily(daily)) parts.push(`${daily} a day can use up a free account's month in a day or two.`);
|
|
36
|
+
if (monthly === 0) parts.push('With no monthly cap, nothing but the daily budget stops a long run.');
|
|
37
|
+
return parts.join(' ');
|
|
38
|
+
}
|
package/lib/ties.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Ties between your own connections: two people you know who know each other.
|
|
2
|
+
//
|
|
3
|
+
// A circle scan reads everyone in someone's list, your own connections among
|
|
4
|
+
// them, and the app sets those aside rather than saving them as 2nd degree
|
|
5
|
+
// (lib/ingest.js splitAlreadyConnected). Each one is a tie between that
|
|
6
|
+
// person and the one whose circle it is. Kept in connection_ties (db/schema.js),
|
|
7
|
+
// for clustering and communities later; nothing reads them yet.
|
|
8
|
+
|
|
9
|
+
/** The pairs to keep: the circle's person with each of your connections in it, each pair once, smaller URL first. */
|
|
10
|
+
export function tiePairs(ownerUrl, urls = []) {
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const out = [];
|
|
13
|
+
if (!ownerUrl) return out;
|
|
14
|
+
for (const url of urls) {
|
|
15
|
+
if (!url || url === ownerUrl) continue;
|
|
16
|
+
const [a, b] = url < ownerUrl ? [url, ownerUrl] : [ownerUrl, url];
|
|
17
|
+
const key = `${a} ${b}`;
|
|
18
|
+
if (seen.has(key)) continue;
|
|
19
|
+
seen.add(key);
|
|
20
|
+
out.push({ a_url: a, b_url: b });
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "six-degrees",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.2",
|
|
4
4
|
"description": "Map your professional network as a galaxy — leverage tiers, bridges into new circles, and the shortest path to anyone. Runs locally on your own data.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Blake Burford",
|
package/scripts/scrape.py
CHANGED
|
@@ -1504,16 +1504,29 @@ def take_profile_view(now=None):
|
|
|
1504
1504
|
with _Locked("linkedin-activity"):
|
|
1505
1505
|
now = now if now is not None else time.time()
|
|
1506
1506
|
data = _read_activity()
|
|
1507
|
-
|
|
1508
|
-
if
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1507
|
+
wait = _profile_wait(data, now)
|
|
1508
|
+
if wait == 0:
|
|
1509
|
+
_charge(data, "profiles", 1, now)
|
|
1510
|
+
return wait
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
def profile_wait(now=None):
|
|
1514
|
+
"""take_profile_view's answer, writing nothing down: 0, seconds to wait, or None."""
|
|
1515
|
+
with _Locked("linkedin-activity"):
|
|
1516
|
+
now = now if now is not None else time.time()
|
|
1517
|
+
return _profile_wait(_read_activity(), now)
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
def _profile_wait(data, now):
|
|
1521
|
+
views = [t for t in data["profiles"] if t > now - DAY_SECONDS]
|
|
1522
|
+
if len(views) >= search_limits()["profiles"]:
|
|
1523
|
+
return None
|
|
1524
|
+
# A time ahead of this clock (another computer's, imported) is left
|
|
1525
|
+
# out of the gap: waiting for it could never end.
|
|
1526
|
+
last = max((t for t in views if t <= now), default=None)
|
|
1527
|
+
if last is not None and now - last < PROFILE_GAP:
|
|
1528
|
+
return int(last + PROFILE_GAP - now) + 1
|
|
1529
|
+
return 0
|
|
1517
1530
|
|
|
1518
1531
|
|
|
1519
1532
|
def search_limits():
|
|
@@ -1529,8 +1542,12 @@ def search_limits():
|
|
|
1529
1542
|
return default
|
|
1530
1543
|
return {"daily": num(data.get("daily"), DEFAULT_DAILY_SEARCHES),
|
|
1531
1544
|
"monthly": num(data.get("monthly"), DEFAULT_MONTHLY_SEARCHES),
|
|
1532
|
-
# 0 is no cap for searches, never for profile views.
|
|
1533
|
-
|
|
1545
|
+
# 0 is no cap for searches, never for profile views. A number typed
|
|
1546
|
+
# in by hand counts as the largest choice under it: never past 100,
|
|
1547
|
+
# and the Scan page shows the same one.
|
|
1548
|
+
"profiles": max((c for c in (10, 25, 50, 100)
|
|
1549
|
+
if c <= (num(data.get("profiles"), DEFAULT_DAILY_PROFILES) or DEFAULT_DAILY_PROFILES)),
|
|
1550
|
+
default=10)}
|
|
1534
1551
|
|
|
1535
1552
|
|
|
1536
1553
|
def linkedin_usage(now=None):
|
|
@@ -1684,6 +1701,12 @@ def _change_progress(change):
|
|
|
1684
1701
|
print(f" (could not note how far this read got: {exc})")
|
|
1685
1702
|
|
|
1686
1703
|
|
|
1704
|
+
# How long each list is, from LinkedIn's "About N results" line on the first
|
|
1705
|
+
# page read (RESULT_COUNT_JS), kept until the read's progress is written: the
|
|
1706
|
+
# app's rings round a dot show pages read against it. Approximate, like the line.
|
|
1707
|
+
_LIST_TOTALS = {}
|
|
1708
|
+
|
|
1709
|
+
|
|
1687
1710
|
def record_bridge_progress(profile_url, name, pages, more, urn=None):
|
|
1688
1711
|
"""Note that someone's connections have been read through page `pages`.
|
|
1689
1712
|
|
|
@@ -1700,6 +1723,9 @@ def record_bridge_progress(profile_url, name, pages, more, urn=None):
|
|
|
1700
1723
|
"at": datetime.now().isoformat(timespec="seconds")}
|
|
1701
1724
|
if urn or old.get("urn"):
|
|
1702
1725
|
entry["urn"] = urn or old.get("urn")
|
|
1726
|
+
total = _LIST_TOTALS.get(profile_url) or old.get("total")
|
|
1727
|
+
if total:
|
|
1728
|
+
entry["total"] = int(total)
|
|
1703
1729
|
mine[profile_url] = entry
|
|
1704
1730
|
_change_progress(change)
|
|
1705
1731
|
|
|
@@ -2461,6 +2487,7 @@ def _scrape_one_bridge(page, bridge_name, bridge_id, profile_url, max_pages=LINK
|
|
|
2461
2487
|
except Exception:
|
|
2462
2488
|
total = None
|
|
2463
2489
|
if total:
|
|
2490
|
+
_LIST_TOTALS[profile_url] = int(total)
|
|
2464
2491
|
pages = -(-total // 10)
|
|
2465
2492
|
print(f" About {total:,} results in their list: {pages} pages"
|
|
2466
2493
|
+ (f", and LinkedIn shows the first {LINKEDIN_MAX_PAGES}." if pages > LINKEDIN_MAX_PAGES else "."))
|
|
@@ -3475,6 +3502,17 @@ def rescrape_bridge(bridge_name, headless=False, max_pages=LINKEDIN_MAX_PAGES):
|
|
|
3475
3502
|
raise BudgetReached(0, searches_left()[1])
|
|
3476
3503
|
if profiles_left() <= 0: # a fresh read opens their profile
|
|
3477
3504
|
raise BudgetReached(0, "profiles")
|
|
3505
|
+
# The minute between profile views is waited out before anything is
|
|
3506
|
+
# deleted: a Stop during it would otherwise leave the circle deleted and unread.
|
|
3507
|
+
wait = profile_wait()
|
|
3508
|
+
if wait is None:
|
|
3509
|
+
raise BudgetReached(0, "profiles")
|
|
3510
|
+
if wait:
|
|
3511
|
+
print(f" Waiting {wait}s before re-mapping: at least {PROFILE_GAP}s pass between any "
|
|
3512
|
+
f"two profile views.", flush=True)
|
|
3513
|
+
if not interruptible_sleep(wait, on_tick=lambda left: print(f" {left}s to go", flush=True), step=10):
|
|
3514
|
+
print(" Stopped before anything was deleted.")
|
|
3515
|
+
return
|
|
3478
3516
|
|
|
3479
3517
|
print(f"Deleting old cluster data...")
|
|
3480
3518
|
bridge_id = delete_bridge_cluster(bridge_name)
|