@ubaidbinwaris/linkedin 1.0.13 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Readme.md +88 -158
- package/index.js +8 -9
- package/package.json +1 -7
- package/src/auth/actions.js +49 -0
- package/src/auth/checkpoint.js +31 -0
- package/src/browser/launcher.js +62 -0
- package/src/login/login.js +83 -7
- package/src/session/SessionLock.js +52 -0
- package/src/session/sessionManager.js +81 -103
- package/cli.js +0 -84
- package/src/cli/repl.js +0 -68
- package/src/cli/userSelection.js +0 -73
package/Readme.md
CHANGED
|
@@ -1,185 +1,115 @@
|
|
|
1
|
-
# LinkedIn
|
|
1
|
+
# LinkedIn Session Manager & Automation Service (v2.0.0)
|
|
2
2
|
|
|
3
|
-
A robust,
|
|
3
|
+
A robust, deterministic backend service foundation for managing multiple LinkedIn accounts with strict concurrency control and session isolation.
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> **v2.0.0 Change**: This major version pivots from a CLI tool to a backend service architecture. It removes "stealth" plugins and "auto-resolution" magic in favor of stability, reliability, and "fail-fast" principles.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Core Features
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **Multi-User Architecture**: Designed to handle hundreds of accounts safely.
|
|
10
|
+
- **Concurrency Control**: In-memory locking (`SessionLock`) prevents parallel login attempts for the same user.
|
|
11
|
+
- **Session Isolation**:
|
|
12
|
+
- Sessions stored as `SHA-256` hashes of emails (privacy).
|
|
13
|
+
- AES-256 Encryption for session data.
|
|
14
|
+
- **Smart Validation**:
|
|
15
|
+
- Trusts sessions validated within the last 10 minutes (reduces feed navigation load).
|
|
16
|
+
- Automatically refreshes older sessions.
|
|
17
|
+
- **Deterministic Flow**:
|
|
18
|
+
- **Launch** -> **Check Session** -> **Login** -> **Fail/Success**.
|
|
19
|
+
- **Fail Fast**: If a checkpoint/challenge is detected, it throws `CHECKPOINT_DETECTED` immediately, allowing the upper layer (API/Worker) to handle it (e.g., notify admin).
|
|
10
20
|
|
|
11
|
-
|
|
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.
|
|
21
|
+
## Installation
|
|
20
22
|
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## 📦 Installation
|
|
24
|
-
|
|
25
|
-
### 1. Install via NPM
|
|
26
23
|
```bash
|
|
27
24
|
npm install @ubaidbinwaris/linkedin
|
|
28
25
|
```
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
## Architecture
|
|
28
|
+
|
|
29
|
+
```mermaid
|
|
30
|
+
graph TD
|
|
31
|
+
A[API/Worker Request] --> B{Acquire User Lock}
|
|
32
|
+
B -- Busy --> C[Throw BUSY Error]
|
|
33
|
+
B -- Acquired --> D[Launch Standard Playwright]
|
|
34
|
+
D --> E{Load Session}
|
|
35
|
+
E -- Valid & Recent --> F[Return Context (Skip Feed)]
|
|
36
|
+
E -- Stale/None --> G[Navigate to Feed]
|
|
37
|
+
G --> H{Is Logged In?}
|
|
38
|
+
H -- Yes --> I[Update Validation Timestamp]
|
|
39
|
+
I --> F
|
|
40
|
+
H -- No --> J[Perform Credential Login]
|
|
41
|
+
J --> K{Checkpoint?}
|
|
42
|
+
K -- Yes --> L[Throw CHECKPOINT_DETECTED]
|
|
43
|
+
K -- No --> M[Login Success]
|
|
44
|
+
M --> I
|
|
45
|
+
F --> N[Release Lock (on Task Completion)]
|
|
36
46
|
```
|
|
37
47
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## ▶️ Usage: CLI Mode
|
|
41
|
-
|
|
42
|
-
You can run the bot directly from the command line.
|
|
48
|
+
## Usage
|
|
43
49
|
|
|
44
|
-
###
|
|
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
|
-
```
|
|
50
|
+
### Basic Login
|
|
54
51
|
|
|
55
|
-
|
|
56
|
-
To manage multiple accounts, create a `users.js` file in the root directory:
|
|
52
|
+
The `loginToLinkedIn` function now handles locking internally. If you try to call it twice for the same user simultaneously, the second call will fail with a `BUSY` error.
|
|
57
53
|
|
|
58
|
-
**File:** `users.js`
|
|
59
54
|
```javascript
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
55
|
+
const { loginToLinkedIn } = require('@ubaidbinwaris/linkedin');
|
|
56
|
+
|
|
57
|
+
(async () => {
|
|
58
|
+
try {
|
|
59
|
+
const { browser, page } = await loginToLinkedIn({
|
|
60
|
+
headless: true
|
|
61
|
+
}, {
|
|
62
|
+
username: 'user@example.com',
|
|
63
|
+
password: 'secret-password'
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
console.log("Logged in and active!");
|
|
67
|
+
|
|
68
|
+
// Output session status
|
|
69
|
+
console.log(`Needs Validation? ${page.context().needsValidation}`);
|
|
70
|
+
|
|
71
|
+
// Do work...
|
|
72
|
+
|
|
73
|
+
await browser.close();
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err.message === 'CHECKPOINT_DETECTED') {
|
|
76
|
+
console.error("Manual intervention required for this account.");
|
|
77
|
+
} else if (err.message.startsWith('BUSY')) {
|
|
78
|
+
console.error("User is currently busy with another task.");
|
|
79
|
+
} else {
|
|
80
|
+
console.error("Login failed:", err);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
76
84
|
```
|
|
77
85
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
## 🔌 Usage: Module Integration
|
|
86
|
+
### Custom Session Storage
|
|
81
87
|
|
|
82
|
-
|
|
88
|
+
You can link this to a database (Redis/Postgres) instead of local files.
|
|
83
89
|
|
|
84
|
-
### Import
|
|
85
90
|
```javascript
|
|
86
|
-
const
|
|
87
|
-
```
|
|
88
|
-
|
|
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
|
+
const { setSessionStorage } = require('@ubaidbinwaris/linkedin');
|
|
91
92
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
// Fetch encrypted session string from your DB
|
|
97
|
-
const user = await db.users.findOne({ email: username });
|
|
98
|
-
return user ? user.linkedinSession : null;
|
|
93
|
+
setSessionStorage({
|
|
94
|
+
read: async (email) => {
|
|
95
|
+
// Fetch encrypted string from DB
|
|
96
|
+
return await db.sessions.findOne({ where: { email } });
|
|
99
97
|
},
|
|
100
|
-
async
|
|
101
|
-
// Save encrypted
|
|
102
|
-
await db.
|
|
103
|
-
{ email: username },
|
|
104
|
-
{ $set: { linkedinSession: data } },
|
|
105
|
-
{ upsert: true }
|
|
106
|
-
);
|
|
98
|
+
write: async (email, data) => {
|
|
99
|
+
// Save encrypted string to DB
|
|
100
|
+
await db.sessions.upsert({ email, data });
|
|
107
101
|
}
|
|
108
102
|
});
|
|
109
103
|
```
|
|
110
104
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
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
|
-
---
|
|
183
|
-
|
|
184
|
-
## 📄 License
|
|
185
|
-
MIT License.
|
|
105
|
+
## Directory Structure
|
|
106
|
+
|
|
107
|
+
* `src/session/`:
|
|
108
|
+
* `SessionLock.js`: In-memory concurrency control.
|
|
109
|
+
* `sessionManager.js`: Hashing, encryption, validation logic.
|
|
110
|
+
* `src/login/`:
|
|
111
|
+
* `login.js`: Deterministic login flow.
|
|
112
|
+
* `src/browser/`:
|
|
113
|
+
* `launcher.js`: Standard Playwright launcher (no stealth plugins).
|
|
114
|
+
* `src/auth/`:
|
|
115
|
+
* `checkpoint.js`: Simple detection logic.
|
package/index.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
+
// Main entry point
|
|
1
2
|
const loginToLinkedIn = require('./src/login/login');
|
|
2
|
-
const
|
|
3
|
+
const { setSessionDir, setSessionStorage } = require('./src/session/sessionManager');
|
|
4
|
+
const { setLogger } = require('./src/utils/logger');
|
|
3
5
|
const config = require('./src/config');
|
|
4
|
-
|
|
5
|
-
// Exporting the main function and other utilities for library usage
|
|
6
|
+
|
|
6
7
|
module.exports = {
|
|
7
8
|
loginToLinkedIn,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
config,
|
|
13
|
-
logger
|
|
9
|
+
setSessionDir,
|
|
10
|
+
setSessionStorage,
|
|
11
|
+
setLogger,
|
|
12
|
+
config
|
|
14
13
|
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ubaidbinwaris/linkedin",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
|
-
"bin": {
|
|
7
|
-
"linkedin": "cli.js"
|
|
8
|
-
},
|
|
9
6
|
"files": [
|
|
10
7
|
"src",
|
|
11
8
|
"index.js",
|
|
@@ -37,9 +34,6 @@
|
|
|
37
34
|
"dependencies": {
|
|
38
35
|
"dotenv": "^17.2.4",
|
|
39
36
|
"playwright": "^1.58.2",
|
|
40
|
-
"playwright-extra": "^4.3.6",
|
|
41
|
-
"puppeteer-extra-plugin-stealth": "^2.11.2",
|
|
42
|
-
"user-agents": "^1.1.669",
|
|
43
37
|
"winston": "^3.19.0"
|
|
44
38
|
}
|
|
45
39
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const logger = require("../utils/logger");
|
|
2
|
+
const { randomDelay } = require("../utils/time");
|
|
3
|
+
|
|
4
|
+
const LINKEDIN_LOGIN = "https://www.linkedin.com/login";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Performs credential-based login.
|
|
8
|
+
*/
|
|
9
|
+
async function performCredentialLogin(page, email, password) {
|
|
10
|
+
logger.info("Proceeding to credential login...");
|
|
11
|
+
|
|
12
|
+
if (!page.url().includes("login") && !page.url().includes("uas/request-password-reset")) {
|
|
13
|
+
await page.goto(LINKEDIN_LOGIN, { waitUntil: 'domcontentloaded' });
|
|
14
|
+
await randomDelay(1000, 2000);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
logger.info("Entering credentials...");
|
|
18
|
+
|
|
19
|
+
await page.click('input[name="session_key"]');
|
|
20
|
+
await randomDelay(500, 1000);
|
|
21
|
+
await page.type('input[name="session_key"]', email, { delay: 100 });
|
|
22
|
+
|
|
23
|
+
await randomDelay(1000, 2000);
|
|
24
|
+
|
|
25
|
+
await page.click('input[name="session_password"]');
|
|
26
|
+
await page.type('input[name="session_password"]', password, { delay: 100 });
|
|
27
|
+
|
|
28
|
+
await randomDelay(1000, 2000);
|
|
29
|
+
|
|
30
|
+
logger.info("Submitting login form...");
|
|
31
|
+
await Promise.all([
|
|
32
|
+
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
|
|
33
|
+
page.click('button[type="submit"]')
|
|
34
|
+
]);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Checks if the user is currently logged in (on feed).
|
|
39
|
+
*/
|
|
40
|
+
async function isLoggedIn(page) {
|
|
41
|
+
try {
|
|
42
|
+
await page.waitForSelector(".global-nav__search, #global-nav-typeahead", { timeout: 10000 });
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { performCredentialLogin, isLoggedIn };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects if a checkpoint/verification is triggered.
|
|
3
|
+
* Returns true if intervention is needed.
|
|
4
|
+
*/
|
|
5
|
+
async function detectCheckpoint(page) {
|
|
6
|
+
try {
|
|
7
|
+
const url = page.url();
|
|
8
|
+
if (
|
|
9
|
+
url.includes("checkpoint") ||
|
|
10
|
+
url.includes("challenge") ||
|
|
11
|
+
url.includes("verification") ||
|
|
12
|
+
url.includes("consumer-login/error")
|
|
13
|
+
) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await page.waitForSelector("h1:has-text('Security Verification')", { timeout: 1000 });
|
|
19
|
+
return true;
|
|
20
|
+
} catch (e) {
|
|
21
|
+
// Element not found
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return false;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logger.error(`Error during checkpoint detection: ${error.message}`);
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { detectCheckpoint };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { chromium } = require("playwright");
|
|
2
|
+
const logger = require("../utils/logger");
|
|
3
|
+
const { loadSession } = require("../session/sessionManager");
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates and launches a browser instance.
|
|
7
|
+
* @param {Object} options - Launch options
|
|
8
|
+
*/
|
|
9
|
+
async function createBrowser(options = {}) {
|
|
10
|
+
const launchOptions = {
|
|
11
|
+
headless: options.headless !== undefined ? options.headless : true,
|
|
12
|
+
args: [
|
|
13
|
+
"--no-sandbox",
|
|
14
|
+
"--disable-setuid-sandbox",
|
|
15
|
+
],
|
|
16
|
+
...options
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
logger.info("Launching browser (Standard Playwright)...");
|
|
20
|
+
return await chromium.launch(launchOptions);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates or loads a browser context.
|
|
25
|
+
* @param {import('playwright').Browser} browser
|
|
26
|
+
* @param {string} email - for session loading
|
|
27
|
+
*/
|
|
28
|
+
async function createContext(browser, email) {
|
|
29
|
+
const contextOptions = {
|
|
30
|
+
userAgent: getRandomUserAgent(),
|
|
31
|
+
viewport: getRandomViewport(),
|
|
32
|
+
locale: 'en-US',
|
|
33
|
+
timezoneId: 'America/New_York',
|
|
34
|
+
permissions: ['geolocation'],
|
|
35
|
+
ignoreHTTPSErrors: true,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
logger.info(`Checking for saved session for ${email}...`);
|
|
39
|
+
const storedContext = await loadSession(browser, contextOptions, email);
|
|
40
|
+
|
|
41
|
+
if (storedContext) {
|
|
42
|
+
logger.info("Session stored context loaded.");
|
|
43
|
+
return storedContext;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
logger.info("No valid session found. Creating new context.");
|
|
47
|
+
return await browser.newContext(contextOptions);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getRandomViewport() {
|
|
51
|
+
const width = 1280 + Math.floor(Math.random() * 640);
|
|
52
|
+
const height = 720 + Math.floor(Math.random() * 360);
|
|
53
|
+
return { width, height };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getRandomUserAgent() {
|
|
57
|
+
const versions = ["120.0.0.0", "121.0.0.0", "122.0.0.0"];
|
|
58
|
+
const version = versions[Math.floor(Math.random() * versions.length)];
|
|
59
|
+
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { createBrowser, createContext };
|
package/src/login/login.js
CHANGED
|
@@ -101,17 +101,92 @@ async function performCredentialLogin(page, email, password) {
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
async function handleCheckpoint(page, options) {
|
|
104
|
+
// Initial check
|
|
104
105
|
if (!(await detectCheckpoint(page))) return;
|
|
105
106
|
|
|
107
|
+
logger.warn("Checkpoint detected.");
|
|
108
|
+
|
|
106
109
|
if (options.headless) {
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
logger.info("Headless mode detected. Attempting auto-resolution...");
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------
|
|
113
|
+
// STRATEGY 1: Click Simple Buttons (Yes, Skip, Continue)
|
|
114
|
+
// ---------------------------------------------------------
|
|
115
|
+
try {
|
|
116
|
+
const clicked = await page.evaluate(() => {
|
|
117
|
+
const candidates = Array.from(document.querySelectorAll('button, a, [role="button"], input[type="submit"], input[type="button"]'));
|
|
118
|
+
const targetText = ['Yes', 'Skip', 'Not now', 'Continue', 'Sign in', 'Verify', 'Let’s do it', 'Next'];
|
|
119
|
+
|
|
120
|
+
const btn = candidates.find(b => {
|
|
121
|
+
const text = (b.innerText || b.value || '').trim();
|
|
122
|
+
return targetText.some(t => text.includes(t));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (btn) {
|
|
126
|
+
btn.click();
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (clicked) {
|
|
133
|
+
logger.info("Clicked a resolution button. Waiting for navigation...");
|
|
134
|
+
await page.waitForTimeout(3000);
|
|
135
|
+
|
|
136
|
+
// Check if resolved
|
|
137
|
+
if (!(await detectCheckpoint(page))) {
|
|
138
|
+
logger.info("Checkpoint resolved via button click!");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
logger.warn(`Auto-resolve button click failed: ${e.message}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ---------------------------------------------------------
|
|
147
|
+
// STRATEGY 2: Mobile App Verification (Wait & Poll)
|
|
148
|
+
// ---------------------------------------------------------
|
|
149
|
+
try {
|
|
150
|
+
const isMobileVerif = await page.evaluate(() => {
|
|
151
|
+
const text = document.body.innerText;
|
|
152
|
+
return text.includes("Open your LinkedIn app") ||
|
|
153
|
+
text.includes("Tap Yes on the prompt") ||
|
|
154
|
+
text.includes("verification request to your device");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (isMobileVerif) {
|
|
158
|
+
logger.info("Mobile verification detected. Waiting 2 minutes for manual approval on device...");
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// Poll for feed URL for 120 seconds
|
|
162
|
+
// We use a loop or waitForFunction
|
|
163
|
+
await page.waitForFunction(() => {
|
|
164
|
+
return window.location.href.includes("/feed") ||
|
|
165
|
+
document.querySelector('.global-nav__search');
|
|
166
|
+
}, { timeout: 120000 });
|
|
167
|
+
|
|
168
|
+
logger.info("Mobile verification successful! Resuming...");
|
|
169
|
+
return;
|
|
170
|
+
} catch (timeoutErr) {
|
|
171
|
+
logger.warn("Mobile verification timed out.");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch (e) {
|
|
175
|
+
logger.warn(`Mobile verification check failed: ${e.message}`);
|
|
176
|
+
}
|
|
109
177
|
|
|
110
|
-
|
|
178
|
+
// Re-check after attempts
|
|
179
|
+
if (await detectCheckpoint(page)) {
|
|
180
|
+
throw new Error("Checkpoint detected in headless mode and auto-resolution failed.");
|
|
181
|
+
}
|
|
111
182
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
183
|
+
} else {
|
|
184
|
+
// Visible mode
|
|
185
|
+
logger.warn("Verification required. Please complete manually.");
|
|
186
|
+
await waitForUserResume(
|
|
187
|
+
"Complete verification in browser, then press ENTER..."
|
|
188
|
+
);
|
|
189
|
+
}
|
|
115
190
|
}
|
|
116
191
|
|
|
117
192
|
async function detectCheckpoint(page) {
|
|
@@ -120,7 +195,8 @@ async function detectCheckpoint(page) {
|
|
|
120
195
|
return (
|
|
121
196
|
url.includes("checkpoint") ||
|
|
122
197
|
url.includes("challenge") ||
|
|
123
|
-
url.includes("verification")
|
|
198
|
+
url.includes("verification") ||
|
|
199
|
+
url.includes("consumer-login/error")
|
|
124
200
|
);
|
|
125
201
|
}
|
|
126
202
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const logger = require("../utils/logger");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory lock map to prevent simultaneous login attempts for the same user.
|
|
5
|
+
* Map<email, Promise<any>>
|
|
6
|
+
*/
|
|
7
|
+
const activeLogins = new Map();
|
|
8
|
+
|
|
9
|
+
const SessionLock = {
|
|
10
|
+
/**
|
|
11
|
+
* Executes a function with an exclusive lock for the given email.
|
|
12
|
+
* If a lock exists, it waits for it to release (or chains onto it?
|
|
13
|
+
* Actually user requested: "if activeLogins.has(email) { return activeLogins.get(email); }"
|
|
14
|
+
* This means if a login is in progress, return that SAME promise. Join the existing attempt.)
|
|
15
|
+
*
|
|
16
|
+
* @param {string} email
|
|
17
|
+
* @param {Function} fn - Async function to execute
|
|
18
|
+
* @returns {Promise<any>}
|
|
19
|
+
*/
|
|
20
|
+
async withLoginLock(email, fn) {
|
|
21
|
+
if (activeLogins.has(email)) {
|
|
22
|
+
logger.info(`[SessionLock] ${email} is already logging in. Joining existing request...`);
|
|
23
|
+
return activeLogins.get(email);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logger.info(`[SessionLock] Acquiring lock for ${email}...`);
|
|
27
|
+
|
|
28
|
+
// Create a promise that wraps the execution
|
|
29
|
+
const promise = (async () => {
|
|
30
|
+
try {
|
|
31
|
+
return await fn();
|
|
32
|
+
} finally {
|
|
33
|
+
activeLogins.delete(email);
|
|
34
|
+
logger.info(`[SessionLock] Releasing lock for ${email}.`);
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
|
|
38
|
+
activeLogins.set(email, promise);
|
|
39
|
+
return promise;
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Checks if a user is currently locked.
|
|
44
|
+
* @param {string} email
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
isLocked(email) {
|
|
48
|
+
return activeLogins.has(email);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
module.exports = SessionLock;
|
|
@@ -2,6 +2,7 @@ const fs = require("fs");
|
|
|
2
2
|
const path = require("path");
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
const logger = require("../utils/logger");
|
|
5
|
+
const SessionLock = require("./SessionLock"); // Keeping specific import, though widely used via login
|
|
5
6
|
|
|
6
7
|
let sessionDir = path.join(process.cwd(), "data", "linkedin");
|
|
7
8
|
|
|
@@ -14,11 +15,7 @@ function setSessionDir(dirPath) {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const ALGORITHM = "aes-256-cbc";
|
|
17
|
-
// Use a fixed key if not provided in env (for development convenience, but ideally should be in env)
|
|
18
|
-
// Ensure the key is 32 bytes.
|
|
19
18
|
const SECRET_KEY = process.env.SESSION_SECRET || "default_insecure_secret_key_32_bytes_long!!";
|
|
20
|
-
|
|
21
|
-
// Ensure key is exactly 32 bytes for aes-256-cbc
|
|
22
19
|
const key = crypto.createHash("sha256").update(String(SECRET_KEY)).digest();
|
|
23
20
|
|
|
24
21
|
function encrypt(text) {
|
|
@@ -39,55 +36,44 @@ function decrypt(text) {
|
|
|
39
36
|
decrypted += decipher.final('utf8');
|
|
40
37
|
return decrypted;
|
|
41
38
|
} catch (error) {
|
|
42
|
-
// If decryption fails, it might be a plain JSON file or invalid key
|
|
43
39
|
return null;
|
|
44
40
|
}
|
|
45
41
|
}
|
|
46
42
|
|
|
47
43
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* @param {string}
|
|
51
|
-
* @returns {string}
|
|
44
|
+
* Generates a SHA-256 hash of the email to use as filename.
|
|
45
|
+
* Privacy + filesystem safety.
|
|
46
|
+
* @param {string} email
|
|
47
|
+
* @returns {string} hex hash
|
|
52
48
|
*/
|
|
53
|
-
function
|
|
54
|
-
if (!
|
|
55
|
-
return
|
|
56
|
-
.replace(/@/g, "_at_")
|
|
57
|
-
.replace(/\./g, "_dot_")
|
|
58
|
-
.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
49
|
+
function getSessionHash(email) {
|
|
50
|
+
if (!email) return "default";
|
|
51
|
+
return crypto.createHash("sha256").update(email).digest("hex");
|
|
59
52
|
}
|
|
60
53
|
|
|
61
|
-
function getSessionPath(
|
|
62
|
-
const filename = `${
|
|
54
|
+
function getSessionPath(email) {
|
|
55
|
+
const filename = `${getSessionHash(email)}.json`;
|
|
63
56
|
return path.join(sessionDir, filename);
|
|
64
57
|
}
|
|
65
58
|
|
|
66
|
-
function sessionExists(
|
|
67
|
-
return fs.existsSync(getSessionPath(
|
|
59
|
+
function sessionExists(email) {
|
|
60
|
+
return fs.existsSync(getSessionPath(email));
|
|
68
61
|
}
|
|
69
62
|
|
|
70
|
-
const { SESSION_MAX_AGE } = require("../config");
|
|
63
|
+
const { SESSION_MAX_AGE } = require("../config"); // Assuming this exists, typically 7 days?
|
|
64
|
+
|
|
65
|
+
// Validation Cache Duration (10 minutes)
|
|
66
|
+
const VALIDATION_CACHE_MS = 10 * 60 * 1000;
|
|
71
67
|
|
|
72
68
|
const defaultStorage = {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
* @param {string} username
|
|
76
|
-
* @returns {Promise<string|null>} Encrypted session string or null
|
|
77
|
-
*/
|
|
78
|
-
async read(username) {
|
|
79
|
-
const filePath = getSessionPath(username);
|
|
69
|
+
async read(email) {
|
|
70
|
+
const filePath = getSessionPath(email);
|
|
80
71
|
if (!fs.existsSync(filePath)) return null;
|
|
81
72
|
return fs.readFileSync(filePath, "utf-8");
|
|
82
73
|
},
|
|
83
74
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* @param {string} username
|
|
87
|
-
* @param {string} data - Encrypted session string
|
|
88
|
-
*/
|
|
89
|
-
async write(username, data) {
|
|
90
|
-
const filePath = getSessionPath(username);
|
|
75
|
+
async write(email, data) {
|
|
76
|
+
const filePath = getSessionPath(email);
|
|
91
77
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
92
78
|
fs.writeFileSync(filePath, data, "utf-8");
|
|
93
79
|
}
|
|
@@ -95,10 +81,6 @@ const defaultStorage = {
|
|
|
95
81
|
|
|
96
82
|
let currentStorage = defaultStorage;
|
|
97
83
|
|
|
98
|
-
/**
|
|
99
|
-
* Sets a custom session storage adapter.
|
|
100
|
-
* @param {Object} adapter - Object with read(username) and write(username, data) methods.
|
|
101
|
-
*/
|
|
102
84
|
function setSessionStorage(adapter) {
|
|
103
85
|
if (adapter && typeof adapter.read === 'function' && typeof adapter.write === 'function') {
|
|
104
86
|
currentStorage = adapter;
|
|
@@ -108,47 +90,43 @@ function setSessionStorage(adapter) {
|
|
|
108
90
|
}
|
|
109
91
|
|
|
110
92
|
/**
|
|
111
|
-
* Saves the browser context
|
|
93
|
+
* Saves the browser context state.
|
|
112
94
|
* @param {import('playwright').BrowserContext} context
|
|
113
|
-
* @param {string}
|
|
95
|
+
* @param {string} email
|
|
96
|
+
* @param {boolean} [isValidated] - If true, updates lastValidatedAt
|
|
114
97
|
*/
|
|
115
|
-
async function saveSession(context,
|
|
98
|
+
async function saveSession(context, email, isValidated = false) {
|
|
116
99
|
try {
|
|
117
100
|
const state = await context.storageState();
|
|
118
101
|
|
|
119
|
-
//
|
|
102
|
+
// Load existing metadata if possible to preserve creation time?
|
|
103
|
+
// For now, simpler to just overwrite, but we might lose 'createdAt' if we had it.
|
|
104
|
+
// Let's just write new.
|
|
105
|
+
|
|
120
106
|
const sessionData = {
|
|
121
|
-
timestamp: Date.now(),
|
|
107
|
+
timestamp: Date.now(), // Last saved
|
|
108
|
+
lastValidatedAt: isValidated ? Date.now() : undefined,
|
|
122
109
|
state: state
|
|
110
|
+
// Future: proxy binding here
|
|
123
111
|
};
|
|
124
112
|
|
|
125
113
|
const jsonString = JSON.stringify(sessionData);
|
|
126
114
|
const encryptedData = encrypt(jsonString);
|
|
127
115
|
|
|
128
|
-
await currentStorage.write(
|
|
116
|
+
await currentStorage.write(email || "default", encryptedData);
|
|
129
117
|
|
|
130
|
-
logger.info(`Session saved
|
|
118
|
+
logger.info(`Session saved for ${email} (Validated: ${isValidated}) 🔒`);
|
|
131
119
|
} catch (error) {
|
|
132
120
|
logger.error(`Failed to save session: ${error.message}`);
|
|
133
121
|
}
|
|
134
122
|
}
|
|
135
123
|
|
|
136
124
|
/**
|
|
137
|
-
* Loads the session
|
|
138
|
-
*
|
|
139
|
-
* @param {import('playwright').Browser} browser
|
|
140
|
-
* @param {Object} options - Context options
|
|
141
|
-
* @param {string} [username] - The username to load session for
|
|
142
|
-
* @returns {Promise<import('playwright').BrowserContext>}
|
|
125
|
+
* Loads the session.
|
|
126
|
+
* @returns {Promise<{ context: import('playwright').BrowserContext, needsValidation: boolean }>}
|
|
143
127
|
*/
|
|
144
|
-
async function loadSession(browser, options = {},
|
|
145
|
-
const user =
|
|
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.
|
|
128
|
+
async function loadSession(browser, options = {}, email) {
|
|
129
|
+
const user = email || "default";
|
|
152
130
|
|
|
153
131
|
try {
|
|
154
132
|
const fileContent = await currentStorage.read(user);
|
|
@@ -158,54 +136,47 @@ async function loadSession(browser, options = {}, username) {
|
|
|
158
136
|
return null;
|
|
159
137
|
}
|
|
160
138
|
|
|
161
|
-
let sessionData;
|
|
162
|
-
let state;
|
|
163
|
-
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// It's the old structure (just storageState)
|
|
173
|
-
state = parsed;
|
|
174
|
-
logger.info("Loaded plain JSON session (Legacy).");
|
|
139
|
+
let sessionData = null;
|
|
140
|
+
let state = null;
|
|
141
|
+
|
|
142
|
+
// Decrypt
|
|
143
|
+
const decrypted = decrypt(fileContent);
|
|
144
|
+
if (decrypted) {
|
|
145
|
+
try {
|
|
146
|
+
sessionData = JSON.parse(decrypted);
|
|
147
|
+
} catch (e) {
|
|
148
|
+
logger.error("Failed to parse session JSON.");
|
|
149
|
+
return null;
|
|
175
150
|
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const decrypted = decrypt(fileContent);
|
|
179
|
-
if (decrypted) {
|
|
151
|
+
} else {
|
|
152
|
+
// Fallback for legacy plain JSON?
|
|
180
153
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (parsedDecrypted.timestamp && parsedDecrypted.state) {
|
|
184
|
-
sessionData = parsedDecrypted;
|
|
185
|
-
} else {
|
|
186
|
-
// It's the old encrypted structure (just storageState)
|
|
187
|
-
state = parsedDecrypted;
|
|
188
|
-
logger.info("Loaded encrypted session (Legacy structure).");
|
|
189
|
-
}
|
|
190
|
-
} catch (parseError) {
|
|
191
|
-
logger.error("Failed to parse decrypted session.");
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
} else {
|
|
195
|
-
logger.error("Failed to decrypt session data. It might be corrupt or key mismatch.");
|
|
196
|
-
return null;
|
|
197
|
-
}
|
|
154
|
+
sessionData = JSON.parse(fileContent);
|
|
155
|
+
} catch(e) {}
|
|
198
156
|
}
|
|
199
157
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const age = Date.now() - sessionData.timestamp;
|
|
203
|
-
if (age > SESSION_MAX_AGE) {
|
|
204
|
-
logger.warn(`Session expired. Age: ${age}ms > Max: ${SESSION_MAX_AGE}ms. Rejecting session.`);
|
|
158
|
+
if (!sessionData || !sessionData.state) {
|
|
159
|
+
logger.warn("Invalid or corrupt session data.");
|
|
205
160
|
return null;
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check Age (Total expiry)
|
|
164
|
+
const age = Date.now() - sessionData.timestamp;
|
|
165
|
+
if (SESSION_MAX_AGE && age > SESSION_MAX_AGE) {
|
|
166
|
+
logger.warn(`Session expired (Age: ${age}ms).`);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
state = sessionData.state;
|
|
171
|
+
|
|
172
|
+
// Check Validation Freshness
|
|
173
|
+
let needsValidation = true;
|
|
174
|
+
if (sessionData.lastValidatedAt) {
|
|
175
|
+
const valAge = Date.now() - sessionData.lastValidatedAt;
|
|
176
|
+
if (valAge < VALIDATION_CACHE_MS) {
|
|
177
|
+
needsValidation = false;
|
|
178
|
+
logger.info(`Session validation cached (Age: ${Math.round(valAge/1000)}s). Skipping checks.`);
|
|
179
|
+
}
|
|
209
180
|
}
|
|
210
181
|
|
|
211
182
|
const context = await browser.newContext({
|
|
@@ -213,7 +184,13 @@ async function loadSession(browser, options = {}, username) {
|
|
|
213
184
|
...options
|
|
214
185
|
});
|
|
215
186
|
|
|
187
|
+
// Attach metadata to context for caller to check?
|
|
188
|
+
// Or return object? Returning object is a breaking change for internal API but we are in v2.
|
|
189
|
+
// Let's attach to context object directly as a property for convenience
|
|
190
|
+
context.needsValidation = needsValidation;
|
|
191
|
+
|
|
216
192
|
return context;
|
|
193
|
+
|
|
217
194
|
} catch (error) {
|
|
218
195
|
logger.error(`Error loading session: ${error.message}`);
|
|
219
196
|
return null;
|
|
@@ -226,5 +203,6 @@ module.exports = {
|
|
|
226
203
|
sessionExists,
|
|
227
204
|
getSessionPath,
|
|
228
205
|
saveSession,
|
|
229
|
-
loadSession
|
|
206
|
+
loadSession,
|
|
207
|
+
SessionLock
|
|
230
208
|
};
|
package/cli.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const logger = require("./src/utils/logger");
|
|
3
|
-
require("dotenv").config();
|
|
4
|
-
|
|
5
|
-
const loginToLinkedIn = require("./src/login/login");
|
|
6
|
-
const REPL = require("./src/cli/repl");
|
|
7
|
-
|
|
8
|
-
let browserInstance = null;
|
|
9
|
-
|
|
10
|
-
// Cleanup function to be called by REPL or SIGINT
|
|
11
|
-
async function cleanup() {
|
|
12
|
-
if (browserInstance) {
|
|
13
|
-
console.log("Closing browser...");
|
|
14
|
-
try {
|
|
15
|
-
await browserInstance.close();
|
|
16
|
-
console.log("Browser closed.");
|
|
17
|
-
} catch (err) {
|
|
18
|
-
console.error("Error closing browser:", err.message);
|
|
19
|
-
}
|
|
20
|
-
browserInstance = null;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Handle Ctrl+C (SIGINT) for graceful shutdown fallback
|
|
25
|
-
process.on("SIGINT", async () => {
|
|
26
|
-
console.log("\nReceived stop signal.");
|
|
27
|
-
await cleanup();
|
|
28
|
-
process.exit(0);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
(async () => {
|
|
32
|
-
try {
|
|
33
|
-
// Check command line arguments
|
|
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
|
-
|
|
59
|
-
const isVisible = args.includes("--visible");
|
|
60
|
-
const isHeadless = !isVisible;
|
|
61
|
-
|
|
62
|
-
// Select User
|
|
63
|
-
const credentials = await selectUser();
|
|
64
|
-
|
|
65
|
-
console.log(`Starting bot in ${isVisible ? "Visible" : "Headless"} Mode...`);
|
|
66
|
-
|
|
67
|
-
// Pass credentials (or null) to login function
|
|
68
|
-
const { browser, context, page } = await loginToLinkedIn({ headless: isHeadless }, credentials);
|
|
69
|
-
browserInstance = browser;
|
|
70
|
-
|
|
71
|
-
console.log("Login successful.");
|
|
72
|
-
|
|
73
|
-
// Start Interactive CLI
|
|
74
|
-
const repl = new REPL({
|
|
75
|
-
browser: browserInstance,
|
|
76
|
-
cleanup: cleanup
|
|
77
|
-
});
|
|
78
|
-
repl.start();
|
|
79
|
-
|
|
80
|
-
} catch (error) {
|
|
81
|
-
console.error("Error occurred:", error);
|
|
82
|
-
process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
})();
|
package/src/cli/repl.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
const readline = require("readline");
|
|
2
|
-
|
|
3
|
-
class REPL {
|
|
4
|
-
constructor(context = {}) {
|
|
5
|
-
this.context = context;
|
|
6
|
-
this.rl = readline.createInterface({
|
|
7
|
-
input: process.stdin,
|
|
8
|
-
output: process.stdout,
|
|
9
|
-
prompt: "linkedin-bot> ",
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
start() {
|
|
14
|
-
console.log("Interactive CLI started. Type 'help' for commands.");
|
|
15
|
-
this.rl.prompt();
|
|
16
|
-
|
|
17
|
-
this.rl.on("line", async (line) => {
|
|
18
|
-
const input = line.trim();
|
|
19
|
-
|
|
20
|
-
switch (input) {
|
|
21
|
-
case "help":
|
|
22
|
-
console.log(`
|
|
23
|
-
Available commands:
|
|
24
|
-
status - Show bot status
|
|
25
|
-
exit - Stop the bot and exit
|
|
26
|
-
quit - Alias for exit
|
|
27
|
-
help - Show this help message
|
|
28
|
-
`);
|
|
29
|
-
break;
|
|
30
|
-
|
|
31
|
-
case "status":
|
|
32
|
-
if (this.context.browser && this.context.browser.isConnected()) {
|
|
33
|
-
console.log("Status: 🟢 Connected");
|
|
34
|
-
const pages = this.context.browser.contexts()[0]?.pages().length || 0;
|
|
35
|
-
console.log(`Open pages: ${pages}`);
|
|
36
|
-
} else {
|
|
37
|
-
console.log("Status: 🔴 Disconnected");
|
|
38
|
-
}
|
|
39
|
-
break;
|
|
40
|
-
|
|
41
|
-
case "exit":
|
|
42
|
-
case "quit":
|
|
43
|
-
console.log("Exiting...");
|
|
44
|
-
this.rl.close();
|
|
45
|
-
if (this.context.cleanup) {
|
|
46
|
-
await this.context.cleanup();
|
|
47
|
-
}
|
|
48
|
-
process.exit(0);
|
|
49
|
-
break;
|
|
50
|
-
|
|
51
|
-
case "":
|
|
52
|
-
break;
|
|
53
|
-
|
|
54
|
-
default:
|
|
55
|
-
console.log(`Unknown command: '${input}'. Type 'help' for available commands.`);
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
this.rl.prompt();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
this.rl.on("close", () => {
|
|
62
|
-
console.log("\nCLI session ended.");
|
|
63
|
-
process.exit(0);
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
module.exports = REPL;
|
package/src/cli/userSelection.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
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 };
|