@ubaidbinwaris/linkedin 1.0.0 → 1.0.7
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/Readme.md +156 -55
- package/cli.js +29 -1
- package/index.js +4 -1
- package/package.json +2 -2
- package/src/cli/userSelection.js +73 -0
- package/src/login/login.js +119 -18
- package/src/session/sessionManager.js +94 -19
- package/src/utils/logger.js +33 -13
package/Readme.md
CHANGED
|
@@ -1,84 +1,185 @@
|
|
|
1
|
-
# LinkedIn Automation Bot
|
|
1
|
+
# LinkedIn Automation Bot & Module 🤖
|
|
2
2
|
|
|
3
|
-
A robust Node.js automation tool designed to interact with LinkedIn using [Playwright](https://playwright.dev/). This
|
|
3
|
+
A robust, stealthy Node.js automation tool designed to interact with LinkedIn using [Playwright](https://playwright.dev/). This package can be used as a standalone **CLI tool** or integrated as a **Module** into larger applications (supporting database storage, custom logging, and headless controls).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **⚠️ Disclaimer**: This tool is for educational purposes only. Automating interactions on LinkedIn violates their User Agreement. Use at your own risk. The authors are not responsible for any account bans or restrictions.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- **Session Management**: Automatically saves and loads session states (cookies & local storage) to prevent frequent re-logins.
|
|
9
|
-
- **Headless & Visible Modes**: configurable execution modes for debugging or background operation.
|
|
10
|
-
- **Interactive CLI**: integrated REPL (Read-Eval-Print Loop) to control the bot instance after initialization.
|
|
11
|
-
- **Smart Validation**: robust checks to verify login status and handle checkpoints/verifications manually if needed.
|
|
12
|
-
- **Human-like Behavior**: Implements random delays, mouse movements, and dynamic user agents.
|
|
7
|
+
---
|
|
13
8
|
|
|
14
|
-
##
|
|
9
|
+
## 🚀 Key Features
|
|
15
10
|
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
11
|
+
- **🕵️♂️ Stealth Automation**: Leverages `puppeteer-extra-plugin-stealth` to minimize detection risks.
|
|
12
|
+
- **👥 Multi-User Support**: Manage multiple LinkedIn accounts with isolated sessions.
|
|
13
|
+
- **💾 Flexible Session Storage**: Store sessions in local files (default) or **inject your own database adapter** (MongoDB, Postgres, Redis, etc.).
|
|
14
|
+
- **🖥️ Dual Modes**:
|
|
15
|
+
- **Headless**: Runs efficiently in the background.
|
|
16
|
+
- **Visible**: Watch the bot work for debugging.
|
|
17
|
+
- **⌨️ CLI & Interactive REPL**: Control the bot directly from the terminal.
|
|
18
|
+
- **🔌 Module Integration**: Easily integrate into existing Express/Node.js servers with custom logging and webhook-style checkpoint handling.
|
|
19
|
+
- **🧠 Smart Validation**: Automatically detects checkpoints (CAPTCHA/Pin) and allows manual resolution via terminal or callback.
|
|
20
|
+
|
|
21
|
+
---
|
|
19
22
|
|
|
20
23
|
## 📦 Installation
|
|
21
24
|
|
|
22
|
-
1.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
```
|
|
25
|
+
### 1. Install via NPM
|
|
26
|
+
```bash
|
|
27
|
+
npm install @ubaidbinwaris/linkedin
|
|
28
|
+
```
|
|
27
29
|
|
|
28
|
-
2.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
### 2. (Optional) Clone for Source Usage
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/UbaidBinWaris/linkedin.git
|
|
33
|
+
cd linkedin
|
|
34
|
+
npm install
|
|
35
|
+
npx playwright install chromium
|
|
36
|
+
```
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npx playwright install chromium
|
|
36
|
-
```
|
|
38
|
+
---
|
|
37
39
|
|
|
38
|
-
##
|
|
40
|
+
## ▶️ Usage: CLI Mode
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
2. Add your LinkedIn credentials:
|
|
42
|
+
You can run the bot directly from the command line.
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
### 1. Quick Start (Single User)
|
|
45
|
+
Create a `.env` file with your credentials:
|
|
46
|
+
```env
|
|
47
|
+
LINKEDIN_EMAIL=your_email@example.com
|
|
48
|
+
LINKEDIN_PASSWORD=your_password
|
|
49
|
+
```
|
|
50
|
+
Then run:
|
|
51
|
+
```bash
|
|
52
|
+
npm start
|
|
53
|
+
```
|
|
47
54
|
|
|
48
|
-
|
|
55
|
+
### 2. Multi-User Mode
|
|
56
|
+
To manage multiple accounts, create a `users.js` file in the root directory:
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
**File:** `users.js`
|
|
59
|
+
```javascript
|
|
60
|
+
module.exports = [
|
|
61
|
+
{ username: "alice@example.com", password: "password123" },
|
|
62
|
+
{ username: "bob@company.com", password: "securePass!" }
|
|
63
|
+
];
|
|
64
|
+
```
|
|
52
65
|
|
|
66
|
+
Run the bot:
|
|
53
67
|
```bash
|
|
54
68
|
npm start
|
|
55
69
|
```
|
|
70
|
+
The CLI will prompt you to select which user to log in as:
|
|
71
|
+
```text
|
|
72
|
+
Select a user to login:
|
|
73
|
+
1. alice@example.com
|
|
74
|
+
2. bob@company.com
|
|
75
|
+
3. Use Environment Variables (.env)
|
|
76
|
+
```
|
|
56
77
|
|
|
57
|
-
|
|
58
|
-
Useful for debugging or if manual intervention (CAPTCHA) is required.
|
|
78
|
+
---
|
|
59
79
|
|
|
60
|
-
|
|
61
|
-
|
|
80
|
+
## 🔌 Usage: Module Integration
|
|
81
|
+
|
|
82
|
+
This package is designed to be embedded in larger systems (e.g., a SaaS backend managing hundreds of accounts).
|
|
83
|
+
|
|
84
|
+
### Import
|
|
85
|
+
```javascript
|
|
86
|
+
const linkedin = require('@ubaidbinwaris/linkedin');
|
|
62
87
|
```
|
|
63
88
|
|
|
64
|
-
###
|
|
65
|
-
|
|
89
|
+
### 1. Custom Session Storage (Database Integration)
|
|
90
|
+
By default, sessions are saved as JSON files in `data/linkedin/`. You can override this to save sessions directly in your database.
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
// Example: Using a generic database
|
|
94
|
+
linkedin.setSessionStorage({
|
|
95
|
+
async read(username) {
|
|
96
|
+
// Fetch encrypted session string from your DB
|
|
97
|
+
const user = await db.users.findOne({ email: username });
|
|
98
|
+
return user ? user.linkedinSession : null;
|
|
99
|
+
},
|
|
100
|
+
async write(username, data) {
|
|
101
|
+
// Save encrypted session string to your DB
|
|
102
|
+
await db.users.updateOne(
|
|
103
|
+
{ email: username },
|
|
104
|
+
{ $set: { linkedinSession: data } },
|
|
105
|
+
{ upsert: true }
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
```
|
|
66
110
|
|
|
67
|
-
|
|
111
|
+
### 2. Custom Logger
|
|
112
|
+
Redirect bot logs to your application's logging system (e.g., Winston, Pino, Bunyan).
|
|
68
113
|
|
|
114
|
+
```javascript
|
|
115
|
+
linkedin.setLogger({
|
|
116
|
+
info: (msg) => console.log(`[BOT INFO] ${msg}`),
|
|
117
|
+
error: (msg) => console.error(`[BOT ERROR] ${msg}`),
|
|
118
|
+
warn: (msg) => console.warn(`[BOT WARN] ${msg}`),
|
|
119
|
+
debug: (msg) => console.debug(`[BOT DEBUG] ${msg}`)
|
|
120
|
+
});
|
|
69
121
|
```
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
122
|
+
|
|
123
|
+
### 3. Login & Headless Control
|
|
124
|
+
When running on a server, you can't use the terminal to solve CAPTCHAs. Use the `onCheckpoint` callback to handle verification requests (e.g., trigger an alert).
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
const credentials = {
|
|
128
|
+
username: "alice@example.com",
|
|
129
|
+
password: "password123"
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const { browser, page } = await linkedin.loginToLinkedIn({
|
|
134
|
+
headless: true,
|
|
135
|
+
// Callback when LinkedIn asks for verification
|
|
136
|
+
onCheckpoint: async () => {
|
|
137
|
+
console.log("⚠️ Checkpoint detected! Pausing for manual intervention...");
|
|
138
|
+
|
|
139
|
+
// Example: Send an email/Slack alert to admin
|
|
140
|
+
await sendAdminAlert(`User ${credentials.username} needs verification!`);
|
|
141
|
+
|
|
142
|
+
// Wait for admin to signal resolution (e.g. via DB flag or API call)
|
|
143
|
+
await waitForAdminResolution();
|
|
144
|
+
|
|
145
|
+
console.log("Resuming login...");
|
|
146
|
+
}
|
|
147
|
+
}, credentials);
|
|
148
|
+
|
|
149
|
+
console.log("Login successful!");
|
|
150
|
+
|
|
151
|
+
// Do automation tasks...
|
|
152
|
+
await browser.close();
|
|
153
|
+
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error("Login failed:", err);
|
|
156
|
+
}
|
|
80
157
|
```
|
|
81
158
|
|
|
82
|
-
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 🛠️ Configuration Options
|
|
162
|
+
|
|
163
|
+
| Option | Type | Default | Description |
|
|
164
|
+
| :--- | :--- | :--- | :--- |
|
|
165
|
+
| `headless` | `boolean` | `false` | Run browser in background. |
|
|
166
|
+
| `slowMo` | `number` | `50` | Delay between actions (ms). |
|
|
167
|
+
| `proxy` | `string` | `undefined` | Proxy URL (e.g., `http://user:pass@host:port`). |
|
|
168
|
+
| `onCheckpoint` | `function` | `null` | Async callback triggered when verification is needed. |
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## ❓ Troubleshooting
|
|
173
|
+
|
|
174
|
+
### "Checkpoint Detected"
|
|
175
|
+
- **CLI Mode**: The bot will pause and ask you to open a visible browser. Press ENTER to open it, solve the CAPTCHA, and the bot will confirm success and resume.
|
|
176
|
+
- **Module Mode**: Ensure you provide an `onCheckpoint` callback to handle this event, otherwise the promise will reject or hang depending on implementation.
|
|
177
|
+
|
|
178
|
+
### Session Files
|
|
179
|
+
- Sessions are encrypted and contain timestamps.
|
|
180
|
+
- If `setSessionStorage` is NOT used, files are stored in `data/linkedin/<sanitized_username>.json`.
|
|
181
|
+
|
|
182
|
+
---
|
|
83
183
|
|
|
84
|
-
|
|
184
|
+
## 📄 License
|
|
185
|
+
MIT License.
|
package/cli.js
CHANGED
|
@@ -32,12 +32,40 @@ process.on("SIGINT", async () => {
|
|
|
32
32
|
try {
|
|
33
33
|
// Check command line arguments
|
|
34
34
|
const args = process.argv.slice(2);
|
|
35
|
+
|
|
36
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
37
|
+
console.log(`
|
|
38
|
+
LinkedIn Automation CLI
|
|
39
|
+
|
|
40
|
+
Usage:
|
|
41
|
+
linkedin [options]
|
|
42
|
+
|
|
43
|
+
Options:
|
|
44
|
+
--visible Run in visible mode (default is headless)
|
|
45
|
+
--help, -h Show this help message
|
|
46
|
+
--version, -v Show version number
|
|
47
|
+
`);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
52
|
+
const packageJson = require("./package.json");
|
|
53
|
+
console.log(`v${packageJson.version}`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { selectUser } = require("./src/cli/userSelection");
|
|
58
|
+
|
|
35
59
|
const isVisible = args.includes("--visible");
|
|
36
60
|
const isHeadless = !isVisible;
|
|
37
61
|
|
|
62
|
+
// Select User
|
|
63
|
+
const credentials = await selectUser();
|
|
64
|
+
|
|
38
65
|
console.log(`Starting bot in ${isVisible ? "Visible" : "Headless"} Mode...`);
|
|
39
66
|
|
|
40
|
-
|
|
67
|
+
// Pass credentials (or null) to login function
|
|
68
|
+
const { browser, context, page } = await loginToLinkedIn({ headless: isHeadless }, credentials);
|
|
41
69
|
browserInstance = browser;
|
|
42
70
|
|
|
43
71
|
console.log("Login successful.");
|
package/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
const loginToLinkedIn = require('./src/login/login');
|
|
2
2
|
const sessionManager = require('./src/session/sessionManager');
|
|
3
3
|
const config = require('./src/config');
|
|
4
|
-
const logger = require('./src/utils/logger');
|
|
4
|
+
const logger = require('./src/utils/logger'); // This is now the proxy object
|
|
5
5
|
// Exporting the main function and other utilities for library usage
|
|
6
6
|
module.exports = {
|
|
7
7
|
loginToLinkedIn,
|
|
8
8
|
sessionManager,
|
|
9
|
+
setSessionDir: sessionManager.setSessionDir,
|
|
10
|
+
setSessionStorage: sessionManager.setSessionStorage,
|
|
11
|
+
setLogger: logger.setLogger,
|
|
9
12
|
config,
|
|
10
13
|
logger
|
|
11
14
|
};
|
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const logger = require('../utils/logger');
|
|
5
|
+
|
|
6
|
+
const USERS_FILE = path.join(process.cwd(), 'users.js');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Loads users from users.js and prompts for selection if multiple exist.
|
|
10
|
+
* @returns {Promise<Object|null>} Selected credentials { username, password } or null if using env.
|
|
11
|
+
*/
|
|
12
|
+
async function selectUser() {
|
|
13
|
+
if (!fs.existsSync(USERS_FILE)) {
|
|
14
|
+
logger.info("No users.js file found. Using environment variables.");
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let users = [];
|
|
19
|
+
try {
|
|
20
|
+
users = require(USERS_FILE);
|
|
21
|
+
if (!Array.isArray(users) || users.length === 0) {
|
|
22
|
+
logger.warn("users.js exists but exports an empty array or invalid format.");
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logger.error(`Error loading users.js: ${error.message}`);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (users.length === 1) {
|
|
31
|
+
logger.info(`Single user found in users.js: ${users[0].username}`);
|
|
32
|
+
return users[0];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Multiple users - Prompt for selection
|
|
36
|
+
console.log('\nSelect a user to login:');
|
|
37
|
+
users.forEach((u, index) => {
|
|
38
|
+
console.log(`${index + 1}. ${u.username}`);
|
|
39
|
+
});
|
|
40
|
+
console.log(`${users.length + 1}. Use Environment Variables (.env)`);
|
|
41
|
+
|
|
42
|
+
const selectedIndex = await new Promise((resolve) => {
|
|
43
|
+
const rl = readline.createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const ask = () => {
|
|
49
|
+
rl.question('\nEnter number: ', (answer) => {
|
|
50
|
+
const num = parseInt(answer);
|
|
51
|
+
if (!isNaN(num) && num >= 1 && num <= users.length + 1) {
|
|
52
|
+
rl.close();
|
|
53
|
+
resolve(num - 1);
|
|
54
|
+
} else {
|
|
55
|
+
console.log('Invalid selection. Try again.');
|
|
56
|
+
ask();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
ask();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (selectedIndex === users.length) {
|
|
64
|
+
logger.info("Selected: Environment Variables");
|
|
65
|
+
return null; // Fallback to env
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const selectedUser = users[selectedIndex];
|
|
69
|
+
logger.info(`Selected user: ${selectedUser.username}`);
|
|
70
|
+
return selectedUser;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { selectUser };
|
package/src/login/login.js
CHANGED
|
@@ -76,14 +76,26 @@ function getRandomUserAgent() {
|
|
|
76
76
|
* @param {number} [options.slowMo=50] - Slow motion delay in ms.
|
|
77
77
|
* @param {string} [options.proxy] - Optional proxy server URL.
|
|
78
78
|
*/
|
|
79
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Main login function.
|
|
81
|
+
* @param {Object} options - Launch options for the browser.
|
|
82
|
+
* @param {boolean} [options.headless=false] - Whether to run in headless mode.
|
|
83
|
+
* @param {number} [options.slowMo=50] - Slow motion delay in ms.
|
|
84
|
+
* @param {string} [options.proxy] - Optional proxy server URL.
|
|
85
|
+
* @param {Function} [options.onCheckpoint] - Callback when verification is needed in headless mode.
|
|
86
|
+
* @param {Object} [credentials] - Optional credentials object { username, password }
|
|
87
|
+
*/
|
|
88
|
+
async function loginToLinkedIn(options = {}, credentials = null) {
|
|
80
89
|
logger.info("Starting LinkedIn login process with stealth mode...");
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
// Determine credentials
|
|
92
|
+
const email = credentials?.username || process.env.LINKEDIN_EMAIL;
|
|
93
|
+
const password = credentials?.password || process.env.LINKEDIN_PASSWORD;
|
|
94
|
+
|
|
95
|
+
if (!email || !password) {
|
|
96
|
+
const errorMsg = "Missing credentials. Provide them in arguments or set LINKEDIN_EMAIL/LINKEDIN_PASSWORD env vars.";
|
|
97
|
+
logger.error(errorMsg);
|
|
98
|
+
throw new Error(errorMsg);
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
const launchOptions = {
|
|
@@ -97,7 +109,7 @@ async function loginToLinkedIn(options = {}) {
|
|
|
97
109
|
...options,
|
|
98
110
|
};
|
|
99
111
|
|
|
100
|
-
logger.info(`Launching browser...`);
|
|
112
|
+
logger.info(`Launching browser for user: ${email}...`);
|
|
101
113
|
const browser = await chromium.launch(launchOptions);
|
|
102
114
|
|
|
103
115
|
let context;
|
|
@@ -116,8 +128,8 @@ async function loginToLinkedIn(options = {}) {
|
|
|
116
128
|
// -----------------------------
|
|
117
129
|
// STEP 1: Try Using Saved Session
|
|
118
130
|
// -----------------------------
|
|
119
|
-
logger.info(
|
|
120
|
-
context = await loadSession(browser, contextOptions);
|
|
131
|
+
logger.info(`Checking for saved session for ${email}...`);
|
|
132
|
+
context = await loadSession(browser, contextOptions, email);
|
|
121
133
|
|
|
122
134
|
if (context) {
|
|
123
135
|
logger.info("Session stored context created.");
|
|
@@ -141,11 +153,57 @@ async function loginToLinkedIn(options = {}) {
|
|
|
141
153
|
|
|
142
154
|
// Check for checkpoint immediately after navigation
|
|
143
155
|
if (await detectCheckpoint(page)) {
|
|
144
|
-
|
|
145
|
-
|
|
156
|
+
if (launchOptions.headless) {
|
|
157
|
+
logger.warn("Checkpoint detected in headless mode.");
|
|
158
|
+
|
|
159
|
+
if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
|
|
160
|
+
logger.info("Triggering onCheckpoint callback...");
|
|
161
|
+
await options.onCheckpoint();
|
|
162
|
+
// We assume the callback resolves when the user has taken action (e.g. solved via visible browser)
|
|
163
|
+
// But here we need to know what to do next.
|
|
164
|
+
// If the user solved it in a *different* browser/session, we might need to reload.
|
|
165
|
+
// For now, let's assume the callback handles the visible switch logic we implemented before,
|
|
166
|
+
// OR validates externally.
|
|
167
|
+
// However, to keep it simple and compatible with our previous logic:
|
|
168
|
+
// If onCheckpoint is provided, we await it, then close this browser and retry (assuming session saved).
|
|
169
|
+
|
|
170
|
+
logger.info("onCheckpoint resolved. Retrying login...");
|
|
171
|
+
await browser.close();
|
|
172
|
+
return loginToLinkedIn(options, credentials);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
logger.info("Switching to visible mode for manual verification...");
|
|
176
|
+
|
|
177
|
+
await waitForUserResume("Press ENTER to open a visible browser to verify your account...");
|
|
178
|
+
|
|
179
|
+
logger.info("Closing headless browser...");
|
|
180
|
+
await browser.close();
|
|
181
|
+
|
|
182
|
+
logger.info("Launching visible browser for verification...");
|
|
183
|
+
// Call recursively in visible mode
|
|
184
|
+
// We pass the same options but force headless: false
|
|
185
|
+
const visibleInstance = await loginToLinkedIn({ ...options, headless: false }, { username: email, password });
|
|
186
|
+
|
|
187
|
+
// Once the visible instance returns, it means login was successful and session is saved.
|
|
188
|
+
logger.info("Verification successful in visible mode.");
|
|
189
|
+
logger.info("Closing visible browser and resuming headless session...");
|
|
190
|
+
await visibleInstance.browser.close();
|
|
191
|
+
|
|
192
|
+
// Restart the original headless request.
|
|
193
|
+
// It should now find the valid session and proceed without checkpoints.
|
|
194
|
+
return loginToLinkedIn(options, { username: email, password });
|
|
195
|
+
|
|
196
|
+
} else {
|
|
197
|
+
logger.warn("Checkpoint detected immediately. Manual verification required.");
|
|
198
|
+
if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
|
|
199
|
+
await options.onCheckpoint();
|
|
200
|
+
} else {
|
|
201
|
+
await waitForUserResume("Complete verification in the opened browser, then press ENTER here to continue...");
|
|
202
|
+
}
|
|
203
|
+
}
|
|
146
204
|
}
|
|
147
205
|
|
|
148
|
-
const { VALIDATION_SELECTORS } = require("../config");
|
|
206
|
+
const { VALIDATION_SELECTORS } = require("../config");
|
|
149
207
|
|
|
150
208
|
// Verify Session Validity
|
|
151
209
|
try {
|
|
@@ -189,9 +247,6 @@ const { VALIDATION_SELECTORS } = require("../config");
|
|
|
189
247
|
await randomDelay(1000, 2000);
|
|
190
248
|
}
|
|
191
249
|
|
|
192
|
-
const email = process.env.LINKEDIN_EMAIL;
|
|
193
|
-
const password = process.env.LINKEDIN_PASSWORD;
|
|
194
|
-
|
|
195
250
|
logger.info("Entering credentials...");
|
|
196
251
|
|
|
197
252
|
// Simulate human typing
|
|
@@ -214,8 +269,54 @@ const { VALIDATION_SELECTORS } = require("../config");
|
|
|
214
269
|
|
|
215
270
|
// Check for checkpoint again
|
|
216
271
|
if (await detectCheckpoint(page)) {
|
|
217
|
-
|
|
218
|
-
|
|
272
|
+
if (launchOptions.headless) {
|
|
273
|
+
logger.warn("Checkpoint detected in headless mode (post-login).");
|
|
274
|
+
|
|
275
|
+
if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
|
|
276
|
+
logger.info("Triggering onCheckpoint callback...");
|
|
277
|
+
await options.onCheckpoint();
|
|
278
|
+
logger.info("onCheckpoint resolved. Retrying login...");
|
|
279
|
+
await browser.close();
|
|
280
|
+
return loginToLinkedIn(options, credentials);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
logger.info("Switching to visible mode for manual verification...");
|
|
284
|
+
|
|
285
|
+
await waitForUserResume("Press ENTER to open a visible browser to verify your account...");
|
|
286
|
+
|
|
287
|
+
logger.info("Closing headless browser...");
|
|
288
|
+
await browser.close();
|
|
289
|
+
|
|
290
|
+
logger.info("Launching visible browser for verification...");
|
|
291
|
+
const visibleInstance = await loginToLinkedIn({ ...options, headless: false }, { username: email, password });
|
|
292
|
+
|
|
293
|
+
logger.info("Verification successful. Resuming headless session...");
|
|
294
|
+
await visibleInstance.browser.close();
|
|
295
|
+
|
|
296
|
+
return loginToLinkedIn(options, { username: email, password });
|
|
297
|
+
} else {
|
|
298
|
+
logger.warn("Checkpoint detected after login attempt. Manual verification required.");
|
|
299
|
+
if (options.onCheckpoint && typeof options.onCheckpoint === 'function') {
|
|
300
|
+
await options.onCheckpoint();
|
|
301
|
+
} else {
|
|
302
|
+
logger.info("Waiting for manual verification in the opened browser...");
|
|
303
|
+
logger.info("Please solve the CAPTCHA/verification. The browser will close automatically when you are redirected to the feed.");
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
// Wait for URL to include '/feed' OR any validation selector to appear
|
|
307
|
+
await page.waitForFunction(() => {
|
|
308
|
+
return window.location.href.includes("/feed") ||
|
|
309
|
+
document.querySelector('.global-nav__search') ||
|
|
310
|
+
document.querySelector('#global-nav-typeahead');
|
|
311
|
+
}, { timeout: 300000 }); // 5 minutes timeout
|
|
312
|
+
|
|
313
|
+
logger.info("Verification detected! resuming...");
|
|
314
|
+
} catch (err) {
|
|
315
|
+
logger.error("Timeout waiting for manual verification.");
|
|
316
|
+
throw new Error("Manual verification timed out.");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
219
320
|
}
|
|
220
321
|
|
|
221
322
|
// -----------------------------
|
|
@@ -233,7 +334,7 @@ const { VALIDATION_SELECTORS } = require("../config");
|
|
|
233
334
|
logger.info("Login confirmed ✅");
|
|
234
335
|
|
|
235
336
|
// Save session state
|
|
236
|
-
await saveSession(context);
|
|
337
|
+
await saveSession(context, email);
|
|
237
338
|
logger.info("Session state saved 💾");
|
|
238
339
|
|
|
239
340
|
return { browser, context, page };
|
|
@@ -3,7 +3,16 @@ const path = require("path");
|
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
const logger = require("../utils/logger");
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
let sessionDir = path.join(process.cwd(), "data", "linkedin");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Sets the directory where session files are stored.
|
|
10
|
+
* @param {string} dirPath - Absolute path to the session directory.
|
|
11
|
+
*/
|
|
12
|
+
function setSessionDir(dirPath) {
|
|
13
|
+
sessionDir = dirPath;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
const ALGORITHM = "aes-256-cbc";
|
|
8
17
|
// Use a fixed key if not provided in env (for development convenience, but ideally should be in env)
|
|
9
18
|
// Ensure the key is 32 bytes.
|
|
@@ -35,21 +44,75 @@ function decrypt(text) {
|
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Sanitizes a username to be safe for filenames.
|
|
49
|
+
* Replaces special characters with underscores or descriptive text.
|
|
50
|
+
* @param {string} username
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
function sanitizeUsername(username) {
|
|
54
|
+
if (!username) return "default_session";
|
|
55
|
+
return username
|
|
56
|
+
.replace(/@/g, "_at_")
|
|
57
|
+
.replace(/\./g, "_dot_")
|
|
58
|
+
.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getSessionPath(username) {
|
|
62
|
+
const filename = `${sanitizeUsername(username)}.json`;
|
|
63
|
+
return path.join(sessionDir, filename);
|
|
40
64
|
}
|
|
41
65
|
|
|
42
|
-
function
|
|
43
|
-
return
|
|
66
|
+
function sessionExists(username) {
|
|
67
|
+
return fs.existsSync(getSessionPath(username));
|
|
44
68
|
}
|
|
45
69
|
|
|
46
70
|
const { SESSION_MAX_AGE } = require("../config");
|
|
47
71
|
|
|
72
|
+
const defaultStorage = {
|
|
73
|
+
/**
|
|
74
|
+
* Reads session data for a user.
|
|
75
|
+
* @param {string} username
|
|
76
|
+
* @returns {Promise<string|null>} Encrypted session string or null
|
|
77
|
+
*/
|
|
78
|
+
async read(username) {
|
|
79
|
+
const filePath = getSessionPath(username);
|
|
80
|
+
if (!fs.existsSync(filePath)) return null;
|
|
81
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Writes session data for a user.
|
|
86
|
+
* @param {string} username
|
|
87
|
+
* @param {string} data - Encrypted session string
|
|
88
|
+
*/
|
|
89
|
+
async write(username, data) {
|
|
90
|
+
const filePath = getSessionPath(username);
|
|
91
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
92
|
+
fs.writeFileSync(filePath, data, "utf-8");
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let currentStorage = defaultStorage;
|
|
97
|
+
|
|
48
98
|
/**
|
|
49
|
-
*
|
|
99
|
+
* Sets a custom session storage adapter.
|
|
100
|
+
* @param {Object} adapter - Object with read(username) and write(username, data) methods.
|
|
101
|
+
*/
|
|
102
|
+
function setSessionStorage(adapter) {
|
|
103
|
+
if (adapter && typeof adapter.read === 'function' && typeof adapter.write === 'function') {
|
|
104
|
+
currentStorage = adapter;
|
|
105
|
+
} else {
|
|
106
|
+
logger.warn("Invalid storage adapter provided. Using default file storage.");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Saves the browser context storage state to an encrypted file/storage with a timestamp.
|
|
50
112
|
* @param {import('playwright').BrowserContext} context
|
|
113
|
+
* @param {string} [username] - The username to associate with this session
|
|
51
114
|
*/
|
|
52
|
-
async function saveSession(context) {
|
|
115
|
+
async function saveSession(context, username) {
|
|
53
116
|
try {
|
|
54
117
|
const state = await context.storageState();
|
|
55
118
|
|
|
@@ -62,29 +125,39 @@ async function saveSession(context) {
|
|
|
62
125
|
const jsonString = JSON.stringify(sessionData);
|
|
63
126
|
const encryptedData = encrypt(jsonString);
|
|
64
127
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
logger.info(
|
|
128
|
+
await currentStorage.write(username || "default", encryptedData);
|
|
129
|
+
|
|
130
|
+
logger.info(`Session saved successfully for ${username || "default"} (Encrypted & Timestamped) 🔒`);
|
|
68
131
|
} catch (error) {
|
|
69
132
|
logger.error(`Failed to save session: ${error.message}`);
|
|
70
133
|
}
|
|
71
134
|
}
|
|
72
135
|
|
|
73
136
|
/**
|
|
74
|
-
* Loads the session from
|
|
137
|
+
* Loads the session from storage and creates a new context.
|
|
75
138
|
* Checks for session expiry.
|
|
76
139
|
* @param {import('playwright').Browser} browser
|
|
77
140
|
* @param {Object} options - Context options
|
|
141
|
+
* @param {string} [username] - The username to load session for
|
|
78
142
|
* @returns {Promise<import('playwright').BrowserContext>}
|
|
79
143
|
*/
|
|
80
|
-
async function loadSession(browser, options = {}) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
144
|
+
async function loadSession(browser, options = {}, username) {
|
|
145
|
+
const user = username || "default";
|
|
146
|
+
|
|
147
|
+
// Check existence if storage supports it, otherwise generic check
|
|
148
|
+
// For custom storage, read() returning null implies not found.
|
|
149
|
+
|
|
150
|
+
// Note: sessionExists is tied to FS. We should deprecate it or update it.
|
|
151
|
+
// But for now, let's rely on read() returning null.
|
|
85
152
|
|
|
86
153
|
try {
|
|
87
|
-
const fileContent =
|
|
154
|
+
const fileContent = await currentStorage.read(user);
|
|
155
|
+
|
|
156
|
+
if (!fileContent) {
|
|
157
|
+
logger.info(`No session found for ${user}.`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
88
161
|
let sessionData;
|
|
89
162
|
let state;
|
|
90
163
|
|
|
@@ -119,7 +192,7 @@ async function loadSession(browser, options = {}) {
|
|
|
119
192
|
return null;
|
|
120
193
|
}
|
|
121
194
|
} else {
|
|
122
|
-
logger.error("Failed to decrypt session
|
|
195
|
+
logger.error("Failed to decrypt session data. It might be corrupt or key mismatch.");
|
|
123
196
|
return null;
|
|
124
197
|
}
|
|
125
198
|
}
|
|
@@ -132,7 +205,7 @@ async function loadSession(browser, options = {}) {
|
|
|
132
205
|
return null;
|
|
133
206
|
}
|
|
134
207
|
state = sessionData.state;
|
|
135
|
-
logger.info(`Loaded active session (Age: ${Math.round(age / 1000 / 60)} mins) 🔓.`);
|
|
208
|
+
logger.info(`Loaded active session for ${username || "default"} (Age: ${Math.round(age / 1000 / 60)} mins) 🔓.`);
|
|
136
209
|
}
|
|
137
210
|
|
|
138
211
|
const context = await browser.newContext({
|
|
@@ -148,6 +221,8 @@ async function loadSession(browser, options = {}) {
|
|
|
148
221
|
}
|
|
149
222
|
|
|
150
223
|
module.exports = {
|
|
224
|
+
setSessionDir,
|
|
225
|
+
setSessionStorage,
|
|
151
226
|
sessionExists,
|
|
152
227
|
getSessionPath,
|
|
153
228
|
saveSession,
|
package/src/utils/logger.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { createLogger, format, transports } = require("winston");
|
|
2
|
+
const winston = require("winston");
|
|
2
3
|
const path = require("path");
|
|
3
4
|
const fs = require("fs");
|
|
4
5
|
|
|
@@ -8,23 +9,42 @@ if (!fs.existsSync(logDir)) {
|
|
|
8
9
|
fs.mkdirSync(logDir, { recursive: true });
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
// Create default logger
|
|
13
|
+
const defaultLogger = winston.createLogger({
|
|
12
14
|
level: "info",
|
|
13
|
-
format: format.combine(
|
|
14
|
-
format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
15
|
-
format.printf(({ timestamp, level, message }) => {
|
|
16
|
-
return `${timestamp} [${level.toUpperCase()}] ${message}`;
|
|
15
|
+
format: winston.format.combine(
|
|
16
|
+
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
|
17
|
+
winston.format.printf(({ timestamp, level, message }) => {
|
|
18
|
+
return `${timestamp} [${level.toUpperCase()}]: ${message}`;
|
|
17
19
|
})
|
|
18
20
|
),
|
|
19
21
|
transports: [
|
|
20
|
-
new transports.File({
|
|
21
|
-
|
|
22
|
-
level: "error",
|
|
23
|
-
}),
|
|
24
|
-
new transports.File({
|
|
25
|
-
filename: path.join(logDir, "combined.log"),
|
|
26
|
-
}),
|
|
22
|
+
new winston.transports.File({ filename: path.join(logDir, "error.log"), level: "error" }),
|
|
23
|
+
new winston.transports.File({ filename: path.join(logDir, "combined.log") }),
|
|
27
24
|
],
|
|
28
25
|
});
|
|
29
26
|
|
|
30
|
-
|
|
27
|
+
let currentLogger = defaultLogger;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Sets a custom logger instance.
|
|
31
|
+
* @param {Object} customLogger - Logger object with info(), error(), warn(), debug() methods.
|
|
32
|
+
*/
|
|
33
|
+
function setLogger(customLogger) {
|
|
34
|
+
if (customLogger && typeof customLogger.info === 'function') {
|
|
35
|
+
currentLogger = customLogger;
|
|
36
|
+
} else {
|
|
37
|
+
console.warn("Invalid logger provided. Using default.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Proxy object to forward calls to the current logger
|
|
42
|
+
const loggerProxy = {
|
|
43
|
+
info: (msg) => currentLogger.info(msg),
|
|
44
|
+
error: (msg) => currentLogger.error(msg),
|
|
45
|
+
warn: (msg) => currentLogger.warn(msg),
|
|
46
|
+
debug: (msg) => currentLogger.debug(msg),
|
|
47
|
+
setLogger // Export configuration method
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
module.exports = loggerProxy;
|