coffeeinabit 0.0.6 → 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,25 +694,40 @@ 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);
702
704
 
703
- this.context = await this.browser.newContext({
705
+ if (!fs.existsSync(userDataPath)) {
706
+ fs.mkdirSync(userDataPath, { recursive: true });
707
+ console.log('[LinkedInAutomation] Created user data directory:', userDataPath);
708
+ }
709
+
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
  }
@@ -737,17 +750,21 @@ export class LinkedInAutomation {
737
750
 
738
751
  const currentUrl = this.page.url();
739
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}`);
740
755
 
741
- await this.browser.close();
756
+ await this.context.close();
742
757
 
743
758
  this.headless = newHeadless;
744
- this.browser = await firefox.launch({ headless: this.headless });
745
759
 
746
- this.context = await this.browser.newContext({
760
+ this.context = await firefox.launchPersistentContext(userDataPath, {
761
+ headless: this.headless,
747
762
  viewport: null,
748
763
  ignoreHTTPSErrors: true
749
764
  });
750
765
 
766
+ this.browser = this.context;
767
+
751
768
  await this.enablePerformanceMode();
752
769
  this.page = await this.context.newPage();
753
770
 
@@ -773,14 +790,12 @@ export class LinkedInAutomation {
773
790
 
774
791
  async checkAndVerifySession() {
775
792
  try {
776
- console.log('[LinkedInAutomation] Navigating to LinkedIn feed to verify session...');
793
+ console.log('[LinkedInAutomation] Navigating to LinkedIn feed to verify persistent session...');
777
794
  await this.page.goto('https://www.linkedin.com/feed/', {
778
795
  waitUntil: 'domcontentloaded',
779
796
  timeout: 30000
780
797
  });
781
798
 
782
- await this.restoreLocalStorage();
783
-
784
799
  await new Promise(resolve => setTimeout(resolve, 3000));
785
800
 
786
801
  let verifyAttempts = 0;
@@ -791,7 +806,7 @@ export class LinkedInAutomation {
791
806
  console.log(`[LinkedInAutomation] Verification attempt ${verifyAttempts + 1}/${maxVerifyAttempts}, URL: ${this.currentUrl}`);
792
807
 
793
808
  if (this.currentUrl.includes('/feed')) {
794
- console.log('[LinkedInAutomation] Session verified! Already logged in.');
809
+ console.log('[LinkedInAutomation] Session verified! Already logged in via persistent context.');
795
810
  this.status = 'linkedin_logged_in';
796
811
  this.emitStatus();
797
812
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coffeeinabit",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "CoffeeInABit App",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -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;