first commit, already have a working version in 12 hours of work

This commit is contained in:
2025-07-27 13:45:18 -05:00
commit c992d0b949
9 changed files with 1097 additions and 0 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
node_modules
linkedin-home.png

36
.gitignore vendored Normal file
View File

@@ -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

23
Dockerfile Normal file
View File

@@ -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"]

15
README.md Normal file
View File

@@ -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.

182
bun.lock Normal file
View File

@@ -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=="],
}
}

624
index.ts Normal file
View File

@@ -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<Connection[]> {
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<boolean> {
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<void> {
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!`);
}

164
package-lock.json generated Normal file
View File

@@ -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"
}
}
}

21
package.json Normal file
View File

@@ -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"
}
}

29
tsconfig.json Normal file
View File

@@ -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
}
}