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 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/kate_yan/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)
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
@@ -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 { safeGoto } from './tools/navigation.js';
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
- const errorResult = {
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.54",
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": "Azam Alamov <azam.alamov@icloud.com>",
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",
@@ -7,7 +7,7 @@
7
7
  import { chromium } from 'playwright';
8
8
 
9
9
  const CONFIG = {
10
- userDataPath: '/Users/kateyanchenka/Documents/coffeeinabit_context/storage_state_team_lamoom_com.json',
10
+ userDataPath: process.env.STORAGE_STATE_PATH || './context/storage_state.json',
11
11
  headless: false
12
12
  };
13
13
 
@@ -9,7 +9,7 @@ import { chromium } from 'playwright';
9
9
  import fs from 'fs';
10
10
 
11
11
  const CONFIG = {
12
- userDataPath: '/Users/kateyanchenka/Documents/coffeeinabit_context/storage_state_team_lamoom_com.json',
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: '/Users/kateyanchenka/Documents/coffeeinabit_context/storage_state_team_lamoom_com.json',
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: '/Users/kateyanchenka/Documents/coffeeinabit_context/storage_state_team_lamoom_com.json',
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: '/Users/kateyanchenka/Documents/coffeeinabit_context/storage_state_team_lamoom_com.json',
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: '/Users/kateyanchenka/Documents/coffeeinabit_context/storage_state_team_lamoom_com.json',
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 encoded URLs that need to be decoded
387
+ * Build map of uniq_user_ids from conversation participants
388
388
  */
389
- function buildEncodedUrlMap(newMessagesByConversation, conversationParticipantsMap) {
390
- const encodedUrlMap = new Map();
389
+ function buildUniqIdMap(newMessagesByConversation, conversationParticipantsMap) {
390
+ const uniqIdMap = new Map();
391
391
  for (const conversationId in newMessagesByConversation) {
392
- const encodedUrl = conversationParticipantsMap.get(conversationId);
393
- if (encodedUrl) {
394
- encodedUrlMap.set(encodedUrl, null);
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] Unique profiles to decode:', encodedUrlMap.size);
400
- return encodedUrlMap;
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
- * This is the core functionality for sending collected messages to the backend
405
+ * Uses raw uniq_user_id (ACoXXX) instead of decoded username
455
406
  */
456
- async function sendMessagesToApi(newMessagesByConversation, conversationParticipantsMap, decodedUrlMap, accessToken) {
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 encodedUrl = conversationParticipantsMap.get(conversationId);
464
- const linkedinUrl = encodedUrl ? decodedUrlMap.get(encodedUrl) : null;
414
+ const uniqUserId = uniqIdMap.get(conversationId);
465
415
 
466
- if (!linkedinUrl) {
467
- console.log(`[GetNewMessages] Skipping conversation ${conversationId} - could not find or decode other person's URL`);
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 to ${linkedinUrl}`);
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
- linkedin_url: linkedinUrl,
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 to ${linkedinUrl}: "${msg.messageText.substring(0, 50)}..."`);
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 encodedUrlMap = buildEncodedUrlMap(newMessagesByConversation, conversationParticipantsMap);
575
- const decodedUrlMap = await decodeProfileUrls(page, encodedUrlMap);
576
-
577
- // ⚠️ SENDING FUNCTIONALITY: Send all collected messages to API
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,
@@ -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
- // Double-check we are on the correct profile URL before extracting (avoid analyzing previous user's profile)
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
- console.warn(`[GetProfile] URL mismatch before extraction: expected "${expectedProfilePath}", got ${currentUrl}. Re-navigating to profile.`);
221
- await navigateToProfile(page, username);
222
- const urlAfterRetry = page.url();
223
- if (!urlAfterRetry.includes(expectedProfilePath)) {
224
- throw new Error(`Profile page URL verification failed. Expected URL to contain "${expectedProfilePath}" but got ${urlAfterRetry}. Refusing to extract to avoid wrong profile.`);
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, expectedProfilePath, 'Pre-extraction');
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, username);
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
- return {
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
+ }
@@ -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
  }