coffeeinabit 0.0.47 → 0.0.49

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.
@@ -1,7 +1,10 @@
1
1
  import axios from 'axios';
2
2
  import { safeGoto } from './navigation.js';
3
3
 
4
- export async function executeGetNewMessages(page, action, accessToken) {
4
+ /**
5
+ * Parse lastChecked timestamp from action parameters
6
+ */
7
+ function parseLastCheckedTimestamp(action) {
5
8
  let lastChecked = action.parameters?.last_checked;
6
9
  if (!lastChecked) {
7
10
  lastChecked = 0;
@@ -12,14 +15,112 @@ export async function executeGetNewMessages(page, action, accessToken) {
12
15
  lastCheckedTimestamp = lastCheckedTimestamp * 1000;
13
16
  }
14
17
 
15
- const allMessageEntities = [];
16
- const processedMessages = new Set();
17
- const conversationParticipantsMap = new Map();
18
+ return lastCheckedTimestamp;
19
+ }
20
+
21
+ /**
22
+ * Extract participant encoded URL from conversation data
23
+ */
24
+ function extractParticipantUrl(conversation) {
25
+ const convUrn = conversation.backendUrn;
26
+ const participants = conversation.conversationParticipants || [];
27
+ const otherPerson = participants.find(p => {
28
+ const member = p.participantType?.member;
29
+ return member && member.distance !== 'SELF';
30
+ });
31
+
32
+ if (otherPerson && otherPerson.hostIdentityUrn) {
33
+ const encodedUrl = otherPerson.hostIdentityUrn.includes('urn:li:fsd_profile:')
34
+ ? otherPerson.hostIdentityUrn.split('urn:li:fsd_profile:')[1]
35
+ : '';
36
+ return { convUrn, encodedUrl };
37
+ }
38
+
39
+ return { convUrn, encodedUrl: null };
40
+ }
41
+
42
+ /**
43
+ * Parse conversation participants from API response
44
+ */
45
+ function parseConversationParticipants(data, conversationParticipantsMap) {
46
+ if (data?.data?.messengerConversationsBySyncToken?.elements) {
47
+ const conversations = data.data.messengerConversationsBySyncToken.elements;
48
+ conversations.forEach(conv => {
49
+ const { convUrn, encodedUrl } = extractParticipantUrl(conv);
50
+ if (encodedUrl) {
51
+ conversationParticipantsMap.set(convUrn, encodedUrl);
52
+ }
53
+ });
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Extract message entity from API response message object
59
+ */
60
+ function extractMessageEntity(msg) {
61
+ const senderInfo = msg.sender || msg.actor;
62
+ const senderMember = senderInfo?.participantType?.member;
63
+ const authorId = senderInfo?.backendUrn || 'unknown';
64
+ const authorName = senderMember
65
+ ? `${senderMember.firstName?.text || ''} ${senderMember.lastName?.text || ''}`.trim()
66
+ : 'Unknown';
67
+
68
+ const hostIdentityUrn = senderInfo?.hostIdentityUrn || '';
69
+ const authorUrl = hostIdentityUrn.includes('urn:li:fsd_profile:')
70
+ ? hostIdentityUrn.split('urn:li:fsd_profile:')[1]
71
+ : '';
72
+
73
+ const conversationId = msg.backendConversationUrn || '';
74
+ const isSelf = senderMember?.distance === 'SELF';
75
+ const messageText = msg.body?.text || '';
76
+ const deliveredAt = msg.deliveredAt;
77
+
78
+ return {
79
+ authorId,
80
+ authorName,
81
+ authorUrl,
82
+ conversationId,
83
+ isSelf,
84
+ messageText,
85
+ deliveredAt
86
+ };
87
+ }
18
88
 
19
- const routeHandler = async (route) => {
89
+ /**
90
+ * Parse messages from API response
91
+ */
92
+ function parseMessagesFromResponse(data, allMessageEntities, processedMessages) {
93
+ if (data?.data?.messengerMessagesBySyncToken?.elements) {
94
+ const elements = data.data.messengerMessagesBySyncToken.elements;
95
+ console.log(`[GetNewMessages] Found messengerMessagesBySyncToken with ${elements.length} messages`);
96
+
97
+ let addedCount = 0;
98
+ let duplicateCount = 0;
99
+
100
+ elements.forEach(msg => {
101
+ const messageEntity = extractMessageEntity(msg);
102
+ const messageKey = `${messageEntity.conversationId}-${messageEntity.deliveredAt}-${messageEntity.messageText}`;
103
+
104
+ if (messageEntity.messageText && messageEntity.deliveredAt && !processedMessages.has(messageKey)) {
105
+ processedMessages.add(messageKey);
106
+ allMessageEntities.push(messageEntity);
107
+ addedCount++;
108
+ } else if (processedMessages.has(messageKey)) {
109
+ duplicateCount++;
110
+ }
111
+ });
112
+
113
+ console.log(`[GetNewMessages] Added ${addedCount} new messages, ${duplicateCount} duplicates, total now: ${allMessageEntities.length}`);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Create route handler for intercepting LinkedIn API calls
119
+ */
120
+ function createRouteHandler(allMessageEntities, processedMessages, conversationParticipantsMap) {
121
+ return async (route) => {
20
122
  const url = route.request().url();
21
123
 
22
- /////
23
124
  if (url.includes('voyagerMessagingGraphQL')) {
24
125
  const requestBody = route.request().postData();
25
126
  if (requestBody) {
@@ -37,27 +138,7 @@ export async function executeGetNewMessages(page, action, accessToken) {
37
138
  try {
38
139
  const responseBody = await response.text();
39
140
  const data = JSON.parse(responseBody);
40
-
41
- if (data?.data?.messengerConversationsBySyncToken?.elements) {
42
- const conversations = data.data.messengerConversationsBySyncToken.elements;
43
- conversations.forEach(conv => {
44
- const convUrn = conv.backendUrn;
45
- const participants = conv.conversationParticipants || [];
46
- const otherPerson = participants.find(p => {
47
- const member = p.participantType?.member;
48
- return member && member.distance !== 'SELF';
49
- });
50
-
51
- if (otherPerson && otherPerson.hostIdentityUrn) {
52
- const encodedUrl = otherPerson.hostIdentityUrn.includes('urn:li:fsd_profile:')
53
- ? otherPerson.hostIdentityUrn.split('urn:li:fsd_profile:')[1]
54
- : '';
55
- if (encodedUrl) {
56
- conversationParticipantsMap.set(convUrn, encodedUrl);
57
- }
58
- }
59
- });
60
- }
141
+ parseConversationParticipants(data, conversationParticipantsMap);
61
142
  } catch (error) {
62
143
  console.error('[GetNewMessages] Error parsing conversations:', error);
63
144
  }
@@ -75,56 +156,8 @@ export async function executeGetNewMessages(page, action, accessToken) {
75
156
  try {
76
157
  const responseBody = await response.text();
77
158
  const data = JSON.parse(responseBody);
78
-
79
159
  console.log(`[GetNewMessages] Intercepted messengerMessages response`);
80
-
81
- if (data?.data?.messengerMessagesBySyncToken?.elements) {
82
- const elements = data.data.messengerMessagesBySyncToken.elements;
83
- console.log(`[GetNewMessages] Found messengerMessagesBySyncToken with ${elements.length} messages`);
84
-
85
- let addedCount = 0;
86
- let duplicateCount = 0;
87
-
88
- elements.forEach(msg => {
89
- const senderInfo = msg.sender || msg.actor;
90
- const senderMember = senderInfo?.participantType?.member;
91
- const authorId = senderInfo?.backendUrn || 'unknown';
92
- const authorName = senderMember
93
- ? `${senderMember.firstName?.text || ''} ${senderMember.lastName?.text || ''}`.trim()
94
- : 'Unknown';
95
-
96
- const hostIdentityUrn = senderInfo?.hostIdentityUrn || '';
97
- const authorUrl = hostIdentityUrn.includes('urn:li:fsd_profile:')
98
- ? hostIdentityUrn.split('urn:li:fsd_profile:')[1]
99
- : '';
100
-
101
- const conversationId = msg.backendConversationUrn || '';
102
- const isSelf = senderMember?.distance === 'SELF';
103
-
104
- const messageText = msg.body?.text || '';
105
- const deliveredAt = msg.deliveredAt;
106
-
107
- const messageKey = `${conversationId}-${deliveredAt}-${messageText}`;
108
-
109
- if (messageText && deliveredAt && !processedMessages.has(messageKey)) {
110
- processedMessages.add(messageKey);
111
- allMessageEntities.push({
112
- authorId: authorId,
113
- authorName: authorName,
114
- authorUrl: authorUrl,
115
- conversationId: conversationId,
116
- isSelf: isSelf,
117
- messageText: messageText,
118
- deliveredAt: deliveredAt
119
- });
120
- addedCount++;
121
- } else if (processedMessages.has(messageKey)) {
122
- duplicateCount++;
123
- }
124
- });
125
-
126
- console.log(`[GetNewMessages] Added ${addedCount} new messages, ${duplicateCount} duplicates, total now: ${allMessageEntities.length}`);
127
- }
160
+ parseMessagesFromResponse(data, allMessageEntities, processedMessages);
128
161
  } catch (error) {
129
162
  console.error('[GetNewMessages] Error parsing response:', error);
130
163
  }
@@ -138,73 +171,382 @@ export async function executeGetNewMessages(page, action, accessToken) {
138
171
 
139
172
  await route.continue();
140
173
  };
174
+ }
175
+
176
+ /**
177
+ * Navigate to messaging page and wait for UI to load
178
+ */
179
+ async function navigateToMessagingPage(page) {
180
+ await safeGoto(page, 'https://www.linkedin.com/messaging/', {
181
+ waitUntil: 'domcontentloaded',
182
+ timeout: 60000
183
+ });
184
+
185
+ await page.waitForLoadState('domcontentloaded');
186
+ await new Promise(resolve => setTimeout(resolve, 2000));
187
+ }
141
188
 
189
+ /**
190
+ * Wait for conversations list to appear
191
+ * Returns early with empty result if not found (STOPPING POINT #1)
192
+ */
193
+ async function waitForConversationsList(page, routeHandler) {
194
+ console.log('[GetNewMessages] Waiting for conversations list to load...');
195
+
142
196
  try {
143
- await page.route('**/voyagerMessagingGraphQL/**', routeHandler);
197
+ await page.waitForSelector('.msg-conversations-container__conversations-list', {
198
+ timeout: 30000,
199
+ state: 'visible'
200
+ });
201
+ } catch (error) {
202
+ console.log('[GetNewMessages] Timeout waiting for conversations list');
203
+ try {
204
+ await page.unroute('**/voyagerMessagingGraphQL/**', routeHandler);
205
+ } catch (unrouteError) {
206
+ console.log('[GetNewMessages] Error unrouting (non-critical):', unrouteError.message);
207
+ }
208
+ // ⚠️ STOPPING POINT #1: Early return when UI not available
209
+ return { success: false, items: null };
210
+ }
211
+
212
+ await new Promise(resolve => setTimeout(resolve, 2000));
213
+ return { success: true, items: null };
214
+ }
144
215
 
145
- await safeGoto(page, 'https://www.linkedin.com/messaging/', {
146
- waitUntil: 'domcontentloaded',
147
- timeout: 60000
216
+ /**
217
+ * Get conversation items from the list
218
+ * Returns early with empty result if none found (STOPPING POINT #2 & #3)
219
+ */
220
+ async function getConversationItems(page, routeHandler) {
221
+ console.log('[GetNewMessages] Looking for conversations list...');
222
+ const conversationsList = await page.locator('.msg-conversations-container__conversations-list').first();
223
+ const listCount = await conversationsList.count();
224
+ console.log('[GetNewMessages] Conversations list count:', listCount);
225
+
226
+ if (listCount === 0) {
227
+ console.log('[GetNewMessages] No conversations list found, exiting');
228
+ try {
229
+ await page.unroute('**/voyagerMessagingGraphQL/**', routeHandler);
230
+ } catch (unrouteError) {
231
+ console.log('[GetNewMessages] Error unrouting (non-critical):', unrouteError.message);
232
+ }
233
+ // ⚠️ STOPPING POINT #2: No conversations found
234
+ return { success: false, items: null };
235
+ }
236
+
237
+ console.log('[GetNewMessages] Waiting for conversation items to load...');
238
+ try {
239
+ await page.waitForSelector('li.msg-conversation-listitem .msg-conversation-listitem__link', {
240
+ timeout: 15000,
241
+ state: 'visible'
148
242
  });
243
+ } catch (error) {
244
+ console.log('[GetNewMessages] No conversation items found');
245
+ try {
246
+ await page.unroute('**/voyagerMessagingGraphQL/**', routeHandler);
247
+ } catch (unrouteError) {
248
+ console.log('[GetNewMessages] Error unrouting (non-critical):', unrouteError.message);
249
+ }
250
+ // ⚠️ STOPPING POINT #3: No conversation items found
251
+ return { success: false, items: null };
252
+ }
149
253
 
150
- await page.waitForLoadState('domcontentloaded');
151
- await new Promise(resolve => setTimeout(resolve, 2000));
254
+ await new Promise(resolve => setTimeout(resolve, 1000));
255
+ const conversationItems = await conversationsList.locator('li.msg-conversation-listitem .msg-conversation-listitem__link').all();
256
+ console.log('[GetNewMessages] Found', conversationItems.length, 'conversation items');
257
+
258
+ await new Promise(resolve => setTimeout(resolve, 2000));
259
+ return { success: true, items: conversationItems };
260
+ }
152
261
 
153
- console.log('[GetNewMessages] Waiting for conversations list to load...');
262
+ /**
263
+ * Scroll up in message container to load older messages
264
+ * Returns true if reached old messages (STOPPING POINT #4)
265
+ */
266
+ async function scrollToLoadOldMessages(page, messageContainer, allMessageEntities, lastCheckedTimestamp) {
267
+ let reachedOldMessages = false;
268
+ let scrollAttempts = 0;
269
+ const maxScrollAttempts = 20;
270
+
271
+ await messageContainer.hover();
272
+ await new Promise(resolve => setTimeout(resolve, 500));
273
+
274
+ let noNewMessagesRetries = 0;
275
+
276
+ while (!reachedOldMessages && scrollAttempts < maxScrollAttempts) {
277
+ const beforeScrollCount = allMessageEntities.length;
278
+ console.log(`[GetNewMessages] Scroll cycle ${scrollAttempts + 1}, current messages: ${beforeScrollCount}`);
154
279
 
155
- try {
156
- await page.waitForSelector('.msg-conversations-container__conversations-list', {
157
- timeout: 30000,
158
- state: 'visible'
280
+
281
+ for (let i = 0; i < 5; i++) {
282
+ await messageContainer.evaluate(function(el) {
283
+ el.scrollBy({ top: -2000, behavior: 'smooth' });
159
284
  });
160
- } catch (error) {
161
- console.log('[GetNewMessages] Timeout waiting for conversations list');
162
- try {
163
- await page.unroute('**/voyagerMessagingGraphQL/**', routeHandler);
164
- } catch (unrouteError) {
165
- console.log('[GetNewMessages] Error unrouting (non-critical):', unrouteError.message);
285
+ console.log(`[GetNewMessages] Scrolled up -2000px (${i + 1}/5)`);
286
+ await new Promise(resolve => setTimeout(resolve, 1000));
287
+ }
288
+
289
+ console.log(`[GetNewMessages] Waiting for LinkedIn to load older messages...`);
290
+ await new Promise(resolve => setTimeout(resolve, 3000));
291
+
292
+ const afterScrollCount = allMessageEntities.length;
293
+ const newMessagesLoaded = afterScrollCount - beforeScrollCount;
294
+ console.log(`[GetNewMessages] After 5 scrolls: ${afterScrollCount} messages (${newMessagesLoaded} new)`);
295
+
296
+ if (newMessagesLoaded === 0) {
297
+ noNewMessagesRetries++;
298
+ console.log(`[GetNewMessages] No new messages loaded, retry ${noNewMessagesRetries}/2`);
299
+
300
+ if (noNewMessagesRetries >= 2) {
301
+ console.log('[GetNewMessages] No new messages after 2 retries, reached the top');
302
+ break;
166
303
  }
167
- return { newMessagesCount: 0, messages: [] };
304
+ } else {
305
+ noNewMessagesRetries = 0;
306
+ console.log(`[GetNewMessages] Loaded ${newMessagesLoaded} new messages, continuing...`);
168
307
  }
169
308
 
170
- await new Promise(resolve => setTimeout(resolve, 2000));
309
+ const oldestMessage = allMessageEntities
310
+ .filter(msg => msg.deliveredAt)
311
+ .sort((a, b) => a.deliveredAt - b.deliveredAt)[0];
312
+
313
+ // ⚠️ STOPPING POINT #4: Check if reached messages older than lastChecked
314
+ if (oldestMessage && oldestMessage.deliveredAt < lastCheckedTimestamp) {
315
+ reachedOldMessages = true;
316
+ console.log('[GetNewMessages] Reached messages older than lastChecked in this conversation');
317
+ }
318
+
319
+ scrollAttempts++;
320
+ }
321
+
322
+ console.log(`[GetNewMessages] Finished scrolling after ${scrollAttempts} attempts`);
323
+ return reachedOldMessages;
324
+ }
171
325
 
172
- console.log('[GetNewMessages] Looking for conversations list...');
173
- const conversationsList = await page.locator('.msg-conversations-container__conversations-list').first();
174
- const listCount = await conversationsList.count();
175
- console.log('[GetNewMessages] Conversations list count:', listCount);
326
+ /**
327
+ * Process a single conversation: click, scroll, and collect messages
328
+ * Returns shouldContinue flag (STOPPING POINT #5)
329
+ */
330
+ async function processConversation(page, item, index, allMessageEntities, lastCheckedTimestamp) {
331
+ const messageCountBefore = allMessageEntities.length;
332
+
333
+ await item.scrollIntoViewIfNeeded();
334
+ await new Promise(resolve => setTimeout(resolve, 300));
335
+ await item.click({ timeout: 10000, force: false });
336
+ await new Promise(resolve => setTimeout(resolve, 2500));
337
+
338
+ const messageContainer = await page.locator('.msg-s-message-list.scrollable').first();
339
+ const containerExists = await messageContainer.count();
340
+
341
+ if (containerExists > 0) {
342
+ await scrollToLoadOldMessages(page, messageContainer, allMessageEntities, lastCheckedTimestamp);
343
+ }
344
+
345
+ const messageCountAfter = allMessageEntities.length;
346
+ const newMessages = messageCountAfter - messageCountBefore;
347
+ console.log(`[GetNewMessages] Total messages from conversation ${index + 1}: ${newMessages}`);
348
+
349
+ let shouldContinue = true;
350
+ if (newMessages > 0) {
351
+ const conversationMessages = allMessageEntities.slice(messageCountBefore);
352
+ const latestInThisConversation = conversationMessages
353
+ .filter(msg => msg.deliveredAt)
354
+ .sort((a, b) => b.deliveredAt - a.deliveredAt)[0];
176
355
 
177
- if (listCount === 0) {
178
- console.log('[GetNewMessages] No conversations list found, exiting');
179
- try {
180
- await page.unroute('**/voyagerMessagingGraphQL/**', routeHandler);
181
- } catch (unrouteError) {
182
- console.log('[GetNewMessages] Error unrouting (non-critical):', unrouteError.message);
183
- }
184
- return { newMessagesCount: 0, messages: [] };
356
+ // ⚠️ STOPPING POINT #5: Stop processing if latest message is older than lastChecked
357
+ if (latestInThisConversation && latestInThisConversation.deliveredAt < lastCheckedTimestamp) {
358
+ console.log('[GetNewMessages] Latest message in conversation is older than lastChecked, stopping conversation loop');
359
+ shouldContinue = false;
360
+ }
361
+ }
362
+
363
+ return shouldContinue;
364
+ }
365
+
366
+ /**
367
+ * Filter and organize new messages by conversation
368
+ */
369
+ function filterAndOrganizeNewMessages(allMessageEntities, lastCheckedTimestamp) {
370
+ const newMessages = allMessageEntities.filter(msg => msg.deliveredAt > lastCheckedTimestamp);
371
+ newMessages.sort((a, b) => a.deliveredAt - b.deliveredAt);
372
+ console.log('[GetNewMessages] New messages count:', newMessages.length);
373
+
374
+ const newMessagesByConversation = {};
375
+ newMessages.forEach(msg => {
376
+ if (!newMessagesByConversation[msg.conversationId]) {
377
+ newMessagesByConversation[msg.conversationId] = [];
378
+ }
379
+ newMessagesByConversation[msg.conversationId].push(msg);
380
+ });
381
+
382
+ console.log('[GetNewMessages] Conversations with new messages:', Object.keys(newMessagesByConversation).length);
383
+ return { newMessages, newMessagesByConversation };
384
+ }
385
+
386
+ /**
387
+ * Build map of encoded URLs that need to be decoded
388
+ */
389
+ function buildEncodedUrlMap(newMessagesByConversation, conversationParticipantsMap) {
390
+ const encodedUrlMap = new Map();
391
+ for (const conversationId in newMessagesByConversation) {
392
+ const encodedUrl = conversationParticipantsMap.get(conversationId);
393
+ if (encodedUrl) {
394
+ encodedUrlMap.set(encodedUrl, null);
395
+ } else {
396
+ console.log(`[GetNewMessages] No participant found for conversation:`, conversationId);
185
397
  }
398
+ }
399
+ console.log('[GetNewMessages] Unique profiles to decode:', encodedUrlMap.size);
400
+ return encodedUrlMap;
401
+ }
186
402
 
187
- console.log('[GetNewMessages] Waiting for conversation items to load...');
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
+
188
417
  try {
189
- await page.waitForSelector('li.msg-conversation-listitem .msg-conversation-listitem__link', {
190
- timeout: 15000,
191
- state: 'visible'
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
192
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++;
193
442
  } catch (error) {
194
- console.log('[GetNewMessages] No conversation items found');
443
+ console.error(`[GetNewMessages] Failed to decode URL ${encodedUrl}:`, error.message);
444
+ decodeCount++;
445
+ continue;
446
+ }
447
+ }
448
+
449
+ return decodedUrlMap;
450
+ }
451
+
452
+ /**
453
+ * ⚠️ SENDING FUNCTIONALITY: Send messages to API endpoint
454
+ * This is the core functionality for sending collected messages to the backend
455
+ */
456
+ async function sendMessagesToApi(newMessagesByConversation, conversationParticipantsMap, decodedUrlMap, accessToken) {
457
+ let sentCount = 0;
458
+ let failedCount = 0;
459
+ let totalToSend = 0;
460
+
461
+ for (const conversationId in newMessagesByConversation) {
462
+ const messages = newMessagesByConversation[conversationId];
463
+ const encodedUrl = conversationParticipantsMap.get(conversationId);
464
+ const linkedinUrl = encodedUrl ? decodedUrlMap.get(encodedUrl) : null;
465
+
466
+ if (!linkedinUrl) {
467
+ console.log(`[GetNewMessages] Skipping conversation ${conversationId} - could not find or decode other person's URL`);
468
+ continue;
469
+ }
470
+
471
+ console.log(`[GetNewMessages] Sending ${messages.length} messages to ${linkedinUrl}`);
472
+ totalToSend += messages.length;
473
+
474
+ for (const msg of messages) {
195
475
  try {
196
- await page.unroute('**/voyagerMessagingGraphQL/**', routeHandler);
197
- } catch (unrouteError) {
198
- console.log('[GetNewMessages] Error unrouting (non-critical):', unrouteError.message);
476
+ const payload = {
477
+ linkedin_url: linkedinUrl,
478
+ message: msg.messageText,
479
+ is_receipent: !msg.isSelf,
480
+ timestamp: msg.deliveredAt
481
+ };
482
+
483
+ console.log(`[GetNewMessages] Posting message: "${msg.messageText.substring(0, 50)}..." (${msg.deliveredAt})`);
484
+
485
+ await axios.post('https://api.coffeeinabit.com/messages', payload, {
486
+ headers: {
487
+ 'Authorization': `Bearer ${accessToken}`,
488
+ 'Content-Type': 'application/json'
489
+ }
490
+ });
491
+
492
+ sentCount++;
493
+ } catch (error) {
494
+ failedCount++;
495
+ console.error(`[GetNewMessages] Failed to send message to ${linkedinUrl}: "${msg.messageText.substring(0, 50)}..."`);
496
+ console.error(`[GetNewMessages] Error:`, error.message);
497
+ if (error.response) {
498
+ console.error(`[GetNewMessages] Response status:`, error.response.status);
499
+ console.error(`[GetNewMessages] Response data:`, error.response.data);
500
+ }
199
501
  }
502
+ }
503
+ }
504
+
505
+ console.log(`[GetNewMessages] Successfully sent ${sentCount}/${totalToSend} messages${failedCount > 0 ? ` (${failedCount} failed)` : ''}`);
506
+ return { sentCount, failedCount, totalToSend };
507
+ }
508
+
509
+ /**
510
+ * Cleanup route handlers
511
+ */
512
+ async function cleanupRouteHandlers(page, routeHandler) {
513
+ console.log('[GetNewMessages] Unrouting message handlers...');
514
+ try {
515
+ await page.unroute('**/voyagerMessagingGraphQL/**', routeHandler);
516
+ console.log('[GetNewMessages] Successfully unrouted message handlers');
517
+ } catch (error) {
518
+ console.log('[GetNewMessages] Error unrouting (non-critical):', error.message);
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Main function to get new messages from LinkedIn
524
+ */
525
+ export async function executeGetNewMessages(page, action, accessToken) {
526
+ const lastCheckedTimestamp = parseLastCheckedTimestamp(action);
527
+
528
+ const allMessageEntities = [];
529
+ const processedMessages = new Set();
530
+ const conversationParticipantsMap = new Map();
531
+
532
+ const routeHandler = createRouteHandler(allMessageEntities, processedMessages, conversationParticipantsMap);
533
+
534
+ try {
535
+ await page.route('**/voyagerMessagingGraphQL/**', routeHandler);
536
+
537
+ await navigateToMessagingPage(page);
538
+
539
+ const conversationsResult = await waitForConversationsList(page, routeHandler);
540
+ if (!conversationsResult.success) {
200
541
  return { newMessagesCount: 0, messages: [] };
201
542
  }
202
543
 
203
- await new Promise(resolve => setTimeout(resolve, 1000));
204
- const conversationItems = await conversationsList.locator('li.msg-conversation-listitem .msg-conversation-listitem__link').all();
205
- console.log('[GetNewMessages] Found', conversationItems.length, 'conversation items');
544
+ const itemsResult = await getConversationItems(page, routeHandler);
545
+ if (!itemsResult.success) {
546
+ return { newMessagesCount: 0, messages: [] };
547
+ }
206
548
 
207
- await new Promise(resolve => setTimeout(resolve, 2000));
549
+ const conversationItems = itemsResult.items;
208
550
  const initialMessageCount = allMessageEntities.length;
209
551
  console.log(`[GetNewMessages] Messages from initially opened conversation: ${initialMessageCount}`);
210
552
 
@@ -213,89 +555,7 @@ export async function executeGetNewMessages(page, action, accessToken) {
213
555
  const item = conversationItems[i];
214
556
 
215
557
  try {
216
- const messageCountBefore = allMessageEntities.length;
217
-
218
- await item.scrollIntoViewIfNeeded();
219
- await new Promise(resolve => setTimeout(resolve, 300));
220
- await item.click({ timeout: 10000, force: false });
221
- await new Promise(resolve => setTimeout(resolve, 2500));
222
-
223
- const messageContainer = await page.locator('.msg-s-message-list.scrollable').first();
224
- const containerExists = await messageContainer.count();
225
-
226
- if (containerExists > 0) {
227
- let reachedOldMessages = false;
228
- let scrollAttempts = 0;
229
- const maxScrollAttempts = 20;
230
-
231
- await messageContainer.hover();
232
- await new Promise(resolve => setTimeout(resolve, 500));
233
-
234
- let noNewMessagesRetries = 0;
235
-
236
- while (!reachedOldMessages && scrollAttempts < maxScrollAttempts) {
237
- const beforeScrollCount = allMessageEntities.length;
238
- console.log(`[GetNewMessages] Scroll cycle ${scrollAttempts + 1}, current messages: ${beforeScrollCount}`);
239
-
240
-
241
- for (let i = 0; i < 5; i++) {
242
- await messageContainer.evaluate((el) => {
243
- el.scrollBy({ top: -2000, behavior: 'smooth' });
244
- });
245
- console.log(`[GetNewMessages] Scrolled up -2000px (${i + 1}/5)`);
246
- await new Promise(resolve => setTimeout(resolve, 1000));
247
- }
248
-
249
- console.log(`[GetNewMessages] Waiting for LinkedIn to load older messages...`);
250
- await new Promise(resolve => setTimeout(resolve, 3000));
251
-
252
- const afterScrollCount = allMessageEntities.length;
253
- const newMessagesLoaded = afterScrollCount - beforeScrollCount;
254
- console.log(`[GetNewMessages] After 5 scrolls: ${afterScrollCount} messages (${newMessagesLoaded} new)`);
255
-
256
- if (newMessagesLoaded === 0) {
257
- noNewMessagesRetries++;
258
- console.log(`[GetNewMessages] No new messages loaded, retry ${noNewMessagesRetries}/2`);
259
-
260
- if (noNewMessagesRetries >= 2) {
261
- console.log('[GetNewMessages] No new messages after 2 retries, reached the top');
262
- break;
263
- }
264
- } else {
265
- noNewMessagesRetries = 0;
266
- console.log(`[GetNewMessages] Loaded ${newMessagesLoaded} new messages, continuing...`);
267
- }
268
-
269
- const oldestMessage = allMessageEntities
270
- .filter(msg => msg.deliveredAt)
271
- .sort((a, b) => a.deliveredAt - b.deliveredAt)[0];
272
-
273
- if (oldestMessage && oldestMessage.deliveredAt < lastCheckedTimestamp) {
274
- reachedOldMessages = true;
275
- console.log('[GetNewMessages] Reached messages older than lastChecked in this conversation');
276
- }
277
-
278
- scrollAttempts++;
279
- }
280
-
281
- console.log(`[GetNewMessages] Finished scrolling after ${scrollAttempts} attempts`);
282
- }
283
-
284
- const messageCountAfter = allMessageEntities.length;
285
- const newMessages = messageCountAfter - messageCountBefore;
286
- console.log(`[GetNewMessages] Total messages from conversation ${i + 1}: ${newMessages}`);
287
-
288
- if (newMessages > 0) {
289
- const conversationMessages = allMessageEntities.slice(messageCountBefore);
290
- const latestInThisConversation = conversationMessages
291
- .filter(msg => msg.deliveredAt)
292
- .sort((a, b) => b.deliveredAt - a.deliveredAt)[0];
293
-
294
- if (latestInThisConversation && latestInThisConversation.deliveredAt < lastCheckedTimestamp) {
295
- console.log('[GetNewMessages] Latest message in conversation is older than lastChecked, stopping conversation loop');
296
- shouldContinue = false;
297
- }
298
- }
558
+ shouldContinue = await processConversation(page, item, i, allMessageEntities, lastCheckedTimestamp);
299
559
  } catch (error) {
300
560
  console.error('[GetNewMessages] Error with conversation:', error.message);
301
561
  }
@@ -304,133 +564,18 @@ export async function executeGetNewMessages(page, action, accessToken) {
304
564
  console.log('[GetNewMessages] Finished processing all conversations, waiting before cleanup...');
305
565
  await new Promise(resolve => setTimeout(resolve, 2000));
306
566
 
307
- console.log('[GetNewMessages] Unrouting message handlers...');
308
- try {
309
- await page.unroute('**/voyagerMessagingGraphQL/**', routeHandler);
310
- console.log('[GetNewMessages] Successfully unrouted message handlers');
311
- } catch (error) {
312
- console.log('[GetNewMessages] Error unrouting (non-critical):', error.message);
313
- }
567
+ await cleanupRouteHandlers(page, routeHandler);
314
568
 
315
569
  console.log('[GetNewMessages] Total messages collected:', allMessageEntities.length);
316
570
  console.log('[GetNewMessages] Conversations with participants mapped:', conversationParticipantsMap.size);
317
571
 
318
- const newMessages = allMessageEntities.filter(msg => msg.deliveredAt > lastCheckedTimestamp);
319
- newMessages.sort((a, b) => a.deliveredAt - b.deliveredAt);
320
- console.log('[GetNewMessages] New messages count:', newMessages.length);
321
-
322
- const newMessagesByConversation = {};
323
- newMessages.forEach(msg => {
324
- if (!newMessagesByConversation[msg.conversationId]) {
325
- newMessagesByConversation[msg.conversationId] = [];
326
- }
327
- newMessagesByConversation[msg.conversationId].push(msg);
328
- });
329
-
330
- console.log('[GetNewMessages] Conversations with new messages:', Object.keys(newMessagesByConversation).length);
331
-
332
- const encodedUrlMap = new Map();
333
- for (const conversationId in newMessagesByConversation) {
334
- const encodedUrl = conversationParticipantsMap.get(conversationId);
335
- if (encodedUrl) {
336
- encodedUrlMap.set(encodedUrl, null);
337
- } else {
338
- console.log(`[GetNewMessages] No participant found for conversation:`, conversationId);
339
- }
340
- }
341
- console.log('[GetNewMessages] Unique profiles to decode:', encodedUrlMap.size);
572
+ const { newMessages, newMessagesByConversation } = filterAndOrganizeNewMessages(allMessageEntities, lastCheckedTimestamp);
342
573
 
343
- const decodedUrlMap = new Map();
344
- const maxDecodeAttempts = Math.min(encodedUrlMap.size, 50);
345
- let decodeCount = 0;
574
+ const encodedUrlMap = buildEncodedUrlMap(newMessagesByConversation, conversationParticipantsMap);
575
+ const decodedUrlMap = await decodeProfileUrls(page, encodedUrlMap);
346
576
 
347
- for (const encodedUrl of encodedUrlMap.keys()) {
348
- if (decodeCount >= maxDecodeAttempts) {
349
- console.log(`[GetNewMessages] Reached max decode attempts (${maxDecodeAttempts}), skipping remaining profiles`);
350
- break;
351
- }
352
-
353
- try {
354
- console.log(`[GetNewMessages] Decoding profile ${decodeCount + 1}/${encodedUrlMap.size}: ${encodedUrl}`);
355
-
356
- const decodePromise = safeGoto(page, `https://www.linkedin.com/in/${encodedUrl}`, {
357
- waitUntil: 'domcontentloaded',
358
- timeout: 30000
359
- });
360
-
361
- const timeoutPromise = new Promise((_, reject) =>
362
- setTimeout(() => reject(new Error('Decode timeout')), 35000)
363
- );
364
-
365
- await Promise.race([decodePromise, timeoutPromise]);
366
- await page.waitForLoadState('domcontentloaded', { timeout: 10000 });
367
- await new Promise(resolve => setTimeout(resolve, 1000));
368
-
369
- const currentUrl = page.url();
370
- const match = currentUrl.match(/linkedin\.com\/in\/([^\/\?]+)/);
371
- if (match && match[1]) {
372
- decodedUrlMap.set(encodedUrl, match[1]);
373
- console.log(`[GetNewMessages] Successfully decoded: ${encodedUrl} -> ${match[1]}`);
374
- } else {
375
- console.log(`[GetNewMessages] Could not extract URL from: ${currentUrl}`);
376
- }
377
- decodeCount++;
378
- } catch (error) {
379
- console.error(`[GetNewMessages] Failed to decode URL ${encodedUrl}:`, error.message);
380
- decodeCount++;
381
- continue;
382
- }
383
- }
384
-
385
- let sentCount = 0;
386
- let failedCount = 0;
387
- let totalToSend = 0;
388
-
389
- for (const conversationId in newMessagesByConversation) {
390
- const messages = newMessagesByConversation[conversationId];
391
- const encodedUrl = conversationParticipantsMap.get(conversationId);
392
- const linkedinUrl = encodedUrl ? decodedUrlMap.get(encodedUrl) : null;
393
-
394
- if (!linkedinUrl) {
395
- console.log(`[GetNewMessages] Skipping conversation ${conversationId} - could not find or decode other person's URL`);
396
- continue;
397
- }
398
-
399
- console.log(`[GetNewMessages] Sending ${messages.length} messages to ${linkedinUrl}`);
400
- totalToSend += messages.length;
401
-
402
- for (const msg of messages) {
403
- try {
404
- const payload = {
405
- linkedin_url: linkedinUrl,
406
- message: msg.messageText,
407
- is_receipent: !msg.isSelf,
408
- timestamp: msg.deliveredAt
409
- };
410
-
411
- console.log(`[GetNewMessages] Posting message: "${msg.messageText.substring(0, 50)}..." (${msg.deliveredAt})`);
412
-
413
- await axios.post('https://api.coffeeinabit.com/messages', payload, {
414
- headers: {
415
- 'Authorization': `Bearer ${accessToken}`,
416
- 'Content-Type': 'application/json'
417
- }
418
- });
419
-
420
- sentCount++;
421
- } catch (error) {
422
- failedCount++;
423
- console.error(`[GetNewMessages] Failed to send message to ${linkedinUrl}: "${msg.messageText.substring(0, 50)}..."`);
424
- console.error(`[GetNewMessages] Error:`, error.message);
425
- if (error.response) {
426
- console.error(`[GetNewMessages] Response status:`, error.response.status);
427
- console.error(`[GetNewMessages] Response data:`, error.response.data);
428
- }
429
- }
430
- }
431
- }
432
-
433
- console.log(`[GetNewMessages] Successfully sent ${sentCount}/${totalToSend} messages${failedCount > 0 ? ` (${failedCount} failed)` : ''}`);
577
+ // ⚠️ SENDING FUNCTIONALITY: Send all collected messages to API
578
+ await sendMessagesToApi(newMessagesByConversation, conversationParticipantsMap, decodedUrlMap, accessToken);
434
579
 
435
580
  return {
436
581
  newMessagesCount: newMessages.length,