coffeeinabit 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,6 +44,7 @@ export class LinkedInAutomation {
44
44
  }
45
45
 
46
46
  async saveLinkedInSession() {
47
+ console.warn('[LinkedInAutomation] saveLinkedInSession is deprecated - using persistent context instead');
47
48
  try {
48
49
  if (!this.context || !this.page) {
49
50
  console.error('[LinkedInAutomation] Cannot save session - no context or page');
@@ -91,6 +92,7 @@ export class LinkedInAutomation {
91
92
  }
92
93
 
93
94
  async loadLinkedInSession() {
95
+ console.warn('[LinkedInAutomation] loadLinkedInSession is deprecated - using persistent context instead');
94
96
  try {
95
97
  const sessionPath = this.getLinkedInSessionPath();
96
98
 
@@ -130,6 +132,7 @@ export class LinkedInAutomation {
130
132
  }
131
133
 
132
134
  async restoreLocalStorage() {
135
+ console.warn('[LinkedInAutomation] restoreLocalStorage is deprecated - using persistent context instead');
133
136
  try {
134
137
  const sessionPath = this.getLinkedInSessionPath();
135
138
 
@@ -322,8 +325,7 @@ export class LinkedInAutomation {
322
325
  if (secondCheck.includes('/feed')) {
323
326
  console.log('[LinkedInAutomation] Second check confirmed - still on /feed/');
324
327
 
325
- console.log('[LinkedInAutomation] Saving LinkedIn session for future use...');
326
- await this.saveLinkedInSession();
328
+ console.log('[LinkedInAutomation] Session will be persisted automatically by launchPersistentContext...');
327
329
 
328
330
  this.status = 'linkedin_logged_in';
329
331
  this.emitStatus();
@@ -384,10 +386,6 @@ export class LinkedInAutomation {
384
386
  if (this.context) {
385
387
  await this.context.close();
386
388
  this.context = null;
387
- }
388
-
389
- if (this.browser) {
390
- await this.browser.close();
391
389
  this.browser = null;
392
390
  }
393
391
 
@@ -696,39 +694,108 @@ export class LinkedInAutomation {
696
694
  }
697
695
 
698
696
  async launchBrowserAndLogin() {
699
- console.log('[LinkedInAutomation] Launching Firefox for LinkedIn login...');
697
+ console.log('[LinkedInAutomation] Launching Firefox with persistent context for LinkedIn login...');
698
+
699
+ this.userEmail = this._currentUserEmail || 'default_user';
700
+ const sanitizedEmail = this.userEmail.replace(/[^a-zA-Z0-9]/g, '_');
701
+ const userDataPath = path.join(this.linkedInSessionDir, `linkedin_auth_${sanitizedEmail}`);
700
702
 
701
- this.browser = await firefox.launch({ headless: this.headless || false });
703
+ console.log('[LinkedInAutomation] User data path:', userDataPath);
704
+
705
+ if (!fs.existsSync(userDataPath)) {
706
+ fs.mkdirSync(userDataPath, { recursive: true });
707
+ console.log('[LinkedInAutomation] Created user data directory:', userDataPath);
708
+ }
702
709
 
703
- this.context = await this.browser.newContext({
710
+ const launchOptions = {
711
+ headless: this.headless || false,
704
712
  viewport: null,
705
713
  ignoreHTTPSErrors: true
706
- });
714
+ };
715
+
716
+ this.context = await firefox.launchPersistentContext(userDataPath, launchOptions);
707
717
 
718
+ console.log('[LinkedInAutomation] Persistent context launched successfully');
719
+
720
+ this.browser = this.context;
721
+
708
722
  await this.enablePerformanceMode();
709
723
  this.page = await this.context.newPage();
710
724
 
711
- const sessionLoaded = await this.loadLinkedInSession();
725
+ console.log('[LinkedInAutomation] Checking if user is already authenticated...');
712
726
 
713
- if (sessionLoaded) {
714
- console.log('[LinkedInAutomation] Saved session loaded, checking if still authenticated...');
715
- await this.checkAndVerifySession();
716
- } else {
717
- console.log('[LinkedInAutomation] No saved session, starting fresh login...');
727
+ const isAuthenticated = await this.checkAndVerifySession();
728
+
729
+ if (!isAuthenticated) {
730
+ console.log('[LinkedInAutomation] Not authenticated, starting fresh login...');
718
731
  await this.startLinkedInLogin();
719
732
  }
720
733
  }
721
734
 
735
+ async updateBrowserVisibility(keepBrowser) {
736
+ if (!this.browser || !this.isRunning) {
737
+ console.log('[LinkedInAutomation] Browser not running, cannot update visibility');
738
+ return;
739
+ }
740
+
741
+ try {
742
+ const newHeadless = !keepBrowser;
743
+
744
+ if (this.headless === newHeadless) {
745
+ console.log('[LinkedInAutomation] Browser visibility already matches setting');
746
+ return;
747
+ }
748
+
749
+ console.log(`[LinkedInAutomation] Updating browser visibility: ${keepBrowser ? 'visible' : 'headless'}`);
750
+
751
+ const currentUrl = this.page.url();
752
+ const currentTitle = await this.page.title();
753
+ const sanitizedEmail = this.userEmail.replace(/[^a-zA-Z0-9]/g, '_');
754
+ const userDataPath = path.join(this.linkedInSessionDir, `linkedin_auth_${sanitizedEmail}`);
755
+
756
+ await this.context.close();
757
+
758
+ this.headless = newHeadless;
759
+
760
+ this.context = await firefox.launchPersistentContext(userDataPath, {
761
+ headless: this.headless,
762
+ viewport: null,
763
+ ignoreHTTPSErrors: true
764
+ });
765
+
766
+ this.browser = this.context;
767
+
768
+ await this.enablePerformanceMode();
769
+ this.page = await this.context.newPage();
770
+
771
+ await this.page.goto(currentUrl, {
772
+ waitUntil: 'domcontentloaded',
773
+ timeout: 30000
774
+ });
775
+
776
+ this.setupPageListeners();
777
+
778
+ console.log('[LinkedInAutomation] Browser visibility updated successfully');
779
+
780
+ this.io.emit('automation_status', {
781
+ status: 'running',
782
+ message: `Browser is now ${keepBrowser ? 'visible' : 'running in headless mode'}`
783
+ });
784
+
785
+ } catch (error) {
786
+ console.error('[LinkedInAutomation] Failed to update browser visibility:', error);
787
+ throw error;
788
+ }
789
+ }
790
+
722
791
  async checkAndVerifySession() {
723
792
  try {
724
- console.log('[LinkedInAutomation] Navigating to LinkedIn feed to verify session...');
793
+ console.log('[LinkedInAutomation] Navigating to LinkedIn feed to verify persistent session...');
725
794
  await this.page.goto('https://www.linkedin.com/feed/', {
726
795
  waitUntil: 'domcontentloaded',
727
796
  timeout: 30000
728
797
  });
729
798
 
730
- await this.restoreLocalStorage();
731
-
732
799
  await new Promise(resolve => setTimeout(resolve, 3000));
733
800
 
734
801
  let verifyAttempts = 0;
@@ -739,7 +806,7 @@ export class LinkedInAutomation {
739
806
  console.log(`[LinkedInAutomation] Verification attempt ${verifyAttempts + 1}/${maxVerifyAttempts}, URL: ${this.currentUrl}`);
740
807
 
741
808
  if (this.currentUrl.includes('/feed')) {
742
- console.log('[LinkedInAutomation] Session verified! Already logged in.');
809
+ console.log('[LinkedInAutomation] Session verified! Already logged in via persistent context.');
743
810
  this.status = 'linkedin_logged_in';
744
811
  this.emitStatus();
745
812
 
package/package.json CHANGED
@@ -1,10 +1,15 @@
1
1
  {
2
2
  "name": "coffeeinabit",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "CoffeeInABit App",
5
5
  "main": "server.js",
6
6
  "type": "module",
7
- "keywords": ["automation", "linkedin", "windows", "playwright"],
7
+ "keywords": [
8
+ "automation",
9
+ "linkedin",
10
+ "windows",
11
+ "playwright"
12
+ ],
8
13
  "author": "Azam Alamov <azam.alamov@icloud.com>",
9
14
  "license": "MIT",
10
15
  "bin": {
@@ -205,16 +205,40 @@
205
205
  }
206
206
  }
207
207
 
208
- function toggleKeepBrowser() {
208
+ async function toggleKeepBrowser() {
209
209
  const keepBrowserToggle = document.getElementById('keepBrowserToggle');
210
210
  const isKeepBrowser = keepBrowserToggle.checked;
211
211
 
212
- if (isKeepBrowser) {
213
- localStorage.setItem('keepBrowser', 'true');
214
- showAlert('info', 'Browser will be visible when automation starts');
215
- } else {
216
- localStorage.setItem('keepBrowser', 'false');
217
- showAlert('info', 'Browser will run in headless mode');
212
+ localStorage.setItem('keepBrowser', isKeepBrowser.toString());
213
+
214
+ try {
215
+ const response = await fetch('/api/automation/settings', {
216
+ method: 'POST',
217
+ headers: {
218
+ 'Content-Type': 'application/json'
219
+ },
220
+ body: JSON.stringify({
221
+ keepBrowser: isKeepBrowser
222
+ })
223
+ });
224
+
225
+ const data = await response.json();
226
+
227
+ if (data.success) {
228
+ if (isKeepBrowser) {
229
+ showAlert('success', 'Browser is now visible');
230
+ } else {
231
+ showAlert('success', 'Browser is now running in headless mode');
232
+ }
233
+ } else {
234
+ showAlert('error', data.error || 'Failed to update browser setting');
235
+ keepBrowserToggle.checked = !isKeepBrowser;
236
+ localStorage.setItem('keepBrowser', (!isKeepBrowser).toString());
237
+ }
238
+ } catch (error) {
239
+ showAlert('error', 'Error updating browser setting: ' + error.message);
240
+ keepBrowserToggle.checked = !isKeepBrowser;
241
+ localStorage.setItem('keepBrowser', (!isKeepBrowser).toString());
218
242
  }
219
243
  }
220
244
 
package/server.js CHANGED
@@ -173,6 +173,26 @@ app.get('/api/automation/status', (req, res) => {
173
173
  res.json(linkedinAutomation.getStatus());
174
174
  });
175
175
 
176
+ app.post('/api/automation/settings', async (req, res) => {
177
+ if (!cloudAuth.isAuthenticated(req.session)) {
178
+ return res.status(401).json({ error: 'Authentication required' });
179
+ }
180
+
181
+ try {
182
+ const { keepBrowser } = req.body;
183
+
184
+ if (typeof keepBrowser !== 'boolean') {
185
+ return res.status(400).json({ error: 'keepBrowser must be a boolean' });
186
+ }
187
+
188
+ await linkedinAutomation.updateBrowserVisibility(keepBrowser);
189
+ res.json({ success: true, message: 'Browser visibility setting updated' });
190
+ } catch (error) {
191
+ console.error('[Server] Failed to update browser settings:', error);
192
+ res.status(500).json({ error: error.message });
193
+ }
194
+ });
195
+
176
196
  io.on('connection', (socket) => {
177
197
  console.log('[Server] Client connected:', socket.id);
178
198
 
@@ -25,38 +25,43 @@ export async function executeGetDailyLinkedInConnections(page, action) {
25
25
  });
26
26
 
27
27
  const now = Date.now();
28
- const newConnections = await page.evaluate(({ lastCheckTimestamp, now }) => {
29
- function parseConnectionDate(str) {
30
- if (!str) return null;
31
- const match = str.match(/Connected on (.+)/i);
32
- if (!match) return null;
33
- const dateStr = match[1].trim();
34
- try {
35
- const timestamp = new Date(dateStr).getTime();
36
- return isNaN(timestamp) ? null : timestamp;
37
- } catch (e) {
38
- return null;
28
+ let allConnectionsMap = new Map();
29
+ let shouldStopScrolling = false;
30
+ let noScrollAttempts = 0;
31
+ const maxNoScrollAttempts = 2;
32
+
33
+ while (!shouldStopScrolling) {
34
+ const currentConnections = await page.evaluate(() => {
35
+ function parseConnectionDate(str) {
36
+ if (!str) return null;
37
+ const match = str.match(/Connected on (.+)/i);
38
+ if (!match) return null;
39
+ const dateStr = match[1].trim();
40
+ try {
41
+ const timestamp = new Date(dateStr).getTime();
42
+ return isNaN(timestamp) ? null : timestamp;
43
+ } catch (e) {
44
+ return null;
45
+ }
39
46
  }
40
- }
41
-
42
- const connections = [];
43
- const allParagraphs = document.querySelectorAll('p');
44
-
45
- allParagraphs.forEach(p => {
46
- const text = p.textContent.trim();
47
- if (text.startsWith('Connected on')) {
48
- const parentDiv = p.parentElement;
49
- if (parentDiv) {
50
- const profileLink = parentDiv.querySelector('a[href*="/in/"]');
51
- if (profileLink) {
52
- const href = profileLink.getAttribute('href');
53
- const usernameMatch = href.match(/\/in\/([^/?]+)/);
54
- if (usernameMatch) {
55
- const username = usernameMatch[1];
56
- const connectionDate = text;
57
- const connectionTimestamp = parseConnectionDate(connectionDate);
58
-
59
- if (!lastCheckTimestamp || (connectionTimestamp && connectionTimestamp >= lastCheckTimestamp)) {
47
+
48
+ const connections = [];
49
+ const allParagraphs = document.querySelectorAll('p');
50
+
51
+ allParagraphs.forEach(p => {
52
+ const text = p.textContent.trim();
53
+ if (text.startsWith('Connected on')) {
54
+ const parentDiv = p.parentElement;
55
+ if (parentDiv) {
56
+ const profileLink = parentDiv.querySelector('a[href*="/in/"]');
57
+ if (profileLink) {
58
+ const href = profileLink.getAttribute('href');
59
+ const usernameMatch = href.match(/\/in\/([^/?]+)/);
60
+ if (usernameMatch) {
61
+ const username = usernameMatch[1];
62
+ const connectionDate = text;
63
+ const connectionTimestamp = parseConnectionDate(connectionDate);
64
+
60
65
  connections.push({
61
66
  username: username,
62
67
  connectionDate: connectionDate,
@@ -66,11 +71,121 @@ export async function executeGetDailyLinkedInConnections(page, action) {
66
71
  }
67
72
  }
68
73
  }
74
+ });
75
+
76
+ return connections;
77
+ });
78
+
79
+ currentConnections.forEach(conn => {
80
+ allConnectionsMap.set(conn.username, conn);
81
+ });
82
+
83
+ console.log(`[get_daily_linkedin_connections] Current page connections: ${currentConnections.length}, Total unique: ${allConnectionsMap.size}`);
84
+
85
+ if (lastCheckTimestamp && currentConnections.length > 0) {
86
+ const oldestConnection = currentConnections.reduce((oldest, current) => {
87
+ if (!oldest.connectionTimestamp) return current;
88
+ if (!current.connectionTimestamp) return oldest;
89
+ return current.connectionTimestamp < oldest.connectionTimestamp ? current : oldest;
90
+ });
91
+
92
+ if (oldestConnection.connectionTimestamp && oldestConnection.connectionTimestamp < lastCheckTimestamp) {
93
+ console.log('[get_daily_linkedin_connections] Reached connections older than last check, stopping');
94
+ shouldStopScrolling = true;
95
+ break;
69
96
  }
97
+ }
98
+
99
+ const scrollInfoBefore = await page.evaluate(() => {
100
+ const main = document.querySelector('main');
101
+ if (main) {
102
+ return {
103
+ scrollTop: main.scrollTop,
104
+ scrollHeight: main.scrollHeight,
105
+ clientHeight: main.clientHeight
106
+ };
107
+ }
108
+ return {
109
+ scrollTop: window.pageYOffset || document.documentElement.scrollTop,
110
+ scrollHeight: document.documentElement.scrollHeight,
111
+ clientHeight: window.innerHeight
112
+ };
70
113
  });
71
114
 
72
- return connections;
73
- }, { lastCheckTimestamp, now });
115
+ console.log(`[get_daily_linkedin_connections] Before scroll - Top: ${scrollInfoBefore.scrollTop}px, Height: ${scrollInfoBefore.scrollHeight}px`);
116
+
117
+ try {
118
+ console.log('[get_daily_linkedin_connections] Scrolling main element...');
119
+
120
+ const mainElement = await page.locator('main').first();
121
+ const containerExists = await mainElement.count();
122
+
123
+ if (containerExists > 0) {
124
+ await mainElement.evaluate(el => {
125
+ console.log('Main element found, scrolling...');
126
+ console.log('Main scrollHeight:', el.scrollHeight);
127
+ console.log('Main clientHeight:', el.clientHeight);
128
+ console.log('Main scrollTop before:', el.scrollTop);
129
+
130
+ el.scrollBy({ top: 2000, behavior: 'smooth' });
131
+
132
+ console.log('Main scrollTop after:', el.scrollTop);
133
+ });
134
+
135
+ console.log('[get_daily_linkedin_connections] Scroll completed');
136
+ } else {
137
+ console.log('[get_daily_linkedin_connections] Main element not found');
138
+ }
139
+ console.log('[get_daily_linkedin_connections] Waiting for content to load...');
140
+ await new Promise(resolve => setTimeout(resolve, 3000));
141
+ } catch (e) {
142
+ console.log(`[get_daily_linkedin_connections] Scroll failed: ${e.message}`);
143
+ console.log('Stack trace:', e.stack);
144
+ }
145
+
146
+ const scrollInfoAfter = await page.evaluate(() => {
147
+ const main = document.querySelector('main');
148
+ if (main) {
149
+ return {
150
+ scrollTop: main.scrollTop,
151
+ scrollHeight: main.scrollHeight,
152
+ clientHeight: main.clientHeight
153
+ };
154
+ }
155
+ return {
156
+ scrollTop: window.pageYOffset || document.documentElement.scrollTop,
157
+ scrollHeight: document.documentElement.scrollHeight,
158
+ clientHeight: window.innerHeight
159
+ };
160
+ });
161
+
162
+ console.log(`[get_daily_linkedin_connections] After scroll - Top: ${scrollInfoAfter.scrollTop}px, Height: ${scrollInfoAfter.scrollHeight}px`);
163
+
164
+ const scrolled = scrollInfoAfter.scrollTop > scrollInfoBefore.scrollTop;
165
+ const heightIncreased = scrollInfoAfter.scrollHeight > scrollInfoBefore.scrollHeight;
166
+
167
+ if (scrolled || heightIncreased) {
168
+ console.log('[get_daily_linkedin_connections] Page scrolled or expanded, continuing...');
169
+ noScrollAttempts = 0;
170
+ } else {
171
+ noScrollAttempts++;
172
+ console.log(`[get_daily_linkedin_connections] No scroll detected (attempt ${noScrollAttempts}/${maxNoScrollAttempts})`);
173
+
174
+ if (noScrollAttempts < maxNoScrollAttempts) {
175
+ console.log('[get_daily_linkedin_connections] Waiting 5 seconds and trying again...');
176
+ await new Promise(resolve => setTimeout(resolve, 5000));
177
+ } else {
178
+ console.log('[get_daily_linkedin_connections] Max attempts reached, stopping scroll');
179
+ shouldStopScrolling = true;
180
+ }
181
+ }
182
+ }
183
+
184
+ const allConnections = Array.from(allConnectionsMap.values());
185
+
186
+ const newConnections = allConnections.filter(conn => {
187
+ return !lastCheckTimestamp || (conn.connectionTimestamp && conn.connectionTimestamp >= lastCheckTimestamp);
188
+ });
74
189
 
75
190
  return {
76
191
  action: 'get_daily_linkedin_connections',
@@ -38,19 +38,15 @@ export async function executeLinkedInSearch(page, action) {
38
38
 
39
39
  try {
40
40
  title = await page.title();
41
- const listItems = await page.locator('ul[role="list"] li').all();
42
-
43
- if (listItems.length > 0) {
44
- const firstItem = listItems[0];
45
- const firstItemHTML = await firstItem.innerHTML().catch(() => '');
46
- console.log('[LinkedInSearch] First item HTML structure:', firstItemHTML.substring(0, 500));
47
- } else {
48
- console.log('[LinkedInSearch] No list items found');
49
- break;
50
- }
51
-
41
+
42
+ // LinkedIn’s new structure
43
+ const htmlContent = await page.content();
44
+
45
+ // Look for cards directly by anchor structure
46
+ const hasCards = await page.locator('a[href*="/in/"]').count();
47
+ console.log(`[LinkedInSearch] Found ${hasCards} profile cards`);
48
+
52
49
  const pageSearchResults = await parseLinkedInSearchResultsFromHTML(htmlContent);
53
-
54
50
  pageResults = pageSearchResults;
55
51
  } catch (error) {
56
52
  pageResults = [];
@@ -65,7 +61,6 @@ export async function executeLinkedInSearch(page, action) {
65
61
 
66
62
  try {
67
63
  await page.goto(nextUrl, {
68
- waitUntil: 'load',
69
64
  timeout: Math.floor(Math.random() * 20000) + 10000
70
65
  });
71
66
  navigationSuccess = true;
@@ -90,52 +85,57 @@ export async function executeLinkedInSearch(page, action) {
90
85
  }
91
86
 
92
87
  async function parseLinkedInSearchResultsFromHTML(htmlContent) {
88
+ const results = [];
93
89
  try {
94
- const results = [];
95
-
96
- const listItemPattern = /<li[^>]*>([\s\S]*?)<\/li>/gi;
97
- const linkPattern = /<a[^>]*href="([^"]*\/in\/[^"]*)"[^>]*>/gi;
98
-
99
- let listItemMatch;
100
-
101
- while ((listItemMatch = listItemPattern.exec(htmlContent)) !== null) {
102
- const listItemHtml = listItemMatch[1];
103
-
104
- const linkMatches = [];
105
- let linkMatch;
106
-
107
- while ((linkMatch = linkPattern.exec(listItemHtml)) !== null) {
108
- linkMatches.push(linkMatch[1]);
109
- }
110
-
111
- if (linkMatches.length > 0) {
112
- const href = linkMatches[0];
113
- let username = '';
114
-
115
- if (href) {
116
- const usernameMatch = href.match(/\/in\/([^/?]+)/);
117
- if (usernameMatch) {
118
- username = usernameMatch[1];
119
- }
120
- }
121
-
122
- const textContent = listItemHtml
123
- .replace(/<[^>]*>/g, ' ')
124
- .replace(/\s+/g, ' ')
125
- .trim();
126
-
127
- results.push({
128
- link: href,
129
- username: username,
130
- item: textContent
131
- });
132
- }
90
+ const cardPattern = /<a[^>]+href="([^"]*linkedin\.com\/in\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
91
+
92
+ let match;
93
+ const seen = new Set();
94
+
95
+ while ((match = cardPattern.exec(htmlContent)) !== null) {
96
+ const [fullMatch, href, contentHtml] = match;
97
+ if (seen.has(href)) continue; // avoid duplicates
98
+ seen.add(href);
99
+
100
+ const usernameMatch = href.match(/\/in\/([^/?#]+)/);
101
+ const username = usernameMatch ? usernameMatch[1] : "";
102
+
103
+ // Extract name
104
+ const nameMatch = contentHtml.match(/>([^<>]{2,50})<\/span>/);
105
+ const name = nameMatch ? nameMatch[1].trim() : "";
106
+
107
+ // Extract headline (title, role)
108
+ const headlineMatch = contentHtml.match(/<div[^>]*>\s*([^<]{5,100})<\/div>/);
109
+ const headline = headlineMatch ? headlineMatch[1].trim() : "";
110
+
111
+ // Extract connection level
112
+ const connectionMatch = contentHtml.match(/\b(1st|2nd|3rd\+)\b/);
113
+ const connectionLevel = connectionMatch ? connectionMatch[1] : "";
114
+
115
+ // Extract location
116
+ const locationMatch = htmlContent
117
+ .slice(cardPattern.lastIndex, cardPattern.lastIndex + 800)
118
+ .match(/([A-Z][a-z]+(?:, [A-Z]{2})?)(?=<)/);
119
+ const location = locationMatch ? locationMatch[1].trim() : "";
120
+
121
+ // Unique ID (message ID-like)
122
+ const uniqueIdMatch = fullMatch.match(/data-view-tracking-scope=.*?contentTrackingId&quot;:&quot;([^&]+)&quot;/);
123
+ const uniqueId = uniqueIdMatch ? uniqueIdMatch[1] : "";
124
+
125
+ results.push({
126
+ username,
127
+ link: href,
128
+ name,
129
+ headline,
130
+ location,
131
+ connectionLevel,
132
+ uniqueId,
133
+ });
133
134
  }
134
-
135
+
135
136
  return results;
136
-
137
137
  } catch (error) {
138
- console.error('[LinkedInSearch] Error parsing search results:', error);
138
+ console.error("[LinkedInSearch] Parsing failed:", error);
139
139
  return [];
140
140
  }
141
- }
141
+ }
@@ -12,6 +12,23 @@ export async function executeGetMessages(page, action) {
12
12
  });
13
13
 
14
14
  await page.waitForLoadState('domcontentloaded');
15
+ await page.waitForLoadState('networkidle');
16
+ await new Promise(resolve => setTimeout(resolve, 2000));
17
+
18
+ await page.evaluate(() => { window.scrollTo(0, 0); });
19
+ await new Promise(resolve => setTimeout(resolve, 500));
20
+ await page.evaluate(() => { window.scrollTo(0, 300); });
21
+ await new Promise(resolve => setTimeout(resolve, 1000));
22
+
23
+ try {
24
+ await page.waitForSelector('button[aria-label^="Message"], main button.artdeco-button--primary', {
25
+ timeout: 5000,
26
+ state: 'attached'
27
+ });
28
+ } catch (error) {
29
+ }
30
+
31
+ await new Promise(resolve => setTimeout(resolve, 500));
15
32
 
16
33
  const pendingButton = await getButtonByText(page, 'Pending', true);
17
34
  if (pendingButton) {
@@ -22,20 +39,204 @@ export async function executeGetMessages(page, action) {
22
39
  };
23
40
  }
24
41
 
25
- const clicked = await page.evaluate(() => {
26
- const buttons = Array.from(document.querySelectorAll('.artdeco-button--primary'));
27
- console.log('found buttons: ', buttons);
28
- const messageBtn = buttons.find(btn =>
29
- btn.textContent.includes('Message') && btn.offsetParent !== null
30
- );
31
- if (messageBtn) {
32
- messageBtn.click();
42
+ let clicked = false;
43
+
44
+ const checkOverlayAppeared = async (page) => {
45
+ const overlaySelectors = [
46
+ '.msg-overlay-conversation-bubble__content-wrapper',
47
+ '.msg-overlay',
48
+ '[class*="msg-overlay"]',
49
+ '[class*="conversation-bubble"]'
50
+ ];
51
+
52
+ for (let attempt = 0; attempt < 15; attempt++) {
53
+ for (const selector of overlaySelectors) {
54
+ try {
55
+ const count = await page.locator(selector).count();
56
+ if (count > 0) {
57
+ return true;
58
+ }
59
+ } catch (error) {
60
+ }
61
+ }
62
+ await new Promise(resolve => setTimeout(resolve, 500));
63
+ }
64
+
65
+ try {
66
+ await page.waitForSelector('.msg-overlay-conversation-bubble__content-wrapper', {
67
+ timeout: 5000,
68
+ state: 'attached'
69
+ });
33
70
  return true;
71
+ } catch (error) {
34
72
  }
35
- console.log('no messageBtn found');
73
+
36
74
  return false;
37
- });
75
+ };
76
+
77
+ const clickStrategies = [
78
+ async () => {
79
+ try {
80
+ const buttons = await page.locator('button[aria-label^="Message"]').all();
81
+ for (const btn of buttons) {
82
+ const isVisible = await btn.isVisible().catch(() => false);
83
+ const ariaLabel = await btn.getAttribute('aria-label').catch(() => '');
84
+ if (isVisible && ariaLabel && ariaLabel.startsWith('Message')) {
85
+ await btn.scrollIntoViewIfNeeded();
86
+ await new Promise(resolve => setTimeout(resolve, 500));
87
+ await btn.click({ timeout: 5000, force: false });
88
+ await new Promise(resolve => setTimeout(resolve, 2000));
89
+ return await checkOverlayAppeared(page);
90
+ }
91
+ }
92
+ } catch (error) {
93
+ }
94
+ return false;
95
+ },
96
+ async () => {
97
+ try {
98
+ const buttons = await page.locator('main button[aria-label^="Message"]').all();
99
+ for (const btn of buttons) {
100
+ const isVisible = await btn.isVisible().catch(() => false);
101
+ if (isVisible) {
102
+ await btn.scrollIntoViewIfNeeded();
103
+ await new Promise(resolve => setTimeout(resolve, 300));
104
+ await btn.click({ timeout: 5000, force: false });
105
+ await new Promise(resolve => setTimeout(resolve, 1000));
106
+ return await checkOverlayAppeared(page);
107
+ }
108
+ }
109
+ } catch (error) {
110
+ }
111
+ return false;
112
+ },
113
+ async () => {
114
+ try {
115
+ const buttons = await page.locator('main button.artdeco-button--primary').all();
116
+ for (const btn of buttons) {
117
+ try {
118
+ const text = await btn.textContent().catch(() => '');
119
+ const ariaLabel = await btn.getAttribute('aria-label').catch(() => '');
120
+ const isVisible = await btn.isVisible().catch(() => false);
121
+
122
+ if (isVisible && text && text.trim().includes('Message') &&
123
+ (!text.includes('Pending')) &&
124
+ (!ariaLabel || !ariaLabel.includes('Pending'))) {
125
+ await btn.scrollIntoViewIfNeeded();
126
+ await new Promise(resolve => setTimeout(resolve, 300));
127
+ await btn.click({ timeout: 5000, force: false });
128
+ await new Promise(resolve => setTimeout(resolve, 1000));
129
+ return await checkOverlayAppeared(page);
130
+ }
131
+ } catch (error) {
132
+ continue;
133
+ }
134
+ }
135
+ } catch (error) {
136
+ }
137
+ return false;
138
+ },
139
+ async () => {
140
+ try {
141
+ const allButtons = await page.locator('main button').all();
142
+ for (const btn of allButtons) {
143
+ try {
144
+ const text = await btn.textContent().catch(() => '');
145
+ const ariaLabel = await btn.getAttribute('aria-label').catch(() => '');
146
+ const isVisible = await btn.isVisible().catch(() => false);
147
+
148
+ if (isVisible && ((ariaLabel && ariaLabel.startsWith('Message')) ||
149
+ (text && text.includes('Message') && !text.includes('Pending')))) {
150
+ await btn.scrollIntoViewIfNeeded();
151
+ await new Promise(resolve => setTimeout(resolve, 300));
152
+ await btn.click({ timeout: 5000, force: false });
153
+ await new Promise(resolve => setTimeout(resolve, 1000));
154
+ return await checkOverlayAppeared(page);
155
+ }
156
+ } catch (error) {
157
+ continue;
158
+ }
159
+ }
160
+ } catch (error) {
161
+ }
162
+ return false;
163
+ },
164
+ async () => {
165
+ try {
166
+ const buttonFound = await page.evaluate(() => {
167
+ const buttons = Array.from(document.querySelectorAll('button'));
168
+ for (const btn of buttons) {
169
+ const ariaLabel = btn.getAttribute('aria-label') || '';
170
+ const textContent = btn.textContent || '';
171
+ const isVisible = btn.offsetParent !== null &&
172
+ window.getComputedStyle(btn).visibility !== 'hidden' &&
173
+ window.getComputedStyle(btn).display !== 'none';
174
+
175
+ if (isVisible && (ariaLabel.startsWith('Message') ||
176
+ (textContent.includes('Message') && !textContent.includes('Pending')))) {
177
+ const rect = btn.getBoundingClientRect();
178
+ if (rect.width > 0 && rect.height > 0) {
179
+ return {
180
+ found: true,
181
+ x: rect.left + rect.width / 2,
182
+ y: rect.top + rect.height / 2
183
+ };
184
+ }
185
+ }
186
+ }
187
+ return { found: false };
188
+ });
189
+
190
+ if (buttonFound.found) {
191
+ await page.mouse.click(buttonFound.x, buttonFound.y);
192
+ await new Promise(resolve => setTimeout(resolve, 1000));
193
+ return await checkOverlayAppeared(page);
194
+ }
195
+ } catch (error) {
196
+ }
197
+ return false;
198
+ },
199
+ async () => {
200
+ try {
201
+ const messageBtn = await getButtonByText(page, 'Message', true);
202
+ if (messageBtn) {
203
+ await messageBtn.scrollIntoViewIfNeeded();
204
+ await new Promise(resolve => setTimeout(resolve, 300));
205
+ await messageBtn.click({ timeout: 5000, force: false });
206
+ await new Promise(resolve => setTimeout(resolve, 1000));
207
+ return await checkOverlayAppeared(page);
208
+ }
209
+ } catch (error) {
210
+ }
211
+ return false;
212
+ },
213
+ async () => {
214
+ try {
215
+ const messageBtn = await getButtonByText(page, 'Message', false);
216
+ if (messageBtn) {
217
+ await messageBtn.scrollIntoViewIfNeeded();
218
+ await new Promise(resolve => setTimeout(resolve, 300));
219
+ await messageBtn.click({ timeout: 5000, force: false });
220
+ await new Promise(resolve => setTimeout(resolve, 1000));
221
+ return await checkOverlayAppeared(page);
222
+ }
223
+ } catch (error) {
224
+ }
225
+ return false;
226
+ }
227
+ ];
38
228
 
229
+ for (const strategy of clickStrategies) {
230
+ try {
231
+ clicked = await strategy();
232
+ if (clicked) {
233
+ break;
234
+ }
235
+ } catch (error) {
236
+ continue;
237
+ }
238
+ }
239
+
39
240
  if (!clicked) {
40
241
  return {
41
242
  action: 'get_messages',
@@ -44,18 +245,75 @@ export async function executeGetMessages(page, action) {
44
245
  };
45
246
  }
46
247
 
47
- await page.waitForSelector('.msg-overlay-conversation-bubble__content-wrapper', { timeout: 10000 });
48
- await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 5000) + 500));
248
+ try {
249
+ await page.waitForSelector('.msg-overlay-conversation-bubble__content-wrapper', { timeout: 15000 });
250
+ } catch (error) {
251
+ await new Promise(resolve => setTimeout(resolve, 2000));
252
+ const overlayExists = await page.locator('.msg-overlay-conversation-bubble__content-wrapper').count();
253
+ if (overlayExists === 0) {
254
+ return {
255
+ action: 'get_messages',
256
+ result: { status: 'error', message: `Clicked Message button but overlay did not appear for ${username}` },
257
+ status: 'failed'
258
+ };
259
+ }
260
+ }
261
+
262
+ await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 2000) + 1000));
49
263
 
50
264
  await page.evaluate(() => {
51
- const wrapper = document.querySelector('.msg-overlay-conversation-bubble__content-wrapper');
52
- if (wrapper) {
53
- wrapper.scrollTo(0, 0);
265
+ const messageList = document.querySelector('.msg-s-message-list');
266
+ if (messageList) {
267
+ messageList.scrollTop = 0;
54
268
  }
55
269
  });
56
- await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 5000) + 500));
270
+ await new Promise(resolve => setTimeout(resolve, 1000));
271
+
272
+ let previousScrollHeight = 0;
273
+ let scrollAttempts = 0;
274
+ const maxScrollAttempts = 10;
275
+
276
+ while (scrollAttempts < maxScrollAttempts) {
277
+ const scrollResult = await page.evaluate(() => {
278
+ const messageList = document.querySelector('.msg-s-message-list');
279
+ if (!messageList) return { shouldContinue: false, scrollHeight: 0 };
280
+
281
+ const currentScrollHeight = messageList.scrollHeight;
282
+ messageList.scrollTop = messageList.scrollHeight;
283
+
284
+ return {
285
+ shouldContinue: true,
286
+ scrollHeight: currentScrollHeight,
287
+ scrollTop: messageList.scrollTop
288
+ };
289
+ });
290
+
291
+ if (!scrollResult.shouldContinue) {
292
+ break;
293
+ }
294
+
295
+ await new Promise(resolve => setTimeout(resolve, 1500));
296
+
297
+ const newScrollHeight = await page.evaluate(() => {
298
+ const messageList = document.querySelector('.msg-s-message-list');
299
+ return messageList ? messageList.scrollHeight : 0;
300
+ });
301
+
302
+ if (newScrollHeight === previousScrollHeight) {
303
+ break;
304
+ }
305
+
306
+ previousScrollHeight = newScrollHeight;
307
+ scrollAttempts++;
308
+ }
309
+
310
+ await new Promise(resolve => setTimeout(resolve, 1000));
57
311
 
58
312
  const content = await page.evaluate(() => {
313
+ const messageList = document.querySelector('.msg-s-message-list');
314
+ if (messageList) {
315
+ return messageList.innerText || '';
316
+ }
59
317
  const wrapper = document.querySelector('.msg-overlay-conversation-bubble__content-wrapper');
60
318
  return wrapper ? wrapper.innerText : '';
61
319
  });
@@ -68,24 +326,32 @@ export async function executeGetMessages(page, action) {
68
326
  }
69
327
 
70
328
  async function getButtonByText(page, text, inMain = false) {
71
- let buttons = [];
72
- if (inMain) {
73
- buttons = await page.locator('main button').all();
74
- } else {
75
- buttons = await page.locator('button').all();
76
- }
77
-
78
- for (const button of buttons) {
79
- try {
80
- const buttonText = await button.textContent();
81
- if (buttonText && buttonText.includes(text)) {
82
- const isVisible = await button.isVisible();
83
- if (isVisible) {
84
- return button;
329
+ try {
330
+ let buttons = [];
331
+ if (inMain) {
332
+ buttons = await page.locator('main button').all();
333
+ } else {
334
+ buttons = await page.locator('button').all();
335
+ }
336
+
337
+ for (const button of buttons) {
338
+ try {
339
+ const buttonText = await button.textContent().catch(() => '');
340
+ const ariaLabel = await button.getAttribute('aria-label').catch(() => '');
341
+ const isVisible = await button.isVisible().catch(() => false);
342
+
343
+ if (isVisible && ((buttonText && buttonText.trim().includes(text)) ||
344
+ (ariaLabel && ariaLabel.includes(text)))) {
345
+ const boundingBox = await button.boundingBox().catch(() => null);
346
+ if (boundingBox && boundingBox.width > 0 && boundingBox.height > 0) {
347
+ return button;
348
+ }
85
349
  }
350
+ } catch (error) {
351
+ continue;
86
352
  }
87
- } catch (error) {
88
353
  }
354
+ } catch (error) {
89
355
  }
90
356
 
91
357
  return null;