six-degrees 0.7.0 → 1.0.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/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +32 -4
- package/.next/standalone/.next/routes-manifest.json +6 -0
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- 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 +1 -1
- 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/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +16 -16
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +14 -14
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/admin-delete/route.js +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 +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 +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 +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 +1 -1
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/backup/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/data/csv/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/restore/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 +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 +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 +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 +3 -3
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/queue/route.js +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 +4 -4
- 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 +1 -1
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/crm/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/messages/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js +1 -1
- package/.next/standalone/.next/server/app/api/social/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/unlock/route.js +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 +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 +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/import/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/import.html +1 -1
- package/.next/standalone/.next/server/app/import.rsc +18 -18
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +18 -18
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +19 -19
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +19 -19
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/insights/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/insights/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/insights/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/insights/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/insights/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/insights/page.js +13 -0
- package/.next/standalone/.next/server/app/insights/page.js.map +5 -0
- package/.next/standalone/.next/server/app/insights/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/insights/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/insights.html +1 -0
- package/.next/standalone/.next/server/app/insights.meta +26 -0
- package/.next/standalone/.next/server/app/insights.rsc +35 -0
- package/.next/standalone/.next/server/app/insights.segments/_full.segment.rsc +35 -0
- package/.next/standalone/.next/server/app/insights.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/insights.segments/insights/__PAGE__.segment.rsc +33 -0
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/launch.html +2 -2
- package/.next/standalone/.next/server/app/launch.rsc +18 -18
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +18 -18
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +19 -19
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +19 -19
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +19 -19
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +21 -20
- package/.next/standalone/.next/server/app/scores/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/scores/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/scores.html +1 -1
- package/.next/standalone/.next/server/app/scores.rsc +18 -18
- package/.next/standalone/.next/server/app/scores.segments/_full.segment.rsc +18 -18
- package/.next/standalone/.next/server/app/scores.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/scores.segments/scores/__PAGE__.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/settings.html +1 -1
- package/.next/standalone/.next/server/app/settings.rsc +19 -19
- package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +19 -19
- package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +16 -16
- 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 +19 -19
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +19 -19
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/social/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/social/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/social.html +1 -1
- package/.next/standalone/.next/server/app/social.rsc +18 -18
- package/.next/standalone/.next/server/app/social.segments/_full.segment.rsc +18 -18
- package/.next/standalone/.next/server/app/social.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/social.segments/social/__PAGE__.segment.rsc +16 -16
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__063s0x7._.js → [root-of-the-server]__0r-0t-9._.js} +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ra6xyg._.js +15 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__20rwfdw._.js → [root-of-the-server]__0rcpjad._.js} +5 -2
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +7 -7
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +1 -1
- package/.next/standalone/.next/server/chunks/lib_02iz_9q._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_05agvk3._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_0v84u66._.js +2 -2
- package/.next/standalone/.next/server/chunks/lib_db-client_0_dch35.js +9 -9
- package/.next/standalone/.next/server/chunks/{lib_rpc_0jnfp2e.js → lib_rpc_00nndai.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_0b17r6v.js → lib_rpc_03ibb-x.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_0doq0t-.js → lib_rpc_03ri3ji.js} +1 -1
- package/.next/standalone/.next/server/chunks/{lib_rpc_0fd2rlw.js → lib_rpc_078ce48.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0a69mgr.js +3 -0
- package/.next/standalone/.next/server/chunks/{lib_rpc_005abye.js → lib_rpc_0cej3t8.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_0cmh4g6.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0nx2_pc.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0r79-w1.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0yx8ynd.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_12p-o9-.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1466dnb.js +3 -0
- package/.next/standalone/.next/server/chunks/{lib_rpc_0is4qoc.js → lib_rpc_1hjjjr4.js} +1 -1
- package/.next/standalone/.next/server/chunks/lib_rpc_1sryxv6.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1trn-x5.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_1ygu_mv.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0egamyq._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_04am_h8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_07zb7lu._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0_79idq._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0gds903._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0klqt9z._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_1k9qfsk._.js → _14jak5s._.js} +6 -2
- package/.next/standalone/.next/server/chunks/ssr/_14nxd3q._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1cj2yog._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1fm1ih_._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_1hdqw0-._.js +7 -0
- package/.next/standalone/.next/server/chunks/ssr/_1k2nmbw._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/{_0gy11ar._.js → _1r62six._.js} +6 -2
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_insights_page_actions_0-0tf75.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_components_0uuff77._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_components_1j3q-h5._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{_01o3i5y._.js → app_components_settings_SectorPicker_1c-29qw.js} +2 -6
- package/.next/standalone/.next/server/chunks/ssr/app_components_ui_0pdmz2z.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_insights_page_1d_chja.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +14 -14
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/app_settings_page_1f8mjj_.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +4 -4
- 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/next-font-manifest.js +1 -1
- package/.next/standalone/.next/server/next-font-manifest.json +4 -0
- 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/prefetch-hints.json +14 -0
- 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/02wnf8dkxegxg.js +34 -0
- package/.next/standalone/.next/static/chunks/0hggugbh84m4t.js +21 -0
- package/.next/{static/chunks/2eo9pqq9gay3r.js → standalone/.next/static/chunks/0ka4prpk3w9wn.js} +4 -4
- package/.next/standalone/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
- package/.next/standalone/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
- package/.next/{static/chunks/44yn0tcd9dfk3.js → standalone/.next/static/chunks/1bkt_hhjzvbh8.js} +1 -1
- package/.next/standalone/.next/static/chunks/1cmphb3kh2ers.css +3 -0
- package/.next/{static/chunks/1qbjermbf68xj.js → standalone/.next/static/chunks/1d8yv3k_atka7.js} +1 -1
- package/.next/standalone/.next/static/chunks/1ods5h86raivz.js +1 -0
- package/.next/standalone/.next/static/chunks/23hc51oa4vkhc.js +5 -0
- package/.next/standalone/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
- package/.next/standalone/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
- package/.next/{static/chunks/2zf4jmz9l3li2.js → standalone/.next/static/chunks/2w-y83mjlpnse.js} +1 -1
- package/.next/standalone/.next/static/chunks/2ws1xf293j-tm.js +5 -0
- package/.next/standalone/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
- package/.next/standalone/.next/static/chunks/3opgg1efrx8q6.js +1 -0
- package/.next/standalone/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
- package/.next/{static/chunks/0an_b6zi1r_qq.js → standalone/.next/static/chunks/3y40z6of3r9py.js} +1 -5
- package/.next/standalone/.next/static/chunks/403dpflln8ah5.js +1 -0
- package/.next/standalone/CHANGELOG.md +83 -0
- package/.next/standalone/README.md +40 -14
- package/.next/standalone/SECURITY.md +17 -4
- package/.next/standalone/app/api/outreach/route.js +13 -73
- package/.next/standalone/app/api/scraper/route.js +106 -5
- package/.next/standalone/app/components/AppHeader.js +22 -6
- package/.next/standalone/app/components/AppTabs.js +4 -0
- package/.next/standalone/app/components/AutoConnect.js +286 -0
- package/.next/standalone/app/components/ChainView.js +75 -2
- package/.next/standalone/app/components/FilterPanel.js +8 -1
- package/.next/standalone/app/components/LinkedInLimits.js +38 -13
- package/.next/standalone/app/components/OutlinkQuest.js +32 -11
- package/.next/standalone/app/components/ScanStatusBar.js +18 -3
- package/.next/standalone/app/components/ScanTrail.js +13 -3
- package/.next/standalone/app/components/SeparationView.js +6 -22
- package/.next/standalone/app/components/Sidebar.js +147 -38
- package/.next/standalone/app/components/insights/Boards.js +275 -0
- package/.next/standalone/app/components/insights/PeopleView.js +266 -0
- package/.next/standalone/app/components/insights/Rail.js +156 -0
- package/.next/standalone/app/components/insights/Report.js +382 -0
- package/.next/standalone/app/components/insights/parts.js +239 -0
- package/.next/standalone/app/components/settings/UsageSection.js +50 -3
- package/.next/standalone/app/globals.css +2 -3
- package/.next/standalone/app/insights/page.js +220 -0
- package/.next/standalone/app/page.js +1 -17
- package/.next/standalone/app/queue/page.js +30 -18
- package/.next/standalone/app/setup/page.js +71 -13
- package/.next/standalone/install.sh +7 -6
- package/.next/standalone/lib/auto-connect.js +145 -0
- package/.next/standalone/lib/companies.js +1 -1
- package/.next/standalone/lib/in-progress.js +46 -0
- package/.next/standalone/lib/insights-board.js +613 -0
- package/.next/standalone/lib/linkedin-limits.js +36 -11
- package/.next/standalone/lib/requests-client.js +10 -0
- package/.next/standalone/lib/requests.js +124 -0
- package/.next/standalone/lib/scan-state.js +1 -0
- package/.next/standalone/lib/scanner-setup.js +82 -3
- package/.next/standalone/lib/scraper-client.js +18 -5
- package/.next/standalone/lib/settings.js +6 -0
- package/.next/standalone/lib/usage.js +5 -0
- package/.next/standalone/package-lock.json +2 -2
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/scripts/build-app.mjs +167 -11
- package/.next/standalone/scripts/build-desktop.mjs +3 -2
- package/.next/standalone/scripts/ci-mac-signing.sh +128 -0
- package/.next/standalone/scripts/entitlements/electron.plist +23 -0
- package/.next/standalone/scripts/entitlements/node.plist +27 -0
- package/.next/standalone/scripts/mac-sign.mjs +563 -0
- package/.next/standalone/scripts/make-dmg-background.mjs +2 -1
- package/.next/standalone/scripts/scrape.py +569 -9
- package/.next/standalone/scripts/set-signing-secrets.sh +96 -0
- package/.next/static/chunks/02wnf8dkxegxg.js +34 -0
- package/.next/static/chunks/0hggugbh84m4t.js +21 -0
- package/.next/{standalone/.next/static/chunks/2eo9pqq9gay3r.js → static/chunks/0ka4prpk3w9wn.js} +4 -4
- package/.next/static/chunks/0s_xxwp7z88vv.js +1 -0
- package/.next/static/chunks/1-nnl8amfm4yh.js +57 -0
- package/.next/{standalone/.next/static/chunks/44yn0tcd9dfk3.js → static/chunks/1bkt_hhjzvbh8.js} +1 -1
- package/.next/static/chunks/1cmphb3kh2ers.css +3 -0
- package/.next/{standalone/.next/static/chunks/1qbjermbf68xj.js → static/chunks/1d8yv3k_atka7.js} +1 -1
- package/.next/static/chunks/1ods5h86raivz.js +1 -0
- package/.next/static/chunks/23hc51oa4vkhc.js +5 -0
- package/.next/static/chunks/27n8k9wxlz5-7.js +1 -0
- package/.next/static/chunks/{0p3g5tfostsep.js → 2q9i7pcxcnph1.js} +2 -2
- package/.next/{standalone/.next/static/chunks/2zf4jmz9l3li2.js → static/chunks/2w-y83mjlpnse.js} +1 -1
- package/.next/static/chunks/2ws1xf293j-tm.js +5 -0
- package/.next/static/chunks/3cqn-r-y5rrw1.js +5 -0
- package/.next/static/chunks/3opgg1efrx8q6.js +1 -0
- package/.next/static/chunks/3p51fpxxi4d5y.js +34 -0
- package/.next/{standalone/.next/static/chunks/0an_b6zi1r_qq.js → static/chunks/3y40z6of3r9py.js} +1 -5
- package/.next/static/chunks/403dpflln8ah5.js +1 -0
- package/CHANGELOG.md +83 -0
- package/README.md +40 -14
- package/SECURITY.md +17 -4
- package/lib/auto-connect.js +145 -0
- package/lib/companies.js +1 -1
- package/lib/in-progress.js +46 -0
- package/lib/insights-board.js +613 -0
- package/lib/linkedin-limits.js +36 -11
- package/lib/requests-client.js +10 -0
- package/lib/requests.js +124 -0
- package/lib/scan-state.js +1 -0
- package/lib/scanner-setup.js +82 -3
- package/lib/scraper-client.js +18 -5
- package/lib/settings.js +6 -0
- package/lib/usage.js +5 -0
- package/package.json +1 -1
- package/scripts/scrape.py +569 -9
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0_4y8z7._.js +0 -8
- package/.next/standalone/.next/server/chunks/_02twlca._.js +0 -10
- package/.next/standalone/.next/server/chunks/lib_rpc_0nef_n7.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_0plrzra.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_13a4m0c.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1if17c-.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1j-zw7v.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1sngt-q.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1v2sgme.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1vh-tgx.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_rpc_1zaubz7.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_02sband._.js +0 -7
- package/.next/standalone/.next/server/chunks/ssr/_189g8lg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/_1f_n00p._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/app_components_0t908t-._.js +0 -3
- package/.next/standalone/.next/static/chunks/0e3g0sil79cus.js +0 -21
- package/.next/standalone/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
- package/.next/standalone/.next/static/chunks/163b-20vwa06i.js +0 -34
- package/.next/standalone/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
- package/.next/standalone/.next/static/chunks/23_1z47al92nz.js +0 -57
- package/.next/standalone/.next/static/chunks/260bomtr214m8.js +0 -34
- package/.next/standalone/.next/static/chunks/2xk2ra4piiuod.js +0 -34
- package/.next/standalone/.next/static/chunks/3b0-14cxqsprt.js +0 -1
- package/.next/standalone/.next/static/chunks/3jarus1_38s2z.js +0 -1
- package/.next/standalone/.next/static/chunks/3ssu341hav_sq.css +0 -3
- package/.next/standalone/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
- package/.next/static/chunks/0e3g0sil79cus.js +0 -21
- package/.next/static/chunks/0zd0t7e60bn4z.js +0 -1
- package/.next/static/chunks/163b-20vwa06i.js +0 -34
- package/.next/static/chunks/1pa-2v1ajvrko.js +0 -1
- package/.next/static/chunks/23_1z47al92nz.js +0 -57
- package/.next/static/chunks/260bomtr214m8.js +0 -34
- package/.next/static/chunks/2xk2ra4piiuod.js +0 -34
- package/.next/static/chunks/3b0-14cxqsprt.js +0 -1
- package/.next/static/chunks/3jarus1_38s2z.js +0 -1
- package/.next/static/chunks/3ssu341hav_sq.css +0 -3
- package/.next/static/chunks/3v1qd3sqhqn0d.js +0 -1
- /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_ssgManifest.js +0 -0
- /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_buildManifest.js +0 -0
- /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{ZVpLBK-2vpllv2HxOFs2d → mwCGF3OV3CWV4SjUgg5T8}/_ssgManifest.js +0 -0
|
@@ -19,6 +19,7 @@ Usage:
|
|
|
19
19
|
python3 scripts/scrape.py --auto-bridge --tiers=S,A --max-bridges=10
|
|
20
20
|
python3 scripts/scrape.py --rescrape "Name" # Delete + re-scrape a bridge
|
|
21
21
|
python3 scripts/scrape.py --auto-bridge --deeper # ...and finish lists read only partly
|
|
22
|
+
python3 scripts/scrape.py --connect URL # Auto: one connection request, no note
|
|
22
23
|
|
|
23
24
|
IMPORTANT: Only scrape ONE bridge at a time. Do NOT batch multiple bridges
|
|
24
25
|
in one session — LinkedIn detects automation and flags your account.
|
|
@@ -1425,7 +1426,8 @@ def note_unclear(profile_url, clear=False):
|
|
|
1425
1426
|
# profile in the app: three app profiles must not triple the budget. The app's
|
|
1426
1427
|
# Scan page reads and edits the same files (lib/linkedin-limits.js).
|
|
1427
1428
|
#
|
|
1428
|
-
# linkedin-activity.json {"searches": [epoch s, ...], "profiles": [...]
|
|
1429
|
+
# linkedin-activity.json {"searches": [epoch s, ...], "profiles": [...],
|
|
1430
|
+
# "invites": [...]} Auto's connection requests (--connect)
|
|
1429
1431
|
# scan-limits.json {"daily": 50, "monthly": 250, "profiles": 50}
|
|
1430
1432
|
# 0 = no cap on searches; profile views always have one
|
|
1431
1433
|
# linkedin-cooldown.json {"until": epoch s, "reason": "...", "set_at": ...}
|
|
@@ -1437,6 +1439,13 @@ DEFAULT_MONTHLY_SEARCHES = 250
|
|
|
1437
1439
|
DEFAULT_DAILY_PROFILES = 50
|
|
1438
1440
|
PROFILE_GAP = 60 # seconds between any two profile opens, whatever opened them
|
|
1439
1441
|
DAY_SECONDS = 24 * 3600
|
|
1442
|
+
WEEK_SECONDS = 7 * DAY_SECONDS
|
|
1443
|
+
# Auto's connection requests (--connect), in any 24 hours and any 7 days. The
|
|
1444
|
+
# app shows and enforces the same two (lib/auto-connect.js INVITE_CAPS; a test
|
|
1445
|
+
# checks they match). LinkedIn doesn't publish its invitation limit; people
|
|
1446
|
+
# commonly report about 100 a week, so the week stays well under it.
|
|
1447
|
+
INVITE_DAY_CAP = 15
|
|
1448
|
+
INVITE_WEEK_CAP = 80
|
|
1440
1449
|
PACIFIC = "America/Los_Angeles"
|
|
1441
1450
|
|
|
1442
1451
|
|
|
@@ -1517,9 +1526,10 @@ def _read_activity():
|
|
|
1517
1526
|
if not isinstance(data, dict):
|
|
1518
1527
|
raise ValueError("not an object")
|
|
1519
1528
|
return {"searches": [float(x) for x in data.get("searches", [])],
|
|
1520
|
-
"profiles": [float(x) for x in data.get("profiles", [])]
|
|
1529
|
+
"profiles": [float(x) for x in data.get("profiles", [])],
|
|
1530
|
+
"invites": [float(x) for x in data.get("invites", [])]}
|
|
1521
1531
|
except FileNotFoundError:
|
|
1522
|
-
return {"searches": [], "profiles": []}
|
|
1532
|
+
return {"searches": [], "profiles": [], "invites": []}
|
|
1523
1533
|
except Exception:
|
|
1524
1534
|
# Kept aside, and replaced by a record that says today's budget is used —
|
|
1525
1535
|
# written, so it holds for the whole day rather than one check. (Failing
|
|
@@ -1531,7 +1541,8 @@ def _read_activity():
|
|
|
1531
1541
|
pass
|
|
1532
1542
|
print(" (the record of LinkedIn searches couldn't be read; counting today as used)")
|
|
1533
1543
|
lim = search_limits()
|
|
1534
|
-
fresh = {"searches": [now] * max(1, lim["daily"] or 1), "profiles": [now] * lim["profiles"]
|
|
1544
|
+
fresh = {"searches": [now] * max(1, lim["daily"] or 1), "profiles": [now] * lim["profiles"],
|
|
1545
|
+
"invites": [now] * INVITE_DAY_CAP}
|
|
1535
1546
|
try:
|
|
1536
1547
|
_write_json_atomic(path, fresh)
|
|
1537
1548
|
except Exception:
|
|
@@ -1540,7 +1551,7 @@ def _read_activity():
|
|
|
1540
1551
|
|
|
1541
1552
|
|
|
1542
1553
|
def charge_linkedin(kind="searches", n=1):
|
|
1543
|
-
"""Write down n searches (or profile views) made just now."""
|
|
1554
|
+
"""Write down n searches (or profile views, or connection requests) made just now."""
|
|
1544
1555
|
with _Locked("linkedin-activity"):
|
|
1545
1556
|
_charge(_read_activity(), kind, n, time.time())
|
|
1546
1557
|
|
|
@@ -1548,9 +1559,9 @@ def charge_linkedin(kind="searches", n=1):
|
|
|
1548
1559
|
def _charge(data, kind, n, now):
|
|
1549
1560
|
"""Add n of `kind` at `now` to the record and write it. The caller holds the lock."""
|
|
1550
1561
|
keep_from = month_start_pacific(now) - 32 * DAY_SECONDS
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
data[
|
|
1562
|
+
for each in ("searches", "profiles", "invites"):
|
|
1563
|
+
data[each] = [t for t in data.get(each, []) if t >= keep_from]
|
|
1564
|
+
data[kind] = data.get(kind, []) + [now] * n
|
|
1554
1565
|
try:
|
|
1555
1566
|
_write_json_atomic(_home() / "linkedin-activity.json", data)
|
|
1556
1567
|
except Exception as exc:
|
|
@@ -4133,6 +4144,529 @@ def rescrape_bridge(bridge_name, headless=False, max_pages=LINKEDIN_MAX_PAGES):
|
|
|
4133
4144
|
scrape_bridge(bridge_name, headless=headless, max_pages=max_pages, fresh=True)
|
|
4134
4145
|
|
|
4135
4146
|
|
|
4147
|
+
# ---------------------------------------------------------------------------
|
|
4148
|
+
# Auto: one connection request, for the person whose Auto was pressed (--connect)
|
|
4149
|
+
# ---------------------------------------------------------------------------
|
|
4150
|
+
# Blake, 2026-10-03: "auto add and basically adds the person for them in the
|
|
4151
|
+
# card or wherever its available ... sometimes when adding it'll prompt the user
|
|
4152
|
+
# to have a email from their work or to send a personal note if they have
|
|
4153
|
+
# premium so if thats the case then have it close out of that in the scanner
|
|
4154
|
+
# for adding but it should be seamless."
|
|
4155
|
+
#
|
|
4156
|
+
# This is the one thing the scanner ever does on LinkedIn besides reading, and
|
|
4157
|
+
# only because a person pressed Auto on one card: one person per run, never a
|
|
4158
|
+
# batch, never on a timer. It is openly slow like everything else here: fixed
|
|
4159
|
+
# waits, a profile view taken like any other (cap and gap), no "human" timing
|
|
4160
|
+
# and no mouse movement made up to look like someone (Blake's standing scanner
|
|
4161
|
+
# boundary). The app looks the person up by id and refuses first (cooldown,
|
|
4162
|
+
# caps, already connected or requested; app/api/scraper/route.js); this checks
|
|
4163
|
+
# the cooldown and the caps again before anything opens.
|
|
4164
|
+
#
|
|
4165
|
+
# How it finds things, without a CSS class (TRAPS §5): by role and accessible
|
|
4166
|
+
# name, the way a screen reader would. Their Connect is a button (or link, or
|
|
4167
|
+
# menu item) labelled "Invite <their name> to connect"; the name must be theirs,
|
|
4168
|
+
# word for word, because the same page lists other people with Connect buttons
|
|
4169
|
+
# of their own ("People also viewed"). When it isn't on the front of their
|
|
4170
|
+
# profile, it is under "More". A bare "Connect" with no name is only taken from
|
|
4171
|
+
# that menu, and only when the menu is what brought it on screen: on the page
|
|
4172
|
+
# itself, a nameless Connect could be anyone's.
|
|
4173
|
+
#
|
|
4174
|
+
# What LinkedIn asks next is answered like this:
|
|
4175
|
+
# a note? "Send without a note", or the only Send there is
|
|
4176
|
+
# a personal note (Premium) the same: never a note, never Premium
|
|
4177
|
+
# their email address closed, nothing sent ("email-needed")
|
|
4178
|
+
# its invitation limit closed, nothing sent ("linkedin-limit")
|
|
4179
|
+
# anything else closed, nothing sent ("not-sent")
|
|
4180
|
+
# It is "sent" only when their profile then shows it Pending (or LinkedIn says
|
|
4181
|
+
# the invitation to them was sent). Anything else after pressing Send is
|
|
4182
|
+
# "unclear", and the app doesn't mark it as sent: never a confident wrong answer
|
|
4183
|
+
# (TRAPS §7). A security check or a sign-in wall is LinkedIn pushing back, as in
|
|
4184
|
+
# every other run: the page is kept, everything pauses for a day, and it stops.
|
|
4185
|
+
#
|
|
4186
|
+
# None of this has been run against LinkedIn's real page: the labels above are
|
|
4187
|
+
# what LinkedIn is known to use, and tests/auto-connect.test.mjs runs it against
|
|
4188
|
+
# a stand-in page. Watch the first real one.
|
|
4189
|
+
|
|
4190
|
+
CONNECT_LOAD_ATTEMPTS = 6 # looks for their profile to render, CONNECT_LOAD_STEP apart
|
|
4191
|
+
CONNECT_LOAD_STEP = 5 # seconds
|
|
4192
|
+
CONNECT_STEP = 1 # seconds between looks for LinkedIn's answer
|
|
4193
|
+
DIALOG_WAIT = 10 # seconds to wait for the invitation window after Connect
|
|
4194
|
+
CONFIRM_WAIT = 12 # seconds to wait for Pending after Send
|
|
4195
|
+
|
|
4196
|
+
# Every result a run can end with, printed as "Connect result: <name>" for the
|
|
4197
|
+
# app (lib/auto-connect.js OUTCOMES has the same names and their words; a test
|
|
4198
|
+
# checks the two lists match).
|
|
4199
|
+
CONNECT_RESULTS = (
|
|
4200
|
+
"sent", "already-pending", "email-needed", "no-connect", "not-sent", "unclear", "not-loaded",
|
|
4201
|
+
"unavailable", "linkedin-limit", "pushback", "stopped", "not-signed-in", "cooldown",
|
|
4202
|
+
"invites-day", "invites-week", "profiles", "not-found",
|
|
4203
|
+
)
|
|
4204
|
+
|
|
4205
|
+
ACTION_ROLES = ("button", "link", "menuitem")
|
|
4206
|
+
INVITE_LABEL = re.compile(r"^\s*Invite\s+(.+?)\s+to\s+connect\s*$", re.I)
|
|
4207
|
+
PENDING_LABEL = re.compile(r"^\s*Pending\b", re.I)
|
|
4208
|
+
BARE_CONNECT = re.compile(r"^\s*Connect\s*$", re.I)
|
|
4209
|
+
BARE_PENDING = re.compile(r"^\s*Pending\s*$", re.I)
|
|
4210
|
+
MORE_LABEL = re.compile(r"^\s*More( actions)?\s*$", re.I)
|
|
4211
|
+
SEND_PLAIN = re.compile(r"^\s*Send without a note\s*$", re.I)
|
|
4212
|
+
SEND_ANY = re.compile(r"^\s*Send( now| invitation)?\s*$", re.I)
|
|
4213
|
+
CLOSE_LABEL = re.compile(r"^\s*(Dismiss|Close|Cancel|Got it|Not now)\s*$", re.I)
|
|
4214
|
+
EMAIL_BOX = re.compile(r"e-?mail", re.I)
|
|
4215
|
+
EMAIL_ASK = re.compile(r"enter (their|his|her|the member.?s) e-?mail|e-?mail( address)? to connect|knows you.{0,60}e-?mail", re.I)
|
|
4216
|
+
INVITE_LIMIT = re.compile(r"invitation limit|reached the (weekly )?limit|too many (pending )?invitations", re.I)
|
|
4217
|
+
PREMIUM_NOTE = re.compile(r"premium|personali[sz]ed (invitation|note)", re.I)
|
|
4218
|
+
SENT_NOTICE = re.compile(r"invitation to (.+?) (?:was|has been) sent", re.I)
|
|
4219
|
+
|
|
4220
|
+
|
|
4221
|
+
def invites_left(now=None):
|
|
4222
|
+
"""(how many more requests Auto may send, "day" | "week": whichever runs out first)."""
|
|
4223
|
+
now = now if now is not None else time.time()
|
|
4224
|
+
invites = _read_activity()["invites"]
|
|
4225
|
+
day = INVITE_DAY_CAP - sum(1 for t in invites if t > now - DAY_SECONDS)
|
|
4226
|
+
week = INVITE_WEEK_CAP - sum(1 for t in invites if t > now - WEEK_SECONDS)
|
|
4227
|
+
return (max(0, day), "day") if day <= week else (max(0, week), "week")
|
|
4228
|
+
|
|
4229
|
+
|
|
4230
|
+
def take_invite(now=None):
|
|
4231
|
+
"""Write down one connection request about to be sent, if the caps allow one.
|
|
4232
|
+
|
|
4233
|
+
None once it is written down; "day" or "week" when that cap is reached and
|
|
4234
|
+
nothing was written. The check and the write are one step under the lock.
|
|
4235
|
+
"""
|
|
4236
|
+
with _Locked("linkedin-activity"):
|
|
4237
|
+
now = now if now is not None else time.time()
|
|
4238
|
+
data = _read_activity()
|
|
4239
|
+
invites = data["invites"]
|
|
4240
|
+
if sum(1 for t in invites if t > now - DAY_SECONDS) >= INVITE_DAY_CAP:
|
|
4241
|
+
return "day"
|
|
4242
|
+
if sum(1 for t in invites if t > now - WEEK_SECONDS) >= INVITE_WEEK_CAP:
|
|
4243
|
+
return "week"
|
|
4244
|
+
_charge(data, "invites", 1, now)
|
|
4245
|
+
return None
|
|
4246
|
+
|
|
4247
|
+
|
|
4248
|
+
def invite_message(kind):
|
|
4249
|
+
"""Why Auto sent nothing: a cap on its requests is reached, and when one frees up."""
|
|
4250
|
+
now = time.time()
|
|
4251
|
+
window = WEEK_SECONDS if kind == "week" else DAY_SECONDS
|
|
4252
|
+
cap = INVITE_WEEK_CAP if kind == "week" else INVITE_DAY_CAP
|
|
4253
|
+
sent = sorted(t for t in _read_activity()["invites"] if t > now - window)
|
|
4254
|
+
extra = len(sent) - cap
|
|
4255
|
+
free = f" The next one frees up at {_when(sent[extra] + window)}." if extra >= 0 else ""
|
|
4256
|
+
span = "7 days" if kind == "week" else "24 hours"
|
|
4257
|
+
return f"Auto has sent {cap} connection requests in the last {span}, its limit.{free} Nothing was opened or sent."
|
|
4258
|
+
|
|
4259
|
+
|
|
4260
|
+
def _names_words(text):
|
|
4261
|
+
"""The words of a name, lower case, letters only (any alphabet)."""
|
|
4262
|
+
return re.findall(r"[^\W\d_]+", str(text or "").lower())
|
|
4263
|
+
|
|
4264
|
+
|
|
4265
|
+
def _same_person(shown, name):
|
|
4266
|
+
"""Does what LinkedIn shows (a label, a heading) name this person: their first and last name, as whole words?"""
|
|
4267
|
+
want = _names_words(str(name or "").split(",")[0])
|
|
4268
|
+
have = set(_names_words(shown))
|
|
4269
|
+
return bool(want) and want[0] in have and want[-1] in have
|
|
4270
|
+
|
|
4271
|
+
|
|
4272
|
+
def _shown(loc, limit=40):
|
|
4273
|
+
"""The items of a locator that are on screen."""
|
|
4274
|
+
out = []
|
|
4275
|
+
try:
|
|
4276
|
+
n = loc.count()
|
|
4277
|
+
except Exception:
|
|
4278
|
+
return out
|
|
4279
|
+
for i in range(min(n, limit)):
|
|
4280
|
+
item = loc.nth(i)
|
|
4281
|
+
try:
|
|
4282
|
+
if item.is_visible():
|
|
4283
|
+
out.append(item)
|
|
4284
|
+
except Exception:
|
|
4285
|
+
pass
|
|
4286
|
+
return out
|
|
4287
|
+
|
|
4288
|
+
|
|
4289
|
+
def _label(item):
|
|
4290
|
+
"""An element's accessible name as LinkedIn wrote it: its aria-label, else its text."""
|
|
4291
|
+
try:
|
|
4292
|
+
label = item.get_attribute("aria-label")
|
|
4293
|
+
if label:
|
|
4294
|
+
return label
|
|
4295
|
+
except Exception:
|
|
4296
|
+
pass
|
|
4297
|
+
try:
|
|
4298
|
+
return item.inner_text() or ""
|
|
4299
|
+
except Exception:
|
|
4300
|
+
return ""
|
|
4301
|
+
|
|
4302
|
+
|
|
4303
|
+
def _enabled(item):
|
|
4304
|
+
try:
|
|
4305
|
+
return item.is_enabled()
|
|
4306
|
+
except Exception:
|
|
4307
|
+
return False
|
|
4308
|
+
|
|
4309
|
+
|
|
4310
|
+
def _in_main(page):
|
|
4311
|
+
"""The profile's <main>, where its own buttons are, or the page if there is none."""
|
|
4312
|
+
try:
|
|
4313
|
+
main = page.locator("main")
|
|
4314
|
+
if main.count() > 0:
|
|
4315
|
+
return main.first
|
|
4316
|
+
except Exception:
|
|
4317
|
+
pass
|
|
4318
|
+
return page
|
|
4319
|
+
|
|
4320
|
+
|
|
4321
|
+
def _actions(scope, name_pattern):
|
|
4322
|
+
"""Every button, link or menu item on screen whose accessible name matches."""
|
|
4323
|
+
found = []
|
|
4324
|
+
for role in ACTION_ROLES:
|
|
4325
|
+
try:
|
|
4326
|
+
found.extend(_shown(scope.get_by_role(role, name=name_pattern)))
|
|
4327
|
+
except Exception:
|
|
4328
|
+
pass
|
|
4329
|
+
return found
|
|
4330
|
+
|
|
4331
|
+
|
|
4332
|
+
def _invite_for(page, name):
|
|
4333
|
+
"""Their own Connect: "Invite <their name> to connect", never someone else's."""
|
|
4334
|
+
for item in _actions(page, INVITE_LABEL):
|
|
4335
|
+
m = INVITE_LABEL.match(_label(item))
|
|
4336
|
+
if m and _same_person(m.group(1), name):
|
|
4337
|
+
return item
|
|
4338
|
+
return None
|
|
4339
|
+
|
|
4340
|
+
|
|
4341
|
+
def _pending_for(page, name):
|
|
4342
|
+
"""Their profile says a request to them is pending, naming them."""
|
|
4343
|
+
return any(_same_person(_label(item), name) for item in _actions(page, PENDING_LABEL))
|
|
4344
|
+
|
|
4345
|
+
|
|
4346
|
+
def _bare_pending(page):
|
|
4347
|
+
"""How many nameless "Pending" buttons their profile shows."""
|
|
4348
|
+
return len(_actions(_in_main(page), BARE_PENDING))
|
|
4349
|
+
|
|
4350
|
+
|
|
4351
|
+
def _sent_shown(page, name, bare_before):
|
|
4352
|
+
"""LinkedIn shows the request to them as sent: Pending naming them, a new
|
|
4353
|
+
"Pending" on their profile, or its notice that the invitation to them was sent."""
|
|
4354
|
+
if _pending_for(page, name):
|
|
4355
|
+
return True
|
|
4356
|
+
if _bare_pending(page) > bare_before:
|
|
4357
|
+
return True
|
|
4358
|
+
try:
|
|
4359
|
+
for item in _shown(page.get_by_text(SENT_NOTICE)):
|
|
4360
|
+
m = SENT_NOTICE.search(_label(item))
|
|
4361
|
+
if m and _same_person(m.group(1), name):
|
|
4362
|
+
return True
|
|
4363
|
+
except Exception:
|
|
4364
|
+
pass
|
|
4365
|
+
return False
|
|
4366
|
+
|
|
4367
|
+
|
|
4368
|
+
def _press_escape(page):
|
|
4369
|
+
try:
|
|
4370
|
+
page.keyboard.press("Escape")
|
|
4371
|
+
except Exception:
|
|
4372
|
+
pass
|
|
4373
|
+
|
|
4374
|
+
|
|
4375
|
+
def _connect_under_more(page, name):
|
|
4376
|
+
"""Connect from their profile's More menu, or None (the menu is closed again)."""
|
|
4377
|
+
more = _actions(_in_main(page), MORE_LABEL)
|
|
4378
|
+
if not more:
|
|
4379
|
+
return None
|
|
4380
|
+
bare_before = len(_actions(page, BARE_CONNECT))
|
|
4381
|
+
print(" Connect isn't on the front of their profile; looking under More...")
|
|
4382
|
+
try:
|
|
4383
|
+
more[0].click(timeout=10000)
|
|
4384
|
+
except Exception:
|
|
4385
|
+
return None
|
|
4386
|
+
time.sleep(CONNECT_STEP)
|
|
4387
|
+
found = _invite_for(page, name)
|
|
4388
|
+
if found:
|
|
4389
|
+
return found
|
|
4390
|
+
# A nameless "Connect" only when opening the menu is what brought it on screen.
|
|
4391
|
+
bare = _actions(page, BARE_CONNECT)
|
|
4392
|
+
if bare_before == 0 and len(bare) == 1:
|
|
4393
|
+
return bare[0]
|
|
4394
|
+
_press_escape(page)
|
|
4395
|
+
return None
|
|
4396
|
+
|
|
4397
|
+
|
|
4398
|
+
def _dialog(page):
|
|
4399
|
+
"""LinkedIn's window on top of the page (the newest), or None."""
|
|
4400
|
+
shown = []
|
|
4401
|
+
for role in ("dialog", "alertdialog"):
|
|
4402
|
+
try:
|
|
4403
|
+
shown.extend(_shown(page.get_by_role(role)))
|
|
4404
|
+
except Exception:
|
|
4405
|
+
pass
|
|
4406
|
+
return shown[-1] if shown else None
|
|
4407
|
+
|
|
4408
|
+
|
|
4409
|
+
def _close_dialog(page, dialog):
|
|
4410
|
+
"""Close LinkedIn's window without answering it: its Dismiss (or Close, Cancel), else Escape."""
|
|
4411
|
+
try:
|
|
4412
|
+
close = _shown(dialog.get_by_role("button", name=CLOSE_LABEL))
|
|
4413
|
+
if close:
|
|
4414
|
+
close[0].click(timeout=5000)
|
|
4415
|
+
else:
|
|
4416
|
+
_press_escape(page)
|
|
4417
|
+
except Exception:
|
|
4418
|
+
_press_escape(page)
|
|
4419
|
+
time.sleep(CONNECT_STEP)
|
|
4420
|
+
|
|
4421
|
+
|
|
4422
|
+
def _asks_for_email(dialog, text):
|
|
4423
|
+
"""The invitation window wants their email address before it sends."""
|
|
4424
|
+
for loc in (lambda: dialog.get_by_role("textbox", name=EMAIL_BOX), lambda: dialog.locator("input[type=email]")):
|
|
4425
|
+
try:
|
|
4426
|
+
if _shown(loc()):
|
|
4427
|
+
return True
|
|
4428
|
+
except Exception:
|
|
4429
|
+
pass
|
|
4430
|
+
return bool(EMAIL_ASK.search(text or ""))
|
|
4431
|
+
|
|
4432
|
+
|
|
4433
|
+
def _send_button(dialog):
|
|
4434
|
+
""""Send without a note", or the one Send there is. Never "Add a note", never Premium."""
|
|
4435
|
+
plain = [b for b in _shown(dialog.get_by_role("button", name=SEND_PLAIN)) if _enabled(b)]
|
|
4436
|
+
if plain:
|
|
4437
|
+
return plain[0]
|
|
4438
|
+
sends = [b for b in _shown(dialog.get_by_role("button", name=SEND_ANY)) if _enabled(b)]
|
|
4439
|
+
return sends[0] if len(sends) == 1 else None
|
|
4440
|
+
|
|
4441
|
+
|
|
4442
|
+
def _dialog_text(dialog):
|
|
4443
|
+
try:
|
|
4444
|
+
return dialog.inner_text() or ""
|
|
4445
|
+
except Exception:
|
|
4446
|
+
return ""
|
|
4447
|
+
|
|
4448
|
+
|
|
4449
|
+
def _connect_pushback(page, why):
|
|
4450
|
+
"""LinkedIn pushed back while Auto was on their profile: keep the page, pause for a day."""
|
|
4451
|
+
print(f" LinkedIn is pushing back ({why}). Stopping here.")
|
|
4452
|
+
_keep_pushback_evidence(page, why)
|
|
4453
|
+
set_cooldown(seconds=DAY_SECONDS, reason=f"LinkedIn pushed back: {why}")
|
|
4454
|
+
|
|
4455
|
+
|
|
4456
|
+
def _confirm_sent(page, name, bare_before):
|
|
4457
|
+
"""After Send: "sent" once their profile shows it, else "unclear" (or "pushback", "linkedin-limit")."""
|
|
4458
|
+
waited = 0
|
|
4459
|
+
while waited < CONFIRM_WAIT:
|
|
4460
|
+
time.sleep(CONNECT_STEP)
|
|
4461
|
+
waited += CONNECT_STEP
|
|
4462
|
+
why = _pushback(page)
|
|
4463
|
+
if why:
|
|
4464
|
+
_connect_pushback(page, why)
|
|
4465
|
+
return "pushback"
|
|
4466
|
+
dialog = _dialog(page)
|
|
4467
|
+
if dialog is not None and INVITE_LIMIT.search(_dialog_text(dialog)):
|
|
4468
|
+
_close_dialog(page, dialog)
|
|
4469
|
+
print(" LinkedIn says this account has reached its invitation limit.")
|
|
4470
|
+
return "linkedin-limit"
|
|
4471
|
+
if _sent_shown(page, name, bare_before):
|
|
4472
|
+
return "sent"
|
|
4473
|
+
leftover = _dialog(page)
|
|
4474
|
+
if leftover is not None:
|
|
4475
|
+
_close_dialog(page, leftover)
|
|
4476
|
+
return "unclear"
|
|
4477
|
+
|
|
4478
|
+
|
|
4479
|
+
def _answer_invitation(page, name, bare_before):
|
|
4480
|
+
"""What LinkedIn asks once Connect is pressed, answered without a note."""
|
|
4481
|
+
dialog = None
|
|
4482
|
+
waited = 0
|
|
4483
|
+
while waited < DIALOG_WAIT:
|
|
4484
|
+
time.sleep(CONNECT_STEP)
|
|
4485
|
+
waited += CONNECT_STEP
|
|
4486
|
+
dialog = _dialog(page)
|
|
4487
|
+
if dialog is not None:
|
|
4488
|
+
break
|
|
4489
|
+
if _sent_shown(page, name, bare_before):
|
|
4490
|
+
# Some accounts send straight from Connect, with nothing to answer.
|
|
4491
|
+
charge_linkedin("invites")
|
|
4492
|
+
print(" LinkedIn sent it straight from Connect; their profile shows it pending.")
|
|
4493
|
+
return "sent"
|
|
4494
|
+
if dialog is None:
|
|
4495
|
+
# Nothing came up and nothing shows it pending. It may have gone, so it
|
|
4496
|
+
# counts toward Auto's caps; it isn't called sent.
|
|
4497
|
+
charge_linkedin("invites")
|
|
4498
|
+
print(" Nothing came up after Connect, and their profile doesn't show it pending.")
|
|
4499
|
+
return "unclear"
|
|
4500
|
+
|
|
4501
|
+
text = _dialog_text(dialog)
|
|
4502
|
+
if _asks_for_email(dialog, text):
|
|
4503
|
+
_close_dialog(page, dialog)
|
|
4504
|
+
print(" LinkedIn wants their email address before it sends this one. Closed that; nothing was sent.")
|
|
4505
|
+
return "email-needed"
|
|
4506
|
+
if INVITE_LIMIT.search(text):
|
|
4507
|
+
_close_dialog(page, dialog)
|
|
4508
|
+
print(" LinkedIn says this account has reached its invitation limit. Closed that; nothing was sent.")
|
|
4509
|
+
return "linkedin-limit"
|
|
4510
|
+
send = _send_button(dialog)
|
|
4511
|
+
if send is None:
|
|
4512
|
+
_close_dialog(page, dialog)
|
|
4513
|
+
print(" LinkedIn's invitation window had no way to send without a note. Closed it; nothing was sent.")
|
|
4514
|
+
return "not-sent"
|
|
4515
|
+
if PREMIUM_NOTE.search(text):
|
|
4516
|
+
print(" LinkedIn offered a personal note (Premium). Sending without one.")
|
|
4517
|
+
if stop_requested() or _window_closed(page):
|
|
4518
|
+
_close_dialog(page, dialog)
|
|
4519
|
+
return "stopped"
|
|
4520
|
+
taken = take_invite()
|
|
4521
|
+
if taken:
|
|
4522
|
+
_close_dialog(page, dialog)
|
|
4523
|
+
print(" " + invite_message(taken))
|
|
4524
|
+
return f"invites-{taken}"
|
|
4525
|
+
print(" Sending without a note...")
|
|
4526
|
+
try:
|
|
4527
|
+
send.click(timeout=10000)
|
|
4528
|
+
except Exception as exc:
|
|
4529
|
+
print(f" Send couldn't be pressed ({str(exc)[:60]}).")
|
|
4530
|
+
_close_dialog(page, dialog)
|
|
4531
|
+
return "unclear"
|
|
4532
|
+
result = _confirm_sent(page, name, bare_before)
|
|
4533
|
+
if result == "sent":
|
|
4534
|
+
print(f" Sent. {name}'s profile shows the request pending.")
|
|
4535
|
+
elif result == "unclear":
|
|
4536
|
+
print(" Send was pressed, but their profile doesn't show the request pending. Not calling it sent.")
|
|
4537
|
+
return result
|
|
4538
|
+
|
|
4539
|
+
|
|
4540
|
+
def _connect_on_page(page, profile_url, name):
|
|
4541
|
+
"""Open their profile (one profile view) and send the request. Returns a CONNECT_RESULTS name."""
|
|
4542
|
+
why = _wait_for_profile_view(page)
|
|
4543
|
+
if why == "budget":
|
|
4544
|
+
print(budget_message("profiles"))
|
|
4545
|
+
return "profiles"
|
|
4546
|
+
if why:
|
|
4547
|
+
return "stopped"
|
|
4548
|
+
|
|
4549
|
+
print(f" Opening {name}'s profile...")
|
|
4550
|
+
try:
|
|
4551
|
+
page.goto(profile_url, wait_until="commit")
|
|
4552
|
+
except Exception as exc:
|
|
4553
|
+
print(f" Navigation slow: {str(exc)[:50]}... carrying on")
|
|
4554
|
+
|
|
4555
|
+
shown = False
|
|
4556
|
+
for _ in range(CONNECT_LOAD_ATTEMPTS):
|
|
4557
|
+
time.sleep(CONNECT_LOAD_STEP)
|
|
4558
|
+
if stop_requested() or _window_closed(page):
|
|
4559
|
+
return "stopped"
|
|
4560
|
+
why = _pushback(page)
|
|
4561
|
+
if why:
|
|
4562
|
+
_connect_pushback(page, why)
|
|
4563
|
+
return "pushback"
|
|
4564
|
+
if _profile_unavailable(page):
|
|
4565
|
+
print(" LinkedIn says their profile isn't available. Nothing was sent.")
|
|
4566
|
+
return "unavailable"
|
|
4567
|
+
try:
|
|
4568
|
+
shown = bool(page.evaluate(PROFILE_SHOWN_JS, name))
|
|
4569
|
+
except Exception:
|
|
4570
|
+
shown = False
|
|
4571
|
+
if shown:
|
|
4572
|
+
break
|
|
4573
|
+
if not shown:
|
|
4574
|
+
print(" Their profile didn't load properly, so nothing was pressed.")
|
|
4575
|
+
return "not-loaded"
|
|
4576
|
+
|
|
4577
|
+
if _pending_for(page, name):
|
|
4578
|
+
print(f" A request to {name} is already pending on LinkedIn. Nothing new was sent.")
|
|
4579
|
+
return "already-pending"
|
|
4580
|
+
bare_before = _bare_pending(page)
|
|
4581
|
+
target = _invite_for(page, name) or _connect_under_more(page, name)
|
|
4582
|
+
if target is None:
|
|
4583
|
+
print(" LinkedIn shows no Connect for them here (only Follow or Message, say). Nothing was sent.")
|
|
4584
|
+
return "no-connect"
|
|
4585
|
+
if stop_requested() or _window_closed(page):
|
|
4586
|
+
return "stopped"
|
|
4587
|
+
left, kind = invites_left()
|
|
4588
|
+
if left <= 0:
|
|
4589
|
+
print(" " + invite_message(kind))
|
|
4590
|
+
return f"invites-{kind}"
|
|
4591
|
+
print(f" Pressing Connect for {name}...")
|
|
4592
|
+
try:
|
|
4593
|
+
target.click(timeout=10000)
|
|
4594
|
+
except Exception as exc:
|
|
4595
|
+
print(f" Connect couldn't be pressed ({str(exc)[:60]}). Nothing was sent.")
|
|
4596
|
+
return "not-sent"
|
|
4597
|
+
return _answer_invitation(page, name, bare_before)
|
|
4598
|
+
|
|
4599
|
+
|
|
4600
|
+
def connect_person(profile_url, name=None, headless=False):
|
|
4601
|
+
"""Send one connection request, without a note, to the person at profile_url.
|
|
4602
|
+
|
|
4603
|
+
Returns one of CONNECT_RESULTS; main() prints it as "Connect result: ...".
|
|
4604
|
+
Nothing opens during a cooldown, with Auto's caps reached, or with no
|
|
4605
|
+
profile view left: each is checked before a browser starts.
|
|
4606
|
+
"""
|
|
4607
|
+
from playwright.sync_api import sync_playwright
|
|
4608
|
+
|
|
4609
|
+
cd = read_cooldown()
|
|
4610
|
+
if cd:
|
|
4611
|
+
print(cooldown_message(cd))
|
|
4612
|
+
return "cooldown"
|
|
4613
|
+
left, kind = invites_left()
|
|
4614
|
+
if left <= 0:
|
|
4615
|
+
print(invite_message(kind))
|
|
4616
|
+
return f"invites-{kind}"
|
|
4617
|
+
if profiles_left() <= 0:
|
|
4618
|
+
print(budget_message("profiles"))
|
|
4619
|
+
return "profiles"
|
|
4620
|
+
if not name:
|
|
4621
|
+
params = {"profile_url": f"eq.{profile_url}", "limit": "1"}
|
|
4622
|
+
if _active_user_id:
|
|
4623
|
+
params["user_id"] = f"eq.{_active_user_id}"
|
|
4624
|
+
rows = read_connections(params=params)
|
|
4625
|
+
if not rows:
|
|
4626
|
+
print(f" {profile_url} isn't in your network on this computer. Nothing was sent.")
|
|
4627
|
+
return "not-found"
|
|
4628
|
+
name = rows[0].get("name") or ""
|
|
4629
|
+
|
|
4630
|
+
print(f"\n=== Sending {name} a connection request, without a note ===\n")
|
|
4631
|
+
result = None
|
|
4632
|
+
try:
|
|
4633
|
+
with sync_playwright() as p:
|
|
4634
|
+
browser = p.chromium.launch_persistent_context(
|
|
4635
|
+
user_data_dir=get_scraper_profile_path(),
|
|
4636
|
+
headless=headless,
|
|
4637
|
+
channel="chrome",
|
|
4638
|
+
args=["--disable-blink-features=AutomationControlled"],
|
|
4639
|
+
timeout=120000,
|
|
4640
|
+
)
|
|
4641
|
+
try:
|
|
4642
|
+
page = browser.pages[0] if browser.pages else browser.new_page()
|
|
4643
|
+
page.set_default_timeout(120000)
|
|
4644
|
+
page.set_default_navigation_timeout(120000)
|
|
4645
|
+
# A security check or a sign-out here is LinkedIn pushing back,
|
|
4646
|
+
# as for a circle scan: it raises, after pausing everything.
|
|
4647
|
+
if not ensure_logged_in(page, stop_on_checkpoint=True):
|
|
4648
|
+
result = "stopped" if stop_requested() else "not-signed-in"
|
|
4649
|
+
else:
|
|
4650
|
+
result = _connect_on_page(page, profile_url, name)
|
|
4651
|
+
finally:
|
|
4652
|
+
try:
|
|
4653
|
+
browser.close()
|
|
4654
|
+
except Exception:
|
|
4655
|
+
pass
|
|
4656
|
+
except LinkedInPushedBack as exc:
|
|
4657
|
+
print(f" LinkedIn pushed back ({exc.reason}). Nothing was sent.")
|
|
4658
|
+
result = "pushback"
|
|
4659
|
+
except Exception:
|
|
4660
|
+
# A stop takes the browser down with it. Anything else is a failure
|
|
4661
|
+
# with its reason, never a result that could read as sent.
|
|
4662
|
+
if result is None:
|
|
4663
|
+
if stop_requested():
|
|
4664
|
+
result = "stopped"
|
|
4665
|
+
else:
|
|
4666
|
+
raise
|
|
4667
|
+
return result
|
|
4668
|
+
|
|
4669
|
+
|
|
4136
4670
|
def run_server(port=5555):
|
|
4137
4671
|
"""
|
|
4138
4672
|
Run a local HTTP server that the website's Setup page can talk to.
|
|
@@ -4397,6 +4931,12 @@ Examples:
|
|
|
4397
4931
|
"The app's Scan buttons use it, since two connections can share a name")
|
|
4398
4932
|
parser.add_argument("--only-unfinished", action="store_true",
|
|
4399
4933
|
help="With --auto-bridge: only people whose read was cut short (Resume all)")
|
|
4934
|
+
parser.add_argument("--connect", type=str,
|
|
4935
|
+
help="Auto: send one connection request, without a note, to the person at this LinkedIn "
|
|
4936
|
+
f"profile URL. Never with a note; at most {INVITE_DAY_CAP} in any 24 hours and "
|
|
4937
|
+
f"{INVITE_WEEK_CAP} in any 7 days")
|
|
4938
|
+
parser.add_argument("--connect-name", type=str, default="",
|
|
4939
|
+
help="With --connect: their name as the app has it (else it is looked up in the app)")
|
|
4400
4940
|
parser.add_argument("--save-photos", action="store_true",
|
|
4401
4941
|
help="Save the profile photos still kept as links to LinkedIn, then exit "
|
|
4402
4942
|
"(every scan also does this at its end)")
|
|
@@ -4408,7 +4948,10 @@ Examples:
|
|
|
4408
4948
|
args.max_pages = max(1, min(LINKEDIN_MAX_PAGES, args.max_pages))
|
|
4409
4949
|
# The Scan page's speed. Signing in searches nothing, so it isn't said there.
|
|
4410
4950
|
_pace = apply_pace()
|
|
4411
|
-
if
|
|
4951
|
+
if args.connect:
|
|
4952
|
+
# Auto reads no pages: only the gap before opening their profile applies.
|
|
4953
|
+
print(f"Speed: {_pace.capitalize()}. Profiles at least {PROFILE_GAP}s apart.")
|
|
4954
|
+
elif not args.login:
|
|
4412
4955
|
print(f"Speed: {_pace.capitalize()}. {PAGE_PAUSE}s before each page of results, {CHUNK_COOLDOWN}s more after "
|
|
4413
4956
|
f"every {SAVE_EVERY_PAGES}, and profiles at least {PROFILE_GAP}s apart.")
|
|
4414
4957
|
|
|
@@ -4454,6 +4997,23 @@ Examples:
|
|
|
4454
4997
|
# A budget or a cooldown ending a run is the design working, not a failure:
|
|
4455
4998
|
# say why and exit 0, so the Scan page doesn't show it as a red error.
|
|
4456
4999
|
try:
|
|
5000
|
+
if args.connect:
|
|
5001
|
+
# Auto (lib/auto-connect.js): one request, then out. Its last line is
|
|
5002
|
+
# the result the app reads; no photos are saved after it, since it
|
|
5003
|
+
# read no one. A pushback or no sign-in ends it as a failure, with why.
|
|
5004
|
+
if not re.match(r"^https?://([a-z]{2,3}\.|www\.)?linkedin\.com/in/[^/?#\s]+/?$", args.connect, re.I):
|
|
5005
|
+
print(" --connect takes a LinkedIn profile URL (https://www.linkedin.com/in/...).", file=sys.stderr)
|
|
5006
|
+
raise SystemExit(2)
|
|
5007
|
+
result = connect_person(args.connect, name=args.connect_name.strip() or None, headless=args.headless)
|
|
5008
|
+
print(f"Connect result: {result}", flush=True)
|
|
5009
|
+
if result == "pushback":
|
|
5010
|
+
print(f"\n LinkedIn pushed back, so nothing more is sent. {_pushback_advice('')}\n", file=sys.stderr)
|
|
5011
|
+
raise SystemExit(1)
|
|
5012
|
+
if result == "not-signed-in":
|
|
5013
|
+
print("\n LinkedIn isn't signed in, so nothing was sent. Sign in with --login (or the Scan page), "
|
|
5014
|
+
"then try again.\n", file=sys.stderr)
|
|
5015
|
+
raise SystemExit(1)
|
|
5016
|
+
raise SystemExit(0)
|
|
4457
5017
|
if args.company:
|
|
4458
5018
|
# The server mode scraped and pushed as one step; the CLI has to do the
|
|
4459
5019
|
# same or the scan appears to work and saves nothing.
|