commit c992d0b949d7933a736792e9b68396ac9d4f5dd7 Author: Mars Ultor Date: Sun Jul 27 13:45:18 2025 -0500 first commit, already have a working version in 12 hours of work diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3845b0a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +node_modules +linkedin-home.png + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab877c1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local +*.png +pagination/** +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store +linkedin_state.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7f18ac8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM mcr.microsoft.com/playwright:v1.43.1-jammy + +# Install dependencies needed for bun installer (unzip & curl) +RUN apt-get update && apt-get install -y unzip curl && rm -rf /var/lib/apt/lists/* + +# Install Bun and add it to PATH +RUN curl -fsSL https://bun.sh/install | bash && \ + echo 'export PATH="/root/.bun/bin:$PATH"' >> ~/.bashrc + +ENV PATH="/root/.bun/bin:$PATH" + +WORKDIR /app + +COPY . . + +RUN bun install || npm install + +RUN npx playwright install + +RUN bun add playwright-extra puppeteer-extra-plugin-stealth + +#CMD ["bun", "index.ts"] + diff --git a/README.md b/README.md new file mode 100644 index 0000000..057d7e2 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# playwright-linkedin-bot + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.15. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..fe92142 --- /dev/null +++ b/bun.lock @@ -0,0 +1,182 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "playwright-linkedin-bot", + "dependencies": { + "dotenv": "^17.2.1", + "pino": "^9.7.0", + "pino-pretty": "^13.0.0", + "playwright": "^1.54.1", + "playwright-extra": "^4.3.6", + "playwright-extra-plugin-stealth": "^0.0.1", + "puppeteer-extra-plugin-stealth": "^2.11.2", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], + + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + + "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], + + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + + "arr-union": ["arr-union@3.1.0", "", {}, "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q=="], + + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], + + "clone-deep": ["clone-deep@0.2.4", "", { "dependencies": { "for-own": "^0.1.3", "is-plain-object": "^2.0.1", "kind-of": "^3.0.2", "lazy-cache": "^1.0.3", "shallow-clone": "^0.1.2" } }, "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "dotenv": ["dotenv@17.2.1", "", {}, "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], + + "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "for-in": ["for-in@1.0.2", "", {}, "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="], + + "for-own": ["for-own@0.1.5", "", { "dependencies": { "for-in": "^1.0.1" } }, "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw=="], + + "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "is-buffer": ["is-buffer@1.1.6", "", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="], + + "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], + + "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="], + + "kind-of": ["kind-of@3.2.2", "", { "dependencies": { "is-buffer": "^1.1.5" } }, "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ=="], + + "lazy-cache": ["lazy-cache@1.0.4", "", {}, "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ=="], + + "merge-deep": ["merge-deep@3.0.3", "", { "dependencies": { "arr-union": "^3.1.0", "clone-deep": "^0.2.4", "kind-of": "^3.0.2" } }, "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mixin-object": ["mixin-object@2.0.1", "", { "dependencies": { "for-in": "^0.1.3", "is-extendable": "^0.1.1" } }, "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "pino": ["pino@9.7.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-vnMCM6xZTb1WDmLvtG2lE/2p+t9hDEIvTWJsu6FejkE62vB7gDhvzrpFR4Cw2to+9JNQxVnkAKVPA1KPB98vWg=="], + + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], + + "pino-pretty": ["pino-pretty@13.0.0", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "secure-json-parse": "^2.4.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^3.1.1" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA=="], + + "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], + + "playwright": ["playwright@1.54.1", "", { "dependencies": { "playwright-core": "1.54.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g=="], + + "playwright-core": ["playwright-core@1.54.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA=="], + + "playwright-extra": ["playwright-extra@4.3.6", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "playwright": "*", "playwright-core": "*" }, "optionalPeers": ["playwright", "playwright-core"] }, "sha512-q2rVtcE8V8K3vPVF1zny4pvwZveHLH8KBuVU2MoE3Jw4OKVoBWsHI9CH9zPydovHHOCDxjGN2Vg+2m644q3ijA=="], + + "playwright-extra-plugin-stealth": ["playwright-extra-plugin-stealth@0.0.1", "", {}, "sha512-eI0Ujf4MXbcupzlVEXaaOnb+Exjt1sFi7t/3KxIA5pVww+WRAXRWdhqTz0glX62jJq2YM8fLu+GyvULpjTpZrw=="], + + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "puppeteer-extra-plugin": ["puppeteer-extra-plugin@3.2.3", "", { "dependencies": { "@types/debug": "^4.1.0", "debug": "^4.1.1", "merge-deep": "^3.0.1" }, "peerDependencies": { "playwright-extra": "*", "puppeteer-extra": "*" }, "optionalPeers": ["playwright-extra", "puppeteer-extra"] }, "sha512-6RNy0e6pH8vaS3akPIKGg28xcryKscczt4wIl0ePciZENGE2yoaQJNd17UiEbdmh5/6WW6dPcfRWT9lxBwCi2Q=="], + + "puppeteer-extra-plugin-stealth": ["puppeteer-extra-plugin-stealth@2.11.2", "", { "dependencies": { "debug": "^4.1.1", "puppeteer-extra-plugin": "^3.2.3", "puppeteer-extra-plugin-user-preferences": "^2.4.1" }, "peerDependencies": { "playwright-extra": "*", "puppeteer-extra": "*" }, "optionalPeers": ["playwright-extra", "puppeteer-extra"] }, "sha512-bUemM5XmTj9i2ZerBzsk2AN5is0wHMNE6K0hXBzBXOzP5m5G3Wl0RHhiqKeHToe/uIH8AoZiGhc1tCkLZQPKTQ=="], + + "puppeteer-extra-plugin-user-data-dir": ["puppeteer-extra-plugin-user-data-dir@2.4.1", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^10.0.0", "puppeteer-extra-plugin": "^3.2.3", "rimraf": "^3.0.2" }, "peerDependencies": { "playwright-extra": "*", "puppeteer-extra": "*" }, "optionalPeers": ["playwright-extra", "puppeteer-extra"] }, "sha512-kH1GnCcqEDoBXO7epAse4TBPJh9tEpVEK/vkedKfjOVOhZAvLkHGc9swMs5ChrJbRnf8Hdpug6TJlEuimXNQ+g=="], + + "puppeteer-extra-plugin-user-preferences": ["puppeteer-extra-plugin-user-preferences@2.4.1", "", { "dependencies": { "debug": "^4.1.1", "deepmerge": "^4.2.2", "puppeteer-extra-plugin": "^3.2.3", "puppeteer-extra-plugin-user-data-dir": "^2.4.1" }, "peerDependencies": { "playwright-extra": "*", "puppeteer-extra": "*" }, "optionalPeers": ["playwright-extra", "puppeteer-extra"] }, "sha512-i1oAZxRbc1bk8MZufKCruCEC3CCafO9RKMkkodZltI4OqibLFXF3tj6HZ4LZ9C5vCXZjYcDWazgtY69mnmrQ9A=="], + + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], + + "shallow-clone": ["shallow-clone@0.1.2", "", { "dependencies": { "is-extendable": "^0.1.1", "kind-of": "^2.0.1", "lazy-cache": "^0.2.3", "mixin-object": "^2.0.1" } }, "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw=="], + + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "mixin-object/for-in": ["for-in@0.1.8", "", {}, "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g=="], + + "shallow-clone/kind-of": ["kind-of@2.0.1", "", { "dependencies": { "is-buffer": "^1.0.2" } }, "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg=="], + + "shallow-clone/lazy-cache": ["lazy-cache@0.2.7", "", {}, "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ=="], + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..4e383f1 --- /dev/null +++ b/index.ts @@ -0,0 +1,624 @@ +import { firefox } from 'playwright'; +import { existsSync, mkdirSync } from 'fs'; +import * as dotenv from 'dotenv'; +import pino from 'pino'; +import { Page } from 'playwright'; +import { Locator } from 'playwright'; +dotenv.config(); +import path from 'path'; + +const logger = pino({ + transport: { + target: "pino-pretty", + options: { + colorize: true, + }, + }, +}); +const STORAGE_PATH = 'linkedin_state.json'; + +export interface Connection { + name: string; // Person's name + profileUrl: string; // Base profile URL without query params + mutualConnections: number; // Number of mutual connections + location?: string; // Location if available + headline?: string; // Professional headline if available +} + +const main = async () => { + logger.info('Starting LinkedIn bot with Firefox on Linux...'); + const browser = await firefox.launch({ + headless: true, + firefoxUserPrefs: { + 'dom.webdriver.enabled': false, + 'media.peerconnection.enabled': false, + 'useAutomationExtension': false, + }, + }); + + const context = existsSync(STORAGE_PATH) + ? await browser.newContext({ + storageState: STORAGE_PATH, + userAgent: + 'Mozilla/5.0 (X11; Linux x86_64; rv:117.0) Gecko/20100101 Firefox/117.0', + viewport: { width: 1280, height: 720 }, + }) + : await browser.newContext({ + userAgent: + 'Mozilla/5.0 (X11; Linux x86_64; rv:117.0) Gecko/20100101 Firefox/117.0', + viewport: { width: 1280, height: 720 }, + }); + + const page = await context.newPage(); + + if (!existsSync(STORAGE_PATH)) { + const username = process.env.LINKEDIN_EMAIL; + const password = process.env.LINKEDIN_PASSWORD; + + if (!username || !password) { + logger.error('Missing LinkedIn credentials in .env file'); + process.exit(1); + } + + logger.info('Navigating to LinkedIn login page...'); + await page.goto('https://www.linkedin.com/login'); + await page.screenshot({ path: '01-login-page.png' }); + + // Close possible popups + try { + const closeSelectors = [ + 'button[aria-label="Dismiss"]', + 'button[aria-label="Close"]', + 'button[data-test-modal-close-button]', + '.artdeco-modal__dismiss', + ]; + for (const selector of closeSelectors) { + const btn = await page.$(selector); + if (btn) { + await btn.click(); + await page.waitForTimeout(1000); + break; + } + } + } catch (err) { + logger.warn('Popup check failed:', err); + } + + // Login input + const usernameSelector = 'input[name="session_key"]'; + await page.waitForSelector(usernameSelector, { state: 'visible', timeout: 15000 }); + await page.click(usernameSelector); + await page.waitForTimeout(2000); + await page.click(usernameSelector); + await page.type(usernameSelector, username, { delay: 100 }); + + const passwordSelector = 'input[name="session_password"]'; + await page.waitForSelector(passwordSelector, { state: 'visible', timeout: 15000 }); + await page.click(passwordSelector); + await page.waitForTimeout(2000); + await page.click(passwordSelector); + await page.type(passwordSelector, password, { delay: 100 }); + + await page.screenshot({ path: '02-filled-credentials.png' }); + + const submitSelector = 'button[type="submit"]'; + await page.waitForSelector(submitSelector, { state: 'visible', timeout: 15000 }); + await page.click(submitSelector); + await page.screenshot({ path: '03-after-submit.png' }); + + // Save session state immediately + await context.storageState({ path: STORAGE_PATH }); + + logger.info('Waiting for feed page...'); + await page.waitForURL('**/feed*', { timeout: 30000, waitUntil: 'networkidle' }); + await page.screenshot({ path: '04-feed.png' }); + } else { + logger.info('Using saved session.'); + await page.goto('https://www.linkedin.com/feed/'); + await page.screenshot({ path: 'feed-page-reuse-session.png' }); + } + + const page_conn_test = await context.newPage(); + await openConnectionsAndScreenshot(page_conn_test); + + const page_connections_secondary = await context.newPage(); + const connections = await scrapeConnections(page_connections_secondary); + logger.info(`Total connections found: ${connections.length}`); + + // Sort by mutual connections (ascending) and take the first 25 + const sortedConnections = connections + .sort((a, b) => a.mutualConnections - b.mutualConnections) + .slice(0, 25); + + logger.info(`Selected top 25 connections with least mutual connections:`); + sortedConnections.forEach((conn, idx) => { + logger.info(`${idx + 1}. ${conn.name} (${conn.mutualConnections} mutual connections)`); + }); + + const page_connection = await context.newPage(); + await connectToAllProfiles(page_connection, sortedConnections); + + logger.info('Waiting 3 seconds before finishing...'); + await page.waitForTimeout(3000); + await browser.close(); + logger.info('Browser closed, done.'); +}; + +main(); + +export async function openConnectionsAndScreenshot(page: Page) { + logger.info('Opening LinkedIn connections page...'); + await page.goto('https://www.linkedin.com/search/results/people/?network=%5B%22S%22%5D&origin=MEMBER_PROFILE_CANNED_SEARCH&page=2', { + waitUntil: 'domcontentloaded', // faster and avoids hanging + timeout: 30000, // reduce from 60s to 30s + }); + + // Let LinkedIn JS load dynamic content + await page.waitForTimeout(5000); + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const screenshotPath = `linkedin_connections_${timestamp}.png`; + + await page.screenshot({ path: screenshotPath, fullPage: true }); + logger.info(`šŸ“ø Screenshot saved to ${screenshotPath}`); +} + +export async function scrapeConnections(page: Page): Promise { + logger.info('šŸ“ø Opening LinkedIn connections page...'); + + // Create pagination directory if it doesn't exist + if (!existsSync('pagination')) { + mkdirSync('pagination'); + } + + const allConnections: Connection[] = []; + let currentPage = 1; + + while (true) { + const url = `https://www.linkedin.com/search/results/people/?network=%5B%22S%22%5D&origin=FACETED_SEARCH&page=${currentPage}`; + logger.info(`🌐 Navigating to page ${currentPage}: ${url}`); + + await page.goto(url, { + waitUntil: 'domcontentloaded', + timeout: 45000, + }); + + await page.waitForTimeout(3000); + + // Screenshot + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const screenshotPath = `pagination/page_${currentPage}_${timestamp}.png`; + await page.screenshot({ path: screenshotPath, fullPage: true }); + logger.info(`šŸ“ø Saved screenshot to ${screenshotPath}`); + + // Wait for search results to load + try { + await page.waitForSelector('.linked-area', { timeout: 10000 }); + } catch (err) { + logger.warn('No .linked-area found on page, might be end of results'); + break; + } + + // Scrape profile cards - using the correct selector based on the HTML structure + const cards = await page.$$('.linked-area'); + const pageConnections: Connection[] = []; + + logger.info(`Found ${cards.length} connection cards on page ${currentPage}`); + + for (let i = 0; i < cards.length; i++) { + try { + const card = cards[i]; + logger.info(`Processing card ${i + 1}...`); + + // Check if this profile has a "Message" button (indicating already connected) + const messageButtonSelectors = [ + 'button[aria-label*="Message"]', + 'button:has-text("Message")', + '.artdeco-button:has-text("Message")' + ]; + + let hasMessageButton = false; + for (const selector of messageButtonSelectors) { + const messageButton = await card.$(selector); + if (messageButton) { + hasMessageButton = true; + const ariaLabel = await messageButton.getAttribute('aria-label'); + logger.info(`Card ${i + 1}: Found message button with aria-label: "${ariaLabel}"`); + break; + } + } + + if (hasMessageButton) { + logger.info(`Card ${i + 1}: Skipping profile - already connected (has Message button)`); + continue; + } + + // Find the profile link - it's an anchor tag with href containing "/in/" + const profileLink = await card.$('a[href*="/in/"]'); + if (!profileLink) { + logger.info(`Card ${i + 1}: No profile link found`); + continue; + } + + const rawUrl = await profileLink.getAttribute('href'); + if (!rawUrl) { + logger.info(`Card ${i + 1}: No href attribute found`); + continue; + } + + const cleanUrl = rawUrl.split('?')[0]; + logger.info(`Card ${i + 1}: Profile URL = ${cleanUrl}`); + + // Extract name - try multiple selectors to find the name + let name = 'Unknown'; + + // Try different name selectors based on LinkedIn's structure + const nameSelectors = [ + 'span[dir="ltr"] span[aria-hidden="true"]', + 'a[href*="/in/"] span[aria-hidden="true"]', + 'a[data-test-app-aware-link] span[aria-hidden="true"]', + '.artdeco-entity-lockup__title a span[aria-hidden="true"]', + 'span.ZGqwDIzKYyWZGPNHFVsMdJIrNpzbSChPdgBEBE span[aria-hidden="true"]' + ]; + + for (const selector of nameSelectors) { + const nameElement = await card.$(selector); + if (nameElement) { + const extractedName = (await nameElement.textContent())?.trim(); + if (extractedName && extractedName !== '') { + name = extractedName; + logger.info(`Card ${i + 1}: Found name with selector "${selector}": ${name}`); + break; + } + } + } + + if (name === 'Unknown') { + // Try to find any link text within profile links + const allProfileLinks = await card.$$('a[href*="/in/"]'); + for (const link of allProfileLinks) { + const linkText = (await link.textContent())?.trim(); + if (linkText && linkText.length > 0 && !linkText.includes('View') && !linkText.includes('mutual')) { + name = linkText; + logger.info(`Card ${i + 1}: Found name from link text: ${name}`); + break; + } + } + } + logger.info(`Card ${i + 1}: Name = ${name}`); + + // Extract location - look for text that appears to be location + const locationElements = await card.$$('div.t-14.t-normal'); + let location = ''; + for (const locEl of locationElements) { + const text = (await locEl.textContent())?.trim() || ''; + // Location usually contains state abbreviations or city names + if (text && (text.includes(',') || text.match(/\b[A-Z]{2}\b/))) { + location = text; + break; + } + } + logger.info(`Card ${i + 1}: Location = ${location || 'Not found'}`); + + // Extract headline - usually the first t-14 t-black t-normal div + const headlineElement = await card.$('div.t-14.t-black.t-normal'); + const headline = headlineElement ? (await headlineElement.textContent())?.trim() || '' : ''; + logger.info(`Card ${i + 1}: Headline = ${headline || 'Not found'}`); + + // Extract mutual connections count + let mutualConnections = 0; + try { + const mutualElement = await card.$('a[href*="facetConnectionOf"] strong'); + if (mutualElement) { + const mutualText = (await mutualElement.textContent())?.trim() || ''; + const match = mutualText.match(/(\d+)/); + if (match) { + mutualConnections = parseInt(match[1], 10); + } + logger.info(`Card ${i + 1}: Mutual connections text = "${mutualText}", parsed = ${mutualConnections}`); + } else { + logger.info(`Card ${i + 1}: No mutual connections element found`); + } + } catch (err) { + logger.info(`Card ${i + 1}: Error extracting mutual connections: ${err}`); + } + + if (name !== 'Unknown' && cleanUrl) { + const connection: Connection = { + name, + profileUrl: cleanUrl, + mutualConnections, + location: location || undefined, + headline: headline || undefined + }; + + logger.info(`Card ${i + 1}: Final connection object:`, connection); + pageConnections.push(connection); + } else { + logger.info(`Card ${i + 1}: Skipped - name: "${name}", URL: "${cleanUrl}"`); + + // Still add it even if name is Unknown since URLs are valid + if (cleanUrl) { + const connection: Connection = { + name: name === 'Unknown' ? `Profile_${i + 1}` : name, + profileUrl: cleanUrl, + mutualConnections, + location: location || undefined, + headline: headline || undefined + }; + + logger.info(`Card ${i + 1}: Adding with fallback name:`, connection); + pageConnections.push(connection); + } + } + } catch (err) { + logger.warn(`Error processing card ${i + 1}:`, err); + continue; + } + } + + logger.info(`šŸ“„ Page ${currentPage}: Collected ${pageConnections.length} profiles (after filtering out already connected).`); + + // Always add connections from this page (even if 0) + allConnections.push(...pageConnections); + + // Only stop if there are no profile cards at all (true end of results) + if (cards.length === 0) { + logger.info('No profile cards found on this page, reached end of results'); + break; + } + + currentPage++; + + // Add delay before next page to be respectful + await page.waitForTimeout(2000); + + // Stop after reasonable number of pages to avoid infinite loops + if (currentPage > 10) { + logger.warn('Reached maximum page limit (10), stopping pagination'); + break; + } + } + + logger.info(`Total connections scraped: ${allConnections.length} (after filtering out already connected profiles)`); + return allConnections; +} + +export async function connectToProfile(page: Page, connection: Connection): Promise { + logger.info(`Attempting to connect to ${connection.name} at ${connection.profileUrl}`); + + try { + // Navigate to the profile + await page.goto(connection.profileUrl, { + waitUntil: 'domcontentloaded', + timeout: 30000, + }); + + // Wait for page to load + await page.waitForTimeout(3000); + + // Scroll to ensure buttons are in viewport + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight / 3)); + await page.waitForTimeout(1000); + + // Look for the Connect button + const connectButtonSelectors = [ + 'button[aria-label*="Invite"][aria-label*="to connect"]', + 'button:has-text("Connect")', + '.artdeco-button--primary:has-text("Connect")', + 'button.artdeco-button--primary[aria-label*="connect"]', + 'button.artdeco-button:has-text("Connect")' + ]; + + let connectButton = null; + for (const selector of connectButtonSelectors) { + try { + connectButton = await page.$(selector); + if (connectButton) { + // Scroll the element into view + await connectButton.scrollIntoViewIfNeeded(); + await page.waitForTimeout(500); + + const isVisible = await connectButton.isVisible(); + if (isVisible) { + logger.info(`Found connect button with selector: ${selector}`); + break; + } else { + logger.info(`Found connect button with selector: ${selector} but not visible, trying next`); + connectButton = null; + } + } + } catch (err) { + // Continue to next selector + continue; + } + } + + if (!connectButton) { + logger.warn(`No connect button found for ${connection.name}`); + return false; + } + + // Check if button is visible and enabled + const isVisible = await connectButton.isVisible(); + const isEnabled = await connectButton.isEnabled(); + + if (!isVisible || !isEnabled) { + logger.warn(`Connect button not clickable for ${connection.name} (visible: ${isVisible}, enabled: ${isEnabled})`); + return false; + } + + // Click the connect button + logger.info(`Clicking connect button for ${connection.name}`); + await connectButton.click(); + + // Wait for potential modal to appear + await page.waitForTimeout(1500); + + // Check for the invitation modal + try { + const modalSelector = '[data-test-modal][role="dialog"]'; + const modal = await page.$(modalSelector); + + if (modal) { + logger.info(`Invitation modal appeared for ${connection.name}`); + + // Look for "Send without a note" button + const sendWithoutNoteSelectors = [ + 'button[aria-label="Send without a note"]', + 'button:has-text("Send without a note")', + '.artdeco-button--primary:has-text("Send without a note")' + ]; + + let sendButton = null; + for (const selector of sendWithoutNoteSelectors) { + try { + sendButton = await modal.$(selector); + if (sendButton) { + logger.info(`Found "Send without a note" button with selector: ${selector}`); + break; + } + } catch (err) { + continue; + } + } + + if (sendButton) { + const isButtonVisible = await sendButton.isVisible(); + const isButtonEnabled = await sendButton.isEnabled(); + + if (isButtonVisible && isButtonEnabled) { + logger.info(`Clicking "Send without a note" for ${connection.name}`); + await sendButton.click(); + + // Wait for modal to disappear + await page.waitForTimeout(2000); + + // Verify the modal is gone (success indicator) + const modalStillExists = await page.$(modalSelector); + if (!modalStillExists) { + logger.info(`Successfully sent connection request to ${connection.name}`); + return true; + } else { + logger.warn(`Modal still exists after clicking send for ${connection.name}`); + return false; + } + } else { + logger.warn(`Send button not clickable for ${connection.name} (visible: ${isButtonVisible}, enabled: ${isButtonEnabled})`); + return false; + } + } else { + logger.warn(`Could not find "Send without a note" button for ${connection.name}`); + return false; + } + } else { + // No modal appeared, connection might have been sent directly + logger.info(`No modal appeared for ${connection.name}, connection may have been sent directly`); + + // Wait a bit and check if connect button changed or disappeared + await page.waitForTimeout(1000); + const updatedConnectButton = await page.$(connectButtonSelectors[0]); + + if (!updatedConnectButton) { + logger.info(`Connect button disappeared for ${connection.name}, likely successful`); + return true; + } else { + // Check if button text changed to something like "Pending" or "Sent" + const buttonText = await updatedConnectButton.textContent(); + if (buttonText && (buttonText.includes('Pending') || buttonText.includes('Sent'))) { + logger.info(`Connect button changed to "${buttonText}" for ${connection.name}, successful`); + return true; + } else { + logger.warn(`Connect button still present with text "${buttonText}" for ${connection.name}`); + return false; + } + } + } + } catch (err) { + logger.error(`Error handling modal for ${connection.name}:`, err); + return false; + } + + } catch (err) { + logger.error(`Error connecting to ${connection.name}:`, err); + return false; + } +} + +export async function connectToAllProfiles(page: Page, connections: Connection[]): Promise { + logger.info(`Starting connection process for ${connections.length} profiles`); + + let successCount = 0; + let failureCount = 0; + let errorCount = 0; + const failedConnections: Connection[] = []; + const errorConnections: { connection: Connection; error: string }[] = []; + + for (let i = 0; i < connections.length; i++) { + const connection = connections[i]; + logger.info(`\n=== Processing ${i + 1}/${connections.length}: ${connection.name} (${connection.mutualConnections} mutual connections) ===`); + + try { + const success = await connectToProfile(page, connection); + + if (success) { + successCount++; + logger.info(`āœ… SUCCESS: Connected to ${connection.name} (${successCount} total successes)`); + } else { + failureCount++; + failedConnections.push(connection); + logger.warn(`āŒ FAILED: Could not connect to ${connection.name} (${failureCount} total failures)`); + } + + // Add delay between connection attempts to be respectful + const delayMs = Math.floor(Math.random() * 3000) + 2000; // 2-5 second random delay + logger.info(`ā³ Waiting ${delayMs}ms before next connection attempt...`); + await page.waitForTimeout(delayMs); + + } catch (error) { + errorCount++; + const errorMessage = error instanceof Error ? error.message : String(error); + errorConnections.push({ connection, error: errorMessage }); + logger.error(`šŸ’„ ERROR processing ${connection.name}: ${errorMessage}`); + + // Longer delay after errors to recover + logger.info(`ā³ Waiting 5 seconds after error before continuing...`); + await page.waitForTimeout(5000); + } + + // Progress update every 10 connections + if ((i + 1) % 10 === 0) { + logger.info(`\nšŸ“Š PROGRESS UPDATE (${i + 1}/${connections.length}):`); + logger.info(` āœ… Successes: ${successCount}`); + logger.info(` āŒ Failures: ${failureCount}`); + logger.info(` šŸ’„ Errors: ${errorCount}`); + logger.info(` šŸ“ˆ Success Rate: ${((successCount / (i + 1)) * 100).toFixed(1)}%`); + } + } + + // Final summary + logger.info(`\nšŸ FINAL RESULTS:`); + logger.info(` Total Processed: ${connections.length}`); + logger.info(` āœ… Successful Connections: ${successCount}`); + logger.info(` āŒ Failed Connections: ${failureCount}`); + logger.info(` šŸ’„ Errors: ${errorCount}`); + logger.info(` šŸ“ˆ Overall Success Rate: ${((successCount / connections.length) * 100).toFixed(1)}%`); + + // Log failed connections for review + if (failedConnections.length > 0) { + logger.warn(`\nāŒ FAILED CONNECTIONS (${failedConnections.length}):`); + failedConnections.forEach((conn, idx) => { + logger.warn(` ${idx + 1}. ${conn.name} - ${conn.profileUrl} (${conn.mutualConnections} mutual)`); + }); + } + + // Log error connections for debugging + if (errorConnections.length > 0) { + logger.error(`\nšŸ’„ ERROR CONNECTIONS (${errorConnections.length}):`); + errorConnections.forEach((item, idx) => { + logger.error(` ${idx + 1}. ${item.connection.name} - Error: ${item.error}`); + }); + } + + logger.info(`\nšŸŽ‰ Connection process completed!`); +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1b62c1d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,164 @@ +{ + "name": "playwright-linkedin-bot", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "playwright-linkedin-bot", + "dependencies": { + "dotenv": "^17.2.1", + "playwright": "^1.54.1", + "playwright-extra": "^4.3.6", + "playwright-extra-plugin-stealth": "^0.0.1" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } + }, + "node_modules/@types/bun": { + "version": "1.2.19", + "resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.19.tgz", + "integrity": "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-types": "1.2.19" + } + }, + "node_modules/@types/node": { + "version": "24.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.8", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/bun-types": { + "version": "1.2.19", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + }, + "peerDependencies": { + "@types/react": "^19" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/debug": { + "version": "4.4.1", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dotenv": { + "version": "17.2.1", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.54.1", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.54.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.54.1", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright-extra": { + "version": "4.3.6", + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "playwright": "*", + "playwright-core": "*" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "playwright-core": { + "optional": true + } + } + }, + "node_modules/playwright-extra-plugin-stealth": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.8.3", + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..956ae8f --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "playwright-linkedin-bot", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "dotenv": "^17.2.1", + "pino": "^9.7.0", + "pino-pretty": "^13.0.0", + "playwright": "^1.54.1", + "playwright-extra": "^4.3.6", + "playwright-extra-plugin-stealth": "^0.0.1", + "puppeteer-extra-plugin-stealth": "^2.11.2" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}