coffeeinabit 0.0.54 → 0.0.55
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/LICENSE +21 -0
- package/Makefile +1 -1
- package/README.md +88 -0
- package/linkedin_automation.js +18 -3
- package/package.json +2 -3
- package/test/browser_runner.js +1 -1
- package/test/browser_server.js +1 -1
- package/test/test_get_content_search_posts.js +170 -0
- package/test/test_get_message_threads.js +1 -1
- package/test/test_get_new_engagers.js +1 -1
- package/test/test_message_simple.js +1 -1
- package/test/test_send_message.js +1 -1
- package/tools/get_content_search_posts.js +183 -0
- package/tools/get_new_messages.js +20 -71
- package/tools/get_profile.js +31 -10
- package/tools/get_profile_username.js +61 -0
- package/tools/navigation.js +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 coffeeinabit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Makefile
CHANGED
|
@@ -6,7 +6,7 @@ patch-and-publish:
|
|
|
6
6
|
@grep -q "//registry.npmjs.org/:_authToken=" ~/.npmrc || (echo "Error: No npm token found in ~/.npmrc" && exit 1)
|
|
7
7
|
@echo "Token found. Publishing..."
|
|
8
8
|
npm version patch
|
|
9
|
-
npm publish --access public || (echo "" && echo "❌ Publish failed! If you see a 2FA error:" && echo " 1. Go to: https://www.npmjs.com/settings
|
|
9
|
+
npm publish --access public || (echo "" && echo "❌ Publish failed! If you see a 2FA error:" && echo " 1. Go to: https://www.npmjs.com/settings/<YOUR_NPM_USERNAME>/tokens" && echo " 2. Create a 'Granular Access Token'" && echo " 3. Enable 'Bypass 2FA' permission" && echo " 4. Update ~/.npmrc with: //registry.npmjs.org/:_authToken=YOUR_NEW_TOKEN" && exit 1)
|
|
10
10
|
|
|
11
11
|
stop:
|
|
12
12
|
pm2 stop "npx coffeeinabit" && pm2 delete "npx coffeeinabit" && pkill -9 -f "npm exec coffeeinabit"; pkill -9 -f "npx.*coffeeinabit"; pkill -9 -f "node.*coffeeinabit"
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# coffeeinabit
|
|
2
|
+
|
|
3
|
+
LinkedIn automation tool powered by Playwright. Runs as a local Express server with Socket.IO for real-time communication with a cloud backend.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
server.js Express + Socket.IO server
|
|
9
|
+
├── cloud_auth.js AWS Cognito authentication
|
|
10
|
+
├── linkedin_automation.js Orchestrates browser automation
|
|
11
|
+
├── tools/ Individual automation actions
|
|
12
|
+
│ ├── send_linkedin_message.js
|
|
13
|
+
│ ├── send_connection_request.js
|
|
14
|
+
│ ├── get_new_engagers.js
|
|
15
|
+
│ ├── get_message_threads.js
|
|
16
|
+
│ ├── get_new_messages.js
|
|
17
|
+
│ ├── get_profile.js
|
|
18
|
+
│ ├── get_linkedin_search_results.js
|
|
19
|
+
│ ├── get_linkedin_updates.js
|
|
20
|
+
│ ├── get_daily_linkedin_connections.js
|
|
21
|
+
│ ├── comment_post.js
|
|
22
|
+
│ ├── like_post.js
|
|
23
|
+
│ └── ...
|
|
24
|
+
├── routes/ Express route handlers
|
|
25
|
+
├── socket/ Socket.IO event handlers
|
|
26
|
+
├── middleware/ Auth middleware
|
|
27
|
+
├── helpers/ Utilities (logging, version check, auto-start)
|
|
28
|
+
└── state/ Application state management
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Prerequisites
|
|
32
|
+
|
|
33
|
+
- Node.js 18+
|
|
34
|
+
- Playwright (installed automatically via `postinstall`)
|
|
35
|
+
|
|
36
|
+
## Setup
|
|
37
|
+
|
|
38
|
+
1. Clone the repository:
|
|
39
|
+
```bash
|
|
40
|
+
git clone <repo-url>
|
|
41
|
+
cd win_app
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
2. Install dependencies:
|
|
45
|
+
```bash
|
|
46
|
+
npm install
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
3. Create a `.env` file:
|
|
50
|
+
```env
|
|
51
|
+
PORT=323
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
4. Provide a Playwright storage state file (exported LinkedIn session):
|
|
55
|
+
```bash
|
|
56
|
+
# Default path: ./context/storage_state.json
|
|
57
|
+
# Or set via environment variable:
|
|
58
|
+
export STORAGE_STATE_PATH=/path/to/your/storage_state.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Running
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm start
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The server starts on `http://localhost:323` (or the port specified in `.env`).
|
|
68
|
+
|
|
69
|
+
## Testing
|
|
70
|
+
|
|
71
|
+
Test scripts are in `test/`. They use Playwright to run automation actions directly.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Start a persistent browser server for tests
|
|
75
|
+
node test/browser_server.js
|
|
76
|
+
|
|
77
|
+
# Run individual test scripts (connects to the browser server)
|
|
78
|
+
node test/test_get_new_engagers.js
|
|
79
|
+
node test/test_get_message_threads.js
|
|
80
|
+
node test/test_message_simple.js
|
|
81
|
+
node test/test_send_message.js
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Set `STORAGE_STATE_PATH` to point to your Playwright storage state file before running tests.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
package/linkedin_automation.js
CHANGED
|
@@ -16,7 +16,9 @@ import { executeGetNewMessages } from './tools/get_new_messages.js';
|
|
|
16
16
|
import { executeGetLinkedInUpdates } from './tools/get_linkedin_updates.js';
|
|
17
17
|
import { executeGetNewEngagers } from './tools/get_new_engagers.js';
|
|
18
18
|
import { executeGetMessageThreads } from './tools/get_message_threads.js';
|
|
19
|
-
import {
|
|
19
|
+
import { executeGetContentSearchPosts } from './tools/get_content_search_posts.js';
|
|
20
|
+
import { executeGetProfileUsername } from './tools/get_profile_username.js';
|
|
21
|
+
import { safeGoto, LinkedInAuthError } from './tools/navigation.js';
|
|
20
22
|
import { humanLikeAmbientMove, resetHumanMouseState } from './tools/human_mouse.js';
|
|
21
23
|
import { logger } from './helpers/logger.js';
|
|
22
24
|
|
|
@@ -1065,6 +1067,14 @@ export class LinkedInAutomation {
|
|
|
1065
1067
|
logger.log('[LinkedInAutomation] Executing get_message_threads action...');
|
|
1066
1068
|
result = await executeGetMessageThreads(this.page, action);
|
|
1067
1069
|
break;
|
|
1070
|
+
case 'get_content_search_posts':
|
|
1071
|
+
logger.log('[LinkedInAutomation] Executing get_content_search_posts action...');
|
|
1072
|
+
result = await executeGetContentSearchPosts(this.page, action);
|
|
1073
|
+
break;
|
|
1074
|
+
case 'get_profile_username':
|
|
1075
|
+
logger.log('[LinkedInAutomation] Executing get_profile_username action...');
|
|
1076
|
+
result = await executeGetProfileUsername(this.page, action);
|
|
1077
|
+
break;
|
|
1068
1078
|
default:
|
|
1069
1079
|
logger.log('[LinkedInAutomation] Unknown action type:', action.action);
|
|
1070
1080
|
result = { error: `Unknown action type: ${action.action}` };
|
|
@@ -1083,8 +1093,13 @@ export class LinkedInAutomation {
|
|
|
1083
1093
|
} catch (error) {
|
|
1084
1094
|
logger.error('[LinkedInAutomation] Error executing action:', action.action, 'Error:', error.message);
|
|
1085
1095
|
logger.error('[LinkedInAutomation] Error stack:', error.stack);
|
|
1086
|
-
|
|
1087
|
-
|
|
1096
|
+
|
|
1097
|
+
if (error instanceof LinkedInAuthError) {
|
|
1098
|
+
logger.error('[LinkedInAutomation] LinkedIn session expired, resetting to authenticating phase');
|
|
1099
|
+
this.transitionToPhase(this.PHASE_AUTHENTICATING);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const errorResult = {
|
|
1088
1103
|
error: error.message,
|
|
1089
1104
|
error_stack: error.stack,
|
|
1090
1105
|
action_logs: actionLogs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coffeeinabit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.55",
|
|
4
4
|
"description": "coffeeinabit app",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"windows",
|
|
11
11
|
"playwright"
|
|
12
12
|
],
|
|
13
|
-
"author": "
|
|
13
|
+
"author": "coffeeinabit",
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"bin": {
|
|
16
16
|
"coffeeinabit": "./server.js"
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"axios": "^1.6.0",
|
|
25
|
-
"coffeeinabit": "^0.0.47",
|
|
26
25
|
"dotenv": "^16.3.1",
|
|
27
26
|
"express": "^4.18.2",
|
|
28
27
|
"express-session": "^1.17.3",
|
package/test/browser_runner.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { chromium } from 'playwright';
|
|
8
8
|
|
|
9
9
|
const CONFIG = {
|
|
10
|
-
userDataPath: '/
|
|
10
|
+
userDataPath: process.env.STORAGE_STATE_PATH || './context/storage_state.json',
|
|
11
11
|
headless: false
|
|
12
12
|
};
|
|
13
13
|
|
package/test/browser_server.js
CHANGED
|
@@ -9,7 +9,7 @@ import { chromium } from 'playwright';
|
|
|
9
9
|
import fs from 'fs';
|
|
10
10
|
|
|
11
11
|
const CONFIG = {
|
|
12
|
-
userDataPath: '/
|
|
12
|
+
userDataPath: process.env.STORAGE_STATE_PATH || './context/storage_state.json',
|
|
13
13
|
debugPort: 9222,
|
|
14
14
|
stateFile: './browser_state.json'
|
|
15
15
|
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test script for get_content_search_posts action
|
|
3
|
+
* Tests scraping LinkedIn content search results (posts about a topic)
|
|
4
|
+
*
|
|
5
|
+
* This is a DRY-RUN test — it only scrapes and logs, does NOT post any comments.
|
|
6
|
+
* The company_name parameter shows what would be passed to the backend for
|
|
7
|
+
* "commenting on behalf of company" flow.
|
|
8
|
+
*
|
|
9
|
+
* Usage: node test/test_get_content_search_posts.js [search_url]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { chromium } from 'playwright';
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import { executeGetContentSearchPosts } from '../tools/get_content_search_posts.js';
|
|
15
|
+
|
|
16
|
+
const CONFIG = {
|
|
17
|
+
userDataPath: process.env.STORAGE_STATE_PATH || './context/storage_state.json',
|
|
18
|
+
headless: false,
|
|
19
|
+
stateFile: './browser_state.json',
|
|
20
|
+
useExistingBrowser: true,
|
|
21
|
+
// Default content search URL for testing
|
|
22
|
+
searchUrl: process.argv[2] || 'https://www.linkedin.com/search/results/content/?keywords=AI%20SDR&origin=SWITCH_SEARCH_VERTICAL',
|
|
23
|
+
// Company name for "comment on behalf of company" flow
|
|
24
|
+
companyName: 'coffeeinabit'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let browser, context, page;
|
|
28
|
+
|
|
29
|
+
async function connectToExistingBrowser() {
|
|
30
|
+
if (!CONFIG.useExistingBrowser) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(CONFIG.stateFile)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const state = JSON.parse(fs.readFileSync(CONFIG.stateFile, 'utf8'));
|
|
40
|
+
|
|
41
|
+
console.log('Connecting to existing browser...');
|
|
42
|
+
console.log(` CDP Endpoint: ${state.cdpEndpoint}`);
|
|
43
|
+
|
|
44
|
+
browser = await chromium.connectOverCDP(state.cdpEndpoint);
|
|
45
|
+
|
|
46
|
+
const contexts = browser.contexts();
|
|
47
|
+
if (contexts.length === 0) {
|
|
48
|
+
console.log('No contexts in existing browser');
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
context = contexts[0];
|
|
53
|
+
const pages = context.pages();
|
|
54
|
+
|
|
55
|
+
if (pages.length === 0) {
|
|
56
|
+
console.log('No pages in existing browser');
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
page = pages[0];
|
|
61
|
+
|
|
62
|
+
console.log('Connected to existing browser!');
|
|
63
|
+
console.log(` Pages open: ${pages.length}`);
|
|
64
|
+
return true;
|
|
65
|
+
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.log(`Could not connect to existing browser: ${error.message}`);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function setupBrowser() {
|
|
73
|
+
const connected = await connectToExistingBrowser();
|
|
74
|
+
|
|
75
|
+
if (!connected) {
|
|
76
|
+
console.log('Launching new browser...');
|
|
77
|
+
browser = await chromium.launch({
|
|
78
|
+
headless: CONFIG.headless,
|
|
79
|
+
args: ['--no-sandbox']
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
context = await browser.newContext({
|
|
83
|
+
storageState: CONFIG.userDataPath,
|
|
84
|
+
viewport: { width: 1280, height: 720 }
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
page = await context.newPage();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('Browser ready\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function runTest() {
|
|
94
|
+
console.log('\n' + '='.repeat(70));
|
|
95
|
+
console.log('DRY-RUN: Testing get_content_search_posts action');
|
|
96
|
+
console.log(`Company: ${CONFIG.companyName} (comments would be on behalf of this company)`);
|
|
97
|
+
console.log('='.repeat(70) + '\n');
|
|
98
|
+
|
|
99
|
+
await setupBrowser();
|
|
100
|
+
|
|
101
|
+
// Create action object like the backend would send
|
|
102
|
+
// company_name is passed in parameters — backend uses it for LLM prompt context
|
|
103
|
+
const action = {
|
|
104
|
+
action: 'get_content_search_posts',
|
|
105
|
+
action_id: 'test_' + Date.now(),
|
|
106
|
+
parameters: {
|
|
107
|
+
url: CONFIG.searchUrl,
|
|
108
|
+
company_name: CONFIG.companyName
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
console.log(`Search URL: ${CONFIG.searchUrl}`);
|
|
113
|
+
console.log(`Company: ${CONFIG.companyName} (dry-run, no comments will be posted)\n`);
|
|
114
|
+
console.log('Executing get_content_search_posts...\n');
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const result = await executeGetContentSearchPosts(page, action);
|
|
118
|
+
|
|
119
|
+
console.log('\n' + '='.repeat(70));
|
|
120
|
+
console.log('DRY-RUN RESULTS (no comments posted)');
|
|
121
|
+
console.log('='.repeat(70));
|
|
122
|
+
|
|
123
|
+
console.log('Status:', result.status);
|
|
124
|
+
console.log('Action:', result.action);
|
|
125
|
+
|
|
126
|
+
if (result.status === 'success') {
|
|
127
|
+
console.log('\nSUCCESS!');
|
|
128
|
+
console.log(`\nStatistics:`);
|
|
129
|
+
console.log(` Total posts scraped: ${result.result.postsCount}`);
|
|
130
|
+
console.log(` Pages processed: ${result.result.pagesProcessed}`);
|
|
131
|
+
console.log(` Search URL: ${result.result.url}`);
|
|
132
|
+
console.log(` Company for comments: ${CONFIG.companyName}`);
|
|
133
|
+
|
|
134
|
+
if (result.result.posts.length > 0) {
|
|
135
|
+
console.log(`\nScraped posts (first 5):`);
|
|
136
|
+
result.result.posts.slice(0, 5).forEach((post, index) => {
|
|
137
|
+
console.log(`\n ${index + 1}. ${post.authorName || 'Unknown Author'} (@${post.authorUsername || '???'})`);
|
|
138
|
+
console.log(` Headline: ${post.authorHeadline || 'N/A'}`);
|
|
139
|
+
console.log(` Text: ${(post.postText || '').substring(0, 120)}...`);
|
|
140
|
+
console.log(` Reactions: ${post.reactionCount} | Comments: ${post.commentCount}`);
|
|
141
|
+
console.log(` URL: ${post.postUrl}`);
|
|
142
|
+
console.log(` [DRY-RUN] Would send to backend for ${CONFIG.companyName} comment generation`);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
console.log(`\nFull results:`);
|
|
146
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
147
|
+
} else {
|
|
148
|
+
console.log('\n No posts found for this search query');
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
console.log('\nFAILED');
|
|
152
|
+
console.log('Error:', result.result.error || 'Unknown error');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log('\n' + '='.repeat(70) + '\n');
|
|
156
|
+
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error('\nTest failed with error:', error.message);
|
|
159
|
+
console.error('Stack trace:', error.stack);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log('DRY-RUN test complete. No comments were posted.\n');
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Run test
|
|
167
|
+
runTest().catch(error => {
|
|
168
|
+
console.error('Fatal error:', error);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
});
|
|
@@ -12,7 +12,7 @@ import { chromium } from 'playwright';
|
|
|
12
12
|
import fs from 'fs';
|
|
13
13
|
|
|
14
14
|
const CONFIG = {
|
|
15
|
-
userDataPath: '/
|
|
15
|
+
userDataPath: process.env.STORAGE_STATE_PATH || './context/storage_state.json',
|
|
16
16
|
headless: false,
|
|
17
17
|
stateFile: './browser_state.json',
|
|
18
18
|
useExistingBrowser: true
|
|
@@ -10,7 +10,7 @@ import fs from 'fs';
|
|
|
10
10
|
import { executeGetNewEngagers } from './tools/get_new_engagers.js';
|
|
11
11
|
|
|
12
12
|
const CONFIG = {
|
|
13
|
-
userDataPath: '/
|
|
13
|
+
userDataPath: process.env.STORAGE_STATE_PATH || './context/storage_state.json',
|
|
14
14
|
headless: false,
|
|
15
15
|
stateFile: './browser_state.json',
|
|
16
16
|
useExistingBrowser: true
|
|
@@ -10,7 +10,7 @@ import fs from 'fs';
|
|
|
10
10
|
import { sendMessageInConversation, openMessageWindow } from './tools/send_linkedin_message.js';
|
|
11
11
|
|
|
12
12
|
const CONFIG = {
|
|
13
|
-
userDataPath: '/
|
|
13
|
+
userDataPath: process.env.STORAGE_STATE_PATH || './context/storage_state.json',
|
|
14
14
|
targetProfile: 'alena-borykava',
|
|
15
15
|
headless: false,
|
|
16
16
|
dryRun: true,
|
|
@@ -9,7 +9,7 @@ import { chromium } from 'playwright';
|
|
|
9
9
|
|
|
10
10
|
// Configuration
|
|
11
11
|
const CONFIG = {
|
|
12
|
-
userDataPath: '/
|
|
12
|
+
userDataPath: process.env.STORAGE_STATE_PATH || './context/storage_state.json',
|
|
13
13
|
targetProfile: 'alena-borykava',
|
|
14
14
|
message: 'Hey, how are you?',
|
|
15
15
|
headless: false,
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { safeGoto } from './navigation.js';
|
|
2
|
+
import { waitRandom } from './human_mouse.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scrape content posts from LinkedIn content search results page.
|
|
6
|
+
* Extracts post URL, text, author info, and engagement counts.
|
|
7
|
+
*/
|
|
8
|
+
export async function executeGetContentSearchPosts(page, action) {
|
|
9
|
+
const searchUrl = action.parameters?.url || action.url;
|
|
10
|
+
|
|
11
|
+
if (!searchUrl) {
|
|
12
|
+
throw new Error('Missing url for get_content_search_posts');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.log('[ContentSearch] Navigating to content search URL:', searchUrl);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await safeGoto(page, searchUrl, {
|
|
19
|
+
waitUntil: 'domcontentloaded',
|
|
20
|
+
timeout: Math.floor(Math.random() * 20000) + 15000
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('[ContentSearch] Navigation failed:', error.message);
|
|
24
|
+
return {
|
|
25
|
+
action: 'get_content_search_posts',
|
|
26
|
+
result: { posts: [], postsCount: 0, url: searchUrl, error: error.message },
|
|
27
|
+
status: 'failed'
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await waitRandom(2000, 4000, page);
|
|
32
|
+
|
|
33
|
+
// Scroll to load more posts (LinkedIn lazy-loads content search results)
|
|
34
|
+
console.log('[ContentSearch] Scrolling to load posts...');
|
|
35
|
+
const scrollIterations = 6;
|
|
36
|
+
for (let i = 0; i < scrollIterations; i++) {
|
|
37
|
+
await page.evaluate(() => window.scrollBy(0, 800));
|
|
38
|
+
await waitRandom(1000, 2000, page);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Scroll back to top before extracting
|
|
42
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
43
|
+
await waitRandom(500, 1000, page);
|
|
44
|
+
|
|
45
|
+
console.log('[ContentSearch] Extracting posts from page...');
|
|
46
|
+
|
|
47
|
+
const posts = await page.evaluate(() => {
|
|
48
|
+
const results = [];
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
|
|
51
|
+
// LinkedIn content search uses various container selectors
|
|
52
|
+
const postContainers = document.querySelectorAll(
|
|
53
|
+
'div.feed-shared-update-v2, div[data-urn], li.reusable-search__result-container'
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
console.log(`[ContentSearch] Found ${postContainers.length} post containers`);
|
|
57
|
+
|
|
58
|
+
postContainers.forEach((container) => {
|
|
59
|
+
try {
|
|
60
|
+
// Extract post URL
|
|
61
|
+
let postUrl = '';
|
|
62
|
+
const activityLink = container.querySelector('a[href*="feed/update/urn"], a[href*="activity:"]');
|
|
63
|
+
if (activityLink) {
|
|
64
|
+
const href = activityLink.getAttribute('href');
|
|
65
|
+
postUrl = href.startsWith('http') ? href : `https://www.linkedin.com${href}`;
|
|
66
|
+
postUrl = postUrl.split('?')[0]; // Clean query params
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Also try data-urn attribute
|
|
70
|
+
if (!postUrl) {
|
|
71
|
+
const urn = container.getAttribute('data-urn');
|
|
72
|
+
if (urn) {
|
|
73
|
+
postUrl = `https://www.linkedin.com/feed/update/${urn}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Skip if no URL or already seen
|
|
78
|
+
if (!postUrl || seen.has(postUrl)) return;
|
|
79
|
+
seen.add(postUrl);
|
|
80
|
+
|
|
81
|
+
// Extract post text
|
|
82
|
+
let postText = '';
|
|
83
|
+
const textEl = container.querySelector(
|
|
84
|
+
'.feed-shared-text, .break-words, .update-components-text, span[dir="ltr"]'
|
|
85
|
+
);
|
|
86
|
+
if (textEl) {
|
|
87
|
+
postText = textEl.innerText?.trim().substring(0, 500) || '';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Extract author name
|
|
91
|
+
let authorName = '';
|
|
92
|
+
const authorNameEl = container.querySelector(
|
|
93
|
+
'.update-components-actor__name span[aria-hidden="true"], ' +
|
|
94
|
+
'.feed-shared-actor__name span[aria-hidden="true"], ' +
|
|
95
|
+
'span.feed-shared-actor__title span[aria-hidden="true"]'
|
|
96
|
+
);
|
|
97
|
+
if (authorNameEl) {
|
|
98
|
+
authorName = authorNameEl.innerText?.trim() || '';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Extract author username from profile link
|
|
102
|
+
let authorUsername = '';
|
|
103
|
+
const profileLink = container.querySelector(
|
|
104
|
+
'a[href*="/in/"]'
|
|
105
|
+
);
|
|
106
|
+
if (profileLink) {
|
|
107
|
+
const profileHref = profileLink.getAttribute('href');
|
|
108
|
+
const match = profileHref.match(/\/in\/([^\/\?#]+)/);
|
|
109
|
+
if (match) {
|
|
110
|
+
authorUsername = match[1];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Extract author headline
|
|
115
|
+
let authorHeadline = '';
|
|
116
|
+
const headlineEl = container.querySelector(
|
|
117
|
+
'.update-components-actor__description, ' +
|
|
118
|
+
'.feed-shared-actor__description, ' +
|
|
119
|
+
'.update-components-actor__subDescription'
|
|
120
|
+
);
|
|
121
|
+
if (headlineEl) {
|
|
122
|
+
authorHeadline = headlineEl.innerText?.trim().substring(0, 200) || '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Extract reaction count
|
|
126
|
+
let reactionCount = 0;
|
|
127
|
+
const reactionsEl = container.querySelector(
|
|
128
|
+
'.social-details-social-counts__reactions-count, ' +
|
|
129
|
+
'button[aria-label*="reaction"] span, ' +
|
|
130
|
+
'span.social-details-social-counts__reactions-count'
|
|
131
|
+
);
|
|
132
|
+
if (reactionsEl) {
|
|
133
|
+
const text = reactionsEl.innerText?.trim() || '0';
|
|
134
|
+
reactionCount = parseInt(text.replace(/,/g, ''), 10) || 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Extract comment count
|
|
138
|
+
let commentCount = 0;
|
|
139
|
+
const commentsEl = container.querySelector(
|
|
140
|
+
'button[aria-label*="comment"] span, ' +
|
|
141
|
+
'a[aria-label*="comment"]'
|
|
142
|
+
);
|
|
143
|
+
if (commentsEl) {
|
|
144
|
+
const text = commentsEl.innerText?.trim() || '0';
|
|
145
|
+
const match = text.match(/(\d[\d,]*)/);
|
|
146
|
+
if (match) {
|
|
147
|
+
commentCount = parseInt(match[1].replace(/,/g, ''), 10) || 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Only add if we have some meaningful data
|
|
152
|
+
if (postText || authorName || authorUsername) {
|
|
153
|
+
results.push({
|
|
154
|
+
postUrl,
|
|
155
|
+
postText,
|
|
156
|
+
authorName,
|
|
157
|
+
authorUsername,
|
|
158
|
+
authorHeadline,
|
|
159
|
+
reactionCount,
|
|
160
|
+
commentCount
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('[ContentSearch] Error parsing post container:', error.message);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return results;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
console.log(`[ContentSearch] Extracted ${posts.length} posts`);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
action: 'get_content_search_posts',
|
|
175
|
+
result: {
|
|
176
|
+
posts,
|
|
177
|
+
postsCount: posts.length,
|
|
178
|
+
url: searchUrl,
|
|
179
|
+
pagesProcessed: 1
|
|
180
|
+
},
|
|
181
|
+
status: 'success'
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -384,97 +384,47 @@ function filterAndOrganizeNewMessages(allMessageEntities, lastCheckedTimestamp)
|
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
/**
|
|
387
|
-
* Build map of
|
|
387
|
+
* Build map of uniq_user_ids from conversation participants
|
|
388
388
|
*/
|
|
389
|
-
function
|
|
390
|
-
const
|
|
389
|
+
function buildUniqIdMap(newMessagesByConversation, conversationParticipantsMap) {
|
|
390
|
+
const uniqIdMap = new Map();
|
|
391
391
|
for (const conversationId in newMessagesByConversation) {
|
|
392
|
-
const
|
|
393
|
-
if (
|
|
394
|
-
|
|
392
|
+
const uniqId = conversationParticipantsMap.get(conversationId);
|
|
393
|
+
if (uniqId) {
|
|
394
|
+
uniqIdMap.set(conversationId, uniqId);
|
|
395
395
|
} else {
|
|
396
396
|
console.log(`[GetNewMessages] No participant found for conversation:`, conversationId);
|
|
397
397
|
}
|
|
398
398
|
}
|
|
399
|
-
console.log('[GetNewMessages]
|
|
400
|
-
return
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
/**
|
|
404
|
-
* Decode encoded LinkedIn profile URLs by visiting them
|
|
405
|
-
*/
|
|
406
|
-
async function decodeProfileUrls(page, encodedUrlMap) {
|
|
407
|
-
const decodedUrlMap = new Map();
|
|
408
|
-
const maxDecodeAttempts = Math.min(encodedUrlMap.size, 50);
|
|
409
|
-
let decodeCount = 0;
|
|
410
|
-
|
|
411
|
-
for (const encodedUrl of encodedUrlMap.keys()) {
|
|
412
|
-
if (decodeCount >= maxDecodeAttempts) {
|
|
413
|
-
console.log(`[GetNewMessages] Reached max decode attempts (${maxDecodeAttempts}), skipping remaining profiles`);
|
|
414
|
-
break;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
try {
|
|
418
|
-
console.log(`[GetNewMessages] Decoding profile ${decodeCount + 1}/${encodedUrlMap.size}: ${encodedUrl}`);
|
|
419
|
-
|
|
420
|
-
const decodePromise = safeGoto(page, `https://www.linkedin.com/in/${encodedUrl}`, {
|
|
421
|
-
waitUntil: 'domcontentloaded',
|
|
422
|
-
timeout: 30000
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
const timeoutPromise = new Promise((_, reject) =>
|
|
426
|
-
setTimeout(() => reject(new Error('Decode timeout')), 35000)
|
|
427
|
-
);
|
|
428
|
-
|
|
429
|
-
await Promise.race([decodePromise, timeoutPromise]);
|
|
430
|
-
await page.waitForLoadState('domcontentloaded', { timeout: 10000 });
|
|
431
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
432
|
-
|
|
433
|
-
const currentUrl = page.url();
|
|
434
|
-
const match = currentUrl.match(/linkedin\.com\/in\/([^\/\?]+)/);
|
|
435
|
-
if (match && match[1]) {
|
|
436
|
-
decodedUrlMap.set(encodedUrl, match[1]);
|
|
437
|
-
console.log(`[GetNewMessages] Successfully decoded: ${encodedUrl} -> ${match[1]}`);
|
|
438
|
-
} else {
|
|
439
|
-
console.log(`[GetNewMessages] Could not extract URL from: ${currentUrl}`);
|
|
440
|
-
}
|
|
441
|
-
decodeCount++;
|
|
442
|
-
} catch (error) {
|
|
443
|
-
console.error(`[GetNewMessages] Failed to decode URL ${encodedUrl}:`, error.message);
|
|
444
|
-
decodeCount++;
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
return decodedUrlMap;
|
|
399
|
+
console.log('[GetNewMessages] Conversations with uniq_user_ids:', uniqIdMap.size);
|
|
400
|
+
return uniqIdMap;
|
|
450
401
|
}
|
|
451
402
|
|
|
452
403
|
/**
|
|
453
404
|
* ⚠️ SENDING FUNCTIONALITY: Send messages to API endpoint
|
|
454
|
-
*
|
|
405
|
+
* Uses raw uniq_user_id (ACoXXX) instead of decoded username
|
|
455
406
|
*/
|
|
456
|
-
async function sendMessagesToApi(newMessagesByConversation,
|
|
407
|
+
async function sendMessagesToApi(newMessagesByConversation, uniqIdMap, accessToken) {
|
|
457
408
|
let sentCount = 0;
|
|
458
409
|
let failedCount = 0;
|
|
459
410
|
let totalToSend = 0;
|
|
460
411
|
|
|
461
412
|
for (const conversationId in newMessagesByConversation) {
|
|
462
413
|
const messages = newMessagesByConversation[conversationId];
|
|
463
|
-
const
|
|
464
|
-
const linkedinUrl = encodedUrl ? decodedUrlMap.get(encodedUrl) : null;
|
|
414
|
+
const uniqUserId = uniqIdMap.get(conversationId);
|
|
465
415
|
|
|
466
|
-
if (!
|
|
467
|
-
console.log(`[GetNewMessages] Skipping conversation ${conversationId} -
|
|
416
|
+
if (!uniqUserId) {
|
|
417
|
+
console.log(`[GetNewMessages] Skipping conversation ${conversationId} - no uniq_user_id found`);
|
|
468
418
|
continue;
|
|
469
419
|
}
|
|
470
420
|
|
|
471
|
-
console.log(`[GetNewMessages] Sending ${messages.length} messages
|
|
421
|
+
console.log(`[GetNewMessages] Sending ${messages.length} messages for uniq_user_id ${uniqUserId}`);
|
|
472
422
|
totalToSend += messages.length;
|
|
473
423
|
|
|
474
424
|
for (const msg of messages) {
|
|
475
425
|
try {
|
|
476
426
|
const payload = {
|
|
477
|
-
|
|
427
|
+
uniq_user_id: uniqUserId,
|
|
478
428
|
message: msg.messageText,
|
|
479
429
|
is_receipent: !msg.isSelf,
|
|
480
430
|
timestamp: msg.deliveredAt
|
|
@@ -492,7 +442,7 @@ async function sendMessagesToApi(newMessagesByConversation, conversationParticip
|
|
|
492
442
|
sentCount++;
|
|
493
443
|
} catch (error) {
|
|
494
444
|
failedCount++;
|
|
495
|
-
console.error(`[GetNewMessages] Failed to send message
|
|
445
|
+
console.error(`[GetNewMessages] Failed to send message for ${uniqUserId}: "${msg.messageText.substring(0, 50)}..."`);
|
|
496
446
|
console.error(`[GetNewMessages] Error:`, error.message);
|
|
497
447
|
if (error.response) {
|
|
498
448
|
console.error(`[GetNewMessages] Response status:`, error.response.status);
|
|
@@ -571,11 +521,10 @@ export async function executeGetNewMessages(page, action, accessToken) {
|
|
|
571
521
|
|
|
572
522
|
const { newMessages, newMessagesByConversation } = filterAndOrganizeNewMessages(allMessageEntities, lastCheckedTimestamp);
|
|
573
523
|
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
await sendMessagesToApi(newMessagesByConversation, conversationParticipantsMap, decodedUrlMap, accessToken);
|
|
524
|
+
const uniqIdMap = buildUniqIdMap(newMessagesByConversation, conversationParticipantsMap);
|
|
525
|
+
|
|
526
|
+
// ⚠️ SENDING FUNCTIONALITY: Send all collected messages to API with raw uniq_user_ids
|
|
527
|
+
await sendMessagesToApi(newMessagesByConversation, uniqIdMap, accessToken);
|
|
579
528
|
|
|
580
529
|
return {
|
|
581
530
|
newMessagesCount: newMessages.length,
|
package/tools/get_profile.js
CHANGED
|
@@ -213,18 +213,33 @@ export async function executeGetProfile(page, action) {
|
|
|
213
213
|
// Navigate to profile page with verification
|
|
214
214
|
await navigateToProfile(page, username);
|
|
215
215
|
|
|
216
|
-
//
|
|
217
|
-
const expectedProfilePath = `/in/${username}`;
|
|
216
|
+
// Check if LinkedIn redirected us (e.g., ACoXXX -> real-username)
|
|
218
217
|
const currentUrl = page.url();
|
|
218
|
+
const expectedProfilePath = `/in/${username}`;
|
|
219
|
+
let redirectedFrom = null;
|
|
220
|
+
let resolvedUsername = username;
|
|
221
|
+
|
|
219
222
|
if (!currentUrl.includes(expectedProfilePath)) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
const match = currentUrl.match(/\/in\/([^\/\?]+)/);
|
|
224
|
+
if (match && match[1]) {
|
|
225
|
+
redirectedFrom = username;
|
|
226
|
+
resolvedUsername = match[1];
|
|
227
|
+
console.log(`[GetProfile] Redirect detected: ${username} -> ${resolvedUsername}`);
|
|
228
|
+
} else {
|
|
229
|
+
console.warn(`[GetProfile] URL mismatch and could not extract username from ${currentUrl}. Re-navigating.`);
|
|
230
|
+
await navigateToProfile(page, username);
|
|
231
|
+
const urlAfterRetry = page.url();
|
|
232
|
+
const retryMatch = urlAfterRetry.match(/\/in\/([^\/\?]+)/);
|
|
233
|
+
if (retryMatch && retryMatch[1] && retryMatch[1] !== username) {
|
|
234
|
+
redirectedFrom = username;
|
|
235
|
+
resolvedUsername = retryMatch[1];
|
|
236
|
+
console.log(`[GetProfile] Redirect detected after retry: ${username} -> ${resolvedUsername}`);
|
|
237
|
+
} else if (!urlAfterRetry.includes(expectedProfilePath)) {
|
|
238
|
+
throw new Error(`Profile page URL verification failed. Expected URL to contain "${expectedProfilePath}" but got ${urlAfterRetry}. Refusing to extract to avoid wrong profile.`);
|
|
239
|
+
}
|
|
225
240
|
}
|
|
226
241
|
}
|
|
227
|
-
await verifyPageUrl(page,
|
|
242
|
+
await verifyPageUrl(page, `/in/${resolvedUsername}`, 'Pre-extraction');
|
|
228
243
|
|
|
229
244
|
// Scroll and extract profile data
|
|
230
245
|
console.log('[GetProfile] Scrolling profile page to load content');
|
|
@@ -242,7 +257,7 @@ export async function executeGetProfile(page, action) {
|
|
|
242
257
|
console.log('[GetProfile] Fetching recent activity');
|
|
243
258
|
let recentPosts = {};
|
|
244
259
|
try {
|
|
245
|
-
await navigateToRecentActivity(page,
|
|
260
|
+
await navigateToRecentActivity(page, resolvedUsername);
|
|
246
261
|
|
|
247
262
|
console.log('[GetProfile] Scrolling recent activity page to load posts');
|
|
248
263
|
await gentleScroll(page, 'down', { stepRange: [140, 260], pauseRange: [80, 160], maxIterations: 40 });
|
|
@@ -259,12 +274,18 @@ export async function executeGetProfile(page, action) {
|
|
|
259
274
|
profileText += `\nPending connection status: ${hasPendingConnection ? 'pending' : 'not pending'}`;
|
|
260
275
|
}
|
|
261
276
|
console.log('[GetProfile] Profile data extraction completed successfully');
|
|
262
|
-
|
|
277
|
+
const result = {
|
|
263
278
|
profileText,
|
|
264
279
|
recentPosts,
|
|
265
280
|
moreButtonOptions,
|
|
266
281
|
isFirstDegreeConnection,
|
|
282
|
+
username: resolvedUsername,
|
|
267
283
|
};
|
|
284
|
+
if (redirectedFrom) {
|
|
285
|
+
result.redirectedFrom = redirectedFrom;
|
|
286
|
+
result.resolvedUsername = resolvedUsername;
|
|
287
|
+
}
|
|
288
|
+
return result;
|
|
268
289
|
|
|
269
290
|
} catch (error) {
|
|
270
291
|
console.error('[GetProfile] Fatal error during profile retrieval:', error.message);
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { safeGoto } from './navigation.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight action that resolves a profile_id (e.g. ACoXXX) to a
|
|
5
|
+
* human-readable username by navigating to the profile and capturing the redirect.
|
|
6
|
+
* No scrolling, no text extraction, no recent activity.
|
|
7
|
+
*/
|
|
8
|
+
export async function executeGetProfileUsername(page, action) {
|
|
9
|
+
if (!page) {
|
|
10
|
+
throw new Error('No page available');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const profileId = action.parameters?.username || action.parameters?.profile_id;
|
|
14
|
+
if (!profileId) {
|
|
15
|
+
throw new Error('Missing username/profile_id for get_profile_username');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (profileId.startsWith('http://') || profileId.startsWith('https://')) {
|
|
19
|
+
console.error(`[GetProfileUsername] Received URL instead of profile_id: ${profileId}`);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log(`[GetProfileUsername] Resolving profile_id: ${profileId}`);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const profileUrl = `https://www.linkedin.com/in/${profileId}`;
|
|
27
|
+
await safeGoto(page, profileUrl, {
|
|
28
|
+
waitUntil: 'domcontentloaded',
|
|
29
|
+
timeout: 60000
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await page.waitForLoadState('domcontentloaded');
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
34
|
+
|
|
35
|
+
// Extract username from current URL after potential redirect
|
|
36
|
+
const currentUrl = page.url();
|
|
37
|
+
const match = currentUrl.match(/\/in\/([^\/\?]+)/);
|
|
38
|
+
const resolvedUsername = match ? match[1] : profileId;
|
|
39
|
+
const redirected = resolvedUsername !== profileId;
|
|
40
|
+
|
|
41
|
+
console.log(`[GetProfileUsername] Resolved: ${profileId} -> ${resolvedUsername} (redirected: ${redirected})`);
|
|
42
|
+
|
|
43
|
+
const result = {
|
|
44
|
+
profile_id: profileId,
|
|
45
|
+
username: resolvedUsername,
|
|
46
|
+
redirected
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Include fields compatible with backend's analyze_get_profile for put_mapping()
|
|
50
|
+
if (redirected) {
|
|
51
|
+
result.redirectedFrom = profileId;
|
|
52
|
+
result.resolvedUsername = resolvedUsername;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return result;
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('[GetProfileUsername] Error:', error.message);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/tools/navigation.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
export class LinkedInAuthError extends Error {
|
|
2
|
+
constructor(url) {
|
|
3
|
+
super(`LinkedIn session expired - redirected to login wall (${url})`);
|
|
4
|
+
this.name = 'LinkedInAuthError';
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function isAuthwall(url) {
|
|
9
|
+
return url && (url.includes('/authwall') || url.includes('/login') || url.includes('/checkpoint'));
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
export async function safeGoto(page, url, options = {}) {
|
|
2
13
|
if (!page) return;
|
|
3
14
|
try {
|
|
@@ -24,7 +35,14 @@ export async function safeGoto(page, url, options = {}) {
|
|
|
24
35
|
|
|
25
36
|
console.log('[Navigation] safeGoto navigating to:', targetHref);
|
|
26
37
|
await page.goto(url, options);
|
|
38
|
+
|
|
39
|
+
// Check if LinkedIn redirected to authwall/login
|
|
40
|
+
const landedUrl = page.url();
|
|
41
|
+
if (!isAuthwall(targetHref) && isAuthwall(landedUrl)) {
|
|
42
|
+
throw new LinkedInAuthError(landedUrl);
|
|
43
|
+
}
|
|
27
44
|
} catch (error) {
|
|
45
|
+
if (error instanceof LinkedInAuthError) throw error;
|
|
28
46
|
console.error('[Navigation] safeGoto failed for url:', url, error);
|
|
29
47
|
throw error;
|
|
30
48
|
}
|