six-degrees 0.2.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 -0
- package/.next/standalone/.next/app-path-routes-manifest.json +31 -0
- package/.next/standalone/.next/build-manifest.json +24 -0
- package/.next/standalone/.next/package.json +1 -0
- package/.next/standalone/.next/prerender-manifest.json +297 -0
- package/.next/standalone/.next/required-server-files.json +371 -0
- package/.next/standalone/.next/routes-manifest.json +222 -0
- package/.next/standalone/.next/server/app/_global-error/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/_global-error/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/_global-error/page/next-font-manifest.json +6 -0
- package/.next/standalone/.next/server/app/_global-error/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/_global-error/page.js +9 -0
- package/.next/standalone/.next/server/app/_global-error/page.js.map +5 -0
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/_global-error.html +1 -0
- package/.next/standalone/.next/server/app/_global-error.meta +22 -0
- package/.next/standalone/.next/server/app/_global-error.rsc +15 -0
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +20 -0
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +15 -0
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -0
- package/.next/standalone/.next/server/app/_not-found/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/_not-found/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/_not-found/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/_not-found/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/_not-found/page.js +12 -0
- package/.next/standalone/.next/server/app/_not-found/page.js.map +5 -0
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/_not-found.html +1 -0
- package/.next/standalone/.next/server/app/_not-found.meta +27 -0
- package/.next/standalone/.next/server/app/_not-found.rsc +18 -0
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +18 -0
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -0
- package/.next/standalone/.next/server/app/api/admin-delete/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/admin-delete/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/admin-delete/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/admin-delete/route.js +7 -0
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/admin-delete/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/admin-delete/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/admin-update/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/admin-update/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/admin-update/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/admin-update/route.js +7 -0
- package/.next/standalone/.next/server/app/api/admin-update/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/admin-update/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/admin-update/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/bridges/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/bridges/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/bridges/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/bridges/route.js +7 -0
- package/.next/standalone/.next/server/app/api/bridges/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/bridges/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/bridges/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/bulk-import/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/bulk-import/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/bulk-import/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/bulk-import/route.js +7 -0
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/bulk-import/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/bulk-import/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/company-scores/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/company-scores/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/company-scores/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/company-scores/route.js +7 -0
- package/.next/standalone/.next/server/app/api/company-scores/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/company-scores/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/company-scores/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/connections/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/connections/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/connections/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/connections/route.js +7 -0
- package/.next/standalone/.next/server/app/api/connections/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/connections/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/connections/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/delete-cluster/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/delete-cluster/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/delete-cluster/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js +7 -0
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/delete-cluster/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/delete-cluster/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/ingest/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/ingest/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/ingest/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/ingest/route.js +8 -0
- package/.next/standalone/.next/server/app/api/ingest/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/ingest/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/ingest/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/network/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/network/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/network/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/network/route.js +7 -0
- package/.next/standalone/.next/server/app/api/network/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/network/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/network/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/notifications/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/notifications/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/notifications/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/notifications/route.js +7 -0
- package/.next/standalone/.next/server/app/api/notifications/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/outreach/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/outreach/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/outreach/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/outreach/route.js +7 -0
- package/.next/standalone/.next/server/app/api/outreach/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/outreach/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/outreach/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/queue/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/queue/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/queue/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/queue/route.js +7 -0
- package/.next/standalone/.next/server/app/api/queue/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/queue/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/queue/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/scraper/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/scraper/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/scraper/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/scraper/route.js +7 -0
- package/.next/standalone/.next/server/app/api/scraper/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/scraper/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/scraper/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/setup-profile/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/setup-profile/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/setup-profile/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/setup-profile/route.js +7 -0
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/setup-profile/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/setup-profile/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/unlock/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/unlock/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/unlock/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/unlock/route.js +7 -0
- package/.next/standalone/.next/server/app/api/unlock/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/unlock/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/unlock/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/update/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/update/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/update/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/update/route.js +7 -0
- package/.next/standalone/.next/server/app/api/update/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/update/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/update/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/update-images/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/update-images/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/update-images/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/update-images/route.js +7 -0
- package/.next/standalone/.next/server/app/api/update-images/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/update-images/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/update-images/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/users/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/users/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/api/users/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/users/route.js +7 -0
- package/.next/standalone/.next/server/app/api/users/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/users/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/users/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/avatars/[file]/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/avatars/[file]/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/avatars/[file]/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/avatars/[file]/route.js +6 -0
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.map +5 -0
- package/.next/standalone/.next/server/app/avatars/[file]/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/avatars/[file]/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/favicon.ico/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/favicon.ico/route/build-manifest.json +12 -0
- package/.next/standalone/.next/server/app/favicon.ico/route.js +7 -0
- package/.next/standalone/.next/server/app/favicon.ico/route.js.map +5 -0
- package/.next/standalone/.next/server/app/favicon.ico/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/favicon.ico.body +0 -0
- package/.next/standalone/.next/server/app/favicon.ico.meta +1 -0
- package/.next/standalone/.next/server/app/import/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/import/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/import/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/import/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/import/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/import/page.js +13 -0
- package/.next/standalone/.next/server/app/import/page.js.map +5 -0
- package/.next/standalone/.next/server/app/import/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/import/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/import.html +1 -0
- package/.next/standalone/.next/server/app/import.meta +26 -0
- package/.next/standalone/.next/server/app/import.rsc +24 -0
- package/.next/standalone/.next/server/app/import.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/import.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/import.segments/import/__PAGE__.segment.rsc +28 -0
- package/.next/standalone/.next/server/app/index.html +1 -0
- package/.next/standalone/.next/server/app/index.meta +21 -0
- package/.next/standalone/.next/server/app/index.rsc +22 -0
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +25 -0
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +22 -0
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/launch/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/launch/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/launch/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/launch/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/launch/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/launch/page.js +13 -0
- package/.next/standalone/.next/server/app/launch/page.js.map +5 -0
- package/.next/standalone/.next/server/app/launch/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/launch/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/launch.html +44 -0
- package/.next/standalone/.next/server/app/launch.meta +26 -0
- package/.next/standalone/.next/server/app/launch.rsc +24 -0
- package/.next/standalone/.next/server/app/launch.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/launch.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/launch.segments/launch/__PAGE__.segment.rsc +28 -0
- package/.next/standalone/.next/server/app/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/page.js +13 -0
- package/.next/standalone/.next/server/app/page.js.map +5 -0
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/paths/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/paths/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/paths/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/paths/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/paths/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/paths/page.js +13 -0
- package/.next/standalone/.next/server/app/paths/page.js.map +5 -0
- package/.next/standalone/.next/server/app/paths/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/paths/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/paths.html +1 -0
- package/.next/standalone/.next/server/app/paths.meta +26 -0
- package/.next/standalone/.next/server/app/paths.rsc +24 -0
- package/.next/standalone/.next/server/app/paths.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/paths.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/paths.segments/paths/__PAGE__.segment.rsc +28 -0
- package/.next/standalone/.next/server/app/profile/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/profile/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/profile/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/profile/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/profile/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/profile/page.js +13 -0
- package/.next/standalone/.next/server/app/profile/page.js.map +5 -0
- package/.next/standalone/.next/server/app/profile/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/profile/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/profile.html +1 -0
- package/.next/standalone/.next/server/app/profile.meta +26 -0
- package/.next/standalone/.next/server/app/profile.rsc +24 -0
- package/.next/standalone/.next/server/app/profile.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/profile.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/profile.segments/profile/__PAGE__.segment.rsc +28 -0
- package/.next/standalone/.next/server/app/queue/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/queue/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/queue/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/queue/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/queue/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/queue/page.js +13 -0
- package/.next/standalone/.next/server/app/queue/page.js.map +5 -0
- package/.next/standalone/.next/server/app/queue/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/queue/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/queue.html +1 -0
- package/.next/standalone/.next/server/app/queue.meta +26 -0
- package/.next/standalone/.next/server/app/queue.rsc +24 -0
- package/.next/standalone/.next/server/app/queue.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/queue.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/queue.segments/queue/__PAGE__.segment.rsc +28 -0
- package/.next/standalone/.next/server/app/setup/page/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/setup/page/build-manifest.json +20 -0
- package/.next/standalone/.next/server/app/setup/page/next-font-manifest.json +11 -0
- package/.next/standalone/.next/server/app/setup/page/react-loadable-manifest.json +1 -0
- package/.next/standalone/.next/server/app/setup/page/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/setup/page.js +13 -0
- package/.next/standalone/.next/server/app/setup/page.js.map +5 -0
- package/.next/standalone/.next/server/app/setup/page.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/setup.html +1 -0
- package/.next/standalone/.next/server/app/setup.meta +26 -0
- package/.next/standalone/.next/server/app/setup.rsc +24 -0
- package/.next/standalone/.next/server/app/setup.segments/_full.segment.rsc +24 -0
- package/.next/standalone/.next/server/app/setup.segments/_tree.segment.rsc +4 -0
- package/.next/standalone/.next/server/app/setup.segments/setup/__PAGE__.segment.rsc +28 -0
- package/.next/standalone/.next/server/app-paths-manifest.json +31 -0
- package/.next/standalone/.next/server/chunks/[externals]__1pd1lps._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0jrmmj-._.js +150 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qx548g._.js +151 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__15bjnu5._.js +5 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1an3leo._.js +150 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1m8zedk._.js +155 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__1zor0ys._.js +3 -0
- package/.next/standalone/.next/server/chunks/[turbopack]_runtime.js +807 -0
- package/.next/standalone/.next/server/chunks/_0_rp-01._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0hzdk79._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0ow7u5r._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0t0zc2d._.js +3 -0
- package/.next/standalone/.next/server/chunks/_0wa631d._.js +3 -0
- package/.next/standalone/.next/server/chunks/_13ccnao._.js +3 -0
- package/.next/standalone/.next/server/chunks/_13es063._.js +3 -0
- package/.next/standalone/.next/server/chunks/_142872j._.js +3 -0
- package/.next/standalone/.next/server/chunks/_18wsb_a._.js +8 -0
- package/.next/standalone/.next/server/chunks/_1_fynia._.js +5 -0
- package/.next/standalone/.next/server/chunks/_1e-4h5h._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1gvvd7n._.js +158 -0
- package/.next/standalone/.next/server/chunks/_1itd1o_._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1iwkh8t._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1moda66._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1p6ghzv._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1xmmi_v._.js +3 -0
- package/.next/standalone/.next/server/chunks/_1z-yrlf._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_admin-delete_route_actions_1p93wag.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_admin-update_route_actions_1sf7bsv.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_bridges_route_actions_1n3pety.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_bulk-import_route_actions_10b3t1k.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_company-scores_route_actions_0qanhj-.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_connections_route_actions_20wls3v.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_delete-cluster_route_actions_0v9iazl.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_ingest_route_actions_1oz_9xc.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_network_route_actions_1r570mb.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_notifications_route_actions_1272ib8.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_outreach_route_actions_1ze1hrq.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_queue_route_actions_099d1-o.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_scraper_route_actions_0d98z8s.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_setup-profile_route_actions_0nk95pf.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_unlock_route_actions_0yzwsc4.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_update-images_route_actions_1rk6h3z.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_update_route_actions_0tvsuhe.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_users_route_actions_0mvbx1f.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_avatars_[file]_route_actions_0177eip.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_0g2jjls.js +3 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0c9d9ti.js +5 -0
- package/.next/standalone/.next/server/chunks/lib_rpc_0mc73lk.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00d8snt._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__00g61b8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__08lf3gy._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bipr5s._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h1_f_2._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0hg5gib._.js +33 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ho-u0w._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0j26pto._.js +14 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mocm6d._.js +33 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ze473p._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1678-w0._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1cnxxc9._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1ll97ry._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__1w9z0zv._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[turbopack]_runtime.js +807 -0
- package/.next/standalone/.next/server/chunks/ssr/_0bf3x4q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_0ggabjc._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_17gui6a._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_1djpk8t._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_0zi5s8-.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_0pt47yr.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_import_page_actions_0bljgrx.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_launch_page_actions_1qmf5nl.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_page_actions_0hhsz1j.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_paths_page_actions_09jbzbd.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_profile_page_actions_1b7qq3l.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_queue_page_actions_1s499-2.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/_next-internal_server_app_setup_page_actions_0u1sm0s.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_import_page_1fmu6_l.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_launch_page_19t0dyd.js +46 -0
- package/.next/standalone/.next/server/chunks/ssr/app_page_1fazqbf.js +18 -0
- package/.next/standalone/.next/server/chunks/ssr/app_paths_page_1ol90m7.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/app_profile_page_0u3-l5y.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/app_queue_page_0c6znd6.js +35 -0
- package/.next/standalone/.next/server/chunks/ssr/app_setup_page_1-282zx.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_0i8v-s3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_01xbj_9._.js +33 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_16vay6l._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1kciwn4._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_1nkgb45._.js +23 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_00nap8h._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_1n3w9lb._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_0wpq8j3._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_forbidden_0symwr9.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_global-error_0q-w892.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules_next_dist_client_components_builtin_unauthorized_0l_sp0x.js +3 -0
- package/.next/standalone/.next/server/edge/chunks/[root-of-the-server]__0pco7a6._.js +11 -0
- package/.next/standalone/.next/server/edge/chunks/node_modules_next_dist_esm_build_templates_edge-wrapper_1nw6_jx.js +3 -0
- package/.next/standalone/.next/server/edge/chunks/turbopack-node_modules_next_dist_esm_build_templates_edge-wrapper_0n2dz80.js +3 -0
- package/.next/standalone/.next/server/functions-config-manifest.json +4 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +24 -0
- package/.next/standalone/.next/server/middleware-manifest.json +34 -0
- package/.next/standalone/.next/server/next-font-manifest.js +1 -0
- package/.next/standalone/.next/server/next-font-manifest.json +39 -0
- package/.next/standalone/.next/server/pages/404.html +1 -0
- package/.next/standalone/.next/server/pages/500.html +1 -0
- package/.next/standalone/.next/server/pages-manifest.json +4 -0
- package/.next/standalone/.next/server/prefetch-hints.json +118 -0
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -0
- package/.next/standalone/.next/server/server-reference-manifest.json +5 -0
- package/.next/standalone/.next/static/N9wokeIiC9rUIGmzBhj6T/_buildManifest.js +11 -0
- package/.next/standalone/.next/static/N9wokeIiC9rUIGmzBhj6T/_clientMiddlewareManifest.js +6 -0
- package/.next/standalone/.next/static/N9wokeIiC9rUIGmzBhj6T/_ssgManifest.js +1 -0
- package/.next/standalone/.next/static/chunks/08k1crid-05dt.js +1 -0
- package/.next/standalone/.next/static/chunks/0bma92pht_c97.js +1 -0
- package/.next/standalone/.next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- package/.next/standalone/.next/static/chunks/19mx3mg6lkumu.js +1 -0
- package/.next/standalone/.next/static/chunks/1odow92u6wxz0.js +1 -0
- package/.next/standalone/.next/static/chunks/1wb_is4wsdm-u.js +1 -0
- package/.next/standalone/.next/static/chunks/1zmpcmngg5c7-.js +2 -0
- package/.next/standalone/.next/static/chunks/2-rtuqsgzmno4.js +22 -0
- package/.next/standalone/.next/static/chunks/220-ne82aus0c.js +44 -0
- package/.next/standalone/.next/static/chunks/2rrf5kzx2fhin.js +2 -0
- package/.next/standalone/.next/static/chunks/2s_k9x4d0gi9h.js +16 -0
- package/.next/standalone/.next/static/chunks/310vm2bl3xxpt.js +31 -0
- package/.next/standalone/.next/static/chunks/34at5d2ifl62i.js +33 -0
- package/.next/standalone/.next/static/chunks/3fntmmi971322.js +1 -0
- package/.next/standalone/.next/static/chunks/3hsi90cejq2dg.js +1 -0
- package/.next/standalone/.next/static/chunks/3pfyuezyqd9ob.js +1 -0
- package/.next/standalone/.next/static/chunks/3ts-_4i6bfkh2.js +1 -0
- package/.next/standalone/.next/static/chunks/40nvbgfn7-072.css +3 -0
- package/.next/standalone/.next/static/chunks/turbopack-43vu1xwipchft.js +1 -0
- package/.next/standalone/.next/static/media/4fa387ec64143e14-s.2tuy5pz7dlieh.woff2 +0 -0
- package/.next/standalone/.next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
- package/.next/standalone/.next/static/media/5ce348bf30bf5439-s.31988l_ccedte.woff2 +0 -0
- package/.next/standalone/.next/static/media/6306c77e7c8268e4-s.2dbetqa9o8jxf.woff2 +0 -0
- package/.next/standalone/.next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
- package/.next/standalone/.next/static/media/797e433ab948586e-s.p.0r6juujl39pe6.woff2 +0 -0
- package/.next/standalone/.next/static/media/7d817b4c03b0c5f1-s.1uyisp29ctx0d.woff2 +0 -0
- package/.next/standalone/.next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
- package/.next/standalone/.next/static/media/bbc41e54d2fcbd21-s.1rgnod-3esatf.woff2 +0 -0
- package/.next/standalone/.next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
- package/.next/standalone/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- package/.next/standalone/.next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
- package/.next/standalone/AGENTS.md +8 -0
- package/.next/standalone/CHANGELOG.md +553 -0
- package/.next/standalone/CLAUDE.md +29 -0
- package/.next/standalone/CODE_OF_CONDUCT.md +39 -0
- package/.next/standalone/CONTRIBUTING.md +99 -0
- package/.next/standalone/LICENSE +21 -0
- package/.next/standalone/README.md +265 -0
- package/.next/standalone/SECURITY.md +46 -0
- package/.next/standalone/app/api/admin-delete/route.js +18 -0
- package/.next/standalone/app/api/admin-update/route.js +23 -0
- package/.next/standalone/app/api/bridges/route.js +43 -0
- package/.next/standalone/app/api/bulk-import/route.js +90 -0
- package/.next/standalone/app/api/company-scores/route.js +62 -0
- package/.next/standalone/app/api/connections/route.js +38 -0
- package/.next/standalone/app/api/delete-cluster/route.js +25 -0
- package/.next/standalone/app/api/ingest/route.js +329 -0
- package/.next/standalone/app/api/network/route.js +49 -0
- package/.next/standalone/app/api/notifications/route.js +50 -0
- package/.next/standalone/app/api/outreach/route.js +81 -0
- package/.next/standalone/app/api/queue/route.js +81 -0
- package/.next/standalone/app/api/scraper/route.js +489 -0
- package/.next/standalone/app/api/setup-profile/route.js +48 -0
- package/.next/standalone/app/api/unlock/route.js +29 -0
- package/.next/standalone/app/api/update/route.js +221 -0
- package/.next/standalone/app/api/update-images/route.js +39 -0
- package/.next/standalone/app/api/users/route.js +73 -0
- package/.next/standalone/app/avatars/[file]/route.js +33 -0
- package/.next/standalone/app/components/Avatar.js +35 -0
- package/.next/standalone/app/components/BridgeRing.js +753 -0
- package/.next/standalone/app/components/ChainView.js +631 -0
- package/.next/standalone/app/components/CompanyScores.js +144 -0
- package/.next/standalone/app/components/EmptyState.js +110 -0
- package/.next/standalone/app/components/FilterPanel.js +184 -0
- package/.next/standalone/app/components/ForceGraph.js +1043 -0
- package/.next/standalone/app/components/GridView.js +166 -0
- package/.next/standalone/app/components/LinkedInLimits.js +143 -0
- package/.next/standalone/app/components/ListView.js +148 -0
- package/.next/standalone/app/components/MappingProgress.js +134 -0
- package/.next/standalone/app/components/OnboardingGate.js +37 -0
- package/.next/standalone/app/components/OrbitGraph.js +630 -0
- package/.next/standalone/app/components/OutlinkQuest.js +212 -0
- package/.next/standalone/app/components/PathsAnalyzer.js +520 -0
- package/.next/standalone/app/components/RingsView.js +277 -0
- package/.next/standalone/app/components/SeparationView.js +564 -0
- package/.next/standalone/app/components/Sidebar.js +1398 -0
- package/.next/standalone/app/components/StaleServerBanner.js +53 -0
- package/.next/standalone/app/components/UpdatePanel.js +246 -0
- package/.next/standalone/app/components/UserProvider.js +116 -0
- package/.next/standalone/app/components/views.js +59 -0
- package/.next/standalone/app/favicon.ico +0 -0
- package/.next/standalone/app/globals.css +33 -0
- package/.next/standalone/app/import/page.js +189 -0
- package/.next/standalone/app/launch/page.js +834 -0
- package/.next/standalone/app/layout.js +37 -0
- package/.next/standalone/app/page.js +490 -0
- package/.next/standalone/app/paths/page.js +561 -0
- package/.next/standalone/app/profile/page.js +572 -0
- package/.next/standalone/app/queue/page.js +685 -0
- package/.next/standalone/app/setup/page.js +562 -0
- package/.next/standalone/bin/six-degrees.mjs +149 -0
- package/.next/standalone/db/schema.js +160 -0
- package/.next/standalone/eslint.config.mjs +34 -0
- package/.next/standalone/install.sh +192 -0
- package/.next/standalone/jsconfig.json +7 -0
- package/.next/standalone/lib/app-version.js +7 -0
- package/.next/standalone/lib/companies.js +241 -0
- package/.next/standalone/lib/csv.js +172 -0
- package/.next/standalone/lib/db-client.js +152 -0
- package/.next/standalone/lib/db.js +288 -0
- package/.next/standalone/lib/demo.js +34 -0
- package/.next/standalone/lib/gate.js +87 -0
- package/.next/standalone/lib/ingest.js +62 -0
- package/.next/standalone/lib/linkedin-limits.js +118 -0
- package/.next/standalone/lib/network.js +13 -0
- package/.next/standalone/lib/paths.js +31 -0
- package/.next/standalone/lib/paused.js +75 -0
- package/.next/standalone/lib/profile.js +55 -0
- package/.next/standalone/lib/promote.js +139 -0
- package/.next/standalone/lib/quest.js +119 -0
- package/.next/standalone/lib/release.js +50 -0
- package/.next/standalone/lib/rpc.js +80 -0
- package/.next/standalone/lib/scan-progress.js +63 -0
- package/.next/standalone/lib/scoring.js +410 -0
- package/.next/standalone/lib/scraper-client.js +65 -0
- package/.next/standalone/lib/separation.js +326 -0
- package/.next/standalone/lib/tiers.js +50 -0
- package/.next/standalone/lib/user.js +32 -0
- package/.next/standalone/middleware.js +48 -0
- package/.next/standalone/next.config.mjs +35 -0
- package/.next/standalone/node_modules/@img/colour/color.cjs +1596 -0
- package/.next/standalone/node_modules/@img/colour/index.cjs +1 -0
- package/.next/standalone/node_modules/@img/colour/package.json +58 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/README.md +46 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/glib-2.0/include/glibconfig.h +219 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/index.js +1 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/lib/libvips-cpp.so.8.18.6 +0 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/package.json +43 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linux-x64/versions.json +30 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/README.md +46 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/glib-2.0/include/glibconfig.h +219 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/index.js +1 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/lib/libvips-cpp.so.8.18.6 +0 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/package.json +43 -0
- package/.next/standalone/node_modules/@img/sharp-libvips-linuxmusl-x64/versions.json +30 -0
- package/.next/standalone/node_modules/@img/sharp-linux-x64/index.cjs +2 -0
- package/.next/standalone/node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64-0.35.4.node +0 -0
- package/.next/standalone/node_modules/@img/sharp-linux-x64/package.json +47 -0
- package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/index.cjs +2 -0
- package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/lib/sharp-linuxmusl-x64-0.35.4.node +0 -0
- package/.next/standalone/node_modules/@img/sharp-linuxmusl-x64/package.json +47 -0
- package/.next/standalone/node_modules/@next/env/dist/index.js +1 -0
- package/.next/standalone/node_modules/@next/env/package.json +36 -0
- package/.next/standalone/node_modules/@swc/helpers/cjs/_interop_require_default.cjs +6 -0
- package/.next/standalone/node_modules/@swc/helpers/cjs/_interop_require_wildcard.cjs +38 -0
- package/.next/standalone/node_modules/@swc/helpers/esm/_interop_require_default.js +4 -0
- package/.next/standalone/node_modules/@swc/helpers/esm/_interop_require_wildcard.js +36 -0
- package/.next/standalone/node_modules/@swc/helpers/package.json +705 -0
- package/.next/standalone/node_modules/client-only/index.js +0 -0
- package/.next/standalone/node_modules/client-only/package.json +19 -0
- package/.next/standalone/node_modules/detect-libc/lib/detect-libc.js +313 -0
- package/.next/standalone/node_modules/detect-libc/lib/elf.js +39 -0
- package/.next/standalone/node_modules/detect-libc/lib/filesystem.js +51 -0
- package/.next/standalone/node_modules/detect-libc/lib/process.js +24 -0
- package/.next/standalone/node_modules/detect-libc/package.json +44 -0
- package/.next/standalone/node_modules/next/dist/build/adapter/setup-node-env.external.js +15 -0
- package/.next/standalone/node_modules/next/dist/build/define-env.js +263 -0
- package/.next/standalone/node_modules/next/dist/build/duration-to-string.js +98 -0
- package/.next/standalone/node_modules/next/dist/build/get-supported-browsers.js +38 -0
- package/.next/standalone/node_modules/next/dist/build/next-config-ts/require-hook.js +85 -0
- package/.next/standalone/node_modules/next/dist/build/next-config-ts/transpile-config.js +140 -0
- package/.next/standalone/node_modules/next/dist/build/output/format.js +84 -0
- package/.next/standalone/node_modules/next/dist/build/output/log.js +135 -0
- package/.next/standalone/node_modules/next/dist/build/segment-config/app/app-segment-config.js +178 -0
- package/.next/standalone/node_modules/next/dist/build/segment-config/app/app-segments.js +147 -0
- package/.next/standalone/node_modules/next/dist/build/segment-config/app/collect-root-param-keys.js +52 -0
- package/.next/standalone/node_modules/next/dist/build/static-paths/app/extract-pathname-route-param-segments-from-loader-tree.js +137 -0
- package/.next/standalone/node_modules/next/dist/build/static-paths/app.js +783 -0
- package/.next/standalone/node_modules/next/dist/build/static-paths/pages.js +169 -0
- package/.next/standalone/node_modules/next/dist/build/static-paths/utils.js +119 -0
- package/.next/standalone/node_modules/next/dist/build/swc/helpers.js +15 -0
- package/.next/standalone/node_modules/next/dist/build/swc/index.js +1409 -0
- package/.next/standalone/node_modules/next/dist/build/swc/install-bindings.js +30 -0
- package/.next/standalone/node_modules/next/dist/build/swc/jest-transformer.js +77 -0
- package/.next/standalone/node_modules/next/dist/build/swc/loaderWorkerPool.js +40 -0
- package/.next/standalone/node_modules/next/dist/build/swc/options.js +411 -0
- package/.next/standalone/node_modules/next/dist/build/swc/types.js +6 -0
- package/.next/standalone/node_modules/next/dist/build/utils.js +1258 -0
- package/.next/standalone/node_modules/next/dist/cli/next-test.js +171 -0
- package/.next/standalone/node_modules/next/dist/client/add-base-path.js +24 -0
- package/.next/standalone/node_modules/next/dist/client/app-call-server.js +34 -0
- package/.next/standalone/node_modules/next/dist/client/app-find-source-map-url.js +39 -0
- package/.next/standalone/node_modules/next/dist/client/assign-location.js +29 -0
- package/.next/standalone/node_modules/next/dist/client/components/app-router-announcer.js +78 -0
- package/.next/standalone/node_modules/next/dist/client/components/app-router-headers.js +128 -0
- package/.next/standalone/node_modules/next/dist/client/components/app-router-instance.js +407 -0
- package/.next/standalone/node_modules/next/dist/client/components/app-router-utils.js +62 -0
- package/.next/standalone/node_modules/next/dist/client/components/app-router.js +522 -0
- package/.next/standalone/node_modules/next/dist/client/components/builtin/default.js +35 -0
- package/.next/standalone/node_modules/next/dist/client/components/builtin/error-styles.js +165 -0
- package/.next/standalone/node_modules/next/dist/client/components/builtin/global-error.js +100 -0
- package/.next/standalone/node_modules/next/dist/client/components/dev-root-http-access-fallback-boundary.js +52 -0
- package/.next/standalone/node_modules/next/dist/client/components/error-boundary.js +148 -0
- package/.next/standalone/node_modules/next/dist/client/components/errors/graceful-degrade-boundary.js +86 -0
- package/.next/standalone/node_modules/next/dist/client/components/errors/root-error-boundary.js +41 -0
- package/.next/standalone/node_modules/next/dist/client/components/forbidden.js +48 -0
- package/.next/standalone/node_modules/next/dist/client/components/handle-isr-error.js +30 -0
- package/.next/standalone/node_modules/next/dist/client/components/hooks-server-context.js +42 -0
- package/.next/standalone/node_modules/next/dist/client/components/http-access-fallback/error-boundary.js +125 -0
- package/.next/standalone/node_modules/next/dist/client/components/http-access-fallback/http-access-fallback.js +72 -0
- package/.next/standalone/node_modules/next/dist/client/components/instant-samples.js +134 -0
- package/.next/standalone/node_modules/next/dist/client/components/is-next-router-error.js +23 -0
- package/.next/standalone/node_modules/next/dist/client/components/links.js +316 -0
- package/.next/standalone/node_modules/next/dist/client/components/match-segments.js +32 -0
- package/.next/standalone/node_modules/next/dist/client/components/nav-failure-handler.js +61 -0
- package/.next/standalone/node_modules/next/dist/client/components/navigation-devtools.js +126 -0
- package/.next/standalone/node_modules/next/dist/client/components/navigation-dynamic-rendering.js +38 -0
- package/.next/standalone/node_modules/next/dist/client/components/navigation-untracked.js +69 -0
- package/.next/standalone/node_modules/next/dist/client/components/navigation.js +246 -0
- package/.next/standalone/node_modules/next/dist/client/components/navigation.react-server.js +75 -0
- package/.next/standalone/node_modules/next/dist/client/components/not-found.js +42 -0
- package/.next/standalone/node_modules/next/dist/client/components/offline.js +172 -0
- package/.next/standalone/node_modules/next/dist/client/components/readonly-url-search-params.js +46 -0
- package/.next/standalone/node_modules/next/dist/client/components/redirect-boundary.js +107 -0
- package/.next/standalone/node_modules/next/dist/client/components/redirect-error.js +43 -0
- package/.next/standalone/node_modules/next/dist/client/components/redirect-status-code.js +24 -0
- package/.next/standalone/node_modules/next/dist/client/components/redirect.js +91 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/compute-changed-path.js +205 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/create-href-from-url.js +21 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/create-initial-router-state.js +161 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/create-router-cache-key.js +32 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/fetch-server-response.js +603 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/is-navigating-to-new-root-layout.js +70 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/ppr-navigations.js +1474 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/committed-state.js +49 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/find-head-in-cache.js +63 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/has-interception-route-in-current-tree.js +38 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/hmr-refresh-reducer.js +29 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/navigate-reducer.js +56 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/refresh-reducer.js +90 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/restore-reducer.js +75 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/server-action-reducer.js +349 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/reducers/server-patch-reducer.js +67 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/router-reducer-types.js +70 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/router-reducer.js +74 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-reducer/set-cache-busting-search-param.js +66 -0
- package/.next/standalone/node_modules/next/dist/client/components/router-transition.js +135 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/bfcache.js +130 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/cache-key.js +56 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/cache-map.js +326 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/cache.js +2684 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/fetch.js +37 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/lru.js +152 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/navigation-testing-lock.js +415 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/navigation.js +773 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/optimistic-routes.js +648 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/prefetch.js +32 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/scheduler.js +1770 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/types.js +84 -0
- package/.next/standalone/node_modules/next/dist/client/components/segment-cache/vary-path.js +326 -0
- package/.next/standalone/node_modules/next/dist/client/components/server-async-storage.js +43 -0
- package/.next/standalone/node_modules/next/dist/client/components/static-generation-bailout.js +42 -0
- package/.next/standalone/node_modules/next/dist/client/components/unauthorized.js +49 -0
- package/.next/standalone/node_modules/next/dist/client/components/unrecognized-action-error.js +39 -0
- package/.next/standalone/node_modules/next/dist/client/components/unresolved-thenable.js +23 -0
- package/.next/standalone/node_modules/next/dist/client/components/unstable-rethrow.js +32 -0
- package/.next/standalone/node_modules/next/dist/client/components/use-action-queue.js +148 -0
- package/.next/standalone/node_modules/next/dist/client/components/use-offline.js +68 -0
- package/.next/standalone/node_modules/next/dist/client/dev/debug-channel.js +428 -0
- package/.next/standalone/node_modules/next/dist/client/flight-data-helpers.js +235 -0
- package/.next/standalone/node_modules/next/dist/client/has-base-path.js +23 -0
- package/.next/standalone/node_modules/next/dist/client/lib/console.js +177 -0
- package/.next/standalone/node_modules/next/dist/client/lib/javascript-url.js +32 -0
- package/.next/standalone/node_modules/next/dist/client/navigation-build-id.js +53 -0
- package/.next/standalone/node_modules/next/dist/client/normalize-trailing-slash.js +37 -0
- package/.next/standalone/node_modules/next/dist/client/remove-base-path.js +32 -0
- package/.next/standalone/node_modules/next/dist/client/route-params.js +249 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/cookies/index.js +339 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/cookies/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/ponyfill/index.js +46 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/ponyfill/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/abort-controller.js.text.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/console.js.text.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/events.js.text.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/index.js +5 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/load.js +18713 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/stream.js +44 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/timers.js.text.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@edge-runtime/primitives/url.js.text.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@hapi/accept/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@hapi/accept/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@mswjs/interceptors/ClientRequest/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@mswjs/interceptors/ClientRequest/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@napi-rs/triples/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@napi-rs/triples/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@opentelemetry/api/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@opentelemetry/api/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@vercel/detect-agent/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/@vercel/detect-agent/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/async-retry/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/async-retry/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/async-sema/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/async-sema/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/busboy/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/busboy/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/bytes/index.js +8 -0
- package/.next/standalone/node_modules/next/dist/compiled/bytes/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/ci-info/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/ci-info/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/commander/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/commander/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/comment-json/index.js +7 -0
- package/.next/standalone/node_modules/next/dist/compiled/comment-json/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/compression/index.js +58 -0
- package/.next/standalone/node_modules/next/dist/compiled/compression/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/conf/index.js +3 -0
- package/.next/standalone/node_modules/next/dist/compiled/conf/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/content-disposition/index.js +6 -0
- package/.next/standalone/node_modules/next/dist/compiled/content-disposition/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/cookie/index.js +7 -0
- package/.next/standalone/node_modules/next/dist/compiled/cookie/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/cross-spawn/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/cross-spawn/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/debug/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/debug/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/edge-runtime/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/edge-runtime/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/find-up/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/find-up/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/fresh/index.js +8 -0
- package/.next/standalone/node_modules/next/dist/compiled/fresh/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/httpxy/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/httpxy/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/image-detector/detector.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/image-size/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/image-size/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/ipaddr.js/ipaddr.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/ipaddr.js/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/is-animated/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/is-animated/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/is-docker/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/is-docker/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/is-wsl/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/is-wsl/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/jsonwebtoken/index.js +11 -0
- package/.next/standalone/node_modules/next/dist/compiled/jsonwebtoken/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/nanoid/index.cjs +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/nanoid/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo-experimental.runtime.prod.js +183 -0
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-page-turbo.runtime.prod.js +183 -0
- package/.next/standalone/node_modules/next/dist/compiled/next-server/app-route-turbo.runtime.prod.js +6 -0
- package/.next/standalone/node_modules/next/dist/compiled/next-server/pages-turbo.runtime.prod.js +20 -0
- package/.next/standalone/node_modules/next/dist/compiled/p-limit/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/p-limit/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/p-queue/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/p-queue/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/path-browserify/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/path-browserify/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/path-to-regexp/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/path-to-regexp/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/picomatch/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/picomatch/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/react-is/cjs/react-is.development.js +133 -0
- package/.next/standalone/node_modules/next/dist/compiled/react-is/cjs/react-is.production.js +130 -0
- package/.next/standalone/node_modules/next/dist/compiled/react-is/index.js +7 -0
- package/.next/standalone/node_modules/next/dist/compiled/react-is/package.json +26 -0
- package/.next/standalone/node_modules/next/dist/compiled/regenerator-runtime/package.json +19 -0
- package/.next/standalone/node_modules/next/dist/compiled/regenerator-runtime/runtime.js +729 -0
- package/.next/standalone/node_modules/next/dist/compiled/semver/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/semver/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/send/index.js +70 -0
- package/.next/standalone/node_modules/next/dist/compiled/send/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/source-map/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/source-map/source-map.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/stacktrace-parser/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/stacktrace-parser/stack-trace-parser.cjs.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/string-hash/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/string-hash/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/strip-ansi/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/strip-ansi/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/superstruct/index.cjs +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/superstruct/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/tar/index.min.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/tar/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/text-table/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/text-table/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/watchpack/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/watchpack/watchpack.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/write-file-atomic/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/write-file-atomic/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/ws/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/ws/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/zod/index.cjs +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/zod/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/zod-validation-error/index.js +1 -0
- package/.next/standalone/node_modules/next/dist/compiled/zod-validation-error/package.json +1 -0
- package/.next/standalone/node_modules/next/dist/experimental/testmode/context.js +57 -0
- package/.next/standalone/node_modules/next/dist/experimental/testmode/fetch.js +158 -0
- package/.next/standalone/node_modules/next/dist/experimental/testmode/httpget.js +33 -0
- package/.next/standalone/node_modules/next/dist/experimental/testmode/server-edge.js +32 -0
- package/.next/standalone/node_modules/next/dist/experimental/testmode/server.js +73 -0
- package/.next/standalone/node_modules/next/dist/export/helpers/create-incremental-cache.js +61 -0
- package/.next/standalone/node_modules/next/dist/lib/batcher.js +65 -0
- package/.next/standalone/node_modules/next/dist/lib/build-custom-route.js +46 -0
- package/.next/standalone/node_modules/next/dist/lib/bundler.js +168 -0
- package/.next/standalone/node_modules/next/dist/lib/client-and-server-references.js +54 -0
- package/.next/standalone/node_modules/next/dist/lib/coalesced-function.js +39 -0
- package/.next/standalone/node_modules/next/dist/lib/compile-error.js +14 -0
- package/.next/standalone/node_modules/next/dist/lib/constants.js +420 -0
- package/.next/standalone/node_modules/next/dist/lib/create-client-router-filter.js +83 -0
- package/.next/standalone/node_modules/next/dist/lib/default-transpiled-packages.json +1 -0
- package/.next/standalone/node_modules/next/dist/lib/detached-promise.js +32 -0
- package/.next/standalone/node_modules/next/dist/lib/detect-typo.js +51 -0
- package/.next/standalone/node_modules/next/dist/lib/download-swc.js +183 -0
- package/.next/standalone/node_modules/next/dist/lib/error-telemetry-utils.js +42 -0
- package/.next/standalone/node_modules/next/dist/lib/fallback.js +100 -0
- package/.next/standalone/node_modules/next/dist/lib/file-exists.js +53 -0
- package/.next/standalone/node_modules/next/dist/lib/find-config.js +102 -0
- package/.next/standalone/node_modules/next/dist/lib/find-pages-dir.js +65 -0
- package/.next/standalone/node_modules/next/dist/lib/find-root.js +280 -0
- package/.next/standalone/node_modules/next/dist/lib/format-cli-help-output.js +84 -0
- package/.next/standalone/node_modules/next/dist/lib/format-dynamic-import-path.js +43 -0
- package/.next/standalone/node_modules/next/dist/lib/format-server-error.js +75 -0
- package/.next/standalone/node_modules/next/dist/lib/framework/boundary-components.js +62 -0
- package/.next/standalone/node_modules/next/dist/lib/framework/boundary-constants.js +36 -0
- package/.next/standalone/node_modules/next/dist/lib/fs/rename.js +87 -0
- package/.next/standalone/node_modules/next/dist/lib/fs/write-atomic.js +28 -0
- package/.next/standalone/node_modules/next/dist/lib/generate-interception-routes-rewrites.js +52 -0
- package/.next/standalone/node_modules/next/dist/lib/get-files-in-dir.js +33 -0
- package/.next/standalone/node_modules/next/dist/lib/get-network-host.js +44 -0
- package/.next/standalone/node_modules/next/dist/lib/get-package-version.js +118 -0
- package/.next/standalone/node_modules/next/dist/lib/get-project-dir.js +51 -0
- package/.next/standalone/node_modules/next/dist/lib/git-worktree.js +74 -0
- package/.next/standalone/node_modules/next/dist/lib/has-necessary-dependencies.js +49 -0
- package/.next/standalone/node_modules/next/dist/lib/helpers/get-cache-directory.js +66 -0
- package/.next/standalone/node_modules/next/dist/lib/helpers/get-npx-command.js +29 -0
- package/.next/standalone/node_modules/next/dist/lib/helpers/get-online.js +50 -0
- package/.next/standalone/node_modules/next/dist/lib/helpers/get-pkg-manager.js +65 -0
- package/.next/standalone/node_modules/next/dist/lib/helpers/get-registry.js +45 -0
- package/.next/standalone/node_modules/next/dist/lib/helpers/get-reserved-port.js +116 -0
- package/.next/standalone/node_modules/next/dist/lib/helpers/git.js +59 -0
- package/.next/standalone/node_modules/next/dist/lib/helpers/install.js +83 -0
- package/.next/standalone/node_modules/next/dist/lib/import-next-warning.js +50 -0
- package/.next/standalone/node_modules/next/dist/lib/inline-static-env.js +112 -0
- package/.next/standalone/node_modules/next/dist/lib/install-dependencies.js +40 -0
- package/.next/standalone/node_modules/next/dist/lib/interop-default.js +16 -0
- package/.next/standalone/node_modules/next/dist/lib/is-api-route.js +15 -0
- package/.next/standalone/node_modules/next/dist/lib/is-app-page-route.js +15 -0
- package/.next/standalone/node_modules/next/dist/lib/is-app-route-route.js +15 -0
- package/.next/standalone/node_modules/next/dist/lib/is-edge-runtime.js +16 -0
- package/.next/standalone/node_modules/next/dist/lib/is-error.js +78 -0
- package/.next/standalone/node_modules/next/dist/lib/is-interception-route-rewrite.js +18 -0
- package/.next/standalone/node_modules/next/dist/lib/is-internal-component.js +36 -0
- package/.next/standalone/node_modules/next/dist/lib/is-serializable-props.js +111 -0
- package/.next/standalone/node_modules/next/dist/lib/known-edge-safe-packages.json +1 -0
- package/.next/standalone/node_modules/next/dist/lib/load-custom-routes.js +594 -0
- package/.next/standalone/node_modules/next/dist/lib/memory/gc-observer.js +53 -0
- package/.next/standalone/node_modules/next/dist/lib/memory/shutdown.js +29 -0
- package/.next/standalone/node_modules/next/dist/lib/memory/startup.js +47 -0
- package/.next/standalone/node_modules/next/dist/lib/memory/trace.js +109 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/constants.js +40 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/default-metadata.js +82 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/generate/icon-mark.js +22 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/generate/utils.js +52 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/get-metadata-route.js +162 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/is-metadata-route.js +221 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/metadata-context.js +18 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/metadata.js +1660 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/resolve-metadata.js +884 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/resolvers/resolve-basics.js +232 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/resolvers/resolve-icons.js +56 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/resolvers/resolve-opengraph.js +199 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/resolvers/resolve-title.js +40 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/resolvers/resolve-url.js +135 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/types/alternative-urls-types.js +7 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/types/extra-types.js +9 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/types/icons.js +6 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/types/manifest-types.js +6 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/types/metadata-interface.js +18 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/types/metadata-types.js +10 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/types/opengraph-types.js +6 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/types/resolvers.js +6 -0
- package/.next/standalone/node_modules/next/dist/lib/metadata/types/twitter-types.js +7 -0
- package/.next/standalone/node_modules/next/dist/lib/mime-type.js +20 -0
- package/.next/standalone/node_modules/next/dist/lib/mkcert.js +204 -0
- package/.next/standalone/node_modules/next/dist/lib/multi-file-writer.js +75 -0
- package/.next/standalone/node_modules/next/dist/lib/needs-experimental-react.js +16 -0
- package/.next/standalone/node_modules/next/dist/lib/non-nullable.js +15 -0
- package/.next/standalone/node_modules/next/dist/lib/normalize-path.js +21 -0
- package/.next/standalone/node_modules/next/dist/lib/oxford-comma-list.js +15 -0
- package/.next/standalone/node_modules/next/dist/lib/page-types.js +18 -0
- package/.next/standalone/node_modules/next/dist/lib/patch-incorrect-lockfile.js +185 -0
- package/.next/standalone/node_modules/next/dist/lib/pick.js +19 -0
- package/.next/standalone/node_modules/next/dist/lib/picocolors.js +177 -0
- package/.next/standalone/node_modules/next/dist/lib/pretty-bytes.js +74 -0
- package/.next/standalone/node_modules/next/dist/lib/profiles-dir.js +45 -0
- package/.next/standalone/node_modules/next/dist/lib/realpath.js +20 -0
- package/.next/standalone/node_modules/next/dist/lib/recursive-copy.js +76 -0
- package/.next/standalone/node_modules/next/dist/lib/recursive-delete.js +137 -0
- package/.next/standalone/node_modules/next/dist/lib/recursive-readdir.js +124 -0
- package/.next/standalone/node_modules/next/dist/lib/redirect-status.js +46 -0
- package/.next/standalone/node_modules/next/dist/lib/require-instrumentation-client.js +32 -0
- package/.next/standalone/node_modules/next/dist/lib/resolve-build-paths.js +182 -0
- package/.next/standalone/node_modules/next/dist/lib/resolve-from.js +65 -0
- package/.next/standalone/node_modules/next/dist/lib/route-pattern-normalizer.js +107 -0
- package/.next/standalone/node_modules/next/dist/lib/scheduler.js +64 -0
- package/.next/standalone/node_modules/next/dist/lib/semver-noop.js +18 -0
- package/.next/standalone/node_modules/next/dist/lib/server-external-packages.jsonc +103 -0
- package/.next/standalone/node_modules/next/dist/lib/setup-exception-listeners.js +11 -0
- package/.next/standalone/node_modules/next/dist/lib/static-env.js +92 -0
- package/.next/standalone/node_modules/next/dist/lib/try-to-parse-path.js +67 -0
- package/.next/standalone/node_modules/next/dist/lib/turbopack-cache-seed.js +171 -0
- package/.next/standalone/node_modules/next/dist/lib/turbopack-warning.js +185 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/diagnosticFormatter.js +243 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/getTypeScriptConfiguration.js +73 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/getTypeScriptIntent.js +52 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/loadTsConfig.js +181 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/missingDependencyError.js +26 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeCheck.js +149 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeCheckCli.js +53 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/runTypeScriptCli.js +256 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/type-paths.js +51 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/writeAppTypeDeclarations.js +77 -0
- package/.next/standalone/node_modules/next/dist/lib/typescript/writeConfigurationDefaults.js +395 -0
- package/.next/standalone/node_modules/next/dist/lib/url.js +75 -0
- package/.next/standalone/node_modules/next/dist/lib/verify-partytown-setup.js +104 -0
- package/.next/standalone/node_modules/next/dist/lib/verify-root-layout.js +165 -0
- package/.next/standalone/node_modules/next/dist/lib/verify-typescript-setup.js +271 -0
- package/.next/standalone/node_modules/next/dist/lib/wait.js +19 -0
- package/.next/standalone/node_modules/next/dist/lib/with-promise-cache.js +24 -0
- package/.next/standalone/node_modules/next/dist/lib/worker.js +234 -0
- package/.next/standalone/node_modules/next/dist/server/ReactDOMServerPages.js +16 -0
- package/.next/standalone/node_modules/next/dist/server/accept-header.js +132 -0
- package/.next/standalone/node_modules/next/dist/server/after/after-context.js +178 -0
- package/.next/standalone/node_modules/next/dist/server/after/after.js +28 -0
- package/.next/standalone/node_modules/next/dist/server/after/awaiter.js +74 -0
- package/.next/standalone/node_modules/next/dist/server/after/builtin-request-context.js +38 -0
- package/.next/standalone/node_modules/next/dist/server/after/index.js +21 -0
- package/.next/standalone/node_modules/next/dist/server/after/run-with-after.js +35 -0
- package/.next/standalone/node_modules/next/dist/server/api-utils/get-cookie-parser.js +22 -0
- package/.next/standalone/node_modules/next/dist/server/api-utils/index.js +217 -0
- package/.next/standalone/node_modules/next/dist/server/api-utils/node/api-resolver.js +395 -0
- package/.next/standalone/node_modules/next/dist/server/api-utils/node/parse-body.js +79 -0
- package/.next/standalone/node_modules/next/dist/server/api-utils/node/try-get-preview-data.js +89 -0
- package/.next/standalone/node_modules/next/dist/server/api-utils/web.js +17 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/action-async-storage-instance.js +14 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/action-async-storage.external.js +13 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/action-handler.js +1125 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/after-task-async-storage-instance.js +14 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/after-task-async-storage.external.js +13 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/app-render-prerender-utils.js +332 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/app-render-render-utils.js +76 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/app-render-scheduling.js +188 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/app-render.js +6496 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/async-local-storage.js +76 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/blocking-route-messages.js +181 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/cache-signal.js +181 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/collect-segment-data.js +937 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/console-async-storage-instance.js +14 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/console-async-storage.external.js +13 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/create-component-styles-and-scripts.js +39 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/create-component-tree.js +806 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/create-error-handler.js +190 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/create-flight-router-state-from-loader-tree.js +154 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/csrf-protection.js +86 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/debug-channel-server.js +47 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/debug-channel-server.node.js +60 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/debug-channel-server.web.js +60 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/dev-validation-error-delivery.js +37 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/dev-validation-events.js +26 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/dev-validation-scheduler.js +87 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/dev-validation-worker-globals.js +41 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/dev-validation-worker-snapshot.js +79 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/dynamic-access-async-storage-instance.js +14 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/dynamic-access-async-storage.external.js +13 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/dynamic-rendering.js +1201 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/encryption-utils-server.js +114 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/encryption-utils.js +93 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/encryption.js +261 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/entry-base.js +265 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/flight-render-result.js +28 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/get-asset-query-string.js +29 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/get-css-inlined-link-tags.js +45 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/get-layer-assets.js +69 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/get-preloadable-fonts.js +39 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/get-script-nonce-from-header.js +32 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/get-short-dynamic-param-type.js +25 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/has-loading-component-in-tree.js +19 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/instant-test-bootstrap.js +20 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/boundary-constants.js +31 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/boundary-impl.js +127 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/boundary-tracking.js +38 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/instant-config.js +241 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/instant-samples.js +432 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/instant-validation-error.js +33 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/instant-validation.js +934 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/instant-validation/stream-utils.js +96 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/interop-default.js +17 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/make-get-server-inserted-html.js +91 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/manifests-singleton.js +327 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/metadata-insertion/create-server-inserted-metadata.js +29 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/module-loading/instrument-module-getter.js +32 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/module-loading/track-dynamic-import.js +57 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/module-loading/track-module-loading.external.js +32 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/module-loading/track-module-loading.instance.js +62 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/parse-and-validate-flight-router-state.js +49 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/postponed-state.js +230 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/prospective-render-utils.js +74 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/react-large-shell-error.js +17 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/react-server.node.js +39 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/render-css-resource.js +57 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/required-scripts.js +76 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/rsc/postpone.js +18 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/rsc/preloads.js +74 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/rsc/taint.js +79 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/segment-explorer-path.js +128 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/server-inserted-html.js +77 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/staged-rendering.js +324 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/stale-time.js +101 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/stream-ops.js +155 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/stream-ops.node.js +842 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/stream-ops.web.js +249 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/strip-flight-headers.js +18 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/sync-io-messages.js +67 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/types.js +65 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/use-flight-response.js +217 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/vary-params.js +261 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/wait-for-response.js +36 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/walk-tree-with-flight-router-state.js +237 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/work-async-storage-instance.js +14 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/work-async-storage.external.js +13 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.js +14 -0
- package/.next/standalone/node_modules/next/dist/server/app-render/work-unit-async-storage.external.js +250 -0
- package/.next/standalone/node_modules/next/dist/server/async-storage/draft-mode-provider.js +62 -0
- package/.next/standalone/node_modules/next/dist/server/async-storage/request-store.js +201 -0
- package/.next/standalone/node_modules/next/dist/server/async-storage/with-store.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/async-storage/work-store.js +98 -0
- package/.next/standalone/node_modules/next/dist/server/base-http/helpers.js +36 -0
- package/.next/standalone/node_modules/next/dist/server/base-http/index.js +54 -0
- package/.next/standalone/node_modules/next/dist/server/base-http/node.js +147 -0
- package/.next/standalone/node_modules/next/dist/server/base-http/web.js +124 -0
- package/.next/standalone/node_modules/next/dist/server/base-server.js +1957 -0
- package/.next/standalone/node_modules/next/dist/server/body-streams.js +122 -0
- package/.next/standalone/node_modules/next/dist/server/cache-dir.js +26 -0
- package/.next/standalone/node_modules/next/dist/server/capsize-font-metrics.json +181516 -0
- package/.next/standalone/node_modules/next/dist/server/ci-info.js +41 -0
- package/.next/standalone/node_modules/next/dist/server/client-component-renderer-logger.js +70 -0
- package/.next/standalone/node_modules/next/dist/server/config-schema.js +800 -0
- package/.next/standalone/node_modules/next/dist/server/config-shared.js +395 -0
- package/.next/standalone/node_modules/next/dist/server/config-utils.js +185 -0
- package/.next/standalone/node_modules/next/dist/server/config.js +1836 -0
- package/.next/standalone/node_modules/next/dist/server/create-deduped-by-callsite-server-error-logger.js +96 -0
- package/.next/standalone/node_modules/next/dist/server/crypto-utils.js +70 -0
- package/.next/standalone/node_modules/next/dist/server/dev/browser-logs/file-logger.js +186 -0
- package/.next/standalone/node_modules/next/dist/server/dev/browser-logs/receive-logs.js +610 -0
- package/.next/standalone/node_modules/next/dist/server/dev/browser-logs/source-map.js +222 -0
- package/.next/standalone/node_modules/next/dist/server/dev/debug-channel.js +112 -0
- package/.next/standalone/node_modules/next/dist/server/dev/dev-indicator-server-state.js +15 -0
- package/.next/standalone/node_modules/next/dist/server/dev/dev-validation-worker-pool.js +232 -0
- package/.next/standalone/node_modules/next/dist/server/dev/dev-validation-worker.js +308 -0
- package/.next/standalone/node_modules/next/dist/server/dev/get-source-map-from-file.js +100 -0
- package/.next/standalone/node_modules/next/dist/server/dev/hot-middleware.js +276 -0
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-rspack.js +171 -0
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-shared-utils.js +56 -0
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-turbopack.js +1756 -0
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-types.js +60 -0
- package/.next/standalone/node_modules/next/dist/server/dev/hot-reloader-webpack.js +1380 -0
- package/.next/standalone/node_modules/next/dist/server/dev/log-requests.js +152 -0
- package/.next/standalone/node_modules/next/dist/server/dev/messages.js +74 -0
- package/.next/standalone/node_modules/next/dist/server/dev/middleware-turbopack.js +422 -0
- package/.next/standalone/node_modules/next/dist/server/dev/middleware-webpack.js +495 -0
- package/.next/standalone/node_modules/next/dist/server/dev/node-stack-frames.js +81 -0
- package/.next/standalone/node_modules/next/dist/server/dev/on-demand-entry-handler.js +773 -0
- package/.next/standalone/node_modules/next/dist/server/dev/parse-version-info.js +108 -0
- package/.next/standalone/node_modules/next/dist/server/dev/require-cache.js +109 -0
- package/.next/standalone/node_modules/next/dist/server/dev/serialized-errors.js +67 -0
- package/.next/standalone/node_modules/next/dist/server/dev/server-action-logger.js +37 -0
- package/.next/standalone/node_modules/next/dist/server/dev/static-paths-worker.js +106 -0
- package/.next/standalone/node_modules/next/dist/server/dev/turbopack-utils.js +727 -0
- package/.next/standalone/node_modules/next/dist/server/dev/use-cache-probe-pool.js +177 -0
- package/.next/standalone/node_modules/next/dist/server/dev/use-cache-probe-worker.js +180 -0
- package/.next/standalone/node_modules/next/dist/server/dynamic-rendering-utils.js +285 -0
- package/.next/standalone/node_modules/next/dist/server/font-utils.js +43 -0
- package/.next/standalone/node_modules/next/dist/server/get-app-route-from-entrypoint.js +30 -0
- package/.next/standalone/node_modules/next/dist/server/get-page-files.js +40 -0
- package/.next/standalone/node_modules/next/dist/server/get-route-from-entrypoint.js +35 -0
- package/.next/standalone/node_modules/next/dist/server/image-optimizer.js +1262 -0
- package/.next/standalone/node_modules/next/dist/server/instrumentation/types.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/instrumentation/utils.js +21 -0
- package/.next/standalone/node_modules/next/dist/server/internal-utils.js +39 -0
- package/.next/standalone/node_modules/next/dist/server/is-private-ip.js +36 -0
- package/.next/standalone/node_modules/next/dist/server/lib/app-dir-module.js +59 -0
- package/.next/standalone/node_modules/next/dist/server/lib/app-info-log.js +135 -0
- package/.next/standalone/node_modules/next/dist/server/lib/async-callback-set.js +29 -0
- package/.next/standalone/node_modules/next/dist/server/lib/cache-control.js +22 -0
- package/.next/standalone/node_modules/next/dist/server/lib/cache-handlers/default.external.js +18 -0
- package/.next/standalone/node_modules/next/dist/server/lib/cache-handlers/default.js +196 -0
- package/.next/standalone/node_modules/next/dist/server/lib/cache-handlers/types.js +8 -0
- package/.next/standalone/node_modules/next/dist/server/lib/chrome-devtools-workspace.js +116 -0
- package/.next/standalone/node_modules/next/dist/server/lib/clone-response.js +77 -0
- package/.next/standalone/node_modules/next/dist/server/lib/cpu-profile.js +56 -0
- package/.next/standalone/node_modules/next/dist/server/lib/decode-query-path-parameter.js +26 -0
- package/.next/standalone/node_modules/next/dist/server/lib/dedupe-fetch.js +163 -0
- package/.next/standalone/node_modules/next/dist/server/lib/dev-bundler-service.js +110 -0
- package/.next/standalone/node_modules/next/dist/server/lib/disk-lru-cache.external.js +63 -0
- package/.next/standalone/node_modules/next/dist/server/lib/encode-header-safe.js +44 -0
- package/.next/standalone/node_modules/next/dist/server/lib/etag.js +56 -0
- package/.next/standalone/node_modules/next/dist/server/lib/experimental/create-env-definitions.js +48 -0
- package/.next/standalone/node_modules/next/dist/server/lib/experimental/ppr.js +47 -0
- package/.next/standalone/node_modules/next/dist/server/lib/find-page-file.js +147 -0
- package/.next/standalone/node_modules/next/dist/server/lib/fix-mojibake.js +25 -0
- package/.next/standalone/node_modules/next/dist/server/lib/format-hostname.js +16 -0
- package/.next/standalone/node_modules/next/dist/server/lib/generate-agent-files.js +180 -0
- package/.next/standalone/node_modules/next/dist/server/lib/i18n-provider.js +129 -0
- package/.next/standalone/node_modules/next/dist/server/lib/implicit-tags.js +87 -0
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/file-system-cache.js +348 -0
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/index.js +547 -0
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/memory-cache.external.js +59 -0
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/shared-cache-controls.external.js +73 -0
- package/.next/standalone/node_modules/next/dist/server/lib/incremental-cache/tags-manifest.external.js +55 -0
- package/.next/standalone/node_modules/next/dist/server/lib/install-code-frame.js +22 -0
- package/.next/standalone/node_modules/next/dist/server/lib/is-ipv6.js +41 -0
- package/.next/standalone/node_modules/next/dist/server/lib/is-non-html-sec-fetch-dest.js +38 -0
- package/.next/standalone/node_modules/next/dist/server/lib/is-rsc-request.js +18 -0
- package/.next/standalone/node_modules/next/dist/server/lib/lazy-module.js +80 -0
- package/.next/standalone/node_modules/next/dist/server/lib/lazy-result.js +46 -0
- package/.next/standalone/node_modules/next/dist/server/lib/lru-cache.js +191 -0
- package/.next/standalone/node_modules/next/dist/server/lib/match-next-data-pathname.js +18 -0
- package/.next/standalone/node_modules/next/dist/server/lib/mock-request.js +441 -0
- package/.next/standalone/node_modules/next/dist/server/lib/module-loader/module-loader.js +8 -0
- package/.next/standalone/node_modules/next/dist/server/lib/module-loader/node-module-loader.js +25 -0
- package/.next/standalone/node_modules/next/dist/server/lib/module-loader/route-module-loader.js +26 -0
- package/.next/standalone/node_modules/next/dist/server/lib/node-fs-methods.js +28 -0
- package/.next/standalone/node_modules/next/dist/server/lib/params-utils.js +70 -0
- package/.next/standalone/node_modules/next/dist/server/lib/parse-stack.js +46 -0
- package/.next/standalone/node_modules/next/dist/server/lib/patch-fetch.js +1027 -0
- package/.next/standalone/node_modules/next/dist/server/lib/patch-set-header.js +45 -0
- package/.next/standalone/node_modules/next/dist/server/lib/postponed-request-body.js +64 -0
- package/.next/standalone/node_modules/next/dist/server/lib/release-compression-stream.js +24 -0
- package/.next/standalone/node_modules/next/dist/server/lib/render-server.js +157 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-server.js +756 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/block-cross-site-dev.js +110 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/build-data-route.js +48 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/build-prefetch-segment-data-route.js +50 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/cache-life-type-utils.js +212 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/decode-path-params.js +42 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/filesystem.js +612 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/instrumentation-globals.external.js +89 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/instrumentation-node-extensions.js +114 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/is-postpone.js +16 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/proxy-request.js +128 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/resolve-routes.js +646 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/root-params-type-utils.js +61 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/route-types-utils.js +326 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/router-server-context.js +26 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/typegen.js +892 -0
- package/.next/standalone/node_modules/next/dist/server/lib/router-utils/types.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/lib/server-action-request-meta.js +53 -0
- package/.next/standalone/node_modules/next/dist/server/lib/server-ipc/utils.js +83 -0
- package/.next/standalone/node_modules/next/dist/server/lib/source-maps.js +241 -0
- package/.next/standalone/node_modules/next/dist/server/lib/start-server.js +540 -0
- package/.next/standalone/node_modules/next/dist/server/lib/streaming-metadata.js +27 -0
- package/.next/standalone/node_modules/next/dist/server/lib/to-route.js +37 -0
- package/.next/standalone/node_modules/next/dist/server/lib/trace/constants.js +208 -0
- package/.next/standalone/node_modules/next/dist/server/lib/trace/local-span-recorder.js +385 -0
- package/.next/standalone/node_modules/next/dist/server/lib/trace/request-insights-identity.js +39 -0
- package/.next/standalone/node_modules/next/dist/server/lib/trace/request-insights.js +308 -0
- package/.next/standalone/node_modules/next/dist/server/lib/trace/span-store.js +69 -0
- package/.next/standalone/node_modules/next/dist/server/lib/trace/tracer.js +392 -0
- package/.next/standalone/node_modules/next/dist/server/lib/trace/utils.js +16 -0
- package/.next/standalone/node_modules/next/dist/server/lib/types.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/lib/utils.js +292 -0
- package/.next/standalone/node_modules/next/dist/server/lib/worker-utils.js +36 -0
- package/.next/standalone/node_modules/next/dist/server/load-components.js +185 -0
- package/.next/standalone/node_modules/next/dist/server/load-default-error-components.js +41 -0
- package/.next/standalone/node_modules/next/dist/server/load-manifest.external.js +117 -0
- package/.next/standalone/node_modules/next/dist/server/match-bundle.js +25 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/get-mcp-middleware.js +49 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/get-or-create-mcp-server.js +51 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/mcp-telemetry-tracker.js +82 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/compile-route.js +86 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-compilation-issues.js +68 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-errors.js +114 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-logs.js +67 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-page-metadata.js +188 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-project-metadata.js +59 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-request-insights.js +57 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-routes.js +136 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/get-server-action-by-id.js +121 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/next-instance-error-state.js +31 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/utils/browser-communication.js +90 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/utils/format-compilation-issues.js +72 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/utils/format-errors.js +137 -0
- package/.next/standalone/node_modules/next/dist/server/mcp/tools/utils/resolve-path-to-route.js +49 -0
- package/.next/standalone/node_modules/next/dist/server/next-server.js +1443 -0
- package/.next/standalone/node_modules/next/dist/server/next-typescript.js +13 -0
- package/.next/standalone/node_modules/next/dist/server/next.js +485 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-baseline.js +25 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/console-dim.external.js +301 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/console-exit.js +49 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/console-file.js +56 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/date.js +52 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/error-inspect.js +8 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/fast-set-immediate.external.js +577 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/global-behaviors.js +33 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/io-utils.js +102 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/node-crypto.js +103 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/process-error-handlers.js +170 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/random.js +28 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/unhandled-rejection.external.js +518 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment-extensions/web-crypto.js +45 -0
- package/.next/standalone/node_modules/next/dist/server/node-environment.js +19 -0
- package/.next/standalone/node_modules/next/dist/server/node-polyfill-crypto.js +21 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/absolute-filename-normalizer.js +33 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/built/app/app-bundle-path-normalizer.js +54 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/built/app/app-filename-normalizer.js +22 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/built/app/app-page-normalizer.js +40 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/built/app/app-pathname-normalizer.js +55 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/built/app/index.js +42 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/built/pages/index.js +44 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/built/pages/pages-bundle-path-normalizer.js +55 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/built/pages/pages-filename-normalizer.js +22 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/built/pages/pages-page-normalizer.js +19 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/built/pages/pages-pathname-normalizer.js +19 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/locale-route-normalizer.js +26 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/normalizer.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/normalizers.js +23 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/prefixing-normalizer.js +26 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/request/base-path.js +25 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/request/next-data.js +38 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/request/pathname-normalizer.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/request/prefix.js +39 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/request/rsc.js +19 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/request/segment-prefix-rsc.js +33 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/request/suffix.js +27 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/underscore-normalizer.js +17 -0
- package/.next/standalone/node_modules/next/dist/server/normalizers/wrap-normalizer-fn.js +17 -0
- package/.next/standalone/node_modules/next/dist/server/og/cache-image-response.js +341 -0
- package/.next/standalone/node_modules/next/dist/server/og/image-response.js +72 -0
- package/.next/standalone/node_modules/next/dist/server/patch-error-inspect.js +507 -0
- package/.next/standalone/node_modules/next/dist/server/pipe-readable.js +212 -0
- package/.next/standalone/node_modules/next/dist/server/post-process.js +39 -0
- package/.next/standalone/node_modules/next/dist/server/render-result.js +246 -0
- package/.next/standalone/node_modules/next/dist/server/render.js +1150 -0
- package/.next/standalone/node_modules/next/dist/server/request/connection.js +135 -0
- package/.next/standalone/node_modules/next/dist/server/request/cookies.js +239 -0
- package/.next/standalone/node_modules/next/dist/server/request/draft-mode.js +238 -0
- package/.next/standalone/node_modules/next/dist/server/request/fallback-params.js +95 -0
- package/.next/standalone/node_modules/next/dist/server/request/headers.js +235 -0
- package/.next/standalone/node_modules/next/dist/server/request/io.js +96 -0
- package/.next/standalone/node_modules/next/dist/server/request/params.js +634 -0
- package/.next/standalone/node_modules/next/dist/server/request/pathname.js +141 -0
- package/.next/standalone/node_modules/next/dist/server/request/root-params.js +202 -0
- package/.next/standalone/node_modules/next/dist/server/request/search-params.js +582 -0
- package/.next/standalone/node_modules/next/dist/server/request/utils.js +84 -0
- package/.next/standalone/node_modules/next/dist/server/request-meta.js +55 -0
- package/.next/standalone/node_modules/next/dist/server/require-hook.js +82 -0
- package/.next/standalone/node_modules/next/dist/server/require.js +109 -0
- package/.next/standalone/node_modules/next/dist/server/response-cache/index.js +323 -0
- package/.next/standalone/node_modules/next/dist/server/response-cache/types.js +41 -0
- package/.next/standalone/node_modules/next/dist/server/response-cache/utils.js +104 -0
- package/.next/standalone/node_modules/next/dist/server/response-cache/web.js +101 -0
- package/.next/standalone/node_modules/next/dist/server/resume-data-cache/cache-store.js +101 -0
- package/.next/standalone/node_modules/next/dist/server/resume-data-cache/resume-data-cache.js +142 -0
- package/.next/standalone/node_modules/next/dist/server/revalidation-utils.js +156 -0
- package/.next/standalone/node_modules/next/dist/server/route-definitions/app-page-route-definition.js +16 -0
- package/.next/standalone/node_modules/next/dist/server/route-definitions/app-route-route-definition.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-definitions/locale-route-definition.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-definitions/pages-api-route-definition.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-definitions/pages-route-definition.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-definitions/route-definition.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-kind.js +32 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-managers/default-route-matcher-manager.js +239 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-managers/dev-route-matcher-manager.js +120 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/app-page-route-matcher-provider.js +55 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/app-route-route-matcher-provider.js +43 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/dev-app-page-route-matcher-provider.js +91 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/dev-app-route-route-matcher-provider.js +112 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/dev-pages-api-route-matcher-provider.js +77 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/dev-pages-route-matcher-provider.js +77 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/file-cache-route-matcher-provider.js +28 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/helpers/file-reader/batched-file-reader.js +106 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/helpers/file-reader/default-file-reader.js +42 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/dev/helpers/file-reader/file-reader.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/helpers/cached-route-matcher-provider.js +30 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/helpers/manifest-loaders/manifest-loader.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/helpers/manifest-loaders/node-manifest-loader.js +34 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/helpers/manifest-loaders/server-manifest-loader.js +20 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/manifest-route-matcher-provider.js +21 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/pages-api-route-matcher-provider.js +55 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/pages-route-matcher-provider.js +64 -0
- package/.next/standalone/node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-matchers/app-page-route-matcher.js +18 -0
- package/.next/standalone/node_modules/next/dist/server/route-matchers/app-route-route-matcher.js +15 -0
- package/.next/standalone/node_modules/next/dist/server/route-matchers/locale-route-matcher.js +68 -0
- package/.next/standalone/node_modules/next/dist/server/route-matchers/pages-api-route-matcher.js +30 -0
- package/.next/standalone/node_modules/next/dist/server/route-matchers/pages-route-matcher.js +30 -0
- package/.next/standalone/node_modules/next/dist/server/route-matchers/route-matcher.js +54 -0
- package/.next/standalone/node_modules/next/dist/server/route-matches/app-page-route-match.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-matches/app-route-route-match.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-matches/locale-route-match.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-matches/pages-api-route-match.js +16 -0
- package/.next/standalone/node_modules/next/dist/server/route-matches/pages-route-match.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-matches/route-match.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/helpers/prerender-manifest-matcher.js +51 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/module.compiled.js +36 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/module.js +176 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/module.render.js +24 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/normalize-request-url.js +62 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/app-router-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.js +87 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/head-manager-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/hooks-client-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/image-config-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/router-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/server-inserted-html.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/entrypoints.js +140 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-compiler-runtime.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-dom.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-runtime.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-server-dom-turbopack-server.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-server-dom-turbopack-static.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-server-dom-webpack-server.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react-server-dom-webpack-static.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/rsc/react.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/entrypoints.js +125 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-compiler-runtime.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-dom-server.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-dom.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-jsx-dev-runtime.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-jsx-runtime.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-server-dom-turbopack-client.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react-server-dom-webpack-client.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-page/vendored/ssr/react.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/helpers/auto-implement-methods.js +85 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/helpers/clean-url.js +24 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/helpers/get-pathname-from-absolute-path.js +29 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/helpers/is-static-gen-enabled.js +15 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/helpers/parsed-url-query-to-params.js +20 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/module.compiled.js +36 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/module.js +948 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/app-route/shared-modules.js +56 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/checks.js +45 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/builtin/_error.js +100 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/module.compiled.js +20 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/module.js +92 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/module.render.js +24 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/pages-handler.js +567 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/app-router-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.js +102 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/head-manager-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/hooks-client-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/html-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/image-config-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/loadable-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/loadable.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/router-context.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages/vendored/contexts/server-inserted-html.js +4 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages-api/module.compiled.js +20 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/pages-api/module.js +58 -0
- package/.next/standalone/node_modules/next/dist/server/route-modules/route-module.js +697 -0
- package/.next/standalone/node_modules/next/dist/server/runtime-reacts.external.js +46 -0
- package/.next/standalone/node_modules/next/dist/server/send-payload.js +88 -0
- package/.next/standalone/node_modules/next/dist/server/send-response.js +70 -0
- package/.next/standalone/node_modules/next/dist/server/serve-static.js +66 -0
- package/.next/standalone/node_modules/next/dist/server/server-route-utils.js +30 -0
- package/.next/standalone/node_modules/next/dist/server/server-utils.js +340 -0
- package/.next/standalone/node_modules/next/dist/server/setup-http-agent-env.js +31 -0
- package/.next/standalone/node_modules/next/dist/server/stream-utils/encoded-tags.js +122 -0
- package/.next/standalone/node_modules/next/dist/server/stream-utils/node-buffered-transform-stream.js +51 -0
- package/.next/standalone/node_modules/next/dist/server/stream-utils/node-web-streams-helper.js +939 -0
- package/.next/standalone/node_modules/next/dist/server/stream-utils/uint8array-helpers.js +74 -0
- package/.next/standalone/node_modules/next/dist/server/typescript/constant.js +98 -0
- package/.next/standalone/node_modules/next/dist/server/typescript/index.js +199 -0
- package/.next/standalone/node_modules/next/dist/server/typescript/rules/client-boundary.js +100 -0
- package/.next/standalone/node_modules/next/dist/server/typescript/rules/config.js +534 -0
- package/.next/standalone/node_modules/next/dist/server/typescript/rules/entry.js +149 -0
- package/.next/standalone/node_modules/next/dist/server/typescript/rules/error.js +38 -0
- package/.next/standalone/node_modules/next/dist/server/typescript/rules/metadata.js +234 -0
- package/.next/standalone/node_modules/next/dist/server/typescript/rules/server-boundary.js +115 -0
- package/.next/standalone/node_modules/next/dist/server/typescript/rules/server.js +79 -0
- package/.next/standalone/node_modules/next/dist/server/typescript/utils.js +212 -0
- package/.next/standalone/node_modules/next/dist/server/use-cache/cache-life-profile.js +84 -0
- package/.next/standalone/node_modules/next/dist/server/use-cache/cache-life.js +102 -0
- package/.next/standalone/node_modules/next/dist/server/use-cache/cache-tag.js +52 -0
- package/.next/standalone/node_modules/next/dist/server/use-cache/clone-cache-entry.js +28 -0
- package/.next/standalone/node_modules/next/dist/server/use-cache/constants.js +44 -0
- package/.next/standalone/node_modules/next/dist/server/use-cache/handlers.js +274 -0
- package/.next/standalone/node_modules/next/dist/server/use-cache/tiered-cache-handler.js +145 -0
- package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-errors.js +59 -0
- package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-probe-globals.js +39 -0
- package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-probe-scheduler.js +117 -0
- package/.next/standalone/node_modules/next/dist/server/use-cache/use-cache-wrapper.js +2610 -0
- package/.next/standalone/node_modules/next/dist/server/utils.js +73 -0
- package/.next/standalone/node_modules/next/dist/server/web/adapter.js +428 -0
- package/.next/standalone/node_modules/next/dist/server/web/edge-route-module-wrapper.js +136 -0
- package/.next/standalone/node_modules/next/dist/server/web/error.js +69 -0
- package/.next/standalone/node_modules/next/dist/server/web/exports/index.js +56 -0
- package/.next/standalone/node_modules/next/dist/server/web/get-edge-preview-props.js +23 -0
- package/.next/standalone/node_modules/next/dist/server/web/globals.js +128 -0
- package/.next/standalone/node_modules/next/dist/server/web/http.js +39 -0
- package/.next/standalone/node_modules/next/dist/server/web/internal-edge-wait-until.js +64 -0
- package/.next/standalone/node_modules/next/dist/server/web/next-url.js +199 -0
- package/.next/standalone/node_modules/next/dist/server/web/sandbox/context.js +468 -0
- package/.next/standalone/node_modules/next/dist/server/web/sandbox/fetch-inline-assets.js +35 -0
- package/.next/standalone/node_modules/next/dist/server/web/sandbox/index.js +28 -0
- package/.next/standalone/node_modules/next/dist/server/web/sandbox/resource-managers.js +123 -0
- package/.next/standalone/node_modules/next/dist/server/web/sandbox/sandbox.js +137 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/adapters/headers.js +293 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/adapters/next-request.js +142 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/adapters/reflect.js +30 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.js +226 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/cookies.js +29 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/fetch-event.js +98 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/image-response.js +22 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/request.js +99 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/response.js +136 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/revalidate.js +226 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/unstable-cache.js +307 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/unstable-no-store.js +51 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/url-pattern.js +13 -0
- package/.next/standalone/node_modules/next/dist/server/web/spec-extension/user-agent.js +46 -0
- package/.next/standalone/node_modules/next/dist/server/web/types.js +6 -0
- package/.next/standalone/node_modules/next/dist/server/web/utils.js +151 -0
- package/.next/standalone/node_modules/next/dist/server/web/web-on-close.js +87 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/action-revalidation-kind.js +31 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/app-router-context.shared-runtime.js +50 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/app-router-types.js +159 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/constants.js +453 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/deep-freeze.js +30 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/deployment-id.js +63 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/dset.js +34 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/encode-uri-path.js +15 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/entry-constants.js +36 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/error-source.js +36 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/errors/code-frame.js +20 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/errors/constants.js +19 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/errors/empty-generate-static-params-error.js +31 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/errors/hard-deprecated-config-error.js +25 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/escape-regexp.js +22 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/get-hostname.js +23 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/hash.js +42 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.js +18 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/head.js +164 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.js +60 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/html-context.shared-runtime.js +40 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/htmlescape.js +52 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/i18n/detect-domain-locale.js +25 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/i18n/get-locale-redirect.js +59 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/i18n/normalize-locale-path.js +54 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/image-blur-svg.js +22 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/image-config-context.shared-runtime.js +19 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/image-config.js +77 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/instant-messages.js +43 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/invariant-error.js +23 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/is-plain-object.js +42 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/is-thenable.js +20 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/isomorphic/path.js +14 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/lazy-dynamic/bailout-to-csr.js +37 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/loadable-context.shared-runtime.js +19 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/loadable.shared-runtime.js +246 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/match-local-pattern.js +46 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/match-remote-pattern.js +63 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/modern-browserslist-target.js +15 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/no-fallback-error.external.js +18 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/normalized-asset-prefix.js +28 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/page-path/absolute-path-to-page.js +26 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/page-path/denormalize-page-path.js +18 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/page-path/ensure-leading-slash.js +18 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/page-path/get-page-paths.js +40 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/page-path/normalize-data-path.js +23 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/page-path/normalize-page-path.js +26 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/page-path/normalize-path-sep.js +19 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/page-path/remove-page-path-tail.js +20 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/promise-with-resolvers.js +26 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/request-insights.js +30 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/adapters.js +139 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/routes/app.js +158 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/add-locale.js +28 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/add-path-prefix.js +20 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/add-path-suffix.js +20 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/app-paths.js +62 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/as-path-to-search-params.js +17 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/cache-busting-search-param.js +70 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/escape-path-delimiters.js +16 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/format-next-pathname-info.js +27 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/format-url.js +111 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/get-dynamic-param.js +174 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/get-next-pathname-info.js +52 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/get-route-from-asset-path.js +25 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/get-segment-param.js +90 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/html-bots.js +17 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/index.js +30 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/interception-prefix-from-param-type.js +33 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/interception-routes.js +108 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/is-bot.js +57 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/is-dynamic.js +26 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.js +31 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/parse-loader-tree.js +30 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/parse-path.js +33 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/parse-relative-url.js +43 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/parse-url.js +38 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/path-has-prefix.js +20 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/path-match.js +49 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/prepare-destination.js +293 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/querystring.js +79 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/relativize-url.js +40 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/remove-path-prefix.js +39 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/remove-trailing-slash.js +22 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/resolve-param-value.js +116 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/route-match-utils.js +113 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/route-matcher.js +45 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/route-regex.js +256 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router/utils/sorted-routes.js +244 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/router-context.shared-runtime.js +18 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/segment-cache/segment-value-encoding.js +99 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/segment-cache/vary-params-decoding.js +66 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/segment.js +102 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.js +35 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/server-reference-info.js +68 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/side-effect.js +63 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/size-limit.js +40 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/turbopack/internal-error.js +53 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/utils/reflect-utils.js +69 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/utils/warn-once.js +22 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/utils.js +216 -0
- package/.next/standalone/node_modules/next/dist/shared/lib/zod.js +101 -0
- package/.next/standalone/node_modules/next/dist/telemetry/agent-name.js +20 -0
- package/.next/standalone/node_modules/next/dist/telemetry/anonymous-meta.js +91 -0
- package/.next/standalone/node_modules/next/dist/telemetry/detached-flush.js +56 -0
- package/.next/standalone/node_modules/next/dist/telemetry/events/build.js +187 -0
- package/.next/standalone/node_modules/next/dist/telemetry/events/index.js +41 -0
- package/.next/standalone/node_modules/next/dist/telemetry/events/plugins.js +51 -0
- package/.next/standalone/node_modules/next/dist/telemetry/events/swc-load-failure.js +55 -0
- package/.next/standalone/node_modules/next/dist/telemetry/events/version.js +68 -0
- package/.next/standalone/node_modules/next/dist/telemetry/flush-telemetry.js +19 -0
- package/.next/standalone/node_modules/next/dist/telemetry/post-telemetry-payload.js +48 -0
- package/.next/standalone/node_modules/next/dist/telemetry/project-id.js +48 -0
- package/.next/standalone/node_modules/next/dist/telemetry/storage.js +305 -0
- package/.next/standalone/node_modules/next/dist/trace/index.js +58 -0
- package/.next/standalone/node_modules/next/dist/trace/report/index.js +37 -0
- package/.next/standalone/node_modules/next/dist/trace/report/to-json-build.js +40 -0
- package/.next/standalone/node_modules/next/dist/trace/report/to-json.js +171 -0
- package/.next/standalone/node_modules/next/dist/trace/report/to-telemetry.js +36 -0
- package/.next/standalone/node_modules/next/dist/trace/shared.js +39 -0
- package/.next/standalone/node_modules/next/dist/trace/trace.js +211 -0
- package/.next/standalone/node_modules/next/package.json +139 -0
- package/.next/standalone/node_modules/react/cjs/react-compiler-runtime.production.js +16 -0
- package/.next/standalone/node_modules/react/cjs/react-jsx-dev-runtime.production.js +14 -0
- package/.next/standalone/node_modules/react/cjs/react-jsx-runtime.production.js +34 -0
- package/.next/standalone/node_modules/react/cjs/react.development.js +1284 -0
- package/.next/standalone/node_modules/react/cjs/react.production.js +542 -0
- package/.next/standalone/node_modules/react/compiler-runtime.js +14 -0
- package/.next/standalone/node_modules/react/index.js +7 -0
- package/.next/standalone/node_modules/react/jsx-dev-runtime.js +7 -0
- package/.next/standalone/node_modules/react/jsx-runtime.js +7 -0
- package/.next/standalone/node_modules/react/package.json +51 -0
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.browser.production.js +6603 -0
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server-legacy.node.production.js +6692 -0
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.browser.production.js +7410 -0
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.edge.production.js +7512 -0
- package/.next/standalone/node_modules/react-dom/cjs/react-dom-server.node.production.js +7707 -0
- package/.next/standalone/node_modules/react-dom/cjs/react-dom.production.js +210 -0
- package/.next/standalone/node_modules/react-dom/index.js +38 -0
- package/.next/standalone/node_modules/react-dom/package.json +117 -0
- package/.next/standalone/node_modules/react-dom/server.browser.js +16 -0
- package/.next/standalone/node_modules/react-dom/server.edge.js +17 -0
- package/.next/standalone/node_modules/react-dom/server.node.js +18 -0
- package/.next/standalone/node_modules/react-dom/static.node.js +14 -0
- package/.next/standalone/node_modules/semver/package.json +38 -0
- package/.next/standalone/node_modules/sharp/dist/channel.cjs +177 -0
- package/.next/standalone/node_modules/sharp/dist/channel.mjs +177 -0
- package/.next/standalone/node_modules/sharp/dist/colour.cjs +199 -0
- package/.next/standalone/node_modules/sharp/dist/colour.mjs +199 -0
- package/.next/standalone/node_modules/sharp/dist/composite.cjs +213 -0
- package/.next/standalone/node_modules/sharp/dist/composite.mjs +213 -0
- package/.next/standalone/node_modules/sharp/dist/constructor.cjs +511 -0
- package/.next/standalone/node_modules/sharp/dist/constructor.mjs +511 -0
- package/.next/standalone/node_modules/sharp/dist/index.cjs +25 -0
- package/.next/standalone/node_modules/sharp/dist/index.d.cts +1999 -0
- package/.next/standalone/node_modules/sharp/dist/index.d.mts +2046 -0
- package/.next/standalone/node_modules/sharp/dist/index.mjs +25 -0
- package/.next/standalone/node_modules/sharp/dist/input.cjs +819 -0
- package/.next/standalone/node_modules/sharp/dist/input.mjs +819 -0
- package/.next/standalone/node_modules/sharp/dist/is.cjs +143 -0
- package/.next/standalone/node_modules/sharp/dist/is.mjs +143 -0
- package/.next/standalone/node_modules/sharp/dist/libvips.cjs +212 -0
- package/.next/standalone/node_modules/sharp/dist/libvips.mjs +212 -0
- package/.next/standalone/node_modules/sharp/dist/operation.cjs +1002 -0
- package/.next/standalone/node_modules/sharp/dist/operation.mjs +1002 -0
- package/.next/standalone/node_modules/sharp/dist/output.cjs +1785 -0
- package/.next/standalone/node_modules/sharp/dist/output.mjs +1785 -0
- package/.next/standalone/node_modules/sharp/dist/resize.cjs +617 -0
- package/.next/standalone/node_modules/sharp/dist/resize.mjs +617 -0
- package/.next/standalone/node_modules/sharp/dist/sharp.cjs +174 -0
- package/.next/standalone/node_modules/sharp/dist/sharp.mjs +174 -0
- package/.next/standalone/node_modules/sharp/dist/utility.cjs +301 -0
- package/.next/standalone/node_modules/sharp/dist/utility.mjs +301 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/classes/comparator.js +143 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/classes/range.js +577 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/classes/semver.js +350 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/clean.js +8 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/cmp.js +54 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/coerce.js +62 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/compare-build.js +9 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/compare-loose.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/compare.js +7 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/diff.js +60 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/eq.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/gt.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/gte.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/inc.js +21 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/lt.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/lte.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/major.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/minor.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/neq.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/parse.js +18 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/patch.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/prerelease.js +8 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/rcompare.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/rsort.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/satisfies.js +12 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/sort.js +5 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/truncate.js +48 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/functions/valid.js +8 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/index.js +93 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/constants.js +37 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/debug.js +11 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/identifiers.js +29 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/lrucache.js +42 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/parse-options.js +17 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/internal/re.js +223 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/package.json +78 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/gtr.js +6 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/intersects.js +9 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/ltr.js +6 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/max-satisfying.js +27 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/min-satisfying.js +26 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/min-version.js +63 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/outside.js +82 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/simplify.js +49 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/subset.js +249 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/to-comparators.js +10 -0
- package/.next/standalone/node_modules/sharp/node_modules/semver/ranges/valid.js +13 -0
- package/.next/standalone/node_modules/sharp/package.json +227 -0
- package/.next/standalone/node_modules/styled-jsx/dist/index/index.js +499 -0
- package/.next/standalone/node_modules/styled-jsx/index.js +1 -0
- package/.next/standalone/node_modules/styled-jsx/package.json +132 -0
- package/.next/standalone/node_modules/styled-jsx/style.js +1 -0
- package/.next/standalone/package-lock.json +7160 -0
- package/.next/standalone/package.json +88 -0
- package/.next/standalone/public/demo-data.json +1 -0
- package/.next/standalone/scripts/audit-avatars.mjs +132 -0
- package/.next/standalone/scripts/build-app.mjs +536 -0
- package/.next/standalone/scripts/check-node.mjs +28 -0
- package/.next/standalone/scripts/check-stale-server.mjs +57 -0
- package/.next/standalone/scripts/gen-synthetic.mjs +213 -0
- package/.next/standalone/scripts/image_store.py +125 -0
- package/.next/standalone/scripts/make-dmg-background.mjs +77 -0
- package/.next/standalone/scripts/prepare-standalone.mjs +42 -0
- package/.next/standalone/scripts/requirements.txt +4 -0
- package/.next/standalone/scripts/sandbox.mjs +59 -0
- package/.next/standalone/scripts/score-connections.sql +80 -0
- package/.next/standalone/scripts/score_new_connections.sql +77 -0
- package/.next/standalone/scripts/scrape.py +3574 -0
- package/.next/standalone/scripts/screenshots.py +116 -0
- package/.next/standalone/scripts/update.mjs +84 -0
- package/.next/standalone/server.js +38 -0
- package/.next/static/N9wokeIiC9rUIGmzBhj6T/_buildManifest.js +11 -0
- package/.next/static/N9wokeIiC9rUIGmzBhj6T/_clientMiddlewareManifest.js +6 -0
- package/.next/static/N9wokeIiC9rUIGmzBhj6T/_ssgManifest.js +1 -0
- package/.next/static/chunks/08k1crid-05dt.js +1 -0
- package/.next/static/chunks/0bma92pht_c97.js +1 -0
- package/.next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- package/.next/static/chunks/19mx3mg6lkumu.js +1 -0
- package/.next/static/chunks/1odow92u6wxz0.js +1 -0
- package/.next/static/chunks/1wb_is4wsdm-u.js +1 -0
- package/.next/static/chunks/1zmpcmngg5c7-.js +2 -0
- package/.next/static/chunks/2-rtuqsgzmno4.js +22 -0
- package/.next/static/chunks/220-ne82aus0c.js +44 -0
- package/.next/static/chunks/2rrf5kzx2fhin.js +2 -0
- package/.next/static/chunks/2s_k9x4d0gi9h.js +16 -0
- package/.next/static/chunks/310vm2bl3xxpt.js +31 -0
- package/.next/static/chunks/34at5d2ifl62i.js +33 -0
- package/.next/static/chunks/3fntmmi971322.js +1 -0
- package/.next/static/chunks/3hsi90cejq2dg.js +1 -0
- package/.next/static/chunks/3pfyuezyqd9ob.js +1 -0
- package/.next/static/chunks/3ts-_4i6bfkh2.js +1 -0
- package/.next/static/chunks/40nvbgfn7-072.css +3 -0
- package/.next/static/chunks/turbopack-43vu1xwipchft.js +1 -0
- package/.next/static/media/4fa387ec64143e14-s.2tuy5pz7dlieh.woff2 +0 -0
- package/.next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
- package/.next/static/media/5ce348bf30bf5439-s.31988l_ccedte.woff2 +0 -0
- package/.next/static/media/6306c77e7c8268e4-s.2dbetqa9o8jxf.woff2 +0 -0
- package/.next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
- package/.next/static/media/797e433ab948586e-s.p.0r6juujl39pe6.woff2 +0 -0
- package/.next/static/media/7d817b4c03b0c5f1-s.1uyisp29ctx0d.woff2 +0 -0
- package/.next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
- package/.next/static/media/bbc41e54d2fcbd21-s.1rgnod-3esatf.woff2 +0 -0
- package/.next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
- package/.next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- package/.next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
- package/CHANGELOG.md +553 -0
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/SECURITY.md +46 -0
- package/bin/six-degrees.mjs +149 -0
- package/db/schema.js +160 -0
- package/lib/app-version.js +7 -0
- package/lib/companies.js +241 -0
- package/lib/csv.js +172 -0
- package/lib/db-client.js +152 -0
- package/lib/db.js +288 -0
- package/lib/demo.js +34 -0
- package/lib/gate.js +87 -0
- package/lib/ingest.js +62 -0
- package/lib/linkedin-limits.js +118 -0
- package/lib/network.js +13 -0
- package/lib/paths.js +31 -0
- package/lib/paused.js +75 -0
- package/lib/profile.js +55 -0
- package/lib/promote.js +139 -0
- package/lib/quest.js +119 -0
- package/lib/release.js +50 -0
- package/lib/rpc.js +80 -0
- package/lib/scan-progress.js +63 -0
- package/lib/scoring.js +410 -0
- package/lib/scraper-client.js +65 -0
- package/lib/separation.js +326 -0
- package/lib/tiers.js +50 -0
- package/lib/user.js +32 -0
- package/package.json +88 -0
- package/public/demo-data.json +1 -0
- package/scripts/audit-avatars.mjs +132 -0
- package/scripts/check-node.mjs +28 -0
- package/scripts/check-stale-server.mjs +57 -0
- package/scripts/image_store.py +125 -0
- package/scripts/requirements.txt +4 -0
- package/scripts/score_new_connections.sql +77 -0
- package/scripts/scrape.py +3574 -0
- package/scripts/update.mjs +84 -0
|
@@ -0,0 +1,3574 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
6 Degrees LinkedIn Scraper
|
|
4
|
+
|
|
5
|
+
Drives Chrome with a persistent profile. You log into LinkedIn by hand once;
|
|
6
|
+
the session is reused on every later run.
|
|
7
|
+
Scrapes your connections into the app running on your own machine.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
pip3 install playwright requests
|
|
11
|
+
playwright install chromium
|
|
12
|
+
|
|
13
|
+
python3 scripts/scrape.py # First run: full scrape. Later: new only
|
|
14
|
+
python3 scripts/scrape.py --full # Force a full re-walk of every page
|
|
15
|
+
python3 scripts/scrape.py --refresh # Only look for newly added connections
|
|
16
|
+
python3 scripts/scrape.py --bridge "Jane Doe" # Scrape one bridge's connections
|
|
17
|
+
python3 scripts/scrape.py --company "Acme" # Scan one company
|
|
18
|
+
python3 scripts/scrape.py --auto-bridge # Map every bridge in turn
|
|
19
|
+
python3 scripts/scrape.py --auto-bridge --tiers=S,A --max-bridges=10
|
|
20
|
+
python3 scripts/scrape.py --rescrape "Name" # Delete + re-scrape a bridge
|
|
21
|
+
python3 scripts/scrape.py --auto-bridge --deeper # ...and finish lists read only partly
|
|
22
|
+
|
|
23
|
+
IMPORTANT: Only scrape ONE bridge at a time. Do NOT batch multiple bridges
|
|
24
|
+
in one session — LinkedIn detects automation and flags your account.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import signal
|
|
31
|
+
import sys
|
|
32
|
+
import time
|
|
33
|
+
import requests
|
|
34
|
+
from datetime import datetime
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from urllib.parse import parse_qs, urlparse
|
|
37
|
+
|
|
38
|
+
# Local helper: download + compress avatars into permanent WebP files so they
|
|
39
|
+
# don't break every ~3 weeks when LinkedIn's signed CDN URLs expire.
|
|
40
|
+
from image_store import localize_images
|
|
41
|
+
|
|
42
|
+
# --- Config ---
|
|
43
|
+
def _load_env_local():
|
|
44
|
+
"""Fallback: read NEXT_PUBLIC_* values from the app's gitignored .env.local."""
|
|
45
|
+
env = {}
|
|
46
|
+
env_file = Path(__file__).resolve().parent.parent / ".env.local"
|
|
47
|
+
if env_file.exists():
|
|
48
|
+
for line in env_file.read_text().splitlines():
|
|
49
|
+
if "=" in line and not line.strip().startswith("#"):
|
|
50
|
+
k, v = line.split("=", 1)
|
|
51
|
+
env[k.strip()] = v.strip()
|
|
52
|
+
return env
|
|
53
|
+
|
|
54
|
+
_ENV_LOCAL = _load_env_local()
|
|
55
|
+
|
|
56
|
+
# The scraper pushes into the app running on THIS machine. It must never
|
|
57
|
+
# default to somebody else's deployment — that would send a user's network to
|
|
58
|
+
# a server they don't control.
|
|
59
|
+
APP_URL = os.getenv("APP_URL", "http://localhost:3000")
|
|
60
|
+
|
|
61
|
+
CONNECTIONS_URL = "https://www.linkedin.com/mynetwork/invite-connect/connections/"
|
|
62
|
+
|
|
63
|
+
# Token for the deployed app's destructive API routes (see middleware.js). Sent
|
|
64
|
+
# as a bearer so --rescrape/delete-cluster still work against the deployment.
|
|
65
|
+
# Not needed for ordinary ingest, which is open.
|
|
66
|
+
ADMIN_TOKEN = os.getenv("ADMIN_TOKEN") or _ENV_LOCAL.get("ADMIN_TOKEN", "")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _assert_local_target():
|
|
70
|
+
"""Refuse to push to a remote host unless explicitly allowed.
|
|
71
|
+
|
|
72
|
+
A misconfigured APP_URL would upload someone's private network to a third
|
|
73
|
+
party, so this fails loudly rather than silently doing it.
|
|
74
|
+
"""
|
|
75
|
+
from urllib.parse import urlparse
|
|
76
|
+
host = (urlparse(APP_URL).hostname or "").lower()
|
|
77
|
+
if host in ("localhost", "127.0.0.1", "::1", "0.0.0.0"):
|
|
78
|
+
return
|
|
79
|
+
if os.getenv("ALLOW_REMOTE_PUSH") == "1":
|
|
80
|
+
print(f"WARNING: pushing scraped data to remote host {host}")
|
|
81
|
+
return
|
|
82
|
+
raise SystemExit(
|
|
83
|
+
f"Refusing to push scraped data to '{APP_URL}'.\n"
|
|
84
|
+
"The scraper writes to the app on your own machine. Start it with "
|
|
85
|
+
"`npm run dev` and leave APP_URL unset, or set ALLOW_REMOTE_PUSH=1 if "
|
|
86
|
+
"you really mean to send your network to a remote server."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def app_headers(json_body=True):
|
|
91
|
+
"""Headers for calls to the app's API routes, incl. admin-route auth."""
|
|
92
|
+
headers = {"Content-Type": "application/json"} if json_body else {}
|
|
93
|
+
if ADMIN_TOKEN:
|
|
94
|
+
headers["Authorization"] = f"Bearer {ADMIN_TOKEN}"
|
|
95
|
+
return headers
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Active user ID — set by the server when a user triggers a scrape
|
|
99
|
+
_active_user_id = None
|
|
100
|
+
|
|
101
|
+
# --- Scraper JS (same logic as our proven browser scripts) ---
|
|
102
|
+
CONNECTIONS_SCRAPER_JS = """
|
|
103
|
+
async () => {
|
|
104
|
+
// Load all connections
|
|
105
|
+
let attempts = 0;
|
|
106
|
+
while (attempts < 120) {
|
|
107
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
108
|
+
await new Promise(r => setTimeout(r, 500));
|
|
109
|
+
const btn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Load more'));
|
|
110
|
+
if (btn) { btn.scrollIntoView(); btn.click(); attempts++; await new Promise(r => setTimeout(r, 1000)); }
|
|
111
|
+
else { if (document.querySelectorAll('a[href*="/in/"]').length / 2 > 100 || attempts > 5) break; attempts++; await new Promise(r => setTimeout(r, 1000)); }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Extract
|
|
115
|
+
const connections = [];
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
const imgMap = {};
|
|
118
|
+
|
|
119
|
+
document.querySelectorAll('a[href*="/in/"]').forEach(link => {
|
|
120
|
+
const href = link.getAttribute('href');
|
|
121
|
+
const profileUrl = href.startsWith('http') ? href.split('?')[0] : 'https://www.linkedin.com' + href.split('?')[0];
|
|
122
|
+
const img = link.querySelector('img');
|
|
123
|
+
|
|
124
|
+
if (img && img.src && img.src.includes('media.licdn.com')) {
|
|
125
|
+
imgMap[profileUrl] = img.src;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (img) return;
|
|
129
|
+
|
|
130
|
+
const wrapper = link.children[0];
|
|
131
|
+
if (!wrapper || wrapper.children.length < 2) return;
|
|
132
|
+
const name = wrapper.children[0].textContent.trim();
|
|
133
|
+
const headline = wrapper.children[1].textContent.trim();
|
|
134
|
+
if (!name || seen.has(profileUrl)) return;
|
|
135
|
+
seen.add(profileUrl);
|
|
136
|
+
|
|
137
|
+
let connectedDate = '';
|
|
138
|
+
let el = link.nextElementSibling;
|
|
139
|
+
for (let i = 0; i < 5 && el; i++) {
|
|
140
|
+
const t = el.textContent.trim();
|
|
141
|
+
if (t.startsWith('Connected on')) { connectedDate = t.replace('Connected on ', ''); break; }
|
|
142
|
+
el = el.nextElementSibling;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
connections.push({ name, headline, profileUrl, connectedDate, imageUrl: imgMap[profileUrl] || '' });
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Merge image URLs
|
|
149
|
+
connections.forEach(c => { if (!c.imageUrl && imgMap[c.profileUrl]) c.imageUrl = imgMap[c.profileUrl]; });
|
|
150
|
+
|
|
151
|
+
return connections;
|
|
152
|
+
}
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
SEARCH_SCRAPER_JS = """
|
|
156
|
+
async (maxPages) => {
|
|
157
|
+
const results = [];
|
|
158
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
159
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
160
|
+
|
|
161
|
+
const urlMap = {};
|
|
162
|
+
document.querySelectorAll('a').forEach(a => {
|
|
163
|
+
const h = a.getAttribute('href') || '';
|
|
164
|
+
if (!h.includes('/in/')) return;
|
|
165
|
+
const url = h.startsWith('http') ? h.split('?')[0] : 'https://www.linkedin.com' + h.split('?')[0];
|
|
166
|
+
const text = a.textContent.trim();
|
|
167
|
+
if (!text || text.length > 60 || text.includes('mutual') || text.includes('Connect') || text.includes('Invite') || text.includes('•')) return;
|
|
168
|
+
if (!urlMap[text]) urlMap[text] = url;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const main = document.querySelector('[role="main"], main');
|
|
172
|
+
const pageText = main ? main.innerText : '';
|
|
173
|
+
const lines = pageText.split('\\n').map(l => l.trim());
|
|
174
|
+
|
|
175
|
+
let i = 0;
|
|
176
|
+
while (i < lines.length) {
|
|
177
|
+
if (urlMap[lines[i]]) {
|
|
178
|
+
const name = lines[i];
|
|
179
|
+
const url = urlMap[name];
|
|
180
|
+
let headline = '';
|
|
181
|
+
let j = i + 1;
|
|
182
|
+
while (j < lines.length && j < i + 8) {
|
|
183
|
+
const l = lines[j].trim();
|
|
184
|
+
if (!l || l.match(/^\\u2022\\s*(1st|2nd|3rd)/) || l === name) { j++; continue; }
|
|
185
|
+
if (l === 'Connect' || l === 'Follow' || l === 'Message' || l.includes('mutual connection') || l.match(/followers$/)) break;
|
|
186
|
+
if (!headline) { headline = l; j++; continue; }
|
|
187
|
+
j++;
|
|
188
|
+
}
|
|
189
|
+
results.push({ name, headline, profileUrl: url, imageUrl: '' });
|
|
190
|
+
}
|
|
191
|
+
i++;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (page < maxPages) {
|
|
195
|
+
const nextBtn = Array.from(document.querySelectorAll('button')).find(b => b.textContent.trim() === 'Next');
|
|
196
|
+
if (!nextBtn) break;
|
|
197
|
+
nextBtn.click();
|
|
198
|
+
await new Promise(r => setTimeout(r, 2500));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return results;
|
|
202
|
+
}
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
FEED_URL = "https://www.linkedin.com/feed/"
|
|
207
|
+
# A first sign-in means email, password, and often a 2FA code from another
|
|
208
|
+
# device. Five minutes was not enough, and timing out used to CLOSE the window
|
|
209
|
+
# mid-login and throw the attempt away. Wait for as long as the window is open:
|
|
210
|
+
# closing it is the honest "I am done" signal, and any timer here is a guess.
|
|
211
|
+
LOGIN_WAIT_SECONDS = int(os.getenv("SIX_DEGREES_LOGIN_WAIT", "1800"))
|
|
212
|
+
MAX_SCROLL_ROUNDS = 400 # ~10 people load per round
|
|
213
|
+
SCROLL_PAUSE_SECONDS = 0.9
|
|
214
|
+
SCROLL_STALL_LIMIT = 10 # rounds with no new names before stopping
|
|
215
|
+
LOGIN_POLL_SECONDS = 2
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _has_session_cookie(target):
|
|
219
|
+
"""The one signal LinkedIn cannot render away.
|
|
220
|
+
|
|
221
|
+
`li_at` is the session cookie. It is set only after a real sign-in and it
|
|
222
|
+
survives redesigns, which the DOM does not: the `nav.global-nav` element an
|
|
223
|
+
earlier version of this check looked for no longer exists on any LinkedIn
|
|
224
|
+
page, so that check silently never matched.
|
|
225
|
+
|
|
226
|
+
Reads the browser CONTEXT, not a page, on purpose. Signing in can open a
|
|
227
|
+
second window or replace the tab, and a check bound to one Page object then
|
|
228
|
+
watches something the user has already navigated away from — it waits
|
|
229
|
+
forever while the session it is waiting for sits right there in the jar.
|
|
230
|
+
"""
|
|
231
|
+
ctx = getattr(target, "context", target)
|
|
232
|
+
try:
|
|
233
|
+
for c in ctx.cookies("https://www.linkedin.com"):
|
|
234
|
+
if c.get("name") == "li_at" and c.get("value"):
|
|
235
|
+
return True
|
|
236
|
+
except Exception:
|
|
237
|
+
pass
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _looks_logged_out(page):
|
|
242
|
+
"""LinkedIn shows several different logged-out pages.
|
|
243
|
+
|
|
244
|
+
Matching on the URL alone is not enough: the signed-out landing page is
|
|
245
|
+
often just linkedin.com/ with a sign-in splash, which slips past a
|
|
246
|
+
login/authwall check and leaves the scraper collecting nothing.
|
|
247
|
+
"""
|
|
248
|
+
url = (page.url or "").lower()
|
|
249
|
+
if any(marker in url for marker in ("/login", "/authwall", "/signup", "/checkpoint", "/uas/")):
|
|
250
|
+
return True
|
|
251
|
+
if _has_session_cookie(page):
|
|
252
|
+
return False
|
|
253
|
+
try:
|
|
254
|
+
if page.query_selector("a[href*='/login'], button[data-tracking-control-name*='sign-in']"):
|
|
255
|
+
return True
|
|
256
|
+
except Exception:
|
|
257
|
+
pass
|
|
258
|
+
return "/feed" not in url and "/mynetwork" not in url
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _page_wall(pg):
|
|
262
|
+
""""checkpoint" if this page is LinkedIn's security check, "signin" if it is a
|
|
263
|
+
sign-in page or wall, else None. Only LinkedIn pages count."""
|
|
264
|
+
try:
|
|
265
|
+
if pg.is_closed():
|
|
266
|
+
return None
|
|
267
|
+
u = urlparse(pg.url or "")
|
|
268
|
+
if not (u.hostname or "").endswith("linkedin.com"):
|
|
269
|
+
return None
|
|
270
|
+
path = u.path.lower()
|
|
271
|
+
if path.startswith("/checkpoint"):
|
|
272
|
+
return "checkpoint"
|
|
273
|
+
if path.startswith(("/authwall", "/uas/", "/login")):
|
|
274
|
+
return "signin"
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _off_the_wall(context):
|
|
281
|
+
"""At least one open LinkedIn page is past any sign-in or security check.
|
|
282
|
+
|
|
283
|
+
Sign-in can finish in another tab or window and leave a stale /login page
|
|
284
|
+
behind, so one page on a wall doesn't mean you aren't signed in.
|
|
285
|
+
"""
|
|
286
|
+
try:
|
|
287
|
+
pages = [pg for pg in context.pages if not pg.is_closed()]
|
|
288
|
+
linkedin = [pg for pg in pages
|
|
289
|
+
if (urlparse(pg.url or "").hostname or "").endswith("linkedin.com")]
|
|
290
|
+
walls = [_page_wall(pg) for pg in linkedin]
|
|
291
|
+
# A security check still open anywhere isn't finished, whatever else
|
|
292
|
+
# is open; a stale sign-in page in another tab is.
|
|
293
|
+
if "checkpoint" in walls:
|
|
294
|
+
return False
|
|
295
|
+
return any(w is None for w in walls)
|
|
296
|
+
except Exception:
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def ensure_logged_in(page, timeout_s=LOGIN_WAIT_SECONDS, log_fn=None, stop_on_checkpoint=False):
|
|
301
|
+
"""Wait for a human to finish logging in, rather than scraping an empty page.
|
|
302
|
+
|
|
303
|
+
The browser uses a persistent profile, so this is a once-per-machine step —
|
|
304
|
+
every later run finds the session already there and returns immediately.
|
|
305
|
+
|
|
306
|
+
The session cookie survives LinkedIn's security check, so the cookie alone
|
|
307
|
+
used to count as signed in and a scan carried on straight past the check
|
|
308
|
+
(TRAPS §35). So: were you signed in when this started? If you were, and
|
|
309
|
+
LinkedIn now shows a security check or a sign-in wall, that is LinkedIn
|
|
310
|
+
pushing back — with stop_on_checkpoint (2nd-degree reads) the run ends
|
|
311
|
+
there, since carrying on after one is what LinkedIn says turns a warning
|
|
312
|
+
into a restriction. If you weren't, it is an ordinary sign-in, and LinkedIn's
|
|
313
|
+
own sign-in and two-factor steps live under /checkpoint too: wait through
|
|
314
|
+
them.
|
|
315
|
+
"""
|
|
316
|
+
context = page.context
|
|
317
|
+
had_session = _has_session_cookie(context)
|
|
318
|
+
|
|
319
|
+
try:
|
|
320
|
+
page.goto(FEED_URL, wait_until="domcontentloaded")
|
|
321
|
+
except Exception:
|
|
322
|
+
pass
|
|
323
|
+
time.sleep(2)
|
|
324
|
+
|
|
325
|
+
wall = _page_wall(page)
|
|
326
|
+
signed_out = had_session and (wall or not _has_session_cookie(context) or _looks_logged_out(page))
|
|
327
|
+
if stop_on_checkpoint and signed_out:
|
|
328
|
+
reason = "a security check" if wall == "checkpoint" else "being signed out"
|
|
329
|
+
_keep_pushback_evidence(page, reason)
|
|
330
|
+
set_cooldown(seconds=DAY_SECONDS, reason=f"LinkedIn pushed back: {reason}")
|
|
331
|
+
raise LinkedInPushedBack(0, reason)
|
|
332
|
+
|
|
333
|
+
if _has_session_cookie(context) and not _looks_logged_out(page) and not wall:
|
|
334
|
+
return True
|
|
335
|
+
|
|
336
|
+
say = log_fn or (lambda m: None)
|
|
337
|
+
if had_session and wall == "checkpoint":
|
|
338
|
+
print()
|
|
339
|
+
print(" LinkedIn wants a security check. Finish it in the browser window;")
|
|
340
|
+
print(" this closes by itself once it's done. Then leave scanning for a day.")
|
|
341
|
+
print()
|
|
342
|
+
say("Waiting for you to finish LinkedIn's security check...")
|
|
343
|
+
else:
|
|
344
|
+
_print_sign_in_banner(say)
|
|
345
|
+
return _wait_for_sign_in(page, context, timeout_s, say)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _print_sign_in_banner(say):
|
|
349
|
+
say("Waiting for you to sign into LinkedIn in the browser window...")
|
|
350
|
+
print()
|
|
351
|
+
print(" ==================================================================")
|
|
352
|
+
print(" Sign into LinkedIn in the browser window that just opened.")
|
|
353
|
+
print()
|
|
354
|
+
print(" Use your email and password. \"Continue with Google\" and \"Sign in")
|
|
355
|
+
print(" with Apple\" do not work here — Google blocks its sign-in flow")
|
|
356
|
+
print(" inside automated browsers, which is why that window comes up")
|
|
357
|
+
print(" greyed out and does nothing.")
|
|
358
|
+
print()
|
|
359
|
+
print(" Nothing is scraped until you are signed in, and the scrape starts")
|
|
360
|
+
print(" by itself the moment you are. Take as long as you need.")
|
|
361
|
+
print(" Close the browser window to cancel.")
|
|
362
|
+
print(" ==================================================================")
|
|
363
|
+
print()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _wait_for_sign_in(page, context, timeout_s, say):
|
|
367
|
+
waited = 0
|
|
368
|
+
while waited < timeout_s:
|
|
369
|
+
time.sleep(LOGIN_POLL_SECONDS)
|
|
370
|
+
waited += LOGIN_POLL_SECONDS
|
|
371
|
+
|
|
372
|
+
# Closing every window is how you say "not now". One page closing is
|
|
373
|
+
# not: signing in legitimately opens and closes tabs.
|
|
374
|
+
try:
|
|
375
|
+
if not [pg for pg in context.pages if not pg.is_closed()]:
|
|
376
|
+
print("\n Browser closed — nothing was scraped.")
|
|
377
|
+
say("Browser closed before sign-in.")
|
|
378
|
+
return False
|
|
379
|
+
except Exception:
|
|
380
|
+
print("\n Browser closed — nothing was scraped.")
|
|
381
|
+
return False
|
|
382
|
+
|
|
383
|
+
# The cookie belongs to the whole browser, so it is found no matter which
|
|
384
|
+
# tab or window the sign-in finished in — but it only counts once a
|
|
385
|
+
# LinkedIn page is past the security check or sign-in wall.
|
|
386
|
+
if _has_session_cookie(context) and _off_the_wall(context):
|
|
387
|
+
print()
|
|
388
|
+
print(" Signed in — LinkedIn is clear.")
|
|
389
|
+
print()
|
|
390
|
+
say("Signed in.")
|
|
391
|
+
return True
|
|
392
|
+
|
|
393
|
+
if waited % 15 == 0:
|
|
394
|
+
mins, secs = divmod(waited, 60)
|
|
395
|
+
note = f"{mins}m{secs:02d}s" if mins else f"{secs}s"
|
|
396
|
+
print(f" waiting for sign-in... ({note})", flush=True)
|
|
397
|
+
if waited % 60 == 0:
|
|
398
|
+
say(f"Still waiting for sign-in... ({note})")
|
|
399
|
+
|
|
400
|
+
print("\n Gave up waiting for sign-in. Whatever you completed is saved in the")
|
|
401
|
+
print(" browser profile — run this again and it will pick up from there.")
|
|
402
|
+
say("Timed out waiting for sign-in.")
|
|
403
|
+
return False
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def open_login_window():
|
|
407
|
+
"""Just sign in, confirm it worked, and exit.
|
|
408
|
+
|
|
409
|
+
Separated out because a sign-in that has to happen inside a run you are also
|
|
410
|
+
trying to watch is two things going wrong at once. Do this first, see it
|
|
411
|
+
succeed, and every later scrape starts immediately.
|
|
412
|
+
"""
|
|
413
|
+
from playwright.sync_api import sync_playwright
|
|
414
|
+
|
|
415
|
+
with sync_playwright() as p:
|
|
416
|
+
browser = p.chromium.launch_persistent_context(
|
|
417
|
+
user_data_dir=get_scraper_profile_path(),
|
|
418
|
+
headless=False,
|
|
419
|
+
channel="chrome",
|
|
420
|
+
args=["--disable-blink-features=AutomationControlled"],
|
|
421
|
+
timeout=120000,
|
|
422
|
+
)
|
|
423
|
+
page = browser.pages[0] if browser.pages else browser.new_page()
|
|
424
|
+
page.set_default_timeout(120000)
|
|
425
|
+
|
|
426
|
+
# The cookie alone isn't enough: it survives a security check, and this is
|
|
427
|
+
# the window you open to finish one (TRAPS §35). ensure_logged_in loads
|
|
428
|
+
# the feed, returns at once if all is well, and otherwise waits for you.
|
|
429
|
+
ok = ensure_logged_in(page)
|
|
430
|
+
if ok:
|
|
431
|
+
print(" LinkedIn is signed in on this machine and clear to use.\n")
|
|
432
|
+
browser.close()
|
|
433
|
+
return ok
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def get_scraper_profile_path():
|
|
437
|
+
"""The Chrome profile the scraper drives.
|
|
438
|
+
|
|
439
|
+
Separate from your everyday Chrome profile so the two never fight over the
|
|
440
|
+
same lock. It holds a real logged-in LinkedIn session, so it lives inside
|
|
441
|
+
the app's data directory (SIX_DEGREES_HOME, default ~/.six-degrees) and is
|
|
442
|
+
removed along with everything else when you delete that folder. Treat it
|
|
443
|
+
like a password: it can be several hundred megabytes and it is the one
|
|
444
|
+
artifact here that grants access to your account.
|
|
445
|
+
"""
|
|
446
|
+
base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees"))
|
|
447
|
+
profile_dir = base / "chrome-profile"
|
|
448
|
+
profile_dir.mkdir(parents=True, exist_ok=True)
|
|
449
|
+
return str(profile_dir)
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def resolve_active_user():
|
|
453
|
+
"""Attach this scrape to the profile the app is showing.
|
|
454
|
+
|
|
455
|
+
The app filters every view by user id, so rows written with no owner land in
|
|
456
|
+
the database and then never appear on screen — the scrape looks like it
|
|
457
|
+
silently did nothing. The app decides which profile is "you" (the one that
|
|
458
|
+
owns the network — lib/profile.js) and this adopts the same answer, so the
|
|
459
|
+
two can never disagree. It used to refuse outright when a stray empty
|
|
460
|
+
profile existed, which stopped every scan with a reason nobody saw.
|
|
461
|
+
|
|
462
|
+
Order: SIX_DEGREES_USER_ID (set by the app when it runs this), then
|
|
463
|
+
SIX_DEGREES_USER by name (for the command line), then ask the app.
|
|
464
|
+
"""
|
|
465
|
+
global _active_user_id
|
|
466
|
+
if _active_user_id:
|
|
467
|
+
return _active_user_id
|
|
468
|
+
|
|
469
|
+
given_id = os.getenv("SIX_DEGREES_USER_ID", "").strip()
|
|
470
|
+
if given_id:
|
|
471
|
+
_active_user_id = given_id
|
|
472
|
+
return _active_user_id
|
|
473
|
+
|
|
474
|
+
wanted = os.getenv("SIX_DEGREES_USER", "").strip().lower()
|
|
475
|
+
try:
|
|
476
|
+
if wanted:
|
|
477
|
+
resp = requests.get(f"{APP_URL}/api/users", headers=app_headers(json_body=False), timeout=15)
|
|
478
|
+
users = resp.json().get("users", []) if resp.status_code == 200 else []
|
|
479
|
+
for u in users:
|
|
480
|
+
if (u.get("name") or "").strip().lower() == wanted:
|
|
481
|
+
_active_user_id = u["id"]
|
|
482
|
+
print(f" Scraping into profile: {u['name']}")
|
|
483
|
+
return _active_user_id
|
|
484
|
+
raise SystemExit(f"No profile named '{os.getenv('SIX_DEGREES_USER')}' in the app.")
|
|
485
|
+
|
|
486
|
+
resp = requests.get(f"{APP_URL}/api/users?me=1", headers=app_headers(json_body=False), timeout=15)
|
|
487
|
+
me = resp.json().get("user") if resp.status_code == 200 else None
|
|
488
|
+
except SystemExit:
|
|
489
|
+
raise
|
|
490
|
+
except Exception:
|
|
491
|
+
print(f"\n Could not reach the app at {APP_URL}.")
|
|
492
|
+
print(" Start it with `npm run dev` in another terminal, then run this again.\n")
|
|
493
|
+
raise SystemExit(1)
|
|
494
|
+
|
|
495
|
+
if not me or not me.get("id"):
|
|
496
|
+
raise SystemExit(f"The app at {APP_URL} did not say which profile to use.")
|
|
497
|
+
|
|
498
|
+
_active_user_id = me["id"]
|
|
499
|
+
print(f" Scraping into profile: {me.get('name')}")
|
|
500
|
+
return _active_user_id
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def read_bridged_ids():
|
|
504
|
+
"""The ids of connections whose circle has already been mapped."""
|
|
505
|
+
params = {}
|
|
506
|
+
if _active_user_id:
|
|
507
|
+
params["userId"] = _active_user_id
|
|
508
|
+
try:
|
|
509
|
+
resp = requests.get(f"{APP_URL}/api/bridges", params=params,
|
|
510
|
+
headers=app_headers(json_body=False), timeout=30)
|
|
511
|
+
if resp.status_code != 200:
|
|
512
|
+
print(" (could not read mapped bridges; treating none as mapped)")
|
|
513
|
+
return set()
|
|
514
|
+
return set(resp.json().get("bridgeIds", []))
|
|
515
|
+
except Exception:
|
|
516
|
+
print(" (could not read mapped bridges; treating none as mapped)")
|
|
517
|
+
return set()
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def read_connections(endpoint="", params=None):
|
|
521
|
+
"""Read connections from the local app.
|
|
522
|
+
|
|
523
|
+
The hosted build queried the cloud database directly. Locally the app owns
|
|
524
|
+
the SQLite file, so the scraper asks it instead of opening the database a
|
|
525
|
+
second time. PostgREST-style values ("eq.1") are unwrapped here so the many
|
|
526
|
+
existing call sites did not have to change.
|
|
527
|
+
"""
|
|
528
|
+
query = {}
|
|
529
|
+
for key, value in (params or {}).items():
|
|
530
|
+
if key in ("select", "order", "limit"):
|
|
531
|
+
if key != "select":
|
|
532
|
+
query[key] = value
|
|
533
|
+
continue
|
|
534
|
+
query[key] = value.split(".", 1)[1] if isinstance(value, str) and value.startswith("eq.") else value
|
|
535
|
+
try:
|
|
536
|
+
resp = requests.get(f"{APP_URL}/api/connections", params=query, headers=app_headers(json_body=False), timeout=30)
|
|
537
|
+
if resp.status_code != 200:
|
|
538
|
+
return []
|
|
539
|
+
return resp.json().get("connections", [])
|
|
540
|
+
except Exception:
|
|
541
|
+
return []
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def delete_bridge_cluster(bridge_name):
|
|
545
|
+
"""Delete all degree-2 connections for a bridge so it can be re-scraped clean."""
|
|
546
|
+
# Find the bridge (read-only)
|
|
547
|
+
bridges = read_connections(params={"name": f"eq.{bridge_name}", "degree": "eq.1", "limit": "1"})
|
|
548
|
+
if not bridges:
|
|
549
|
+
print(f"Bridge '{bridge_name}' not found.")
|
|
550
|
+
return None
|
|
551
|
+
|
|
552
|
+
bridge_id = bridges[0]["id"]
|
|
553
|
+
existing = read_connections(params={"source_connection_id": f"eq.{bridge_id}", "degree": "eq.2", "select": "id"})
|
|
554
|
+
count = len(existing)
|
|
555
|
+
|
|
556
|
+
if count == 0:
|
|
557
|
+
print(f" No existing cluster data for {bridge_name}.")
|
|
558
|
+
return bridge_id
|
|
559
|
+
|
|
560
|
+
# Delete via Vercel API route (has service_role key)
|
|
561
|
+
resp = requests.post(f"{APP_URL}/api/delete-cluster", json={"bridgeId": bridge_id}, headers=app_headers())
|
|
562
|
+
if resp.status_code == 200:
|
|
563
|
+
print(f" Deleted {count} old records for {bridge_name}.")
|
|
564
|
+
else:
|
|
565
|
+
print(f" Delete failed: {resp.status_code} {resp.text[:100]}")
|
|
566
|
+
|
|
567
|
+
return bridge_id
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class SaveFailed(Exception):
|
|
571
|
+
"""The app refused what was scraped.
|
|
572
|
+
|
|
573
|
+
It used to print a one-line "Push error" and carry on — "Done! 0 new
|
|
574
|
+
degree-2 connections added" — to the next person, spending more LinkedIn
|
|
575
|
+
views on people who could not be saved either. TRAPS §32.
|
|
576
|
+
"""
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
class LinkedInPushedBack(Exception):
|
|
580
|
+
"""A results page would not open after END_CHECKS tries.
|
|
581
|
+
|
|
582
|
+
That is what LinkedIn blocking the account's search looked like on
|
|
583
|
+
2026-09-24: page 28 of a list never loaded. The batch used to note it for
|
|
584
|
+
that person and carry on with the next one after a cooldown — straight
|
|
585
|
+
into the same block. Raised after saving, so `found` is what was read.
|
|
586
|
+
"""
|
|
587
|
+
|
|
588
|
+
def __init__(self, found=0, reason="a page that would not open"):
|
|
589
|
+
super().__init__(reason)
|
|
590
|
+
self.found = found
|
|
591
|
+
self.reason = reason
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
class BudgetReached(Exception):
|
|
595
|
+
"""Today's or this month's search budget is used. Raised after saving."""
|
|
596
|
+
|
|
597
|
+
def __init__(self, found=0, kind="daily"):
|
|
598
|
+
super().__init__(f"the {kind} search budget")
|
|
599
|
+
self.found = found
|
|
600
|
+
self.kind = kind
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
class CoolingDown(Exception):
|
|
604
|
+
"""A cooldown lock is active: nothing may search LinkedIn until it lifts."""
|
|
605
|
+
|
|
606
|
+
def __init__(self, cd):
|
|
607
|
+
super().__init__(cooldown_message(cd))
|
|
608
|
+
self.cd = cd
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
class NotSignedIn(Exception):
|
|
612
|
+
"""LinkedIn wasn't signed in, or the window was closed while waiting for it.
|
|
613
|
+
|
|
614
|
+
Not a fact about the person being read. It used to come back as "no
|
|
615
|
+
connections", and that person was noted as hidden and skipped for good — a
|
|
616
|
+
stop pressed while the browser was opening did exactly that. TRAPS §34.
|
|
617
|
+
"""
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
class SearchLimitReached(Exception):
|
|
621
|
+
"""LinkedIn says this account has used up its searches for the month.
|
|
622
|
+
|
|
623
|
+
Every page of someone's connections is a search, and a free account has a
|
|
624
|
+
monthly allowance that reading whole lists uses up fast. Raised after what
|
|
625
|
+
was read has been saved, so `found` is how many that was; the record of how
|
|
626
|
+
far the read got means the next run carries on from the same page.
|
|
627
|
+
"""
|
|
628
|
+
|
|
629
|
+
def __init__(self, found=0):
|
|
630
|
+
super().__init__("LinkedIn's monthly search limit")
|
|
631
|
+
self.found = found
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def push_connections(connections, degree=1, bridge_id=None, user_id=None, on_saved=None):
|
|
635
|
+
"""Push connections via Vercel API route (which has write access).
|
|
636
|
+
No local keys needed — the server handles auth.
|
|
637
|
+
|
|
638
|
+
on_saved runs as soon as the app has the rows, before the photos: those can
|
|
639
|
+
take a while, and a stop that lands during them must not lose the note of
|
|
640
|
+
how far a read got.
|
|
641
|
+
"""
|
|
642
|
+
|
|
643
|
+
# Send to Vercel API route — it writes with the service_role key
|
|
644
|
+
payload = {
|
|
645
|
+
"connections": connections,
|
|
646
|
+
"type": "degree2" if degree == 2 else "degree1",
|
|
647
|
+
"bridgeId": bridge_id,
|
|
648
|
+
"userId": user_id or _active_user_id,
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
resp = requests.post(
|
|
652
|
+
f"{APP_URL}/api/ingest",
|
|
653
|
+
headers=app_headers(),
|
|
654
|
+
json=payload,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
if resp.status_code == 200:
|
|
658
|
+
result = resp.json()
|
|
659
|
+
# "saved" is what was actually new; an older app only sends "processed",
|
|
660
|
+
# which counted everything sent and made nothing look like something.
|
|
661
|
+
inserted = result.get("saved", result.get("processed", 0))
|
|
662
|
+
promoted = result.get("promoted", 0)
|
|
663
|
+
notes = []
|
|
664
|
+
if result.get("alreadyKnown"):
|
|
665
|
+
notes.append(f"{result['alreadyKnown']} already on file")
|
|
666
|
+
if result.get("alreadyConnected"):
|
|
667
|
+
notes.append(f"{result['alreadyConnected']} already your connections")
|
|
668
|
+
if result.get("duplicates"):
|
|
669
|
+
notes.append(f"{result['duplicates']} repeats")
|
|
670
|
+
print(f" Sent {len(connections)} → {inserted} new" + (f" ({', '.join(notes)})" if notes else ""))
|
|
671
|
+
if promoted:
|
|
672
|
+
# Someone you were introduced to has accepted. The bridge that
|
|
673
|
+
# produced them is kept on their row, so the path stays visible.
|
|
674
|
+
# Worded carefully: this read as "N new 2nd-degree people added" when it
|
|
675
|
+
# means the opposite — people already in a bridge's circle who are also
|
|
676
|
+
# your connections, now counted once, with who introduced you kept.
|
|
677
|
+
print(f" {promoted} were already in a bridge's circle — merged into your connections, keeping who introduced you")
|
|
678
|
+
else:
|
|
679
|
+
print(f" Push error: {resp.status_code} {resp.text[:200]}")
|
|
680
|
+
raise SaveFailed(f"the app answered {resp.status_code}: {resp.text[:160]}")
|
|
681
|
+
|
|
682
|
+
if on_saved:
|
|
683
|
+
on_saved()
|
|
684
|
+
|
|
685
|
+
# Batch update profile images separately (faster than inline)
|
|
686
|
+
images_to_update = [
|
|
687
|
+
{"profileUrl": c["profileUrl"], "imageUrl": c["imageUrl"]}
|
|
688
|
+
for c in connections
|
|
689
|
+
if c.get("imageUrl") and c.get("profileUrl")
|
|
690
|
+
]
|
|
691
|
+
# Capture avatars permanently: download + compress each while its signed URL is
|
|
692
|
+
# still fresh, then store the local /avatars/*.webp path instead of the licdn URL.
|
|
693
|
+
images_to_update = localize_images(images_to_update)
|
|
694
|
+
if images_to_update:
|
|
695
|
+
print(f" Updating {len(images_to_update)} profile images...")
|
|
696
|
+
# Send in batches of 100 to avoid timeout
|
|
697
|
+
for i in range(0, len(images_to_update), 100):
|
|
698
|
+
batch = images_to_update[i:i+100]
|
|
699
|
+
r = requests.post(
|
|
700
|
+
f"{APP_URL}/api/update-images",
|
|
701
|
+
headers=app_headers(),
|
|
702
|
+
json={"images": batch},
|
|
703
|
+
)
|
|
704
|
+
if r.status_code == 200:
|
|
705
|
+
result = r.json()
|
|
706
|
+
print(f" Images {i+1}-{min(i+100, len(images_to_update))}: {result.get('updated', 0)} updated")
|
|
707
|
+
|
|
708
|
+
# Check for unlocked paths (read-only lookup)
|
|
709
|
+
if degree == 1:
|
|
710
|
+
print(" Checking for unlocked paths...")
|
|
711
|
+
pending = read_connections(params={"unlock_status": "eq.pending", "degree": "eq.2", "select": "id,profile_url,name"})
|
|
712
|
+
d1_urls = {c.get("profileUrl", "").strip() for c in connections if c.get("profileUrl")}
|
|
713
|
+
for p in pending:
|
|
714
|
+
if p["profile_url"] in d1_urls:
|
|
715
|
+
# Unlock via API route
|
|
716
|
+
requests.post(f"{APP_URL}/api/unlock", json={"connectionId": p["id"]}, headers=app_headers())
|
|
717
|
+
print(f" *** PATH UNLOCKED: {p['name']}! Run --bridge \"{p['name']}\" to scrape their network ***")
|
|
718
|
+
|
|
719
|
+
return inserted
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def push_company(people, company_name):
|
|
723
|
+
"""Push company-scraped people to database as degree 3 (company scan).
|
|
724
|
+
Uses the same Vercel API route but with type='company'."""
|
|
725
|
+
connections = []
|
|
726
|
+
for p in people:
|
|
727
|
+
connections.append({
|
|
728
|
+
"name": p.get("name", "").strip(),
|
|
729
|
+
"headline": p.get("headline", "").strip(),
|
|
730
|
+
"profileUrl": p.get("profileUrl", "").strip(),
|
|
731
|
+
"imageUrl": p.get("imageUrl", ""),
|
|
732
|
+
})
|
|
733
|
+
|
|
734
|
+
connections = [c for c in connections if c.get("name") and c.get("profileUrl")]
|
|
735
|
+
if not connections:
|
|
736
|
+
print(f" No valid people to push for {company_name}")
|
|
737
|
+
return 0
|
|
738
|
+
|
|
739
|
+
payload = {
|
|
740
|
+
"connections": connections,
|
|
741
|
+
"type": "company",
|
|
742
|
+
"companyName": company_name,
|
|
743
|
+
"userId": _active_user_id,
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
resp = requests.post(
|
|
747
|
+
f"{APP_URL}/api/ingest",
|
|
748
|
+
headers=app_headers(),
|
|
749
|
+
json=payload,
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
if resp.status_code == 200:
|
|
753
|
+
result = resp.json()
|
|
754
|
+
inserted = result.get("processed", 0)
|
|
755
|
+
print(f" Pushed {len(connections)} → {inserted} saved to database")
|
|
756
|
+
else:
|
|
757
|
+
print(f" Push error: {resp.status_code} {resp.text[:200]}")
|
|
758
|
+
raise SaveFailed(f"the app answered {resp.status_code}: {resp.text[:160]}")
|
|
759
|
+
|
|
760
|
+
# Update profile images
|
|
761
|
+
images_to_update = [
|
|
762
|
+
{"profileUrl": c["profileUrl"], "imageUrl": c["imageUrl"]}
|
|
763
|
+
for c in connections
|
|
764
|
+
if c.get("imageUrl") and c.get("profileUrl")
|
|
765
|
+
]
|
|
766
|
+
# Capture avatars permanently (download + compress while the signed URL is fresh).
|
|
767
|
+
images_to_update = localize_images(images_to_update)
|
|
768
|
+
if images_to_update:
|
|
769
|
+
for i in range(0, len(images_to_update), 100):
|
|
770
|
+
batch = images_to_update[i:i+100]
|
|
771
|
+
r = requests.post(
|
|
772
|
+
f"{APP_URL}/api/update-images",
|
|
773
|
+
headers=app_headers(),
|
|
774
|
+
json={"images": batch},
|
|
775
|
+
)
|
|
776
|
+
if r.status_code == 200:
|
|
777
|
+
print(f" Images {i+1}-{min(i+100, len(images_to_update))}: {r.json().get('updated', 0)} updated")
|
|
778
|
+
|
|
779
|
+
return inserted
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def scrape_full(headless=False):
|
|
783
|
+
"""Full first-time scrape: uses search page to get ALL connections with images.
|
|
784
|
+
Paginates through /search/results/people/?network=["F"] (10 results per page).
|
|
785
|
+
This is the setup method — captures everything including profile photos."""
|
|
786
|
+
from playwright.sync_api import sync_playwright
|
|
787
|
+
|
|
788
|
+
cd = read_cooldown()
|
|
789
|
+
if cd:
|
|
790
|
+
raise CoolingDown(cd)
|
|
791
|
+
if searches_left()[0] <= 0:
|
|
792
|
+
raise BudgetReached(0, searches_left()[1])
|
|
793
|
+
|
|
794
|
+
print("\n=== Full Account Setup Scrape ===\n")
|
|
795
|
+
print("This scrapes ALL connections via the search page (not the connections page).")
|
|
796
|
+
print("Takes 3-5 minutes for ~500 connections.\n")
|
|
797
|
+
|
|
798
|
+
with sync_playwright() as p:
|
|
799
|
+
browser = p.chromium.launch_persistent_context(
|
|
800
|
+
user_data_dir=get_scraper_profile_path(),
|
|
801
|
+
headless=headless,
|
|
802
|
+
channel="chrome",
|
|
803
|
+
args=["--disable-blink-features=AutomationControlled"],
|
|
804
|
+
timeout=120000,
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
page = browser.pages[0] if browser.pages else browser.new_page()
|
|
808
|
+
page.set_default_timeout(120000)
|
|
809
|
+
page.set_default_navigation_timeout(120000)
|
|
810
|
+
|
|
811
|
+
# Check login
|
|
812
|
+
try:
|
|
813
|
+
page.goto("https://www.linkedin.com/", wait_until="domcontentloaded")
|
|
814
|
+
except:
|
|
815
|
+
pass
|
|
816
|
+
if not ensure_logged_in(page):
|
|
817
|
+
browser.close()
|
|
818
|
+
return
|
|
819
|
+
|
|
820
|
+
# Navigate to 1st-degree search
|
|
821
|
+
search_url = "https://www.linkedin.com/search/results/people/?network=%5B%22F%22%5D&origin=FACETED_SEARCH"
|
|
822
|
+
print("Opening 1st-degree connections search...")
|
|
823
|
+
try:
|
|
824
|
+
charge_linkedin("searches")
|
|
825
|
+
page.goto(search_url, wait_until="commit")
|
|
826
|
+
except:
|
|
827
|
+
pass
|
|
828
|
+
time.sleep(8)
|
|
829
|
+
|
|
830
|
+
all_connections = []
|
|
831
|
+
all_images = []
|
|
832
|
+
page_num = 1
|
|
833
|
+
|
|
834
|
+
while page_num <= 100:
|
|
835
|
+
print(f" Page {page_num}... ", end="", flush=True)
|
|
836
|
+
time.sleep(3)
|
|
837
|
+
|
|
838
|
+
# Extract connections + images from current page
|
|
839
|
+
page_data = page.evaluate("""
|
|
840
|
+
() => {
|
|
841
|
+
const urlMap = {};
|
|
842
|
+
const imgMap = {};
|
|
843
|
+
document.querySelectorAll('a').forEach(a => {
|
|
844
|
+
const h = a.getAttribute('href') || '';
|
|
845
|
+
if (!h.includes('/in/')) return;
|
|
846
|
+
const url = h.startsWith('http') ? h.split('?')[0] : 'https://www.linkedin.com' + h.split('?')[0];
|
|
847
|
+
const img = a.querySelector('img');
|
|
848
|
+
if (img && img.src && img.src.includes('media.licdn.com')) {
|
|
849
|
+
imgMap[url] = img.src;
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const text = a.textContent.trim();
|
|
853
|
+
if (!text || text.length > 60 || text.includes('mutual') || text.includes('Connect')) return;
|
|
854
|
+
if (!urlMap[text]) urlMap[text] = url;
|
|
855
|
+
});
|
|
856
|
+
const main = document.querySelector('[role="main"], main');
|
|
857
|
+
const pageText = main ? main.innerText : '';
|
|
858
|
+
const lines = pageText.split('\\n').map(l => l.trim());
|
|
859
|
+
const results = [];
|
|
860
|
+
let i = 0;
|
|
861
|
+
while (i < lines.length) {
|
|
862
|
+
if (urlMap[lines[i]]) {
|
|
863
|
+
const name = lines[i], url = urlMap[name];
|
|
864
|
+
let headline = '';
|
|
865
|
+
let j = i + 1;
|
|
866
|
+
while (j < lines.length && j < i + 8) {
|
|
867
|
+
const l = lines[j].trim();
|
|
868
|
+
if (!l || l.match(/^\\u2022\\s*(1st|2nd|3rd)/) || l === name) { j++; continue; }
|
|
869
|
+
if (l === 'Connect' || l === 'Follow' || l === 'Message' || l.includes('mutual connection')) break;
|
|
870
|
+
if (!headline) { headline = l; j++; continue; }
|
|
871
|
+
j++;
|
|
872
|
+
}
|
|
873
|
+
results.push({ name, headline, profileUrl: url, imageUrl: imgMap[url] || '' });
|
|
874
|
+
}
|
|
875
|
+
i++;
|
|
876
|
+
}
|
|
877
|
+
return results;
|
|
878
|
+
}
|
|
879
|
+
""")
|
|
880
|
+
|
|
881
|
+
new_count = len(page_data)
|
|
882
|
+
all_connections.extend(page_data)
|
|
883
|
+
img_count = sum(1 for c in page_data if c.get("imageUrl"))
|
|
884
|
+
print(f"{new_count} found ({img_count} with images)")
|
|
885
|
+
|
|
886
|
+
# Click Next
|
|
887
|
+
try:
|
|
888
|
+
next_btn = page.locator('button:has-text("Next")').first
|
|
889
|
+
if next_btn.is_visible(timeout=3000):
|
|
890
|
+
if searches_left()[0] <= 0:
|
|
891
|
+
print(" " + budget_message(searches_left()[1]))
|
|
892
|
+
break
|
|
893
|
+
charge_linkedin("searches")
|
|
894
|
+
next_btn.click()
|
|
895
|
+
page_num += 1
|
|
896
|
+
time.sleep(3)
|
|
897
|
+
else:
|
|
898
|
+
print(" No more pages.")
|
|
899
|
+
break
|
|
900
|
+
except:
|
|
901
|
+
print(" No more pages.")
|
|
902
|
+
break
|
|
903
|
+
|
|
904
|
+
print(f"\nTotal: {len(all_connections)} connections, {sum(1 for c in all_connections if c.get('imageUrl'))} with images")
|
|
905
|
+
browser.close()
|
|
906
|
+
|
|
907
|
+
# Push connections
|
|
908
|
+
print(f"\nPushing {len(all_connections)} connections to the app...")
|
|
909
|
+
inserted = push_connections(all_connections, degree=1)
|
|
910
|
+
print(f"Done! {inserted} new.")
|
|
911
|
+
|
|
912
|
+
return all_connections
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
# The connections page keeps its list inside <main>, which is its own scroll
|
|
916
|
+
# container: the window itself never scrolls, so window.scrollTo() — what this
|
|
917
|
+
# scraper used to call — is a no-op there. Nothing new ever loaded and the run
|
|
918
|
+
# ended with the first ten people. Scroll the real container instead, and send a
|
|
919
|
+
# genuine wheel event too, because the list loads on an intersection observer.
|
|
920
|
+
SCROLL_CONTAINER_JS = """
|
|
921
|
+
() => {
|
|
922
|
+
let best = null;
|
|
923
|
+
document.querySelectorAll('main, [role="main"], div').forEach(el => {
|
|
924
|
+
const cs = getComputedStyle(el);
|
|
925
|
+
if (!/(auto|scroll)/.test(cs.overflowY)) return;
|
|
926
|
+
if (el.scrollHeight <= el.clientHeight + 20) return;
|
|
927
|
+
if (!best || el.scrollHeight > best.scrollHeight) best = el;
|
|
928
|
+
});
|
|
929
|
+
const target = best || document.scrollingElement || document.body;
|
|
930
|
+
target.scrollTop = target.scrollHeight;
|
|
931
|
+
window.scrollTo(0, document.body.scrollHeight);
|
|
932
|
+
return target.scrollTop;
|
|
933
|
+
}
|
|
934
|
+
"""
|
|
935
|
+
|
|
936
|
+
# Anchor everything on the profile link, never on class names (LinkedIn's are
|
|
937
|
+
# hashed per deploy) and never on guessing which text line goes with which URL.
|
|
938
|
+
# Each person renders two <a> tags pointing at the same profile: one wrapping
|
|
939
|
+
# the photo, one wrapping the name and headline. Group by href and merge.
|
|
940
|
+
CONNECTIONS_EXTRACT_JS = r"""
|
|
941
|
+
() => {
|
|
942
|
+
const root = document.querySelector('main') || document.querySelector('[role="main"]') || document.body;
|
|
943
|
+
const clean = s => (s || '').replace(/[’']s profile picture$/i, '').trim();
|
|
944
|
+
const byHref = new Map();
|
|
945
|
+
root.querySelectorAll('a[href*="/in/"]').forEach(a => {
|
|
946
|
+
if (a.closest('nav, header, footer')) return;
|
|
947
|
+
const raw = a.getAttribute('href') || '';
|
|
948
|
+
if (!raw.includes('/in/')) return;
|
|
949
|
+
let url = raw.startsWith('http') ? raw : 'https://www.linkedin.com' + raw;
|
|
950
|
+
url = url.split('?')[0].split('#')[0].replace(/\/+$/, '') + '/';
|
|
951
|
+
let rec = byHref.get(url);
|
|
952
|
+
if (!rec) { rec = { profileUrl: url, name: '', headline: '', imageUrl: '', _named: false }; byHref.set(url, rec); }
|
|
953
|
+
const img = a.querySelector('img');
|
|
954
|
+
if (img) {
|
|
955
|
+
if (!rec.imageUrl && img.src && img.src.includes('licdn.com') && !/ghost|^data:/.test(img.src)) rec.imageUrl = img.src;
|
|
956
|
+
// Fallback only: the alt text reads "Jane Doe's profile picture".
|
|
957
|
+
if (!rec.name && img.alt) rec.name = clean(img.alt);
|
|
958
|
+
}
|
|
959
|
+
const lines = (a.innerText || '').split('\n').map(s => s.trim()).filter(Boolean);
|
|
960
|
+
if (lines.length) {
|
|
961
|
+
// The text anchor is authoritative; it always wins over the image alt.
|
|
962
|
+
if (!rec._named) { rec.name = clean(lines[0]); rec._named = true; }
|
|
963
|
+
if (!rec.headline && lines[1] && !/^Connected on/i.test(lines[1])) rec.headline = lines[1];
|
|
964
|
+
}
|
|
965
|
+
// "Connected on September 22, 2026" sits in the card, outside the link. It is
|
|
966
|
+
// what "newest first" goes by, and it went missing when this extractor was
|
|
967
|
+
// rewritten. Climb to the nearest block holding exactly ONE such line: more
|
|
968
|
+
// than one means we have reached the list itself and would take a neighbour's.
|
|
969
|
+
if (!rec.connectedDate) {
|
|
970
|
+
let el = a;
|
|
971
|
+
for (let k = 0; k < 8 && el.parentElement; k++) {
|
|
972
|
+
el = el.parentElement;
|
|
973
|
+
const hits = (el.innerText || '').match(/Connected on [A-Za-z]+\.? \d{1,2}, \d{4}/g);
|
|
974
|
+
if (!hits) continue;
|
|
975
|
+
if (hits.length === 1) rec.connectedDate = hits[0].replace(/^Connected on /, '');
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
return [...byHref.values()].filter(r => r.name && r.name.length >= 2).map(({ _named, ...r }) => r);
|
|
981
|
+
}
|
|
982
|
+
"""
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _reported_connection_count(page):
|
|
986
|
+
"""The "754 connections" line the page prints above the list."""
|
|
987
|
+
try:
|
|
988
|
+
return page.evaluate(
|
|
989
|
+
"""() => { const m = document.body.innerText.match(/([\\d,]+)\\s+connections?/i);
|
|
990
|
+
return m ? parseInt(m[1].replace(/,/g, '')) : null; }"""
|
|
991
|
+
)
|
|
992
|
+
except Exception:
|
|
993
|
+
return None
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def scrape_connections(headless=False, full_walk=False, log_fn=None):
|
|
997
|
+
"""Read your connections from the connections page.
|
|
998
|
+
|
|
999
|
+
full_walk=False is the incremental refresh: it walks from the most recently
|
|
1000
|
+
added and stops once it has seen a long run of people already on file.
|
|
1001
|
+
full_walk=True keeps going to the bottom of the list, which is the way to
|
|
1002
|
+
collect an entire network in one pass.
|
|
1003
|
+
"""
|
|
1004
|
+
from playwright.sync_api import sync_playwright
|
|
1005
|
+
|
|
1006
|
+
say = log_fn or (lambda m: None)
|
|
1007
|
+
# Not a search, so no budget — but after LinkedIn pushed back (a security
|
|
1008
|
+
# check, above all) nothing automated should open it until the cooldown lifts.
|
|
1009
|
+
cd = read_cooldown()
|
|
1010
|
+
if cd:
|
|
1011
|
+
raise CoolingDown(cd)
|
|
1012
|
+
mode = "Full Connection Scrape" if full_walk else "Smart Connection Refresh"
|
|
1013
|
+
print(f"\n=== {mode} ===\n")
|
|
1014
|
+
|
|
1015
|
+
existing_urls = set()
|
|
1016
|
+
if not full_walk:
|
|
1017
|
+
try:
|
|
1018
|
+
params = {"degree": "eq.1", "select": "profile_url", "limit": "5000"}
|
|
1019
|
+
if _active_user_id:
|
|
1020
|
+
params["user_id"] = f"eq.{_active_user_id}"
|
|
1021
|
+
existing_urls = {e["profile_url"] for e in read_connections(params=params)}
|
|
1022
|
+
print(f" {len(existing_urls)} existing connections on file")
|
|
1023
|
+
except Exception:
|
|
1024
|
+
pass
|
|
1025
|
+
|
|
1026
|
+
collected = {}
|
|
1027
|
+
reported = None
|
|
1028
|
+
|
|
1029
|
+
with sync_playwright() as p:
|
|
1030
|
+
browser = p.chromium.launch_persistent_context(
|
|
1031
|
+
user_data_dir=get_scraper_profile_path(),
|
|
1032
|
+
headless=headless,
|
|
1033
|
+
channel="chrome",
|
|
1034
|
+
args=["--disable-blink-features=AutomationControlled"],
|
|
1035
|
+
timeout=120000,
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
page = browser.pages[0] if browser.pages else browser.new_page()
|
|
1039
|
+
page.set_default_timeout(120000)
|
|
1040
|
+
page.set_default_navigation_timeout(120000)
|
|
1041
|
+
|
|
1042
|
+
if not ensure_logged_in(page, log_fn=log_fn):
|
|
1043
|
+
browser.close()
|
|
1044
|
+
return []
|
|
1045
|
+
|
|
1046
|
+
print("Opening connections page...")
|
|
1047
|
+
say("Opening your connections page...")
|
|
1048
|
+
try:
|
|
1049
|
+
page.goto(CONNECTIONS_URL, wait_until="domcontentloaded")
|
|
1050
|
+
except Exception:
|
|
1051
|
+
pass
|
|
1052
|
+
try:
|
|
1053
|
+
# Wait for the list itself, not a fixed sleep: a slow render used to
|
|
1054
|
+
# look identical to an empty network.
|
|
1055
|
+
page.wait_for_selector('a[href*="/in/"]', timeout=60000)
|
|
1056
|
+
except Exception:
|
|
1057
|
+
print(" No connections rendered on the page.")
|
|
1058
|
+
say("No connections rendered — is the page loading?")
|
|
1059
|
+
browser.close()
|
|
1060
|
+
return []
|
|
1061
|
+
time.sleep(3)
|
|
1062
|
+
|
|
1063
|
+
reported = _reported_connection_count(page)
|
|
1064
|
+
if reported:
|
|
1065
|
+
print(f" LinkedIn reports {reported} connections")
|
|
1066
|
+
say(f"LinkedIn reports {reported} connections")
|
|
1067
|
+
|
|
1068
|
+
known_streak = 0
|
|
1069
|
+
stalls = 0
|
|
1070
|
+
extract_errors = 0
|
|
1071
|
+
|
|
1072
|
+
for round_num in range(MAX_SCROLL_ROUNDS):
|
|
1073
|
+
if stop_requested():
|
|
1074
|
+
print(" Stopped — keeping what was collected so far.")
|
|
1075
|
+
break
|
|
1076
|
+
try:
|
|
1077
|
+
page.evaluate(SCROLL_CONTAINER_JS)
|
|
1078
|
+
except Exception:
|
|
1079
|
+
pass
|
|
1080
|
+
try:
|
|
1081
|
+
page.mouse.move(700, 500)
|
|
1082
|
+
page.mouse.wheel(0, 3000)
|
|
1083
|
+
except Exception:
|
|
1084
|
+
pass
|
|
1085
|
+
time.sleep(SCROLL_PAUSE_SECONDS)
|
|
1086
|
+
|
|
1087
|
+
before = len(collected)
|
|
1088
|
+
try:
|
|
1089
|
+
rows = page.evaluate(CONNECTIONS_EXTRACT_JS)
|
|
1090
|
+
extract_errors = 0
|
|
1091
|
+
except Exception as exc:
|
|
1092
|
+
# Never let this look like "you have no connections": say it out
|
|
1093
|
+
# loud and give up rather than reporting a confident zero.
|
|
1094
|
+
rows = []
|
|
1095
|
+
extract_errors += 1
|
|
1096
|
+
if extract_errors == 1:
|
|
1097
|
+
print(f" Could not read the page: {exc}")
|
|
1098
|
+
say("Could not read the connections page.")
|
|
1099
|
+
if extract_errors >= 3:
|
|
1100
|
+
print(" Aborting — the page could not be read three times running.")
|
|
1101
|
+
browser.close()
|
|
1102
|
+
return []
|
|
1103
|
+
for row in rows:
|
|
1104
|
+
url = row.get("profileUrl")
|
|
1105
|
+
if not url or url in collected:
|
|
1106
|
+
continue
|
|
1107
|
+
collected[url] = row
|
|
1108
|
+
if url in existing_urls:
|
|
1109
|
+
known_streak += 1
|
|
1110
|
+
else:
|
|
1111
|
+
known_streak = 0
|
|
1112
|
+
gained = len(collected) - before
|
|
1113
|
+
|
|
1114
|
+
if gained:
|
|
1115
|
+
stalls = 0
|
|
1116
|
+
else:
|
|
1117
|
+
stalls += 1
|
|
1118
|
+
# Older layouts paginate with a button instead of scrolling.
|
|
1119
|
+
try:
|
|
1120
|
+
more = page.locator('button:has-text("Load more"), button:has-text("Show more")').first
|
|
1121
|
+
if more.count() and more.is_visible(timeout=500):
|
|
1122
|
+
more.click()
|
|
1123
|
+
time.sleep(1.5)
|
|
1124
|
+
stalls = 0
|
|
1125
|
+
except Exception:
|
|
1126
|
+
pass
|
|
1127
|
+
|
|
1128
|
+
if gained and len(collected) % 50 < gained:
|
|
1129
|
+
msg = f" {len(collected)}" + (f" / {reported}" if reported else "") + " collected"
|
|
1130
|
+
print(msg, flush=True)
|
|
1131
|
+
say(msg.strip())
|
|
1132
|
+
|
|
1133
|
+
if stalls >= SCROLL_STALL_LIMIT:
|
|
1134
|
+
print(f" Reached the end of the list ({len(collected)} collected).")
|
|
1135
|
+
break
|
|
1136
|
+
if reported and len(collected) >= reported:
|
|
1137
|
+
print(f" Collected all {len(collected)} connections.")
|
|
1138
|
+
break
|
|
1139
|
+
if not full_walk and known_streak >= 20:
|
|
1140
|
+
print(f" Caught up — {known_streak} already on file in a row.")
|
|
1141
|
+
break
|
|
1142
|
+
|
|
1143
|
+
browser.close()
|
|
1144
|
+
|
|
1145
|
+
found = list(collected.values())
|
|
1146
|
+
new_rows = [r for r in found if r["profileUrl"] not in existing_urls] if existing_urls else found
|
|
1147
|
+
with_photos = sum(1 for r in found if r.get("imageUrl"))
|
|
1148
|
+
if existing_urls:
|
|
1149
|
+
print(f"\nCollected {len(found)} connections ({with_photos} with photos); {len(new_rows)} new.")
|
|
1150
|
+
say(f"Collected {len(found)} connections, {len(new_rows)} new")
|
|
1151
|
+
else:
|
|
1152
|
+
# A full walk does not load what is already saved, so it cannot say what is
|
|
1153
|
+
# new — it used to call all 814 "new", then the app said "0 new". The app's
|
|
1154
|
+
# "Sent … → N new" line below is the one that knows.
|
|
1155
|
+
print(f"\nCollected {len(found)} connections ({with_photos} with photos).")
|
|
1156
|
+
say(f"Collected {len(found)} connections")
|
|
1157
|
+
|
|
1158
|
+
# Only meaningful on a full walk: a refresh stops early on purpose.
|
|
1159
|
+
if full_walk and reported and len(found) < reported * 0.5:
|
|
1160
|
+
print(f" WARNING: LinkedIn reported {reported} but only {len(found)} were read.")
|
|
1161
|
+
print(" Re-run, and if it repeats the page layout has probably changed again.")
|
|
1162
|
+
say(f"Warning: only read {len(found)} of {reported}")
|
|
1163
|
+
|
|
1164
|
+
if new_rows:
|
|
1165
|
+
print(f"\nPushing {len(new_rows)} connections to the app...")
|
|
1166
|
+
push_connections(new_rows, degree=1)
|
|
1167
|
+
else:
|
|
1168
|
+
print("\nNothing new to push.")
|
|
1169
|
+
|
|
1170
|
+
print(f"\nDone. Visit {APP_URL} to see your network.")
|
|
1171
|
+
return new_rows
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
BRIDGE_COOLDOWN = 120 # after a real scrape: many page views, be polite
|
|
1175
|
+
BRIDGE_LOAD_ATTEMPTS = 6 # how many times to look for the connections URN
|
|
1176
|
+
BRIDGE_LOAD_STEP = 5 # seconds between looks
|
|
1177
|
+
LINKEDIN_MAX_PAGES = 100 # LinkedIn's people search never goes past page 100
|
|
1178
|
+
END_CHECKS = 3 # looks for another page before calling a list finished
|
|
1179
|
+
SAVE_EVERY_PAGES = 10 # save as a long read goes, so a stop loses little
|
|
1180
|
+
# Pace between searches. 0.1.6 read a page every ~6 s — 27 searches in three and
|
|
1181
|
+
# a half minutes — and LinkedIn blocked the account's search on the 28th. These
|
|
1182
|
+
# are a stopgap until the pacing is set from research (TRAPS §35): a rest before
|
|
1183
|
+
# every search, and a longer one after every SAVE_EVERY_PAGES. Fixed, not
|
|
1184
|
+
# randomised: the point is fewer searches an hour, not looking like a person.
|
|
1185
|
+
PAGE_PAUSE = 20 # seconds before each next page of results
|
|
1186
|
+
CHUNK_COOLDOWN = 60 # extra seconds after every SAVE_EVERY_PAGES pages
|
|
1187
|
+
LEGACY_PAGES_READ = 10 # how far every read before 0.1.6 went, at most
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
# Someone whose connections are hidden can never produce a 2nd-degree row. The
|
|
1191
|
+
# "who still needs bridging" query is "everyone with no 2nd-degree rows", so
|
|
1192
|
+
# without a record of the attempt they come back in the list on every run — and
|
|
1193
|
+
# because the list is sorted by tier, the same person is retried first, forever.
|
|
1194
|
+
# That is what made auto-bridge look stuck rather than slow.
|
|
1195
|
+
def _skips_path():
|
|
1196
|
+
base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees"))
|
|
1197
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
1198
|
+
return base / "bridge-skips.json"
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
def load_bridge_skips():
|
|
1202
|
+
try:
|
|
1203
|
+
return json.loads(_skips_path().read_text())
|
|
1204
|
+
except Exception:
|
|
1205
|
+
return {}
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
def record_bridge_skip(profile_url, name, reason):
|
|
1209
|
+
skips = load_bridge_skips()
|
|
1210
|
+
skips[profile_url] = {
|
|
1211
|
+
"name": name,
|
|
1212
|
+
"reason": reason,
|
|
1213
|
+
"at": datetime.now().isoformat(timespec="seconds"),
|
|
1214
|
+
}
|
|
1215
|
+
try:
|
|
1216
|
+
_skips_path().write_text(json.dumps(skips, indent=2))
|
|
1217
|
+
except Exception as exc:
|
|
1218
|
+
print(f" (could not record the skip: {exc})")
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
# People whose reads came back unclear. Nothing about them is concluded, but
|
|
1222
|
+
# without a count, two of them side by side would stop every batch at the same
|
|
1223
|
+
# place, forever. After UNCLEAR_LIMIT unclear reads they go to the back of the
|
|
1224
|
+
# queue — still tried, just not first. A clear read wipes the count.
|
|
1225
|
+
UNCLEAR_LIMIT = 2
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def _unclear_path():
|
|
1229
|
+
base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees"))
|
|
1230
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
1231
|
+
return base / "bridge-unclear.json"
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def load_unclear():
|
|
1235
|
+
try:
|
|
1236
|
+
data = json.loads(_unclear_path().read_text())
|
|
1237
|
+
return data if isinstance(data, dict) else {}
|
|
1238
|
+
except Exception:
|
|
1239
|
+
return {}
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
def note_unclear(profile_url, clear=False):
|
|
1243
|
+
data = load_unclear()
|
|
1244
|
+
if clear:
|
|
1245
|
+
if profile_url not in data:
|
|
1246
|
+
return
|
|
1247
|
+
data.pop(profile_url, None)
|
|
1248
|
+
else:
|
|
1249
|
+
n = int((data.get(profile_url) or {}).get("n") or 0) + 1
|
|
1250
|
+
data[profile_url] = {"n": n, "at": datetime.now().isoformat(timespec="seconds")}
|
|
1251
|
+
try:
|
|
1252
|
+
_unclear_path().write_text(json.dumps(data, indent=2))
|
|
1253
|
+
except Exception:
|
|
1254
|
+
pass
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
# ---------------------------------------------------------------------------
|
|
1258
|
+
# How much this machine has asked of LinkedIn, and whether it may ask now.
|
|
1259
|
+
#
|
|
1260
|
+
# LinkedIn's limit on people search for free accounts is monthly: it resets at
|
|
1261
|
+
# midnight Pacific on the 1st, with no published number (Help Center a564226;
|
|
1262
|
+
# reports put it around 250–350 a month). Every page of someone's connections
|
|
1263
|
+
# is one search. 0.1.6 spent a night's worth in minutes and search was blocked
|
|
1264
|
+
# (TRAPS §35). So every search and profile view is written down here, and a run
|
|
1265
|
+
# stops — saving what it read, carrying on later from the same page — when the
|
|
1266
|
+
# day's or the month's budget is used. A cooldown lock is set when LinkedIn
|
|
1267
|
+
# pushes back, and nothing searches until it lifts.
|
|
1268
|
+
#
|
|
1269
|
+
# These belong to the LinkedIn account (the one Chrome profile), not to a
|
|
1270
|
+
# profile in the app: three app profiles must not triple the budget. The app's
|
|
1271
|
+
# Scan page reads and edits the same files (lib/linkedin-limits.js).
|
|
1272
|
+
#
|
|
1273
|
+
# linkedin-activity.json {"searches": [epoch s, ...], "profiles": [...]}
|
|
1274
|
+
# scan-limits.json {"daily": 50, "monthly": 250} 0 = no cap
|
|
1275
|
+
# linkedin-cooldown.json {"until": epoch s, "reason": "...", "set_at": ...}
|
|
1276
|
+
# ---------------------------------------------------------------------------
|
|
1277
|
+
DEFAULT_DAILY_SEARCHES = 50
|
|
1278
|
+
DEFAULT_MONTHLY_SEARCHES = 250
|
|
1279
|
+
DAY_SECONDS = 24 * 3600
|
|
1280
|
+
PACIFIC = "America/Los_Angeles"
|
|
1281
|
+
|
|
1282
|
+
|
|
1283
|
+
def _home():
|
|
1284
|
+
base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees"))
|
|
1285
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
1286
|
+
return base
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def _write_json_atomic(path, data):
|
|
1290
|
+
import tempfile
|
|
1291
|
+
fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=path.name + ".", suffix=".tmp")
|
|
1292
|
+
try:
|
|
1293
|
+
with os.fdopen(fd, "w") as f:
|
|
1294
|
+
json.dump(data, f, indent=2)
|
|
1295
|
+
f.flush()
|
|
1296
|
+
os.fsync(f.fileno())
|
|
1297
|
+
os.replace(tmp, path)
|
|
1298
|
+
except Exception:
|
|
1299
|
+
try:
|
|
1300
|
+
os.unlink(tmp)
|
|
1301
|
+
except OSError:
|
|
1302
|
+
pass
|
|
1303
|
+
raise
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
class _Locked:
|
|
1307
|
+
"""An exclusive lock around a read-modify-write, so two runs can't lose counts."""
|
|
1308
|
+
|
|
1309
|
+
def __init__(self, name):
|
|
1310
|
+
self.path = _home() / f".{name}.lock"
|
|
1311
|
+
self.f = None
|
|
1312
|
+
|
|
1313
|
+
def __enter__(self):
|
|
1314
|
+
self.f = open(self.path, "a")
|
|
1315
|
+
try:
|
|
1316
|
+
import fcntl # macOS and Linux; Windows has none
|
|
1317
|
+
fcntl.flock(self.f, fcntl.LOCK_EX)
|
|
1318
|
+
except ImportError:
|
|
1319
|
+
pass
|
|
1320
|
+
return self
|
|
1321
|
+
|
|
1322
|
+
def __exit__(self, *exc):
|
|
1323
|
+
try:
|
|
1324
|
+
import fcntl
|
|
1325
|
+
fcntl.flock(self.f, fcntl.LOCK_UN)
|
|
1326
|
+
except ImportError:
|
|
1327
|
+
pass
|
|
1328
|
+
finally:
|
|
1329
|
+
self.f.close()
|
|
1330
|
+
|
|
1331
|
+
|
|
1332
|
+
def month_start_pacific(now=None):
|
|
1333
|
+
"""Epoch seconds of midnight Pacific on the 1st of this month: when LinkedIn resets."""
|
|
1334
|
+
from zoneinfo import ZoneInfo
|
|
1335
|
+
tz = ZoneInfo(PACIFIC)
|
|
1336
|
+
t = datetime.fromtimestamp(now if now is not None else time.time(), tz)
|
|
1337
|
+
return t.replace(day=1, hour=0, minute=0, second=0, microsecond=0).timestamp()
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def next_month_start_pacific(now=None):
|
|
1341
|
+
from zoneinfo import ZoneInfo
|
|
1342
|
+
tz = ZoneInfo(PACIFIC)
|
|
1343
|
+
t = datetime.fromtimestamp(now if now is not None else time.time(), tz)
|
|
1344
|
+
y, m = (t.year + 1, 1) if t.month == 12 else (t.year, t.month + 1)
|
|
1345
|
+
return datetime(y, m, 1, tzinfo=tz).timestamp()
|
|
1346
|
+
|
|
1347
|
+
|
|
1348
|
+
def _read_activity():
|
|
1349
|
+
"""The record, or — if it can't be read — a record that says the day is used.
|
|
1350
|
+
|
|
1351
|
+
Failing open would read a damaged file as "nothing used yet". The damaged
|
|
1352
|
+
copy is kept beside it.
|
|
1353
|
+
"""
|
|
1354
|
+
path = _home() / "linkedin-activity.json"
|
|
1355
|
+
try:
|
|
1356
|
+
data = json.loads(path.read_text())
|
|
1357
|
+
if not isinstance(data, dict):
|
|
1358
|
+
raise ValueError("not an object")
|
|
1359
|
+
return {"searches": [float(x) for x in data.get("searches", [])],
|
|
1360
|
+
"profiles": [float(x) for x in data.get("profiles", [])]}
|
|
1361
|
+
except FileNotFoundError:
|
|
1362
|
+
return {"searches": [], "profiles": []}
|
|
1363
|
+
except Exception:
|
|
1364
|
+
# Kept aside, and replaced by a record that says today's budget is used —
|
|
1365
|
+
# written, so it holds for the whole day rather than one check. (Failing
|
|
1366
|
+
# open would read a damaged file as nothing searched.)
|
|
1367
|
+
now = time.time()
|
|
1368
|
+
try:
|
|
1369
|
+
path.rename(path.with_name(f"{path.name}.corrupt-{int(now)}"))
|
|
1370
|
+
except OSError:
|
|
1371
|
+
pass
|
|
1372
|
+
print(" (the record of LinkedIn searches couldn't be read; counting today as used)")
|
|
1373
|
+
fresh = {"searches": [now] * max(1, search_limits()["daily"] or 1), "profiles": []}
|
|
1374
|
+
try:
|
|
1375
|
+
_write_json_atomic(path, fresh)
|
|
1376
|
+
except Exception:
|
|
1377
|
+
pass
|
|
1378
|
+
return fresh
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
def charge_linkedin(kind="searches", n=1):
|
|
1382
|
+
"""Write down n searches (or profile views) made just now."""
|
|
1383
|
+
now = time.time()
|
|
1384
|
+
keep_from = month_start_pacific(now) - 32 * DAY_SECONDS
|
|
1385
|
+
with _Locked("linkedin-activity"):
|
|
1386
|
+
data = _read_activity()
|
|
1387
|
+
data[kind] = [t for t in data.get(kind, []) if t >= keep_from] + [now] * n
|
|
1388
|
+
other = "profiles" if kind == "searches" else "searches"
|
|
1389
|
+
data[other] = [t for t in data.get(other, []) if t >= keep_from]
|
|
1390
|
+
try:
|
|
1391
|
+
_write_json_atomic(_home() / "linkedin-activity.json", data)
|
|
1392
|
+
except Exception as exc:
|
|
1393
|
+
print(f" (could not write down LinkedIn activity: {exc})")
|
|
1394
|
+
|
|
1395
|
+
|
|
1396
|
+
def search_limits():
|
|
1397
|
+
try:
|
|
1398
|
+
data = json.loads((_home() / "scan-limits.json").read_text())
|
|
1399
|
+
except Exception:
|
|
1400
|
+
data = {}
|
|
1401
|
+
def num(v, default):
|
|
1402
|
+
try:
|
|
1403
|
+
v = int(v)
|
|
1404
|
+
return v if v >= 0 else default
|
|
1405
|
+
except (TypeError, ValueError):
|
|
1406
|
+
return default
|
|
1407
|
+
return {"daily": num(data.get("daily"), DEFAULT_DAILY_SEARCHES),
|
|
1408
|
+
"monthly": num(data.get("monthly"), DEFAULT_MONTHLY_SEARCHES)}
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
def linkedin_usage(now=None):
|
|
1412
|
+
now = now if now is not None else time.time()
|
|
1413
|
+
data = _read_activity()
|
|
1414
|
+
month0 = month_start_pacific(now)
|
|
1415
|
+
return {
|
|
1416
|
+
"searches_today": sum(1 for t in data["searches"] if t > now - DAY_SECONDS),
|
|
1417
|
+
"searches_month": sum(1 for t in data["searches"] if t >= month0),
|
|
1418
|
+
"profiles_today": sum(1 for t in data["profiles"] if t > now - DAY_SECONDS),
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
def searches_left(now=None):
|
|
1423
|
+
"""(how many searches may still be made, "daily" | "monthly" — whichever runs out first)."""
|
|
1424
|
+
use, lim = linkedin_usage(now), search_limits()
|
|
1425
|
+
left_day = max(0, lim["daily"] - use["searches_today"]) if lim["daily"] else 10 ** 9
|
|
1426
|
+
left_month = max(0, lim["monthly"] - use["searches_month"]) if lim["monthly"] else 10 ** 9
|
|
1427
|
+
return (left_day, "daily") if left_day <= left_month else (left_month, "monthly")
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
def _when(ts):
|
|
1431
|
+
t = datetime.fromtimestamp(ts)
|
|
1432
|
+
hour = t.hour % 12 or 12
|
|
1433
|
+
return f"{t.strftime('%a %b')} {t.day}, {hour}:{t.minute:02d} {'AM' if t.hour < 12 else 'PM'}"
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def _day(ts):
|
|
1437
|
+
t = datetime.fromtimestamp(ts)
|
|
1438
|
+
return f"{t.strftime('%b')} {t.day}"
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
def budget_message(kind):
|
|
1442
|
+
lim = search_limits()
|
|
1443
|
+
if kind == "monthly":
|
|
1444
|
+
when = _day(next_month_start_pacific())
|
|
1445
|
+
return (f"This month's search budget ({lim['monthly']}) is used. It starts again on {when} "
|
|
1446
|
+
f"(LinkedIn's month); the next run carries on from the same page.")
|
|
1447
|
+
return (f"Today's search budget ({lim['daily']}) is used — it counts the last 24 hours. "
|
|
1448
|
+
f"The next run carries on from the same page.")
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def read_cooldown(now=None):
|
|
1452
|
+
"""The active cooldown, or None."""
|
|
1453
|
+
now = now if now is not None else time.time()
|
|
1454
|
+
try:
|
|
1455
|
+
data = json.loads((_home() / "linkedin-cooldown.json").read_text())
|
|
1456
|
+
if float(data.get("until", 0)) > now:
|
|
1457
|
+
return data
|
|
1458
|
+
except Exception:
|
|
1459
|
+
pass
|
|
1460
|
+
return None
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
def set_cooldown(seconds=None, until=None, reason=""):
|
|
1464
|
+
"""Nothing searches LinkedIn until this lifts. Never shortens an existing one."""
|
|
1465
|
+
now = time.time()
|
|
1466
|
+
until = until if until is not None else now + seconds
|
|
1467
|
+
current = read_cooldown(now)
|
|
1468
|
+
if current and float(current["until"]) >= until:
|
|
1469
|
+
return
|
|
1470
|
+
try:
|
|
1471
|
+
_write_json_atomic(_home() / "linkedin-cooldown.json",
|
|
1472
|
+
{"until": until, "reason": reason, "set_at": now})
|
|
1473
|
+
print(f" Scanning paused until {_when(until)}"
|
|
1474
|
+
f" ({reason}). The Scan page can lift it.")
|
|
1475
|
+
except Exception as exc:
|
|
1476
|
+
print(f" (could not set the cooldown: {exc})")
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
def cooldown_message(cd):
|
|
1480
|
+
until = _when(float(cd["until"]))
|
|
1481
|
+
return (f"Scanning is paused until {until} — {cd.get('reason') or 'LinkedIn pushed back'}. "
|
|
1482
|
+
f"Lift it on the Scan page if you're sure LinkedIn is fine.")
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def clear_bridge_skips():
|
|
1486
|
+
try:
|
|
1487
|
+
_skips_path().unlink()
|
|
1488
|
+
print("Cleared the skip list — every bridge will be tried again.")
|
|
1489
|
+
except FileNotFoundError:
|
|
1490
|
+
print("No skips recorded.")
|
|
1491
|
+
|
|
1492
|
+
|
|
1493
|
+
# How far each person's connections have been read.
|
|
1494
|
+
#
|
|
1495
|
+
# Reading someone's whole list can take a hundred pages and a quarter of an hour.
|
|
1496
|
+
# Without a note of where a read got to, a stop, LinkedIn's monthly search limit
|
|
1497
|
+
# or a crash threw all of it away and the next run started again at page 1 — and
|
|
1498
|
+
# before 0.1.6 nobody's list was read past page 10 at all, with no way to go
|
|
1499
|
+
# back for the rest. Every save now notes the last page read and whether
|
|
1500
|
+
# LinkedIn had more, and the next run carries on from the page after. Kept per
|
|
1501
|
+
# profile in the app, beside the skip list. TRAPS §34.
|
|
1502
|
+
#
|
|
1503
|
+
# {"<profile id>": {"<their profile url>": {
|
|
1504
|
+
# "name": ..., "pages": 25, # read through page 25
|
|
1505
|
+
# "more": true, # LinkedIn had a page 26
|
|
1506
|
+
# "urn": ..., # the id their connections are searched by
|
|
1507
|
+
# "hidden": true, # only when their list had gone private
|
|
1508
|
+
# "at": ...}}}
|
|
1509
|
+
def _progress_path():
|
|
1510
|
+
base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees"))
|
|
1511
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
1512
|
+
return base / "bridge-progress.json"
|
|
1513
|
+
|
|
1514
|
+
|
|
1515
|
+
def _progress_owner():
|
|
1516
|
+
return str(_active_user_id or "default")
|
|
1517
|
+
|
|
1518
|
+
|
|
1519
|
+
def _read_progress_file():
|
|
1520
|
+
try:
|
|
1521
|
+
data = json.loads(_progress_path().read_text())
|
|
1522
|
+
return data if isinstance(data, dict) else {}
|
|
1523
|
+
except Exception:
|
|
1524
|
+
return {}
|
|
1525
|
+
|
|
1526
|
+
|
|
1527
|
+
def load_bridge_progress():
|
|
1528
|
+
"""How far each of this profile's people has been read, by profile URL."""
|
|
1529
|
+
mine = _read_progress_file().get(_progress_owner())
|
|
1530
|
+
return mine if isinstance(mine, dict) else {}
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
def _change_progress(change):
|
|
1534
|
+
data = _read_progress_file()
|
|
1535
|
+
mine = data.get(_progress_owner())
|
|
1536
|
+
if not isinstance(mine, dict):
|
|
1537
|
+
mine = {}
|
|
1538
|
+
change(mine)
|
|
1539
|
+
data[_progress_owner()] = mine
|
|
1540
|
+
try:
|
|
1541
|
+
path = _progress_path()
|
|
1542
|
+
tmp = path.with_name(path.name + ".tmp")
|
|
1543
|
+
tmp.write_text(json.dumps(data, indent=2))
|
|
1544
|
+
tmp.replace(path) # never a half-written file
|
|
1545
|
+
except Exception as exc:
|
|
1546
|
+
print(f" (could not note how far this read got: {exc})")
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
def record_bridge_progress(profile_url, name, pages, more, urn=None):
|
|
1550
|
+
"""Note that someone's connections have been read through page `pages`.
|
|
1551
|
+
|
|
1552
|
+
It never winds the note back. A read from page 1 that stops short of an
|
|
1553
|
+
earlier, deeper one leaves the deeper one standing: those pages are still
|
|
1554
|
+
saved. forget_bridge_progress() is for when they are not.
|
|
1555
|
+
"""
|
|
1556
|
+
def change(mine):
|
|
1557
|
+
old = mine.get(profile_url) or {}
|
|
1558
|
+
read, left = int(pages), bool(more)
|
|
1559
|
+
if int(old.get("pages") or 0) > read:
|
|
1560
|
+
read, left = int(old["pages"]), bool(old.get("more"))
|
|
1561
|
+
entry = {"name": name, "pages": read, "more": left,
|
|
1562
|
+
"at": datetime.now().isoformat(timespec="seconds")}
|
|
1563
|
+
if urn or old.get("urn"):
|
|
1564
|
+
entry["urn"] = urn or old.get("urn")
|
|
1565
|
+
mine[profile_url] = entry
|
|
1566
|
+
_change_progress(change)
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def mark_bridge_hidden(profile_url, name):
|
|
1570
|
+
"""Their list was readable once and is not now. What is mapped stays."""
|
|
1571
|
+
def change(mine):
|
|
1572
|
+
old = mine.get(profile_url) or {"pages": LEGACY_PAGES_READ, "more": True}
|
|
1573
|
+
mine[profile_url] = {**old, "name": name, "hidden": True,
|
|
1574
|
+
"at": datetime.now().isoformat(timespec="seconds")}
|
|
1575
|
+
_change_progress(change)
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
def forget_bridge_progress(profile_url):
|
|
1579
|
+
_change_progress(lambda mine: mine.pop(profile_url, None))
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
def next_page_to_read(entry, retry_hidden=False):
|
|
1583
|
+
"""The page to carry on from for someone already mapped, or None if there is nothing left.
|
|
1584
|
+
|
|
1585
|
+
No note at all means they were mapped before 0.1.6 kept one, and every read
|
|
1586
|
+
then stopped at page 10.
|
|
1587
|
+
"""
|
|
1588
|
+
if entry is None:
|
|
1589
|
+
return LEGACY_PAGES_READ + 1
|
|
1590
|
+
if entry.get("hidden") and not retry_hidden:
|
|
1591
|
+
return None
|
|
1592
|
+
if not entry.get("more"):
|
|
1593
|
+
return None
|
|
1594
|
+
nxt = int(entry.get("pages") or 0) + 1
|
|
1595
|
+
return nxt if nxt <= LINKEDIN_MAX_PAGES else None
|
|
1596
|
+
|
|
1597
|
+
|
|
1598
|
+
# Stopping cleanly.
|
|
1599
|
+
#
|
|
1600
|
+
# A long auto-bridge run is the one thing here you will want to interrupt, and
|
|
1601
|
+
# killing the process outright leaves a Chrome window with a live LinkedIn
|
|
1602
|
+
# session orphaned behind it. So SIGTERM sets a flag instead: the loop notices
|
|
1603
|
+
# between people, and while counting down, and exits with a summary.
|
|
1604
|
+
_stop_requested = False
|
|
1605
|
+
|
|
1606
|
+
|
|
1607
|
+
def _request_stop(signum, _frame):
|
|
1608
|
+
global _stop_requested
|
|
1609
|
+
if not _stop_requested:
|
|
1610
|
+
_stop_requested = True
|
|
1611
|
+
print("\n Stopping after the current step — closing the browser cleanly...", flush=True)
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
def install_stop_handler():
|
|
1615
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
1616
|
+
try:
|
|
1617
|
+
signal.signal(sig, _request_stop)
|
|
1618
|
+
except (ValueError, OSError):
|
|
1619
|
+
pass # not the main thread (server mode) — nothing to install
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
def stop_requested():
|
|
1623
|
+
return _stop_requested
|
|
1624
|
+
|
|
1625
|
+
|
|
1626
|
+
def interruptible_sleep(seconds, on_tick=None, step=5):
|
|
1627
|
+
"""Sleep, but notice a stop request. Returns False if we were interrupted."""
|
|
1628
|
+
waited = 0
|
|
1629
|
+
while waited < seconds:
|
|
1630
|
+
if stop_requested():
|
|
1631
|
+
return False
|
|
1632
|
+
chunk = min(step, seconds - waited)
|
|
1633
|
+
time.sleep(chunk)
|
|
1634
|
+
waited += chunk
|
|
1635
|
+
if on_tick and waited < seconds:
|
|
1636
|
+
on_tick(seconds - waited)
|
|
1637
|
+
return not stop_requested()
|
|
1638
|
+
|
|
1639
|
+
|
|
1640
|
+
|
|
1641
|
+
|
|
1642
|
+
def _profile_unavailable(page):
|
|
1643
|
+
"""LinkedIn's explicit "you cannot see this" pages.
|
|
1644
|
+
|
|
1645
|
+
Worth checking separately: these never grow a connections link, so without
|
|
1646
|
+
this the scraper spends its whole budget waiting for something that is not
|
|
1647
|
+
coming.
|
|
1648
|
+
"""
|
|
1649
|
+
try:
|
|
1650
|
+
return page.evaluate("""
|
|
1651
|
+
() => {
|
|
1652
|
+
const t = (document.body.innerText || '').toLowerCase();
|
|
1653
|
+
return t.includes('this profile is not available')
|
|
1654
|
+
|| t.includes('profile unavailable')
|
|
1655
|
+
|| t.includes("this page doesn't exist")
|
|
1656
|
+
|| t.includes('page not found');
|
|
1657
|
+
}
|
|
1658
|
+
""")
|
|
1659
|
+
except Exception:
|
|
1660
|
+
return False
|
|
1661
|
+
|
|
1662
|
+
|
|
1663
|
+
def _find_connection_urn(page):
|
|
1664
|
+
"""The id LinkedIn uses to search someone's connections, read from an href."""
|
|
1665
|
+
try:
|
|
1666
|
+
return page.evaluate("""
|
|
1667
|
+
() => {
|
|
1668
|
+
for (const link of document.querySelectorAll('a')) {
|
|
1669
|
+
const m = (link.href || '').match(/connectionOf[=%5B%22"]*([A-Za-z0-9_:-]+)/);
|
|
1670
|
+
if (m) return m[1];
|
|
1671
|
+
}
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
""")
|
|
1675
|
+
except Exception:
|
|
1676
|
+
return None
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
# What one page of someone's connections holds.
|
|
1680
|
+
#
|
|
1681
|
+
# A raw string (r"""), like every snippet of JavaScript in this file must be. In
|
|
1682
|
+
# a plain string Python turns the "\n" in split('\n') into a real line break
|
|
1683
|
+
# before the browser sees it, and the whole function is a syntax error —
|
|
1684
|
+
# "Invalid or unexpected token". That broke every 2nd-degree scan on page 1 from
|
|
1685
|
+
# 2026-09-09 until it was noticed. tests/scraper-js.test.mjs now parses every
|
|
1686
|
+
# snippet on every PR. TRAPS §31.
|
|
1687
|
+
BRIDGE_RESULTS_JS = r"""
|
|
1688
|
+
() => {
|
|
1689
|
+
// Build URL map from profile links
|
|
1690
|
+
// Anchor on the profile link, never on the anchor's text.
|
|
1691
|
+
//
|
|
1692
|
+
// This used to build a name -> URL map keyed by the link text, first
|
|
1693
|
+
// one wins. Any two results sharing display text collapsed onto a
|
|
1694
|
+
// single URL — and "LinkedIn Member" is the text for every
|
|
1695
|
+
// out-of-network person in a 2nd-degree search, so one person's photo
|
|
1696
|
+
// was handed to everybody who happened to be unnamed. That is where
|
|
1697
|
+
// the duplicate faces came from.
|
|
1698
|
+
const clean = (t) => (t || '').replace(/[\u2019']s profile picture$/i, '').trim();
|
|
1699
|
+
const byHref = new Map();
|
|
1700
|
+
const root = document.querySelector('[role="main"], main') || document.body;
|
|
1701
|
+
|
|
1702
|
+
root.querySelectorAll('a[href*="/in/"]').forEach((a) => {
|
|
1703
|
+
if (a.closest('nav, header, footer')) return;
|
|
1704
|
+
const raw = a.getAttribute('href') || '';
|
|
1705
|
+
if (!raw.includes('/in/')) return;
|
|
1706
|
+
let url = raw.startsWith('http') ? raw : 'https://www.linkedin.com' + raw;
|
|
1707
|
+
url = url.split('?')[0].split('#')[0].replace(/\/+$/, '') + '/';
|
|
1708
|
+
|
|
1709
|
+
let rec = byHref.get(url);
|
|
1710
|
+
if (!rec) { rec = { profileUrl: url, name: '', headline: '', imageUrl: '', named: false }; byHref.set(url, rec); }
|
|
1711
|
+
|
|
1712
|
+
const img = a.querySelector('img');
|
|
1713
|
+
if (img && img.src && img.src.includes('media.licdn.com') && !/ghost/.test(img.src)) {
|
|
1714
|
+
if (!rec.imageUrl) rec.imageUrl = img.src;
|
|
1715
|
+
if (!rec.name && img.alt) rec.name = clean(img.alt);
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
const lines = (a.innerText || '').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
1719
|
+
if (lines.length && !rec.named) {
|
|
1720
|
+
const first = clean(lines[0]);
|
|
1721
|
+
if (first && first.length <= 60 && !/^(Connect|Follow|Message|Invite)$/i.test(first)) {
|
|
1722
|
+
rec.name = first;
|
|
1723
|
+
rec.named = true;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// Headline lives beside the name in the result card, not inside the
|
|
1728
|
+
// anchor — walk up to the card and take the first real line after
|
|
1729
|
+
// the name. Not simply the next line: LinkedIn puts a screen-reader
|
|
1730
|
+
// line ("View Jane Doe's profile") straight after the name, and taking
|
|
1731
|
+
// that saved it as everybody's headline. Skip lines that are chrome.
|
|
1732
|
+
const chrome = (l) => /^View\b.*profile$/i.test(l)
|
|
1733
|
+
|| /^(Connect|Follow|Message|Invite|Pending)$/i.test(l)
|
|
1734
|
+
|| /mutual connection/i.test(l)
|
|
1735
|
+
|| /followers$/i.test(l)
|
|
1736
|
+
|| /degree connection/i.test(l)
|
|
1737
|
+
|| /^(1st|2nd|3rd\+?)$/i.test(l)
|
|
1738
|
+
|| /^\u2022/.test(l);
|
|
1739
|
+
if (!rec.headline) {
|
|
1740
|
+
let card = a;
|
|
1741
|
+
for (let k = 0; k < 6 && card.parentElement; k++) {
|
|
1742
|
+
card = card.parentElement;
|
|
1743
|
+
const cardLines = (card.innerText || '').split('\n').map((l) => l.trim()).filter(Boolean);
|
|
1744
|
+
const idx = cardLines.findIndex((l) => clean(l) === rec.name);
|
|
1745
|
+
if (idx >= 0) {
|
|
1746
|
+
const cand = cardLines.slice(idx + 1, idx + 5).find((l) => !chrome(l) && clean(l) !== rec.name);
|
|
1747
|
+
if (cand) rec.headline = cand;
|
|
1748
|
+
break;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
|
|
1754
|
+
const results = [...byHref.values()]
|
|
1755
|
+
.filter((r) => r.name && r.name.length >= 2)
|
|
1756
|
+
.map(({ named, ...r }) => r);
|
|
1757
|
+
return results;
|
|
1758
|
+
}
|
|
1759
|
+
"""
|
|
1760
|
+
|
|
1761
|
+
# The profile links on the page, as a fingerprint: how a click on Next is known
|
|
1762
|
+
# to have brought up a different page rather than the same one again.
|
|
1763
|
+
RESULT_LINKS_JS = r"""
|
|
1764
|
+
() => {
|
|
1765
|
+
const root = document.querySelector('[role="main"], main') || document.body;
|
|
1766
|
+
const out = new Set();
|
|
1767
|
+
root.querySelectorAll('a[href*="/in/"]').forEach((a) => {
|
|
1768
|
+
if (a.closest('nav, header, footer')) return;
|
|
1769
|
+
out.add((a.getAttribute('href') || '').split('?')[0]);
|
|
1770
|
+
});
|
|
1771
|
+
return [...out].sort();
|
|
1772
|
+
}
|
|
1773
|
+
"""
|
|
1774
|
+
|
|
1775
|
+
# "About 1,340 results" above the list. Only for the log: it is approximate, and
|
|
1776
|
+
# the end of a list is decided by the pages themselves.
|
|
1777
|
+
RESULT_COUNT_JS = r"""
|
|
1778
|
+
() => {
|
|
1779
|
+
const root = document.querySelector('[role="main"], main') || document.body;
|
|
1780
|
+
const text = (root.innerText || '').slice(0, 3000);
|
|
1781
|
+
const m = text.match(/(?:About\s+)?(\d[\d,.]*)\s*([KkMm])?\+?\s+results?\b/);
|
|
1782
|
+
if (!m) return null;
|
|
1783
|
+
let n = parseFloat(m[1].replace(/,/g, ''));
|
|
1784
|
+
if (m[2]) n *= /k/i.test(m[2]) ? 1000 : 1000000;
|
|
1785
|
+
return Number.isFinite(n) && n > 0 ? Math.round(n) : null;
|
|
1786
|
+
}
|
|
1787
|
+
"""
|
|
1788
|
+
|
|
1789
|
+
# LinkedIn's wording when a free account has used its searches for the month.
|
|
1790
|
+
SEARCH_LIMIT_JS = r"""
|
|
1791
|
+
() => {
|
|
1792
|
+
const t = (document.body.innerText || '').toLowerCase();
|
|
1793
|
+
// Reached, not approaching: "approaching the commercial use limit" is a
|
|
1794
|
+
// warning (PUSHBACK_JS), and reporting it as the limit sent people off to
|
|
1795
|
+
// wait for next month.
|
|
1796
|
+
const approaching = t.includes('approaching the commercial use limit');
|
|
1797
|
+
return (t.includes('commercial use limit') && !approaching)
|
|
1798
|
+
|| t.includes('reached the monthly limit')
|
|
1799
|
+
|| t.includes('reached your monthly limit');
|
|
1800
|
+
}
|
|
1801
|
+
"""
|
|
1802
|
+
|
|
1803
|
+
# LinkedIn pushing back, in its own words (Help Center articles a1393432,
|
|
1804
|
+
# a1340567, a1339220) and by where it sends the page. Any of these ends the
|
|
1805
|
+
# batch: carrying on after an "unusual activity" warning or a security check is
|
|
1806
|
+
# what LinkedIn says turns a warning into a restriction. TRAPS §35.
|
|
1807
|
+
PUSHBACK_JS = r"""
|
|
1808
|
+
() => {
|
|
1809
|
+
const path = location.pathname.toLowerCase();
|
|
1810
|
+
if (path.startsWith('/checkpoint')) return 'a security check';
|
|
1811
|
+
if (path.startsWith('/authwall') || path.startsWith('/uas/') || path.startsWith('/login')) return 'being signed out';
|
|
1812
|
+
const t = (document.body.innerText || '').toLowerCase();
|
|
1813
|
+
if (t.includes('unusual activity from your account')) return 'an unusual-activity warning';
|
|
1814
|
+
if (t.includes('ability to view profiles has been temporarily restricted')
|
|
1815
|
+
|| t.includes('profile viewing temporarily restricted')) return 'profile viewing being restricted';
|
|
1816
|
+
if (t.includes('restricted your account') || t.includes('account has been temporarily restricted')) return 'the account being restricted';
|
|
1817
|
+
if (t.includes('approaching the commercial use limit')) return 'the monthly search limit coming up';
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
"""
|
|
1821
|
+
|
|
1822
|
+
# Did their profile page really render? Their first name in the tab title or the
|
|
1823
|
+
# page heading means LinkedIn served the profile — so a missing connections link
|
|
1824
|
+
# is them hiding their list, not a page that failed to load (TRAPS §35).
|
|
1825
|
+
PROFILE_SHOWN_JS = r"""
|
|
1826
|
+
(name) => {
|
|
1827
|
+
// Whole words only, and never against " | LinkedIn": every tab is titled
|
|
1828
|
+
// LinkedIn, and "li", "lin" or "ed" are inside it — a blank page counted as
|
|
1829
|
+
// Li Wei's profile.
|
|
1830
|
+
const norm = (s) => String(s || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
1831
|
+
const first = norm(name).split(' ')[0];
|
|
1832
|
+
if (!first || first.length < 2) return false;
|
|
1833
|
+
const esc = first.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1834
|
+
const word = new RegExp('(^|[^\\p{L}])' + esc + '([^\\p{L}]|$)', 'u');
|
|
1835
|
+
const title = norm(document.title).replace(/^\(\d+\)\s*/, '').replace(/\s*\|\s*linkedin\s*$/, '');
|
|
1836
|
+
const h1 = document.querySelector('main h1, h1');
|
|
1837
|
+
const heading = norm(h1 && h1.innerText);
|
|
1838
|
+
return word.test(heading) || (title !== 'linkedin' && word.test(title));
|
|
1839
|
+
}
|
|
1840
|
+
"""
|
|
1841
|
+
|
|
1842
|
+
# LinkedIn's empty-search message: positive evidence that a list has ended, as
|
|
1843
|
+
# opposed to a page that simply didn't load.
|
|
1844
|
+
NO_RESULTS_JS = r"""
|
|
1845
|
+
() => {
|
|
1846
|
+
const root = document.querySelector('[role="main"], main') || document.body;
|
|
1847
|
+
return /no results found/i.test(root.innerText || '');
|
|
1848
|
+
}
|
|
1849
|
+
"""
|
|
1850
|
+
|
|
1851
|
+
|
|
1852
|
+
def _window_closed(page):
|
|
1853
|
+
"""Closing the browser window is how you say "stop". Treated as a stop."""
|
|
1854
|
+
global _stop_requested
|
|
1855
|
+
try:
|
|
1856
|
+
closed = page.is_closed()
|
|
1857
|
+
except Exception:
|
|
1858
|
+
closed = True
|
|
1859
|
+
if closed and not stop_requested():
|
|
1860
|
+
_stop_requested = True
|
|
1861
|
+
print("\n The browser window was closed — stopping, and keeping what was read.")
|
|
1862
|
+
return closed
|
|
1863
|
+
|
|
1864
|
+
|
|
1865
|
+
def _pushback(page):
|
|
1866
|
+
"""The reason LinkedIn is pushing back, or None."""
|
|
1867
|
+
try:
|
|
1868
|
+
return page.evaluate(PUSHBACK_JS)
|
|
1869
|
+
except Exception:
|
|
1870
|
+
return None
|
|
1871
|
+
|
|
1872
|
+
|
|
1873
|
+
def _keep_pushback_evidence(page, reason):
|
|
1874
|
+
"""Keep what the page showed, so the wording can be recognised next time.
|
|
1875
|
+
|
|
1876
|
+
The real notices have never been captured (PHASES.md). Local only, beside
|
|
1877
|
+
the database; nothing here leaves the machine.
|
|
1878
|
+
"""
|
|
1879
|
+
try:
|
|
1880
|
+
text = page.evaluate("() => (document.body.innerText || '').slice(0, 2000)")
|
|
1881
|
+
except Exception:
|
|
1882
|
+
text = ""
|
|
1883
|
+
try:
|
|
1884
|
+
base = Path(os.environ.get("SIX_DEGREES_HOME") or (Path.home() / ".six-degrees")) / "pushback"
|
|
1885
|
+
base.mkdir(parents=True, exist_ok=True)
|
|
1886
|
+
stamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
|
|
1887
|
+
(base / f"{stamp}.txt").write_text(f"reason: {reason}\nurl: {getattr(page, 'url', '')}\n\n{text}\n")
|
|
1888
|
+
print(f" (What the page showed is kept in {base}/{stamp}.txt)")
|
|
1889
|
+
except Exception:
|
|
1890
|
+
pass
|
|
1891
|
+
|
|
1892
|
+
|
|
1893
|
+
def _pushback_advice(reason):
|
|
1894
|
+
"""What to do next, by what LinkedIn did."""
|
|
1895
|
+
reason = reason or ""
|
|
1896
|
+
if "security check" in reason:
|
|
1897
|
+
if os.environ.get("SIX_DEGREES_FROM_APP"):
|
|
1898
|
+
return ('Finish the check by hand: on the Scan page, step 2, press "Open LinkedIn". '
|
|
1899
|
+
"Then leave scanning for a day.")
|
|
1900
|
+
return "Finish the check by hand: run with --login. Then leave scanning for a day."
|
|
1901
|
+
if "signed out" in reason:
|
|
1902
|
+
return "Sign in again (Open LinkedIn on the Scan page, or --login), then leave it a day."
|
|
1903
|
+
return "Leave it at least a day before scanning again."
|
|
1904
|
+
|
|
1905
|
+
|
|
1906
|
+
def _pushed_back(page, reach, reason):
|
|
1907
|
+
"""Note push-back on this read: the rest is left for later, never marked finished."""
|
|
1908
|
+
reach["pushed_back"] = True
|
|
1909
|
+
reach["pushback_reason"] = reason
|
|
1910
|
+
reach["more"] = True
|
|
1911
|
+
print(f" LinkedIn is pushing back ({reason}). Stopping here.")
|
|
1912
|
+
_keep_pushback_evidence(page, reason)
|
|
1913
|
+
|
|
1914
|
+
|
|
1915
|
+
# Which page the pagination bar says is showing, when it says.
|
|
1916
|
+
CURRENT_PAGE_JS = r"""
|
|
1917
|
+
() => {
|
|
1918
|
+
for (const el of document.querySelectorAll('[aria-current="true"], [aria-current="page"]')) {
|
|
1919
|
+
const t = (el.innerText || el.textContent || '').trim();
|
|
1920
|
+
if (/^\d{1,3}$/.test(t)) return parseInt(t, 10);
|
|
1921
|
+
}
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
"""
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
def _url_page(url):
|
|
1928
|
+
"""The page number in a LinkedIn search URL; page 1 when there is none."""
|
|
1929
|
+
try:
|
|
1930
|
+
return int(parse_qs(urlparse(url).query).get("page", ["1"])[0])
|
|
1931
|
+
except (ValueError, TypeError):
|
|
1932
|
+
return 1
|
|
1933
|
+
|
|
1934
|
+
|
|
1935
|
+
def _result_links(page):
|
|
1936
|
+
try:
|
|
1937
|
+
return page.evaluate(RESULT_LINKS_JS)
|
|
1938
|
+
except Exception:
|
|
1939
|
+
return []
|
|
1940
|
+
|
|
1941
|
+
|
|
1942
|
+
def _scroll_to_foot(page):
|
|
1943
|
+
try:
|
|
1944
|
+
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
|
1945
|
+
except Exception:
|
|
1946
|
+
pass
|
|
1947
|
+
|
|
1948
|
+
|
|
1949
|
+
def _read_results_page(page):
|
|
1950
|
+
"""The people on the current page, looked for END_CHECKS times if it seems empty."""
|
|
1951
|
+
for attempt in range(1, END_CHECKS + 1):
|
|
1952
|
+
found = page.evaluate(BRIDGE_RESULTS_JS)
|
|
1953
|
+
if found:
|
|
1954
|
+
return found
|
|
1955
|
+
if attempt < END_CHECKS:
|
|
1956
|
+
_scroll_to_foot(page)
|
|
1957
|
+
time.sleep(2 * attempt)
|
|
1958
|
+
return []
|
|
1959
|
+
|
|
1960
|
+
|
|
1961
|
+
def _find_next(page):
|
|
1962
|
+
"""LinkedIn's Next button when there is another page, else None.
|
|
1963
|
+
|
|
1964
|
+
Looked for END_CHECKS times, scrolling to the foot of the list and waiting a
|
|
1965
|
+
little longer each time: the pagination bar renders late, and calling a list
|
|
1966
|
+
finished because the button was slow would stop that person short for good.
|
|
1967
|
+
A Next that is there but disabled is LinkedIn saying this is the last page.
|
|
1968
|
+
"""
|
|
1969
|
+
for attempt in range(1, END_CHECKS + 1):
|
|
1970
|
+
try:
|
|
1971
|
+
btn = page.locator('button:has-text("Next")').first
|
|
1972
|
+
if btn.is_visible() and btn.is_enabled():
|
|
1973
|
+
return btn
|
|
1974
|
+
except Exception:
|
|
1975
|
+
pass
|
|
1976
|
+
if attempt < END_CHECKS:
|
|
1977
|
+
print(f" No Next button yet; looking again ({attempt + 1} of {END_CHECKS})...")
|
|
1978
|
+
_scroll_to_foot(page)
|
|
1979
|
+
time.sleep(2 * attempt)
|
|
1980
|
+
return None
|
|
1981
|
+
|
|
1982
|
+
|
|
1983
|
+
def _wait_for_new_results(page, before, seconds):
|
|
1984
|
+
"""True once the profile links differ from `before` — the page really changed."""
|
|
1985
|
+
waited = 0
|
|
1986
|
+
while waited < seconds:
|
|
1987
|
+
time.sleep(1)
|
|
1988
|
+
waited += 1
|
|
1989
|
+
now = _result_links(page)
|
|
1990
|
+
if now and now != before:
|
|
1991
|
+
return True
|
|
1992
|
+
return False
|
|
1993
|
+
|
|
1994
|
+
|
|
1995
|
+
def _advance(page, pg):
|
|
1996
|
+
"""Go from results page pg to pg + 1. Returns "moved", "end" or "stuck".
|
|
1997
|
+
|
|
1998
|
+
"end" only once _find_next has looked END_CHECKS times. A click that leaves
|
|
1999
|
+
the same results showing is tried again, up to END_CHECKS times; after that
|
|
2000
|
+
the read stops as "stuck", which leaves the rest to the next run rather than
|
|
2001
|
+
calling the list finished.
|
|
2002
|
+
"""
|
|
2003
|
+
before = _result_links(page)
|
|
2004
|
+
btn = _find_next(page)
|
|
2005
|
+
if btn is None:
|
|
2006
|
+
# The end of a list is only believed from a live page that was showing
|
|
2007
|
+
# results. A closed window or a blank page proves nothing, and a wrong
|
|
2008
|
+
# "end" marks a list finished for good.
|
|
2009
|
+
if _window_closed(page) or not before:
|
|
2010
|
+
return "stuck"
|
|
2011
|
+
return "end"
|
|
2012
|
+
for attempt in range(1, END_CHECKS + 1):
|
|
2013
|
+
charge_linkedin("searches") # every click that can load a page counts
|
|
2014
|
+
try:
|
|
2015
|
+
btn.click(timeout=15000)
|
|
2016
|
+
except Exception:
|
|
2017
|
+
pass
|
|
2018
|
+
if _wait_for_new_results(page, before, 10):
|
|
2019
|
+
return "moved"
|
|
2020
|
+
if _url_page(page.url) == pg + 1:
|
|
2021
|
+
# The click landed and the page is just slow. Clicking again could
|
|
2022
|
+
# skip a page, so give it longer instead.
|
|
2023
|
+
return "moved" if _wait_for_new_results(page, before, 15) else "stuck"
|
|
2024
|
+
print(f" Page {pg + 1} did not open; clicking Next again ({attempt + 1} of {END_CHECKS})...")
|
|
2025
|
+
btn = _find_next(page)
|
|
2026
|
+
if btn is None:
|
|
2027
|
+
return "stuck"
|
|
2028
|
+
return "stuck"
|
|
2029
|
+
|
|
2030
|
+
|
|
2031
|
+
def _dedupe(connections):
|
|
2032
|
+
"""Keep each profile once: the app refused a whole batch that named anyone twice (TRAPS §32)."""
|
|
2033
|
+
seen, unique = set(), []
|
|
2034
|
+
for c in connections:
|
|
2035
|
+
url = c.get("profileUrl")
|
|
2036
|
+
if url and url not in seen:
|
|
2037
|
+
seen.add(url)
|
|
2038
|
+
unique.append(c)
|
|
2039
|
+
return unique
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
def _page_limit_hint(next_page):
|
|
2043
|
+
if os.environ.get("SIX_DEGREES_FROM_APP"):
|
|
2044
|
+
return (f'To read the rest, set "Read up to" to every page on the Scan page and keep '
|
|
2045
|
+
f'"Also finish people already mapped" ticked: the next run carries on from page {next_page}.')
|
|
2046
|
+
return f"Run again with --deeper, and no --max-pages, to carry on from page {next_page}."
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
def _scrape_one_bridge(page, bridge_name, bridge_id, profile_url, max_pages=LINKEDIN_MAX_PAGES,
|
|
2050
|
+
start_page=1, urn=None, on_save=None):
|
|
2051
|
+
"""
|
|
2052
|
+
Read one person's connections, from start_page until their list ends or
|
|
2053
|
+
max_pages. Takes an already-open Playwright page.
|
|
2054
|
+
|
|
2055
|
+
Returns (connections, status, reach). `connections` is what has not already
|
|
2056
|
+
been handed to on_save(connections, last_page, more, urn), which is called
|
|
2057
|
+
every SAVE_EVERY_PAGES pages of a long read. `reach` says how far it got:
|
|
2058
|
+
last the last page read (start_page - 1 if none)
|
|
2059
|
+
more LinkedIn has pages after `last` that were not read
|
|
2060
|
+
urn the id LinkedIn searches their connections by
|
|
2061
|
+
found how many people were read in all
|
|
2062
|
+
limited LinkedIn's monthly search limit ended the read
|
|
2063
|
+
|
|
2064
|
+
Timing is calibrated from successful runs:
|
|
2065
|
+
- 30s profile render wait (LinkedIn is slow)
|
|
2066
|
+
- 10s after search URL navigation
|
|
2067
|
+
- 3s on each page before reading it
|
|
2068
|
+
- Playwright locator click for Next (not JS — more reliable)
|
|
2069
|
+
"""
|
|
2070
|
+
slug = profile_url.split("/in/")[1].rstrip("/")
|
|
2071
|
+
reach = {"last": start_page - 1, "more": False, "urn": urn, "found": 0, "limited": False,
|
|
2072
|
+
"pushed_back": False}
|
|
2073
|
+
|
|
2074
|
+
if urn:
|
|
2075
|
+
# Known from an earlier read, so their profile needn't be opened again —
|
|
2076
|
+
# one fewer profile view, which is what LinkedIn counts most.
|
|
2077
|
+
print(f" Going straight to {bridge_name}'s connections (their search id is known).")
|
|
2078
|
+
else:
|
|
2079
|
+
# Step 1: Navigate to profile (wait_until="commit" — don't wait for full load)
|
|
2080
|
+
print(f" Opening {bridge_name}'s profile...")
|
|
2081
|
+
charge_linkedin("profiles")
|
|
2082
|
+
try:
|
|
2083
|
+
page.goto(profile_url, wait_until="commit")
|
|
2084
|
+
except Exception as e:
|
|
2085
|
+
print(f" Navigation slow: {str(e)[:50]}... continuing anyway")
|
|
2086
|
+
|
|
2087
|
+
# Steps 2 and 3, together. The URN is what we are actually after, and it is
|
|
2088
|
+
# read from a link's href — so look for it on every pass instead of waiting
|
|
2089
|
+
# for a "500+ connections" link first and only then extracting. A public
|
|
2090
|
+
# profile now finishes as soon as the link exists, and a private one is not
|
|
2091
|
+
# charged an extra scroll-and-wait on the way to the same answer.
|
|
2092
|
+
for attempt in range(BRIDGE_LOAD_ATTEMPTS):
|
|
2093
|
+
time.sleep(BRIDGE_LOAD_STEP)
|
|
2094
|
+
|
|
2095
|
+
why = _pushback(page)
|
|
2096
|
+
if why:
|
|
2097
|
+
_pushed_back(page, reach, why)
|
|
2098
|
+
return [], "pushback", reach
|
|
2099
|
+
|
|
2100
|
+
if _profile_unavailable(page):
|
|
2101
|
+
print(" Profile is not viewable — skipping.")
|
|
2102
|
+
return [], "private", reach
|
|
2103
|
+
|
|
2104
|
+
urn = _find_connection_urn(page)
|
|
2105
|
+
if urn:
|
|
2106
|
+
print(f" Profile loaded ({(attempt + 1) * BRIDGE_LOAD_STEP}s)")
|
|
2107
|
+
break
|
|
2108
|
+
|
|
2109
|
+
# The connections block lazy-loads; nudge it once part-way through.
|
|
2110
|
+
if attempt == BRIDGE_LOAD_ATTEMPTS // 2:
|
|
2111
|
+
try:
|
|
2112
|
+
page.evaluate("window.scrollTo(0, 600)")
|
|
2113
|
+
except Exception:
|
|
2114
|
+
pass
|
|
2115
|
+
elif attempt < BRIDGE_LOAD_ATTEMPTS - 1:
|
|
2116
|
+
print(f" Still loading... ({(attempt + 1) * BRIDGE_LOAD_STEP}s)")
|
|
2117
|
+
|
|
2118
|
+
if not urn:
|
|
2119
|
+
why = _pushback(page)
|
|
2120
|
+
if why:
|
|
2121
|
+
_pushed_back(page, reach, why)
|
|
2122
|
+
return [], "pushback", reach
|
|
2123
|
+
shown = False
|
|
2124
|
+
try:
|
|
2125
|
+
shown = bool(page.evaluate(PROFILE_SHOWN_JS, bridge_name))
|
|
2126
|
+
except Exception:
|
|
2127
|
+
pass
|
|
2128
|
+
if stop_requested() or _window_closed(page):
|
|
2129
|
+
return [], "stopped", reach
|
|
2130
|
+
if not shown:
|
|
2131
|
+
# Their profile never rendered, so this says nothing about them.
|
|
2132
|
+
# Marking them hidden here is what made a block skip-list people
|
|
2133
|
+
# for good; this counts towards the batch's circuit breaker.
|
|
2134
|
+
print(" Their profile didn't load properly, so they're not marked hidden.")
|
|
2135
|
+
return [], "unclear", reach
|
|
2136
|
+
print(f" Could not find URN — connections are private.")
|
|
2137
|
+
return [], "private", reach
|
|
2138
|
+
|
|
2139
|
+
print(f" Found URN: {urn[:30]}...")
|
|
2140
|
+
reach["urn"] = urn
|
|
2141
|
+
|
|
2142
|
+
# Step 4: search everyone they are connected to. network=["F","S","O"] is
|
|
2143
|
+
# 1st, 2nd and 3rd+ — all of them; the app drops your own connections when
|
|
2144
|
+
# it saves. It used to be announced as a "3rd+ filter", which it never was.
|
|
2145
|
+
search_url = f"https://www.linkedin.com/search/results/people/?network=%5B%22F%22%2C%22S%22%2C%22O%22%5D&connectionOf=%5B%22{urn}%22%5D"
|
|
2146
|
+
left, kind = searches_left()
|
|
2147
|
+
if left <= 0:
|
|
2148
|
+
reach["budget"] = kind
|
|
2149
|
+
reach["more"] = True
|
|
2150
|
+
print(" " + budget_message(kind))
|
|
2151
|
+
return [], "budget", reach
|
|
2152
|
+
if start_page > 1:
|
|
2153
|
+
search_url += f"&page={start_page}"
|
|
2154
|
+
print(f" Opening their connections at page {start_page}...")
|
|
2155
|
+
else:
|
|
2156
|
+
print(" Opening their connections...")
|
|
2157
|
+
charge_linkedin("searches")
|
|
2158
|
+
try:
|
|
2159
|
+
page.goto(search_url, wait_until="commit")
|
|
2160
|
+
except:
|
|
2161
|
+
pass
|
|
2162
|
+
|
|
2163
|
+
# Step 5: Wait for search results — 10s initial + up to 30s retry
|
|
2164
|
+
# Successful runs: results appear within 10-15s
|
|
2165
|
+
time.sleep(10)
|
|
2166
|
+
results_loaded = False
|
|
2167
|
+
for attempt in range(6):
|
|
2168
|
+
try:
|
|
2169
|
+
page.wait_for_selector('a[href*="/in/"]', timeout=5000)
|
|
2170
|
+
print(" Search results loaded!")
|
|
2171
|
+
results_loaded = True
|
|
2172
|
+
break
|
|
2173
|
+
except:
|
|
2174
|
+
print(f" Still loading... ({10 + (attempt+1)*5}s)")
|
|
2175
|
+
time.sleep(5)
|
|
2176
|
+
|
|
2177
|
+
try:
|
|
2178
|
+
at_limit = bool(page.evaluate(SEARCH_LIMIT_JS))
|
|
2179
|
+
except Exception:
|
|
2180
|
+
at_limit = False
|
|
2181
|
+
why = _pushback(page)
|
|
2182
|
+
if why:
|
|
2183
|
+
_pushed_back(page, reach, why)
|
|
2184
|
+
return [], "pushback", reach
|
|
2185
|
+
if at_limit:
|
|
2186
|
+
print(" LinkedIn's monthly search limit.")
|
|
2187
|
+
reach["limited"] = reach["more"] = True
|
|
2188
|
+
_keep_pushback_evidence(page, "the monthly search limit")
|
|
2189
|
+
return [], "limited", reach
|
|
2190
|
+
|
|
2191
|
+
if not results_loaded:
|
|
2192
|
+
if stop_requested() or _window_closed(page):
|
|
2193
|
+
reach["more"] = True
|
|
2194
|
+
return [], "stopped", reach
|
|
2195
|
+
try:
|
|
2196
|
+
says_none = bool(page.evaluate(NO_RESULTS_JS))
|
|
2197
|
+
except Exception:
|
|
2198
|
+
says_none = False
|
|
2199
|
+
if says_none:
|
|
2200
|
+
# LinkedIn's own empty state: a real answer, not push-back.
|
|
2201
|
+
print(" LinkedIn says there are no results here.")
|
|
2202
|
+
if start_page > 1:
|
|
2203
|
+
return [], "success", reach # carrying on: their list ended
|
|
2204
|
+
return [], "empty", reach
|
|
2205
|
+
# Their connections link was there, so their list is visible: a search
|
|
2206
|
+
# that won't open is LinkedIn failing us, not them hiding anything.
|
|
2207
|
+
_pushed_back(page, reach, "a search that would not open")
|
|
2208
|
+
return [], "pushback", reach
|
|
2209
|
+
|
|
2210
|
+
if start_page > 1:
|
|
2211
|
+
# Carrying on only works if LinkedIn really opened that page. If it
|
|
2212
|
+
# quietly showed page 1 instead, reading on would note pages as read
|
|
2213
|
+
# that never were.
|
|
2214
|
+
shown = None
|
|
2215
|
+
try:
|
|
2216
|
+
shown = page.evaluate(CURRENT_PAGE_JS)
|
|
2217
|
+
except Exception:
|
|
2218
|
+
pass
|
|
2219
|
+
at = _url_page(page.url)
|
|
2220
|
+
if at != start_page or (shown is not None and shown != start_page):
|
|
2221
|
+
print(f" LinkedIn opened page {shown or at}, not page {start_page}. "
|
|
2222
|
+
"Stopping so no page is noted as read that wasn't.")
|
|
2223
|
+
reach["more"] = True
|
|
2224
|
+
return [], "error", reach
|
|
2225
|
+
|
|
2226
|
+
try:
|
|
2227
|
+
total = page.evaluate(RESULT_COUNT_JS)
|
|
2228
|
+
except Exception:
|
|
2229
|
+
total = None
|
|
2230
|
+
if total:
|
|
2231
|
+
pages = -(-total // 10)
|
|
2232
|
+
print(f" About {total:,} results in their list: {pages} pages"
|
|
2233
|
+
+ (f", and LinkedIn shows the first {LINKEDIN_MAX_PAGES}." if pages > LINKEDIN_MAX_PAGES else "."))
|
|
2234
|
+
until = "until their list ends" if max_pages >= LINKEDIN_MAX_PAGES else f"up to page {max_pages}"
|
|
2235
|
+
print(f" Reading from page {start_page} {until}, saving every {SAVE_EVERY_PAGES} pages.")
|
|
2236
|
+
|
|
2237
|
+
# Step 6: read each page, then move to the next, until the list ends. Every
|
|
2238
|
+
# page is a search on your account.
|
|
2239
|
+
chunk, chunk_pages, pg = [], 0, start_page
|
|
2240
|
+
while True:
|
|
2241
|
+
if stop_requested() or _window_closed(page):
|
|
2242
|
+
reach["more"] = True
|
|
2243
|
+
print(f" Stopped at page {pg}; the next run carries on from there.")
|
|
2244
|
+
break
|
|
2245
|
+
print(f" Page {pg}... ", end="", flush=True)
|
|
2246
|
+
try:
|
|
2247
|
+
time.sleep(3)
|
|
2248
|
+
if page.evaluate(SEARCH_LIMIT_JS):
|
|
2249
|
+
print("LinkedIn's monthly search limit.")
|
|
2250
|
+
reach["limited"] = reach["more"] = True
|
|
2251
|
+
_keep_pushback_evidence(page, "the monthly search limit")
|
|
2252
|
+
break
|
|
2253
|
+
why = _pushback(page)
|
|
2254
|
+
if why:
|
|
2255
|
+
print()
|
|
2256
|
+
_pushed_back(page, reach, why)
|
|
2257
|
+
break
|
|
2258
|
+
page_results = _read_results_page(page)
|
|
2259
|
+
except Exception:
|
|
2260
|
+
if stop_requested() or _window_closed(page): # the browser went down with the stop
|
|
2261
|
+
reach["more"] = True
|
|
2262
|
+
print("stopped.")
|
|
2263
|
+
break
|
|
2264
|
+
raise
|
|
2265
|
+
if not page_results:
|
|
2266
|
+
why = _pushback(page)
|
|
2267
|
+
if why:
|
|
2268
|
+
print()
|
|
2269
|
+
_pushed_back(page, reach, why)
|
|
2270
|
+
break
|
|
2271
|
+
ended = False
|
|
2272
|
+
try:
|
|
2273
|
+
ended = bool(page.evaluate(NO_RESULTS_JS))
|
|
2274
|
+
except Exception:
|
|
2275
|
+
pass
|
|
2276
|
+
if pg == start_page and start_page > 1 and not ended:
|
|
2277
|
+
# Carrying on, and the very first page is blank without saying
|
|
2278
|
+
# "No results found". That is how a search LinkedIn is limiting
|
|
2279
|
+
# can look, so it is not taken as the end: a wrong "finished"
|
|
2280
|
+
# would drop the rest of their list for good. TRAPS §35.
|
|
2281
|
+
reach["more"] = True
|
|
2282
|
+
reach["unsure"] = True
|
|
2283
|
+
print(f"blank. That can mean their list ended, or that LinkedIn is limiting "
|
|
2284
|
+
f"searches, so they are left for the next run to check.")
|
|
2285
|
+
break
|
|
2286
|
+
print(f"nothing on it after {END_CHECKS} looks — that's the end of their list.")
|
|
2287
|
+
break
|
|
2288
|
+
|
|
2289
|
+
page_results = [c for c in page_results if slug not in c.get("profileUrl", "")]
|
|
2290
|
+
chunk.extend(page_results)
|
|
2291
|
+
chunk_pages += 1
|
|
2292
|
+
reach["last"] = pg
|
|
2293
|
+
reach["found"] += len(page_results)
|
|
2294
|
+
print(f"{len(page_results)} found (total: {reach['found']})")
|
|
2295
|
+
|
|
2296
|
+
if pg >= LINKEDIN_MAX_PAGES:
|
|
2297
|
+
print(f" That was page {pg}, as far as LinkedIn's search goes.")
|
|
2298
|
+
break
|
|
2299
|
+
if pg >= max_pages:
|
|
2300
|
+
reach["more"] = _find_next(page) is not None or _window_closed(page)
|
|
2301
|
+
if reach["more"]:
|
|
2302
|
+
print(f" Stopped at the {max_pages}-page limit, and LinkedIn has more. "
|
|
2303
|
+
+ _page_limit_hint(pg + 1))
|
|
2304
|
+
else:
|
|
2305
|
+
print(" No more pages — that's all of their list.")
|
|
2306
|
+
break
|
|
2307
|
+
|
|
2308
|
+
# The budget is checked before every search, and it ends the read on its
|
|
2309
|
+
# own path: through the page-limit branch, a missing Next button would
|
|
2310
|
+
# have marked the list finished.
|
|
2311
|
+
left, kind = searches_left()
|
|
2312
|
+
if left <= 0:
|
|
2313
|
+
# Looking for Next costs nothing. No Next on a live page of results
|
|
2314
|
+
# means the list is finished — not paused at the next page forever.
|
|
2315
|
+
if _find_next(page) is None and not _window_closed(page) and _result_links(page):
|
|
2316
|
+
print(f" No Next button after {END_CHECKS} looks — that's all of their list.")
|
|
2317
|
+
break
|
|
2318
|
+
reach["budget"] = kind
|
|
2319
|
+
reach["more"] = True
|
|
2320
|
+
print(" " + budget_message(kind))
|
|
2321
|
+
break
|
|
2322
|
+
|
|
2323
|
+
# Rest before the next search, and longer after every SAVE_EVERY_PAGES.
|
|
2324
|
+
rest = PAGE_PAUSE
|
|
2325
|
+
if (pg - start_page + 1) % SAVE_EVERY_PAGES == 0:
|
|
2326
|
+
rest += CHUNK_COOLDOWN
|
|
2327
|
+
print(f" {SAVE_EVERY_PAGES} pages read; resting {rest}s before the next search.")
|
|
2328
|
+
if not interruptible_sleep(rest) or _window_closed(page):
|
|
2329
|
+
reach["more"] = True
|
|
2330
|
+
print(f" Stopped before page {pg + 1}; the next run carries on from there.")
|
|
2331
|
+
break
|
|
2332
|
+
|
|
2333
|
+
try:
|
|
2334
|
+
step = _advance(page, pg)
|
|
2335
|
+
except Exception:
|
|
2336
|
+
if not stop_requested():
|
|
2337
|
+
raise
|
|
2338
|
+
step = "stuck"
|
|
2339
|
+
if step == "end":
|
|
2340
|
+
print(f" No Next button after {END_CHECKS} looks — that's all of their list.")
|
|
2341
|
+
break
|
|
2342
|
+
if step == "stuck":
|
|
2343
|
+
reach["more"] = True
|
|
2344
|
+
if not stop_requested() and not _window_closed(page):
|
|
2345
|
+
print(f" Page {pg + 1} would not open after {END_CHECKS} tries. LinkedIn may be "
|
|
2346
|
+
f"limiting searches; the next run carries on from there.")
|
|
2347
|
+
_pushed_back(page, reach, _pushback(page) or "a page that would not open")
|
|
2348
|
+
break
|
|
2349
|
+
pg += 1
|
|
2350
|
+
|
|
2351
|
+
# Save as it goes, so a stop, a crash or the search limit costs at most
|
|
2352
|
+
# the last few pages rather than the whole list.
|
|
2353
|
+
if on_save and chunk_pages >= SAVE_EVERY_PAGES:
|
|
2354
|
+
on_save(_dedupe(chunk), reach["last"], True, reach["urn"])
|
|
2355
|
+
chunk, chunk_pages = [], 0
|
|
2356
|
+
|
|
2357
|
+
# Filter out the bridge themselves (already done page by page) and keep each
|
|
2358
|
+
# profile once: the same person can turn up on more than one page, and a
|
|
2359
|
+
# mutual connection repeats under many results.
|
|
2360
|
+
chunk = _dedupe(chunk)
|
|
2361
|
+
print(f" Read {reach['found']} in all, pages {start_page}–{reach['last']}"
|
|
2362
|
+
if reach["last"] >= start_page else " Nothing read.")
|
|
2363
|
+
|
|
2364
|
+
return chunk, "success", reach
|
|
2365
|
+
|
|
2366
|
+
|
|
2367
|
+
class BridgeRead(list):
|
|
2368
|
+
"""The connections read, plus how the read went (`status`).
|
|
2369
|
+
|
|
2370
|
+
A list, so everything that only wants the people — the CLI, server mode —
|
|
2371
|
+
keeps working. auto_bridge_all reads `status` to tell "their list is hidden"
|
|
2372
|
+
(LinkedIn served their profile, with no connections link) from "unclear"
|
|
2373
|
+
(the profile never rendered), which must never be recorded as hidden.
|
|
2374
|
+
"""
|
|
2375
|
+
status = "success"
|
|
2376
|
+
|
|
2377
|
+
|
|
2378
|
+
def _read(connections, status):
|
|
2379
|
+
out = BridgeRead(connections or [])
|
|
2380
|
+
out.status = status
|
|
2381
|
+
return out
|
|
2382
|
+
|
|
2383
|
+
|
|
2384
|
+
def scrape_bridge(bridge_name, headless=False, max_pages=LINKEDIN_MAX_PAGES, deeper=False, fresh=False,
|
|
2385
|
+
profile_url=None):
|
|
2386
|
+
"""Read one person's connections into the app (opens its own browser).
|
|
2387
|
+
|
|
2388
|
+
deeper: carry on from the page the last read of them stopped at, instead of
|
|
2389
|
+
starting again at page 1.
|
|
2390
|
+
fresh: their mapped circle was just deleted, so forget how far it was read.
|
|
2391
|
+
|
|
2392
|
+
Returns every connection read ([] when none; None when they could not be
|
|
2393
|
+
looked up). Raises SaveFailed if the app refuses a save, NotSignedIn if
|
|
2394
|
+
LinkedIn isn't signed in, and, after saving, SearchLimitReached if
|
|
2395
|
+
LinkedIn's monthly limit ends the read or LinkedInPushedBack if a page
|
|
2396
|
+
would not open.
|
|
2397
|
+
"""
|
|
2398
|
+
from playwright.sync_api import sync_playwright
|
|
2399
|
+
|
|
2400
|
+
# Nothing searches while a cooldown is on, or with no budget left. Checked
|
|
2401
|
+
# before a browser opens: even the sign-in check is a LinkedIn page view.
|
|
2402
|
+
cd = read_cooldown()
|
|
2403
|
+
if cd:
|
|
2404
|
+
raise CoolingDown(cd)
|
|
2405
|
+
left, kind = searches_left()
|
|
2406
|
+
if left <= 0:
|
|
2407
|
+
print(budget_message(kind))
|
|
2408
|
+
raise BudgetReached(0, kind)
|
|
2409
|
+
|
|
2410
|
+
# Find the bridge (read-only, filtered by active user). By profile URL when
|
|
2411
|
+
# given — Resume uses it, since two connections can share a name — else by name.
|
|
2412
|
+
params = ({"profile_url": f"eq.{profile_url}", "degree": "eq.1", "limit": "1"} if profile_url
|
|
2413
|
+
else {"name": f"eq.{bridge_name}", "degree": "eq.1", "limit": "1"})
|
|
2414
|
+
if _active_user_id:
|
|
2415
|
+
params["user_id"] = f"eq.{_active_user_id}"
|
|
2416
|
+
bridges = read_connections(params=params)
|
|
2417
|
+
if not bridges:
|
|
2418
|
+
print(f"Bridge '{bridge_name or profile_url}' not found in database.")
|
|
2419
|
+
return
|
|
2420
|
+
|
|
2421
|
+
bridge = bridges[0]
|
|
2422
|
+
bridge_id = bridge["id"]
|
|
2423
|
+
profile_url = bridge["profile_url"]
|
|
2424
|
+
bridge_name = bridge.get("name") or bridge_name
|
|
2425
|
+
|
|
2426
|
+
if fresh:
|
|
2427
|
+
forget_bridge_progress(profile_url)
|
|
2428
|
+
start_page, urn = 1, None
|
|
2429
|
+
if deeper:
|
|
2430
|
+
entry = load_bridge_progress().get(profile_url)
|
|
2431
|
+
nxt = next_page_to_read(entry, retry_hidden=True)
|
|
2432
|
+
if nxt is None:
|
|
2433
|
+
print(f"Every page of {bridge_name}'s connections has been read already.")
|
|
2434
|
+
return _read([], "finished")
|
|
2435
|
+
if nxt > max_pages:
|
|
2436
|
+
print(f"Pages 1–{nxt - 1} of {bridge_name}'s connections are read already. "
|
|
2437
|
+
f"Choose more than {max_pages} pages to read further.")
|
|
2438
|
+
return _read([], "finished")
|
|
2439
|
+
start_page, urn = nxt, (entry or {}).get("urn")
|
|
2440
|
+
|
|
2441
|
+
print(f"\n=== {'Carrying on with' if start_page > 1 else 'Scraping'} connections of {bridge_name} ===\n")
|
|
2442
|
+
|
|
2443
|
+
read = []
|
|
2444
|
+
|
|
2445
|
+
def save(connections, last, more, urn_now):
|
|
2446
|
+
if not connections:
|
|
2447
|
+
record_bridge_progress(profile_url, bridge_name, last, more, urn_now)
|
|
2448
|
+
return
|
|
2449
|
+
print(f" Saving {len(connections)} (through page {last})...")
|
|
2450
|
+
push_connections(connections, degree=2, bridge_id=bridge_id,
|
|
2451
|
+
on_saved=lambda: record_bridge_progress(profile_url, bridge_name, last, more, urn_now))
|
|
2452
|
+
read.extend(connections)
|
|
2453
|
+
|
|
2454
|
+
outcome = None
|
|
2455
|
+
try:
|
|
2456
|
+
with sync_playwright() as p:
|
|
2457
|
+
browser = p.chromium.launch_persistent_context(
|
|
2458
|
+
user_data_dir=get_scraper_profile_path(),
|
|
2459
|
+
headless=headless,
|
|
2460
|
+
channel="chrome",
|
|
2461
|
+
args=["--disable-blink-features=AutomationControlled"],
|
|
2462
|
+
timeout=120000,
|
|
2463
|
+
)
|
|
2464
|
+
try:
|
|
2465
|
+
page = browser.pages[0] if browser.pages else browser.new_page()
|
|
2466
|
+
page.set_default_timeout(120000)
|
|
2467
|
+
page.set_default_navigation_timeout(120000)
|
|
2468
|
+
|
|
2469
|
+
# No page load before this: ensure_logged_in has to see whether
|
|
2470
|
+
# the session cookie was there *before* LinkedIn answered, or a
|
|
2471
|
+
# sign-out between people looks like a first sign-in.
|
|
2472
|
+
if not ensure_logged_in(page, stop_on_checkpoint=True):
|
|
2473
|
+
raise NotSignedIn()
|
|
2474
|
+
|
|
2475
|
+
outcome = _scrape_one_bridge(page, bridge_name, bridge_id, profile_url,
|
|
2476
|
+
max_pages=max_pages, start_page=start_page, urn=urn,
|
|
2477
|
+
on_save=save)
|
|
2478
|
+
finally:
|
|
2479
|
+
try:
|
|
2480
|
+
browser.close()
|
|
2481
|
+
except Exception:
|
|
2482
|
+
pass
|
|
2483
|
+
except (SaveFailed, NotSignedIn, LinkedInPushedBack):
|
|
2484
|
+
if stop_requested():
|
|
2485
|
+
return _read([], "stopped")
|
|
2486
|
+
raise
|
|
2487
|
+
except Exception:
|
|
2488
|
+
# A stop takes the browser down with it, and closing it can then fail.
|
|
2489
|
+
# If the read itself got to the end, what it found is still saved below.
|
|
2490
|
+
if outcome is None:
|
|
2491
|
+
if stop_requested():
|
|
2492
|
+
return _read([], "stopped")
|
|
2493
|
+
raise
|
|
2494
|
+
|
|
2495
|
+
connections, status, reach = outcome
|
|
2496
|
+
|
|
2497
|
+
if connections:
|
|
2498
|
+
print(f"\nSaving {len(connections)} connections to the app...")
|
|
2499
|
+
save(connections, reach["last"], reach["more"], reach["urn"])
|
|
2500
|
+
elif status == "success" and (start_page > 1 or reach["last"] >= 1):
|
|
2501
|
+
# Nothing unsaved, but where the read ended is still worth noting — for a
|
|
2502
|
+
# read carried on from page 11 that found nothing, it means "finished".
|
|
2503
|
+
record_bridge_progress(profile_url, bridge_name, reach["last"], reach["more"], reach["urn"])
|
|
2504
|
+
elif status == "private" and start_page > 1:
|
|
2505
|
+
# Only reached when their profile clearly rendered without a connections
|
|
2506
|
+
# link; a profile that didn't render is "unclear" and marks nothing.
|
|
2507
|
+
mark_bridge_hidden(profile_url, bridge_name)
|
|
2508
|
+
print(" Their connections are not visible any more — keeping what is mapped.")
|
|
2509
|
+
|
|
2510
|
+
if status == "stopped" or stop_requested():
|
|
2511
|
+
return _read(read, "stopped")
|
|
2512
|
+
if reach["limited"]:
|
|
2513
|
+
set_cooldown(until=next_month_start_pacific(), reason="LinkedIn's monthly search limit")
|
|
2514
|
+
raise SearchLimitReached(len(read))
|
|
2515
|
+
if reach.get("pushed_back"):
|
|
2516
|
+
why = reach.get("pushback_reason") or "a page that would not open"
|
|
2517
|
+
set_cooldown(seconds=DAY_SECONDS, reason=f"LinkedIn pushed back: {why}")
|
|
2518
|
+
raise LinkedInPushedBack(len(read), why)
|
|
2519
|
+
if reach.get("budget"):
|
|
2520
|
+
raise BudgetReached(len(read), reach["budget"])
|
|
2521
|
+
|
|
2522
|
+
if not read:
|
|
2523
|
+
print(f"\nDone! No connections found ({status}).")
|
|
2524
|
+
return _read([], status)
|
|
2525
|
+
|
|
2526
|
+
left = (f" LinkedIn has more; the next run carries on from page {reach['last'] + 1}."
|
|
2527
|
+
if reach["more"] else " That's all of their list.")
|
|
2528
|
+
print(f"\nDone! {len(read)} of {bridge_name}'s connections read and saved.{left}")
|
|
2529
|
+
return _read(read, status)
|
|
2530
|
+
|
|
2531
|
+
|
|
2532
|
+
def scrape_company(company_name, headless=False, log_fn=None):
|
|
2533
|
+
"""Scrape all people at a specific company from LinkedIn search.
|
|
2534
|
+
Returns list of {name, headline, profileUrl, imageUrl}."""
|
|
2535
|
+
from playwright.sync_api import sync_playwright
|
|
2536
|
+
|
|
2537
|
+
def log(msg):
|
|
2538
|
+
print(msg)
|
|
2539
|
+
if log_fn:
|
|
2540
|
+
log_fn(msg)
|
|
2541
|
+
|
|
2542
|
+
log(f"Scraping employees at: {company_name}")
|
|
2543
|
+
|
|
2544
|
+
# A company scan is people searches too: the same cooldown and budget.
|
|
2545
|
+
cd = read_cooldown()
|
|
2546
|
+
if cd:
|
|
2547
|
+
log(cooldown_message(cd))
|
|
2548
|
+
return []
|
|
2549
|
+
if searches_left()[0] <= 0:
|
|
2550
|
+
log(budget_message(searches_left()[1]))
|
|
2551
|
+
return []
|
|
2552
|
+
|
|
2553
|
+
with sync_playwright() as p:
|
|
2554
|
+
browser = p.chromium.launch_persistent_context(
|
|
2555
|
+
user_data_dir=get_scraper_profile_path(),
|
|
2556
|
+
headless=headless,
|
|
2557
|
+
channel="chrome",
|
|
2558
|
+
args=["--disable-blink-features=AutomationControlled"],
|
|
2559
|
+
timeout=120000,
|
|
2560
|
+
)
|
|
2561
|
+
|
|
2562
|
+
page = browser.pages[0] if browser.pages else browser.new_page()
|
|
2563
|
+
page.set_default_timeout(120000)
|
|
2564
|
+
page.set_default_navigation_timeout(120000)
|
|
2565
|
+
|
|
2566
|
+
# Check login
|
|
2567
|
+
try:
|
|
2568
|
+
page.goto("https://www.linkedin.com/", wait_until="domcontentloaded")
|
|
2569
|
+
except:
|
|
2570
|
+
pass
|
|
2571
|
+
if not ensure_logged_in(page):
|
|
2572
|
+
log("Not signed into LinkedIn — sign in and run this again.")
|
|
2573
|
+
browser.close()
|
|
2574
|
+
return []
|
|
2575
|
+
|
|
2576
|
+
# === STEP 1: Find the people search page for this company ===
|
|
2577
|
+
# Strategy: try company page first, fall back to search feed
|
|
2578
|
+
import urllib.parse
|
|
2579
|
+
import re
|
|
2580
|
+
people_url = None
|
|
2581
|
+
company_id = None
|
|
2582
|
+
|
|
2583
|
+
# --- Approach A: Direct company page ---
|
|
2584
|
+
company_slug = company_name.lower().replace(' ', '-').replace('.', '').replace(',', '')
|
|
2585
|
+
company_page_url = f"https://www.linkedin.com/company/{urllib.parse.quote(company_slug)}/"
|
|
2586
|
+
log(f" Trying company page: {company_slug}")
|
|
2587
|
+
try:
|
|
2588
|
+
page.goto(company_page_url, wait_until="commit")
|
|
2589
|
+
except:
|
|
2590
|
+
pass
|
|
2591
|
+
time.sleep(6)
|
|
2592
|
+
|
|
2593
|
+
# Check if the company page loaded (not a 404 or unavailable)
|
|
2594
|
+
page_ok = page.evaluate("""
|
|
2595
|
+
() => {
|
|
2596
|
+
const title = document.title || '';
|
|
2597
|
+
const body = document.body?.innerText || '';
|
|
2598
|
+
if (title.includes('Page not found') || title.includes('404')) return false;
|
|
2599
|
+
if (body.includes('page doesn') || body.includes('not available') || body.includes('doesn\\'t exist')) return false;
|
|
2600
|
+
// Check if we see a company name heading
|
|
2601
|
+
const h1 = document.querySelector('h1');
|
|
2602
|
+
return h1 && h1.textContent.trim().length > 0;
|
|
2603
|
+
}
|
|
2604
|
+
""")
|
|
2605
|
+
|
|
2606
|
+
if page_ok:
|
|
2607
|
+
log(f" Company page loaded ✓")
|
|
2608
|
+
# Extract people URL from company page ("See all X employees" link)
|
|
2609
|
+
people_url = page.evaluate("""
|
|
2610
|
+
() => {
|
|
2611
|
+
const links = document.querySelectorAll('a');
|
|
2612
|
+
for (const link of links) {
|
|
2613
|
+
const h = link.href || '';
|
|
2614
|
+
if (h.includes('currentCompany') && h.includes('/search/results/people/')) {
|
|
2615
|
+
return h;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
// Try extracting numeric company ID from page source
|
|
2619
|
+
const scripts = document.querySelectorAll('script, code');
|
|
2620
|
+
for (const s of scripts) {
|
|
2621
|
+
const text = s.textContent || '';
|
|
2622
|
+
const match = text.match(/"companyId":(\\d+)/);
|
|
2623
|
+
if (match) {
|
|
2624
|
+
return 'https://www.linkedin.com/search/results/people/?currentCompany=%5B%22' + match[1] + '%22%5D&origin=FACETED_SEARCH';
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
return null;
|
|
2628
|
+
}
|
|
2629
|
+
""")
|
|
2630
|
+
if people_url:
|
|
2631
|
+
log(f" Got people URL from company page")
|
|
2632
|
+
else:
|
|
2633
|
+
log(f" Company page unavailable, using search fallback...")
|
|
2634
|
+
|
|
2635
|
+
# --- Approach B: Search feed fallback ---
|
|
2636
|
+
if not people_url:
|
|
2637
|
+
log(f" Searching LinkedIn feed for: {company_name}")
|
|
2638
|
+
search_url = f"https://www.linkedin.com/search/results/all/?keywords={urllib.parse.quote(company_name)}"
|
|
2639
|
+
try:
|
|
2640
|
+
charge_linkedin("searches")
|
|
2641
|
+
page.goto(search_url, wait_until="commit")
|
|
2642
|
+
except:
|
|
2643
|
+
pass
|
|
2644
|
+
time.sleep(8)
|
|
2645
|
+
|
|
2646
|
+
# Look for People section links with currentCompany
|
|
2647
|
+
people_url = page.evaluate("""
|
|
2648
|
+
() => {
|
|
2649
|
+
const links = document.querySelectorAll('a');
|
|
2650
|
+
for (const link of links) {
|
|
2651
|
+
const h = link.href || '';
|
|
2652
|
+
if (h.includes('currentCompany') && h.includes('/search/results/people/')) {
|
|
2653
|
+
return h;
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
for (const link of links) {
|
|
2657
|
+
const text = (link.textContent || '').trim().toLowerCase();
|
|
2658
|
+
const h = link.href || '';
|
|
2659
|
+
if ((text.includes('see all') && text.includes('people')) && h.includes('/search/results/people/')) {
|
|
2660
|
+
return h;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
return null;
|
|
2664
|
+
}
|
|
2665
|
+
""")
|
|
2666
|
+
|
|
2667
|
+
if not people_url:
|
|
2668
|
+
# Scroll down — People section may be below fold
|
|
2669
|
+
log(f" Scrolling to find People section...")
|
|
2670
|
+
page.evaluate("window.scrollTo(0, document.body.scrollHeight / 2)")
|
|
2671
|
+
time.sleep(3)
|
|
2672
|
+
people_url = page.evaluate("""
|
|
2673
|
+
() => {
|
|
2674
|
+
const links = document.querySelectorAll('a');
|
|
2675
|
+
for (const link of links) {
|
|
2676
|
+
const h = link.href || '';
|
|
2677
|
+
if (h.includes('currentCompany') && h.includes('/search/results/people/')) {
|
|
2678
|
+
return h;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
return null;
|
|
2682
|
+
}
|
|
2683
|
+
""")
|
|
2684
|
+
|
|
2685
|
+
if not people_url:
|
|
2686
|
+
# Try clicking the "People" tab on the search page
|
|
2687
|
+
log(f" Trying People tab...")
|
|
2688
|
+
try:
|
|
2689
|
+
people_btn = page.locator('button:has-text("People")').first
|
|
2690
|
+
if people_btn.is_visible(timeout=3000):
|
|
2691
|
+
charge_linkedin("searches")
|
|
2692
|
+
people_btn.click()
|
|
2693
|
+
time.sleep(5)
|
|
2694
|
+
current = page.url
|
|
2695
|
+
if 'currentCompany' in current:
|
|
2696
|
+
people_url = current
|
|
2697
|
+
except:
|
|
2698
|
+
pass
|
|
2699
|
+
|
|
2700
|
+
if people_url:
|
|
2701
|
+
log(f" Got people URL from search feed")
|
|
2702
|
+
|
|
2703
|
+
if not people_url:
|
|
2704
|
+
log(f"Could not find people for '{company_name}' via company page or search")
|
|
2705
|
+
browser.close()
|
|
2706
|
+
return []
|
|
2707
|
+
|
|
2708
|
+
# === STEP 2: Extract company ID and navigate to people search ===
|
|
2709
|
+
id_match = re.search(r'currentCompany=%5B%22(\d+)%22%5D', people_url)
|
|
2710
|
+
if not id_match:
|
|
2711
|
+
id_match = re.search(r'currentCompany=\[%22(\d+)%22\]', people_url)
|
|
2712
|
+
if id_match:
|
|
2713
|
+
company_id = id_match.group(1)
|
|
2714
|
+
log(f" Company ID: {company_id}")
|
|
2715
|
+
|
|
2716
|
+
# Build a clean base URL — strip any existing network filter
|
|
2717
|
+
# We'll select all degree tabs on the page instead
|
|
2718
|
+
base_url = re.sub(r'[&?]network=[^&]*', '', people_url)
|
|
2719
|
+
# Clean up any double && or trailing ?&
|
|
2720
|
+
base_url = base_url.replace('&&', '&').replace('?&', '?').rstrip('?').rstrip('&')
|
|
2721
|
+
|
|
2722
|
+
log(f" Opening people search...")
|
|
2723
|
+
try:
|
|
2724
|
+
charge_linkedin("searches")
|
|
2725
|
+
page.goto(base_url, wait_until="commit")
|
|
2726
|
+
except:
|
|
2727
|
+
pass
|
|
2728
|
+
time.sleep(8)
|
|
2729
|
+
|
|
2730
|
+
# === STEP 3: Select ALL degree tabs (1st + 2nd + 3rd) ===
|
|
2731
|
+
# LinkedIn shows degree filter buttons at the top of search results.
|
|
2732
|
+
# We need to click each one that isn't already selected.
|
|
2733
|
+
log(f" Selecting all degree filters...")
|
|
2734
|
+
|
|
2735
|
+
# Click each degree button: "1st", "2nd", "3rd+"
|
|
2736
|
+
for degree_label in ['1st', '2nd', '3rd\\+', '3rd']:
|
|
2737
|
+
try:
|
|
2738
|
+
btn = page.locator(f'button:has-text("{degree_label.replace(chr(92), "")}")').first
|
|
2739
|
+
if btn.is_visible(timeout=2000):
|
|
2740
|
+
# Check if already active/selected
|
|
2741
|
+
is_active = btn.evaluate("""
|
|
2742
|
+
el => {
|
|
2743
|
+
const cl = el.className || '';
|
|
2744
|
+
const aria = el.getAttribute('aria-pressed') || el.getAttribute('aria-checked') || '';
|
|
2745
|
+
return cl.includes('active') || cl.includes('selected') || aria === 'true';
|
|
2746
|
+
}
|
|
2747
|
+
""")
|
|
2748
|
+
if not is_active:
|
|
2749
|
+
charge_linkedin("searches")
|
|
2750
|
+
btn.click()
|
|
2751
|
+
log(f" Clicked {degree_label.replace(chr(92), '')} ✓")
|
|
2752
|
+
time.sleep(2)
|
|
2753
|
+
else:
|
|
2754
|
+
log(f" {degree_label.replace(chr(92), '')} already selected ✓")
|
|
2755
|
+
except:
|
|
2756
|
+
pass
|
|
2757
|
+
|
|
2758
|
+
# Give LinkedIn time to reload results with all filters
|
|
2759
|
+
time.sleep(5)
|
|
2760
|
+
|
|
2761
|
+
# Verify the URL now has the network filter — if not, the buttons
|
|
2762
|
+
# might be pills/links instead. Try clicking connection degree links.
|
|
2763
|
+
current_url = page.url
|
|
2764
|
+
if 'network' not in current_url:
|
|
2765
|
+
log(f" Buttons didn't set filter, trying degree links...")
|
|
2766
|
+
# Some LinkedIn layouts use links instead of buttons for degree filters
|
|
2767
|
+
degree_links = page.evaluate("""
|
|
2768
|
+
() => {
|
|
2769
|
+
const results = [];
|
|
2770
|
+
document.querySelectorAll('a, button').forEach(el => {
|
|
2771
|
+
const text = (el.textContent || '').trim();
|
|
2772
|
+
if (/^(1st|2nd|3rd\\+?)$/.test(text)) {
|
|
2773
|
+
results.push(text);
|
|
2774
|
+
el.click();
|
|
2775
|
+
}
|
|
2776
|
+
});
|
|
2777
|
+
return results;
|
|
2778
|
+
}
|
|
2779
|
+
""")
|
|
2780
|
+
if degree_links:
|
|
2781
|
+
charge_linkedin("searches", len(degree_links)) # each click reloads the results
|
|
2782
|
+
log(f" Clicked degree links: {degree_links}")
|
|
2783
|
+
time.sleep(5)
|
|
2784
|
+
|
|
2785
|
+
# If STILL no network filter and we have the company ID, build the URL manually
|
|
2786
|
+
current_url = page.url
|
|
2787
|
+
if 'network' not in current_url and company_id:
|
|
2788
|
+
log(f" Degree buttons not found — using URL with all degrees")
|
|
2789
|
+
all_degrees_url = f"https://www.linkedin.com/search/results/people/?currentCompany=%5B%22{company_id}%22%5D&network=%5B%22F%22%2C%22S%22%2C%22O%22%5D&origin=FACETED_SEARCH"
|
|
2790
|
+
try:
|
|
2791
|
+
charge_linkedin("searches")
|
|
2792
|
+
page.goto(all_degrees_url, wait_until="commit")
|
|
2793
|
+
except:
|
|
2794
|
+
pass
|
|
2795
|
+
time.sleep(8)
|
|
2796
|
+
|
|
2797
|
+
# === STEP 4: Paginate and scrape all results ===
|
|
2798
|
+
all_people = []
|
|
2799
|
+
seen_urls = set()
|
|
2800
|
+
page_num = 1
|
|
2801
|
+
|
|
2802
|
+
while page_num <= 20:
|
|
2803
|
+
pass
|
|
2804
|
+
log(f" Page {page_num}...")
|
|
2805
|
+
time.sleep(3)
|
|
2806
|
+
|
|
2807
|
+
page_data = page.evaluate("""
|
|
2808
|
+
() => {
|
|
2809
|
+
const urlMap = {};
|
|
2810
|
+
const imgMap = {};
|
|
2811
|
+
document.querySelectorAll('img').forEach(img => {
|
|
2812
|
+
if (!img.src || !img.src.includes('media.licdn.com')) return;
|
|
2813
|
+
const link = img.closest('a');
|
|
2814
|
+
if (link) {
|
|
2815
|
+
const h = link.getAttribute('href') || '';
|
|
2816
|
+
if (h.includes('/in/')) {
|
|
2817
|
+
const url = h.startsWith('http') ? h.split('?')[0] : 'https://www.linkedin.com' + h.split('?')[0];
|
|
2818
|
+
imgMap[url] = img.src;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
});
|
|
2822
|
+
document.querySelectorAll('a').forEach(a => {
|
|
2823
|
+
const h = a.getAttribute('href') || '';
|
|
2824
|
+
if (!h.includes('/in/')) return;
|
|
2825
|
+
const url = h.startsWith('http') ? h.split('?')[0] : 'https://www.linkedin.com' + h.split('?')[0];
|
|
2826
|
+
const img = a.querySelector('img');
|
|
2827
|
+
if (img && img.src && img.src.includes('media.licdn.com')) { imgMap[url] = img.src; return; }
|
|
2828
|
+
const text = a.textContent.trim();
|
|
2829
|
+
if (!text || text.length > 60 || text.includes('mutual') || text.includes('Connect')) return;
|
|
2830
|
+
if (!urlMap[text]) urlMap[text] = url;
|
|
2831
|
+
});
|
|
2832
|
+
const main = document.querySelector('[role="main"], main');
|
|
2833
|
+
const pageText = main ? main.innerText : '';
|
|
2834
|
+
const lines = pageText.split('\\n').map(l => l.trim());
|
|
2835
|
+
const results = [];
|
|
2836
|
+
let i = 0;
|
|
2837
|
+
while (i < lines.length) {
|
|
2838
|
+
if (urlMap[lines[i]]) {
|
|
2839
|
+
const name = lines[i], url = urlMap[name];
|
|
2840
|
+
let headline = '';
|
|
2841
|
+
let j = i + 1;
|
|
2842
|
+
while (j < lines.length && j < i + 8) {
|
|
2843
|
+
const l = lines[j].trim();
|
|
2844
|
+
if (!l || l.match(/^\\u2022\\s*(1st|2nd|3rd)/) || l === name) { j++; continue; }
|
|
2845
|
+
if (l === 'Connect' || l === 'Follow' || l === 'Message' || l.includes('mutual connection')) break;
|
|
2846
|
+
if (!headline) { headline = l; j++; continue; }
|
|
2847
|
+
j++;
|
|
2848
|
+
}
|
|
2849
|
+
results.push({ name, headline, profileUrl: url, imageUrl: imgMap[url] || '' });
|
|
2850
|
+
}
|
|
2851
|
+
i++;
|
|
2852
|
+
}
|
|
2853
|
+
return results;
|
|
2854
|
+
}
|
|
2855
|
+
""")
|
|
2856
|
+
|
|
2857
|
+
new_on_page = 0
|
|
2858
|
+
for p in page_data:
|
|
2859
|
+
url = p.get('profileUrl', '')
|
|
2860
|
+
if url and url not in seen_urls:
|
|
2861
|
+
seen_urls.add(url)
|
|
2862
|
+
all_people.append(p)
|
|
2863
|
+
new_on_page += 1
|
|
2864
|
+
|
|
2865
|
+
img_count = sum(1 for p in page_data if p.get('imageUrl'))
|
|
2866
|
+
log(f" {len(page_data)} on page, {new_on_page} new ({img_count} photos) — total: {len(all_people)}")
|
|
2867
|
+
|
|
2868
|
+
if len(page_data) == 0:
|
|
2869
|
+
break
|
|
2870
|
+
|
|
2871
|
+
# Click Next
|
|
2872
|
+
try:
|
|
2873
|
+
next_btn = page.locator('button:has-text("Next")').first
|
|
2874
|
+
if next_btn.is_visible(timeout=3000):
|
|
2875
|
+
if searches_left()[0] <= 0: # before paying for a page, not after
|
|
2876
|
+
log(budget_message(searches_left()[1]))
|
|
2877
|
+
break
|
|
2878
|
+
charge_linkedin("searches")
|
|
2879
|
+
next_btn.click()
|
|
2880
|
+
page_num += 1
|
|
2881
|
+
time.sleep(3)
|
|
2882
|
+
else:
|
|
2883
|
+
break
|
|
2884
|
+
except:
|
|
2885
|
+
break
|
|
2886
|
+
|
|
2887
|
+
log(f"Total: {len(all_people)} people found at {company_name}")
|
|
2888
|
+
browser.close()
|
|
2889
|
+
|
|
2890
|
+
return all_people
|
|
2891
|
+
|
|
2892
|
+
|
|
2893
|
+
def auto_bridge_all(headless=False, log_fn=None, retry_private=False, max_bridges=0, tiers=None,
|
|
2894
|
+
order="newest", max_pages=LINKEDIN_MAX_PAGES, deeper=False, only_unfinished=False):
|
|
2895
|
+
"""Map everyone whose circle is not mapped yet, in the order chosen.
|
|
2896
|
+
|
|
2897
|
+
Picks up from where it left off. With deeper, it also finishes people
|
|
2898
|
+
already mapped whose list goes on, from the page their last read stopped at.
|
|
2899
|
+
"""
|
|
2900
|
+
|
|
2901
|
+
def log(msg):
|
|
2902
|
+
print(msg)
|
|
2903
|
+
if log_fn:
|
|
2904
|
+
log_fn(msg)
|
|
2905
|
+
|
|
2906
|
+
cd = read_cooldown()
|
|
2907
|
+
if cd:
|
|
2908
|
+
log(cooldown_message(cd))
|
|
2909
|
+
return []
|
|
2910
|
+
left, kind = searches_left()
|
|
2911
|
+
if left <= 0:
|
|
2912
|
+
log(budget_message(kind))
|
|
2913
|
+
return []
|
|
2914
|
+
use, lim = linkedin_usage(), search_limits()
|
|
2915
|
+
log(f"Search budget: {use['searches_today']} of {lim['daily'] or 'no limit'} used today, "
|
|
2916
|
+
f"{use['searches_month']} of {lim['monthly'] or 'no limit'} this month")
|
|
2917
|
+
|
|
2918
|
+
# Get all degree-1 connections (filtered by active user)
|
|
2919
|
+
D1_LIMIT = 20000
|
|
2920
|
+
newest = order != "score"
|
|
2921
|
+
# "added" is the order rows were saved in: each scan saves LinkedIn's list
|
|
2922
|
+
# top to bottom, and that list is "recently added" first.
|
|
2923
|
+
d1_params = {"degree": "eq.1", "select": "id,name,tier,power_score,profile_url,created_at,connected_date",
|
|
2924
|
+
"order": "added" if newest else "power_score.desc", "limit": str(D1_LIMIT)}
|
|
2925
|
+
if _active_user_id:
|
|
2926
|
+
d1_params["user_id"] = f"eq.{_active_user_id}"
|
|
2927
|
+
all_d1 = read_connections(params=d1_params)
|
|
2928
|
+
if len(all_d1) >= D1_LIMIT:
|
|
2929
|
+
log(f"WARNING: only the first {D1_LIMIT} connections were read; some will be missed.")
|
|
2930
|
+
|
|
2931
|
+
# Who already has a mapped circle. Asked as one row per bridge rather than
|
|
2932
|
+
# by pulling every 2nd-degree row and de-duplicating — that carried a limit
|
|
2933
|
+
# of 2000, so past that many the answer was quietly wrong and a resumed run
|
|
2934
|
+
# re-scraped people it had already done.
|
|
2935
|
+
bridged_ids = read_bridged_ids()
|
|
2936
|
+
|
|
2937
|
+
# Someone counts as mapped once any of their list has been read, even if
|
|
2938
|
+
# everyone on it was already one of your connections and nothing was saved —
|
|
2939
|
+
# otherwise they would be read again from page 1 on every run.
|
|
2940
|
+
progress = load_bridge_progress()
|
|
2941
|
+
mapped = set(bridged_ids) | {c["id"] for c in all_d1 if c.get("profile_url") in progress}
|
|
2942
|
+
|
|
2943
|
+
# Filter unbridged, sort by tier priority (S first, then by score)
|
|
2944
|
+
tier_order = {"S": 0, "A": 1, "B": 2, "C": 3, "D": 4}
|
|
2945
|
+
unbridged = [c for c in all_d1 if c["id"] not in mapped]
|
|
2946
|
+
|
|
2947
|
+
# People already found to be private are not tried again. Without this they
|
|
2948
|
+
# reappear every run, in the same place, and the run never gets past them.
|
|
2949
|
+
skips = {} if retry_private else load_bridge_skips()
|
|
2950
|
+
skipped_now = [c for c in unbridged if c.get("profile_url") in skips]
|
|
2951
|
+
unbridged = [c for c in unbridged if c.get("profile_url") not in skips]
|
|
2952
|
+
|
|
2953
|
+
# People already mapped whose list goes on past where it was read.
|
|
2954
|
+
unfinished = []
|
|
2955
|
+
for c in all_d1:
|
|
2956
|
+
if c["id"] not in mapped:
|
|
2957
|
+
continue
|
|
2958
|
+
nxt = next_page_to_read(progress.get(c.get("profile_url")), retry_hidden=retry_private)
|
|
2959
|
+
if nxt is not None and nxt <= max_pages:
|
|
2960
|
+
unfinished.append({**c, "_from_page": nxt})
|
|
2961
|
+
if only_unfinished:
|
|
2962
|
+
# "Resume all": only people whose read was cut short, nobody new.
|
|
2963
|
+
unbridged = list(unfinished)
|
|
2964
|
+
deeper = True
|
|
2965
|
+
elif deeper:
|
|
2966
|
+
unbridged = unbridged + unfinished
|
|
2967
|
+
|
|
2968
|
+
# Choosing tiers rather than "everything" or "only what is new".
|
|
2969
|
+
#
|
|
2970
|
+
# New connections need no special mode: the list of who still needs bridging
|
|
2971
|
+
# is recomputed from the data every run, so anyone added since simply
|
|
2972
|
+
# appears in it, sorted into their tier. What is worth choosing is how far
|
|
2973
|
+
# down the list to go — most people never want to map their D-tier circles,
|
|
2974
|
+
# and that choice is the one that costs rate limit.
|
|
2975
|
+
# Newest-first goes strictly by the date you connected, across every tier —
|
|
2976
|
+
# that is what "my most recent connection" means. Tiers choose who is included
|
|
2977
|
+
# in highest-tier-first order.
|
|
2978
|
+
if tiers and newest:
|
|
2979
|
+
log("Going by the date you connected, so every tier is included")
|
|
2980
|
+
if tiers and not newest:
|
|
2981
|
+
wanted = {t.strip().upper() for t in tiers if t and t.strip()}
|
|
2982
|
+
before = len(unbridged)
|
|
2983
|
+
unbridged = [c for c in unbridged if (c.get("tier") or "D").upper() in wanted]
|
|
2984
|
+
log(f"Limiting to {'/'.join(sorted(wanted, key=lambda t: tier_order.get(t, 9)))}-tier "
|
|
2985
|
+
f"({len(unbridged)} of {before} outstanding)")
|
|
2986
|
+
|
|
2987
|
+
if newest:
|
|
2988
|
+
# Your newest connections first, by LinkedIn's "Connected on" date. People
|
|
2989
|
+
# without one yet (saved before dates were captured; one full scan fills
|
|
2990
|
+
# them in) come after, in the order they were saved — LinkedIn's own
|
|
2991
|
+
# "recently added" order within a scan. sort() is stable, so reverse=True
|
|
2992
|
+
# keeps that order among equal keys.
|
|
2993
|
+
unbridged.sort(key=lambda c: (1 if c.get("connected_date") else 0,
|
|
2994
|
+
c.get("connected_date") or c.get("created_at") or ""),
|
|
2995
|
+
reverse=True)
|
|
2996
|
+
undated = sum(1 for c in unbridged if not c.get("connected_date"))
|
|
2997
|
+
log("Order: newest connections first, by the date you connected")
|
|
2998
|
+
if undated:
|
|
2999
|
+
log(f" {undated} have no connection date yet — run \"Scan my whole network\" once to fill them in")
|
|
3000
|
+
else:
|
|
3001
|
+
unbridged.sort(key=lambda c: (tier_order.get(c.get("tier", "D"), 4), -(float(c.get("power_score", 0)))))
|
|
3002
|
+
log("Order: highest tier first")
|
|
3003
|
+
# Always said, so a read that stops at a page limit is never a mystery.
|
|
3004
|
+
if max_pages >= LINKEDIN_MAX_PAGES:
|
|
3005
|
+
log(f"Reading every page of each person's connections until their list ends "
|
|
3006
|
+
f"(LinkedIn shows {LINKEDIN_MAX_PAGES} pages at most), saving every {SAVE_EVERY_PAGES}")
|
|
3007
|
+
else:
|
|
3008
|
+
log(f"Reading up to {max_pages} pages per person (about {max_pages * 10} of their connections)")
|
|
3009
|
+
|
|
3010
|
+
unclear = load_unclear()
|
|
3011
|
+
parked = [c for c in unbridged if int((unclear.get(c.get("profile_url")) or {}).get("n") or 0) >= UNCLEAR_LIMIT]
|
|
3012
|
+
if parked:
|
|
3013
|
+
parked_ids = {c["id"] for c in parked}
|
|
3014
|
+
unbridged = [c for c in unbridged if c["id"] not in parked_ids] + parked
|
|
3015
|
+
log(f"{len(parked)} came back unclear {UNCLEAR_LIMIT}+ times, so they go to the back of the queue")
|
|
3016
|
+
|
|
3017
|
+
finishing = sum(1 for c in unbridged if c.get("_from_page", 1) > 1)
|
|
3018
|
+
log(f"Found {len(unbridged) - finishing} unbridged connections")
|
|
3019
|
+
log(f"Already bridged: {len(mapped)}")
|
|
3020
|
+
if deeper:
|
|
3021
|
+
log(f"Finishing {finishing} already mapped whose list goes on, from the page each stopped at")
|
|
3022
|
+
elif unfinished:
|
|
3023
|
+
log(f"({len(unfinished)} already mapped have more pages to read. "
|
|
3024
|
+
f"Tick \"Also finish people already mapped\" to include them.)")
|
|
3025
|
+
if skipped_now:
|
|
3026
|
+
log(f"Skipping {len(skipped_now)} whose connections are hidden (--retry-private to try again)")
|
|
3027
|
+
|
|
3028
|
+
if not unbridged:
|
|
3029
|
+
log("Nothing left to bridge.")
|
|
3030
|
+
return []
|
|
3031
|
+
|
|
3032
|
+
# Show plan
|
|
3033
|
+
tier_counts = {}
|
|
3034
|
+
for c in unbridged:
|
|
3035
|
+
t = c.get("tier", "D")
|
|
3036
|
+
tier_counts[t] = tier_counts.get(t, 0) + 1
|
|
3037
|
+
for t in ["S", "A", "B", "C", "D"]:
|
|
3038
|
+
if tier_counts.get(t, 0) > 0:
|
|
3039
|
+
log(f" {t}-tier: {tier_counts[t]} to bridge")
|
|
3040
|
+
|
|
3041
|
+
if max_bridges and max_bridges > 0:
|
|
3042
|
+
unbridged = unbridged[:max_bridges]
|
|
3043
|
+
log(f"Stopping after {len(unbridged)} this run.")
|
|
3044
|
+
|
|
3045
|
+
# Two people in a row whose reads were unclear — a profile that never
|
|
3046
|
+
# rendered, a failure — is how LinkedIn limiting us looks from here, so two
|
|
3047
|
+
# in a row end the batch. Only unclear reads count. A profile that rendered
|
|
3048
|
+
# with no connections link is them hiding their list: that is recorded and
|
|
3049
|
+
# counts as LinkedIn answering normally. Nothing unclear is ever recorded,
|
|
3050
|
+
# so there is nothing to take back (an earlier version recorded, then
|
|
3051
|
+
# un-recorded, and two hidden people side by side stalled every batch).
|
|
3052
|
+
# TRAPS §35.
|
|
3053
|
+
BREAKER = 2
|
|
3054
|
+
streak = 0
|
|
3055
|
+
stopped_early = None # why the batch ended before its list did
|
|
3056
|
+
|
|
3057
|
+
results = []
|
|
3058
|
+
for i, person in enumerate(unbridged):
|
|
3059
|
+
if stop_requested():
|
|
3060
|
+
log("Stopped.")
|
|
3061
|
+
break
|
|
3062
|
+
left, kind = searches_left()
|
|
3063
|
+
if left <= 0:
|
|
3064
|
+
log(budget_message(kind))
|
|
3065
|
+
stopped_early = f"the {kind} search budget"
|
|
3066
|
+
break
|
|
3067
|
+
|
|
3068
|
+
name = person["name"]
|
|
3069
|
+
tier = person.get("tier", "?")
|
|
3070
|
+
score = person.get("power_score", "?")
|
|
3071
|
+
url = person.get("profile_url", "")
|
|
3072
|
+
from_page = person.get("_from_page", 1)
|
|
3073
|
+
|
|
3074
|
+
log(f"[{i+1}/{len(unbridged)}] {name} ({tier}-tier, score {score})"
|
|
3075
|
+
+ (f", carrying on from page {from_page}" if from_page > 1 else ""))
|
|
3076
|
+
|
|
3077
|
+
healthy = False # LinkedIn answered normally for this person
|
|
3078
|
+
try:
|
|
3079
|
+
result = scrape_bridge(name, headless=headless, max_pages=max_pages, deeper=from_page > 1,
|
|
3080
|
+
profile_url=url or None)
|
|
3081
|
+
count = len(result) if result else 0
|
|
3082
|
+
status = getattr(result, "status", None)
|
|
3083
|
+
if count > 0:
|
|
3084
|
+
healthy = True
|
|
3085
|
+
results.append({"name": name, "tier": tier, "found": count, "status": "done"})
|
|
3086
|
+
log(f" {count} connections read")
|
|
3087
|
+
elif stop_requested() or status == "stopped":
|
|
3088
|
+
pass # stopped part-way: nothing to conclude about them
|
|
3089
|
+
elif result is None:
|
|
3090
|
+
results.append({"name": name, "tier": tier, "found": 0, "status": "error"})
|
|
3091
|
+
log(" Could not find them in the app to read their list")
|
|
3092
|
+
elif status == "private":
|
|
3093
|
+
# Their profile rendered, with no connections link: a real answer.
|
|
3094
|
+
healthy = True
|
|
3095
|
+
results.append({"name": name, "tier": tier, "found": 0, "status": "private"})
|
|
3096
|
+
if from_page > 1:
|
|
3097
|
+
log(" Their connections are hidden now — keeping what's mapped")
|
|
3098
|
+
elif url:
|
|
3099
|
+
record_bridge_skip(url, name, "no visible connections")
|
|
3100
|
+
log(" Connections are hidden — noted, and skipped from now on")
|
|
3101
|
+
elif status == "empty":
|
|
3102
|
+
healthy = True
|
|
3103
|
+
results.append({"name": name, "tier": tier, "found": 0, "status": "private"})
|
|
3104
|
+
if url:
|
|
3105
|
+
record_bridge_skip(url, name, "LinkedIn showed no one in their list")
|
|
3106
|
+
log(" LinkedIn shows no one in their list — noted, and skipped from now on")
|
|
3107
|
+
elif status == "finished":
|
|
3108
|
+
healthy = True
|
|
3109
|
+
results.append({"name": name, "tier": tier, "found": 0, "status": "finished"})
|
|
3110
|
+
log(f" Nothing past page {from_page - 1}: that was all of their list")
|
|
3111
|
+
elif status == "success" and from_page > 1:
|
|
3112
|
+
entry = load_bridge_progress().get(url) or {}
|
|
3113
|
+
if entry and not entry.get("more"):
|
|
3114
|
+
healthy = True
|
|
3115
|
+
results.append({"name": name, "tier": tier, "found": 0, "status": "finished"})
|
|
3116
|
+
log(f" Nothing past page {from_page - 1}: that was all of their list")
|
|
3117
|
+
else:
|
|
3118
|
+
results.append({"name": name, "tier": tier, "found": 0, "status": "unclear"})
|
|
3119
|
+
log(" Couldn't tell whether their list goes on; the next run checks again")
|
|
3120
|
+
else:
|
|
3121
|
+
# "unclear", or a search that opened but showed nobody: nothing
|
|
3122
|
+
# is recorded, and it counts towards the breaker.
|
|
3123
|
+
results.append({"name": name, "tier": tier, "found": 0, "status": "unclear"})
|
|
3124
|
+
log(" Nothing came back, and it wasn't clear why — not marked hidden; tried again next time")
|
|
3125
|
+
except KeyboardInterrupt:
|
|
3126
|
+
log("Stopped.")
|
|
3127
|
+
raise
|
|
3128
|
+
except BudgetReached as exc:
|
|
3129
|
+
results.append({"name": name, "tier": tier, "found": exc.found, "status": "done" if exc.found else "unclear"})
|
|
3130
|
+
log(" " + budget_message(exc.kind))
|
|
3131
|
+
stopped_early = f"the {exc.kind} search budget"
|
|
3132
|
+
break
|
|
3133
|
+
except CoolingDown as exc:
|
|
3134
|
+
log(" " + str(exc))
|
|
3135
|
+
stopped_early = "a cooldown"
|
|
3136
|
+
break
|
|
3137
|
+
except NotSignedIn:
|
|
3138
|
+
results.append({"name": name, "tier": tier, "found": 0, "status": "error"})
|
|
3139
|
+
log(" LinkedIn isn't signed in, so nothing was read. Stopping the batch: sign in")
|
|
3140
|
+
log(" on the Scan page, then run it again.")
|
|
3141
|
+
stopped_early = "not signed in"
|
|
3142
|
+
break
|
|
3143
|
+
except LinkedInPushedBack as exc:
|
|
3144
|
+
results.append({"name": name, "tier": tier, "found": exc.found, "status": "done" if exc.found else "error"})
|
|
3145
|
+
log(f" LinkedIn pushed back: {exc.reason}.")
|
|
3146
|
+
log(" Stopping the batch rather than carrying on into it. Everything read is saved, and")
|
|
3147
|
+
log(" the next run carries on from the same page.")
|
|
3148
|
+
log(" " + _pushback_advice(exc.reason))
|
|
3149
|
+
stopped_early = "LinkedIn pushed back"
|
|
3150
|
+
break
|
|
3151
|
+
except SearchLimitReached as exc:
|
|
3152
|
+
results.append({"name": name, "tier": tier, "found": exc.found, "status": "done" if exc.found else "error"})
|
|
3153
|
+
log(" LinkedIn says this account has reached its monthly search limit.")
|
|
3154
|
+
log(" Everything read so far is saved. Once the limit resets (the start of next month")
|
|
3155
|
+
log(" for a free account) the next run carries on from the same page.")
|
|
3156
|
+
stopped_early = "the monthly search limit"
|
|
3157
|
+
break
|
|
3158
|
+
except SaveFailed as exc:
|
|
3159
|
+
results.append({"name": name, "tier": tier, "found": 0, "status": "error"})
|
|
3160
|
+
log(f" The app could not save these connections: {str(exc)[:120]}")
|
|
3161
|
+
log(" Stopping the batch: anyone after this would cost LinkedIn views and not be saved either.")
|
|
3162
|
+
stopped_early = "a failed save"
|
|
3163
|
+
break
|
|
3164
|
+
except BaseException as exc:
|
|
3165
|
+
# One bad profile must never end the run. BaseException rather than
|
|
3166
|
+
# Exception on purpose: a SystemExit raised deep in a helper would
|
|
3167
|
+
# otherwise take the whole loop down with it.
|
|
3168
|
+
results.append({"name": name, "tier": tier, "found": 0, "status": "error"})
|
|
3169
|
+
log(f" Failed: {str(exc)[:80]}")
|
|
3170
|
+
|
|
3171
|
+
if stop_requested():
|
|
3172
|
+
continue # the loop's own check says "Stopped." and ends it
|
|
3173
|
+
if url and results and results[-1]["name"] == name:
|
|
3174
|
+
if healthy:
|
|
3175
|
+
note_unclear(url, clear=True)
|
|
3176
|
+
elif results[-1]["status"] == "unclear":
|
|
3177
|
+
note_unclear(url)
|
|
3178
|
+
if healthy:
|
|
3179
|
+
streak = 0
|
|
3180
|
+
else:
|
|
3181
|
+
streak += 1
|
|
3182
|
+
if streak >= BREAKER:
|
|
3183
|
+
log(f" {streak} people in a row with no clear answer (a profile that didn't load, or")
|
|
3184
|
+
log(" a failure), which is how LinkedIn limiting us looks. Stopping the batch;")
|
|
3185
|
+
log(" nobody was marked hidden for it. Leave it at least a day.")
|
|
3186
|
+
stopped_early = "unclear reads in a row"
|
|
3187
|
+
set_cooldown(seconds=6 * 3600, reason="two people in a row with no clear answer")
|
|
3188
|
+
break
|
|
3189
|
+
|
|
3190
|
+
# Cooldown between bridges (skip on the last one). The same full pause
|
|
3191
|
+
# after a person who came back with nothing: it used to be 15 s, which
|
|
3192
|
+
# made the loop go faster exactly when LinkedIn was pushing back.
|
|
3193
|
+
if i < len(unbridged) - 1:
|
|
3194
|
+
cooldown = BRIDGE_COOLDOWN
|
|
3195
|
+
log(f" Waiting {cooldown}s before the next one...")
|
|
3196
|
+
if not interruptible_sleep(cooldown, on_tick=lambda left: log(f" {left}s to go"), step=15):
|
|
3197
|
+
log("Stopped.")
|
|
3198
|
+
break
|
|
3199
|
+
|
|
3200
|
+
# Summary
|
|
3201
|
+
success = sum(1 for r in results if r["status"] == "done")
|
|
3202
|
+
private = sum(1 for r in results if r["status"] == "private")
|
|
3203
|
+
errors = sum(1 for r in results if r["status"] in ("error", "unclear"))
|
|
3204
|
+
finished = sum(1 for r in results if r["status"] == "finished")
|
|
3205
|
+
head = ("Stopped" if stop_requested()
|
|
3206
|
+
else f"Stopped early ({stopped_early})" if stopped_early
|
|
3207
|
+
else "Complete")
|
|
3208
|
+
log(f"{head}: {success} bridged / {private} hidden / {errors} failed or unclear"
|
|
3209
|
+
+ (f" / {finished} had nothing more" if finished else ""))
|
|
3210
|
+
if any(r["status"] == "private" for r in results):
|
|
3211
|
+
log("Hidden profiles are remembered and will be skipped next time.")
|
|
3212
|
+
|
|
3213
|
+
return results
|
|
3214
|
+
|
|
3215
|
+
|
|
3216
|
+
def rescrape_bridge(bridge_name, headless=False, max_pages=LINKEDIN_MAX_PAGES):
|
|
3217
|
+
"""Delete all existing cluster data for a bridge and re-scrape from scratch."""
|
|
3218
|
+
print(f"\n=== Re-scraping {bridge_name} (delete + fresh scrape) ===\n")
|
|
3219
|
+
|
|
3220
|
+
# Check before deleting anything: a cooldown or an empty budget would leave
|
|
3221
|
+
# their circle deleted and not read again.
|
|
3222
|
+
cd = read_cooldown()
|
|
3223
|
+
if cd:
|
|
3224
|
+
raise CoolingDown(cd)
|
|
3225
|
+
if searches_left()[0] <= 0:
|
|
3226
|
+
raise BudgetReached(0, searches_left()[1])
|
|
3227
|
+
|
|
3228
|
+
print(f"Deleting old cluster data...")
|
|
3229
|
+
bridge_id = delete_bridge_cluster(bridge_name)
|
|
3230
|
+
if bridge_id is None:
|
|
3231
|
+
return
|
|
3232
|
+
|
|
3233
|
+
scrape_bridge(bridge_name, headless=headless, max_pages=max_pages, fresh=True)
|
|
3234
|
+
|
|
3235
|
+
|
|
3236
|
+
def run_server(port=5555):
|
|
3237
|
+
"""
|
|
3238
|
+
Run a local HTTP server that the website's Setup page can talk to.
|
|
3239
|
+
Start once: python3 scripts/scrape.py --server
|
|
3240
|
+
Then use the buttons on the Setup page — no more terminal commands.
|
|
3241
|
+
"""
|
|
3242
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
3243
|
+
import threading
|
|
3244
|
+
|
|
3245
|
+
# Track current job
|
|
3246
|
+
state = {"running": False, "log": [], "result": None}
|
|
3247
|
+
|
|
3248
|
+
# Only the app served from this machine may drive the scraper. This used to
|
|
3249
|
+
# answer Access-Control-Allow-Origin: * , which let any website the operator
|
|
3250
|
+
# happened to visit start a scrape of their LinkedIn account and read the
|
|
3251
|
+
# results back — including the batched auto-bridge run this file's own
|
|
3252
|
+
# docstring warns gets accounts flagged.
|
|
3253
|
+
ALLOWED_ORIGINS = {
|
|
3254
|
+
"http://localhost:3000", "http://127.0.0.1:3000",
|
|
3255
|
+
"http://localhost:3210", "http://127.0.0.1:3210",
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
class ScrapeHandler(BaseHTTPRequestHandler):
|
|
3259
|
+
def _cors(self):
|
|
3260
|
+
origin = self.headers.get("Origin")
|
|
3261
|
+
if origin in ALLOWED_ORIGINS:
|
|
3262
|
+
self.send_header("Access-Control-Allow-Origin", origin)
|
|
3263
|
+
self.send_header("Vary", "Origin")
|
|
3264
|
+
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
3265
|
+
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
|
3266
|
+
|
|
3267
|
+
def _reject_cross_site(self):
|
|
3268
|
+
"""True (and already responded) if this request came from another site.
|
|
3269
|
+
|
|
3270
|
+
A cross-origin POST with a simple content type reaches this server
|
|
3271
|
+
with no preflight, so refusing the response is not enough — the
|
|
3272
|
+
scrape would already have started.
|
|
3273
|
+
"""
|
|
3274
|
+
origin = self.headers.get("Origin")
|
|
3275
|
+
if origin and origin not in ALLOWED_ORIGINS:
|
|
3276
|
+
self.send_response(403)
|
|
3277
|
+
self.send_header("Content-Type", "application/json")
|
|
3278
|
+
self.end_headers()
|
|
3279
|
+
self.wfile.write(json.dumps({"error": "cross-site request refused"}).encode())
|
|
3280
|
+
return True
|
|
3281
|
+
return False
|
|
3282
|
+
|
|
3283
|
+
def do_OPTIONS(self):
|
|
3284
|
+
self.send_response(200)
|
|
3285
|
+
self._cors()
|
|
3286
|
+
self.end_headers()
|
|
3287
|
+
|
|
3288
|
+
def do_GET(self):
|
|
3289
|
+
if self._reject_cross_site():
|
|
3290
|
+
return
|
|
3291
|
+
if self.path == "/status":
|
|
3292
|
+
self.send_response(200)
|
|
3293
|
+
self._cors()
|
|
3294
|
+
self.send_header("Content-Type", "application/json")
|
|
3295
|
+
self.end_headers()
|
|
3296
|
+
self.wfile.write(json.dumps({
|
|
3297
|
+
"running": state["running"],
|
|
3298
|
+
"log": state["log"][-20:],
|
|
3299
|
+
"result": state["result"],
|
|
3300
|
+
}).encode())
|
|
3301
|
+
return
|
|
3302
|
+
|
|
3303
|
+
if self.path == "/ping":
|
|
3304
|
+
self.send_response(200)
|
|
3305
|
+
self._cors()
|
|
3306
|
+
self.send_header("Content-Type", "application/json")
|
|
3307
|
+
self.end_headers()
|
|
3308
|
+
self.wfile.write(b'{"ok":true}')
|
|
3309
|
+
return
|
|
3310
|
+
|
|
3311
|
+
self.send_response(404)
|
|
3312
|
+
self.end_headers()
|
|
3313
|
+
|
|
3314
|
+
def do_POST(self):
|
|
3315
|
+
if self._reject_cross_site():
|
|
3316
|
+
return
|
|
3317
|
+
content_len = int(self.headers.get("Content-Length", 0))
|
|
3318
|
+
body = json.loads(self.rfile.read(content_len)) if content_len else {}
|
|
3319
|
+
|
|
3320
|
+
if self.path == "/scrape":
|
|
3321
|
+
if state["running"]:
|
|
3322
|
+
self.send_response(409)
|
|
3323
|
+
self._cors()
|
|
3324
|
+
self.send_header("Content-Type", "application/json")
|
|
3325
|
+
self.end_headers()
|
|
3326
|
+
self.wfile.write(b'{"error":"Scrape already running"}')
|
|
3327
|
+
return
|
|
3328
|
+
|
|
3329
|
+
action = body.get("action", "connections")
|
|
3330
|
+
bridge_name = body.get("bridge", "")
|
|
3331
|
+
user_id = body.get("userId", "")
|
|
3332
|
+
|
|
3333
|
+
# Set active user for this scrape session
|
|
3334
|
+
global _active_user_id
|
|
3335
|
+
_active_user_id = user_id or None
|
|
3336
|
+
|
|
3337
|
+
state["running"] = True
|
|
3338
|
+
state["log"] = [f"Starting {action}" + (f": {bridge_name}" if bridge_name else "")]
|
|
3339
|
+
state["result"] = None
|
|
3340
|
+
|
|
3341
|
+
def run_scrape():
|
|
3342
|
+
global _stop_requested
|
|
3343
|
+
_stop_requested = False # a window closed in an earlier job stops only that job
|
|
3344
|
+
try:
|
|
3345
|
+
if action == "full-scrape":
|
|
3346
|
+
state["log"].append("Full account setup — walking your whole connections list...")
|
|
3347
|
+
result = scrape_connections(
|
|
3348
|
+
full_walk=True,
|
|
3349
|
+
log_fn=lambda msg: state["log"].append(msg))
|
|
3350
|
+
count = len(result) if result else 0
|
|
3351
|
+
state["result"] = {"status": "done", "action": "full-scrape", "found": count}
|
|
3352
|
+
state["log"].append(f"Done! {count} connections with images captured")
|
|
3353
|
+
elif action == "company":
|
|
3354
|
+
company = body.get("company", "")
|
|
3355
|
+
state["log"].append(f"Scanning company: {company}")
|
|
3356
|
+
people = scrape_company(company, log_fn=lambda msg: state["log"].append(msg))
|
|
3357
|
+
# Push scraped people to database so they persist
|
|
3358
|
+
if people:
|
|
3359
|
+
state["log"].append(f"Saving {len(people)} people to database...")
|
|
3360
|
+
push_company(people, company)
|
|
3361
|
+
state["log"].append(f"Saved to database ✓")
|
|
3362
|
+
state["result"] = {"status": "done", "action": "company", "found": len(people), "people": people}
|
|
3363
|
+
state["log"].append(f"Done! {len(people)} people found at {company}")
|
|
3364
|
+
elif action == "auto-bridge":
|
|
3365
|
+
state["log"].append("Auto-bridging all connections (S-tier first)...")
|
|
3366
|
+
state["log"].append("2 min cooldown between each bridge")
|
|
3367
|
+
results = auto_bridge_all(log_fn=lambda msg: state["log"].append(msg))
|
|
3368
|
+
success = sum(1 for r in results if r.get("status") == "done")
|
|
3369
|
+
state["result"] = {"status": "done", "action": "auto-bridge", "found": success, "total": len(results)}
|
|
3370
|
+
state["log"].append(f"Done! {success}/{len(results)} bridges mapped")
|
|
3371
|
+
elif action == "bridge" and bridge_name:
|
|
3372
|
+
state["log"].append(f"Scraping bridge: {bridge_name}")
|
|
3373
|
+
result = scrape_bridge(bridge_name)
|
|
3374
|
+
count = len(result) if result else 0
|
|
3375
|
+
state["result"] = {"status": "done", "action": "bridge", "bridge": bridge_name, "found": count}
|
|
3376
|
+
state["log"].append(f"Done! {count} connections found")
|
|
3377
|
+
elif action == "rescrape" and bridge_name:
|
|
3378
|
+
state["log"].append(f"Re-scraping: {bridge_name}")
|
|
3379
|
+
rescrape_bridge(bridge_name)
|
|
3380
|
+
state["result"] = {"status": "done", "action": "rescrape", "bridge": bridge_name}
|
|
3381
|
+
state["log"].append("Done! Re-scrape complete")
|
|
3382
|
+
else:
|
|
3383
|
+
# Refresh connections only — no auto-bridge
|
|
3384
|
+
state["log"].append("Checking for new connections...")
|
|
3385
|
+
result = scrape_connections(
|
|
3386
|
+
log_fn=lambda msg: state["log"].append(msg))
|
|
3387
|
+
new_count = len(result) if result else 0
|
|
3388
|
+
state["log"].append(f" {new_count} connections scanned")
|
|
3389
|
+
state["result"] = {"status": "done", "action": "refresh", "found": new_count}
|
|
3390
|
+
state["log"].append("Refresh complete!")
|
|
3391
|
+
except Exception as e:
|
|
3392
|
+
state["result"] = {"status": "error", "error": str(e)}
|
|
3393
|
+
state["log"].append(f"Error: {str(e)[:100]}")
|
|
3394
|
+
finally:
|
|
3395
|
+
state["running"] = False
|
|
3396
|
+
|
|
3397
|
+
threading.Thread(target=run_scrape, daemon=True).start()
|
|
3398
|
+
|
|
3399
|
+
self.send_response(200)
|
|
3400
|
+
self._cors()
|
|
3401
|
+
self.send_header("Content-Type", "application/json")
|
|
3402
|
+
self.end_headers()
|
|
3403
|
+
self.wfile.write(b'{"started":true}')
|
|
3404
|
+
return
|
|
3405
|
+
|
|
3406
|
+
self.send_response(404)
|
|
3407
|
+
self.end_headers()
|
|
3408
|
+
|
|
3409
|
+
def log_message(self, format, *args):
|
|
3410
|
+
pass # Suppress default HTTP logs
|
|
3411
|
+
|
|
3412
|
+
import socket
|
|
3413
|
+
# Kill any existing server on this port
|
|
3414
|
+
try:
|
|
3415
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
3416
|
+
s.bind(("127.0.0.1", port))
|
|
3417
|
+
s.close()
|
|
3418
|
+
except OSError:
|
|
3419
|
+
print(f" Port {port} in use — killing old server...")
|
|
3420
|
+
import subprocess
|
|
3421
|
+
subprocess.run(f"lsof -ti:{port} | xargs kill -9", shell=True, capture_output=True)
|
|
3422
|
+
time.sleep(1)
|
|
3423
|
+
|
|
3424
|
+
server = HTTPServer(("127.0.0.1", port), ScrapeHandler)
|
|
3425
|
+
print(f"\n{'='*50}")
|
|
3426
|
+
print(f" 6 Degrees Scraper Server")
|
|
3427
|
+
print(f" Running on http://localhost:{port}")
|
|
3428
|
+
print(f"{'='*50}")
|
|
3429
|
+
print(f"\n Open your visualization and use the Setup page buttons.")
|
|
3430
|
+
print(f" Press Ctrl+C to stop.\n")
|
|
3431
|
+
|
|
3432
|
+
try:
|
|
3433
|
+
server.serve_forever()
|
|
3434
|
+
except KeyboardInterrupt:
|
|
3435
|
+
print("\nServer stopped.")
|
|
3436
|
+
server.server_close()
|
|
3437
|
+
|
|
3438
|
+
|
|
3439
|
+
if __name__ == "__main__":
|
|
3440
|
+
parser = argparse.ArgumentParser(
|
|
3441
|
+
description="6 Degrees LinkedIn Scraper",
|
|
3442
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
3443
|
+
epilog="""
|
|
3444
|
+
Examples:
|
|
3445
|
+
python3 scripts/scrape.py --server # Start local server (use Setup page buttons)
|
|
3446
|
+
python3 scripts/scrape.py # Scrape your degree-1 connections
|
|
3447
|
+
python3 scripts/scrape.py --login # Sign in once, then exit
|
|
3448
|
+
python3 scripts/scrape.py --full # Walk the whole connections list
|
|
3449
|
+
python3 scripts/scrape.py --refresh # Only what is new since last run
|
|
3450
|
+
python3 scripts/scrape.py --bridge "Jane Doe" # Scrape one bridge's connections
|
|
3451
|
+
python3 scripts/scrape.py --rescrape "Name" # Delete + re-scrape a bridge
|
|
3452
|
+
""",
|
|
3453
|
+
)
|
|
3454
|
+
parser.add_argument("--full", action="store_true",
|
|
3455
|
+
help="Walk your whole connections list top to bottom (first-time scrape)")
|
|
3456
|
+
parser.add_argument("--refresh", action="store_true",
|
|
3457
|
+
help="Only look for connections added since the last run")
|
|
3458
|
+
parser.add_argument("--search", action="store_true",
|
|
3459
|
+
help="Legacy: collect via the people-search pages instead of the connections page")
|
|
3460
|
+
parser.add_argument("--login", action="store_true",
|
|
3461
|
+
help="Just sign into LinkedIn and save the session, then exit")
|
|
3462
|
+
parser.add_argument("--server", action="store_true", help="Start local scraper server (use website buttons)")
|
|
3463
|
+
parser.add_argument("--bridge", type=str, help="Name of one bridge person to scrape")
|
|
3464
|
+
parser.add_argument("--rescrape", type=str, help="Delete + re-scrape a bridge's cluster from scratch")
|
|
3465
|
+
parser.add_argument("--company", type=str, help="Scan everyone the app can see at one company")
|
|
3466
|
+
parser.add_argument("--auto-bridge", action="store_true",
|
|
3467
|
+
help="Map every bridge in turn, highest tier first")
|
|
3468
|
+
parser.add_argument("--retry-private", action="store_true",
|
|
3469
|
+
help="With --auto-bridge: try people previously found to be hidden")
|
|
3470
|
+
parser.add_argument("--clear-skips", action="store_true",
|
|
3471
|
+
help="Forget every hidden-profile skip and start clean")
|
|
3472
|
+
parser.add_argument("--max-bridges", type=int, default=0,
|
|
3473
|
+
help="With --auto-bridge: stop after this many people (0 = no limit)")
|
|
3474
|
+
parser.add_argument("--tiers", type=str, default="",
|
|
3475
|
+
help="With --auto-bridge: only these tiers, e.g. --tiers=S,A")
|
|
3476
|
+
parser.add_argument("--order", choices=("newest", "score"), default="newest",
|
|
3477
|
+
help="With --auto-bridge: newest connections first (default), or highest tier first")
|
|
3478
|
+
parser.add_argument("--max-pages", type=int, default=LINKEDIN_MAX_PAGES,
|
|
3479
|
+
help="Result pages to read per person. By default every page, until their "
|
|
3480
|
+
"list ends (LinkedIn shows 100 at most). Every page is a search on your account.")
|
|
3481
|
+
parser.add_argument("--deeper", action="store_true",
|
|
3482
|
+
help="Carry on with people already mapped, from the page their last read "
|
|
3483
|
+
"stopped at. With --auto-bridge: alongside new people. With --bridge: that person.")
|
|
3484
|
+
parser.add_argument("--bridge-url", type=str,
|
|
3485
|
+
help="Carry on with one person, found by their LinkedIn profile URL (implies --deeper)")
|
|
3486
|
+
parser.add_argument("--only-unfinished", action="store_true",
|
|
3487
|
+
help="With --auto-bridge: only people whose read was cut short (Resume all)")
|
|
3488
|
+
parser.add_argument("--headless", action="store_true", help="Run browser in headless mode")
|
|
3489
|
+
args = parser.parse_args()
|
|
3490
|
+
args.max_pages = max(1, min(LINKEDIN_MAX_PAGES, args.max_pages))
|
|
3491
|
+
|
|
3492
|
+
# A failed save ends the run with its reason, not a traceback (TRAPS §32).
|
|
3493
|
+
def _say_why(exc_type, exc, tb):
|
|
3494
|
+
if issubclass(exc_type, SaveFailed):
|
|
3495
|
+
print(f"\n The app could not save what was scraped: {exc}\n", file=sys.stderr)
|
|
3496
|
+
elif issubclass(exc_type, NotSignedIn):
|
|
3497
|
+
print("\n LinkedIn isn't signed in, so nothing was read. Sign in with --login, then run it again.\n",
|
|
3498
|
+
file=sys.stderr)
|
|
3499
|
+
elif issubclass(exc_type, (BudgetReached, CoolingDown)):
|
|
3500
|
+
msg = budget_message(exc.kind) if issubclass(exc_type, BudgetReached) else str(exc)
|
|
3501
|
+
print(f"\n {msg}\n", file=sys.stderr)
|
|
3502
|
+
elif issubclass(exc_type, LinkedInPushedBack):
|
|
3503
|
+
print(f"\n LinkedIn pushed back: {getattr(exc, 'reason', exc)}. What was read is saved. "
|
|
3504
|
+
f"{_pushback_advice(getattr(exc, 'reason', ''))}\n", file=sys.stderr)
|
|
3505
|
+
elif issubclass(exc_type, SearchLimitReached):
|
|
3506
|
+
print("\n LinkedIn says this account has reached its monthly search limit. What was read "
|
|
3507
|
+
"is saved; run again with --deeper once it resets.\n", file=sys.stderr)
|
|
3508
|
+
else:
|
|
3509
|
+
sys.__excepthook__(exc_type, exc, tb)
|
|
3510
|
+
sys.excepthook = _say_why
|
|
3511
|
+
|
|
3512
|
+
install_stop_handler()
|
|
3513
|
+
_assert_local_target()
|
|
3514
|
+
|
|
3515
|
+
if args.clear_skips:
|
|
3516
|
+
clear_bridge_skips()
|
|
3517
|
+
raise SystemExit(0)
|
|
3518
|
+
|
|
3519
|
+
if args.login:
|
|
3520
|
+
raise SystemExit(0 if open_login_window() else 1)
|
|
3521
|
+
|
|
3522
|
+
if not args.server:
|
|
3523
|
+
resolve_active_user()
|
|
3524
|
+
|
|
3525
|
+
# A budget or a cooldown ending a run is the design working, not a failure:
|
|
3526
|
+
# say why and exit 0, so the Scan page doesn't show it as a red error.
|
|
3527
|
+
try:
|
|
3528
|
+
if args.company:
|
|
3529
|
+
# The server mode scraped and pushed as one step; the CLI has to do the
|
|
3530
|
+
# same or the scan appears to work and saves nothing.
|
|
3531
|
+
people = scrape_company(args.company, headless=args.headless)
|
|
3532
|
+
if people:
|
|
3533
|
+
print(f"\nSaving {len(people)} people from {args.company}...")
|
|
3534
|
+
push_company(people, args.company)
|
|
3535
|
+
print(f"Done. {len(people) if people else 0} found at {args.company}.")
|
|
3536
|
+
elif args.auto_bridge:
|
|
3537
|
+
results = auto_bridge_all(headless=args.headless, retry_private=args.retry_private,
|
|
3538
|
+
max_bridges=args.max_bridges,
|
|
3539
|
+
tiers=args.tiers.split(",") if args.tiers else None,
|
|
3540
|
+
order=args.order, max_pages=args.max_pages, deeper=args.deeper,
|
|
3541
|
+
only_unfinished=args.only_unfinished)
|
|
3542
|
+
done = sum(1 for r in results if r.get("status") == "done")
|
|
3543
|
+
print(f"\nDone. {done}/{len(results)} bridges mapped.")
|
|
3544
|
+
elif args.search:
|
|
3545
|
+
scrape_full(headless=args.headless)
|
|
3546
|
+
elif args.full:
|
|
3547
|
+
scrape_connections(headless=args.headless, full_walk=True)
|
|
3548
|
+
elif args.refresh:
|
|
3549
|
+
scrape_connections(headless=args.headless)
|
|
3550
|
+
elif args.server:
|
|
3551
|
+
run_server()
|
|
3552
|
+
elif args.rescrape:
|
|
3553
|
+
rescrape_bridge(args.rescrape, headless=args.headless, max_pages=args.max_pages)
|
|
3554
|
+
elif args.bridge_url:
|
|
3555
|
+
scrape_bridge(None, headless=args.headless, max_pages=args.max_pages, deeper=True,
|
|
3556
|
+
profile_url=args.bridge_url)
|
|
3557
|
+
elif args.bridge:
|
|
3558
|
+
scrape_bridge(args.bridge, headless=args.headless, max_pages=args.max_pages, deeper=args.deeper)
|
|
3559
|
+
else:
|
|
3560
|
+
# Nothing collected yet means this is a first run, and the full scrape
|
|
3561
|
+
# is the one that walks every search page and captures photos. Running
|
|
3562
|
+
# the incremental refresh here is what made a fresh install look broken.
|
|
3563
|
+
existing = read_connections(params={"degree": "eq.1", "limit": "1"})
|
|
3564
|
+
if existing:
|
|
3565
|
+
print("\nExisting connections found — checking for new ones only.")
|
|
3566
|
+
print("Use --full to re-walk everything.\n")
|
|
3567
|
+
scrape_connections(headless=args.headless)
|
|
3568
|
+
else:
|
|
3569
|
+
print("\nNo connections yet — walking your whole connections list.")
|
|
3570
|
+
print("This takes a couple of minutes and captures photos.\n")
|
|
3571
|
+
scrape_connections(headless=args.headless, full_walk=True)
|
|
3572
|
+
except (BudgetReached, CoolingDown) as exc:
|
|
3573
|
+
print("\n " + (budget_message(exc.kind) if isinstance(exc, BudgetReached) else str(exc)))
|
|
3574
|
+
raise SystemExit(0)
|