coffeeinabit 0.0.59 → 0.0.63

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.
@@ -47,6 +47,8 @@ export class LinkedInAutomation {
47
47
  this.ambientMouseTimeout = null;
48
48
  this.cloudAuth = new CloudAuth();
49
49
  this._currentRefreshToken = null;
50
+ this._lastPageActivityAt = 0;
51
+ this._lastScreenshotUploadAt = 0;
50
52
  this._currentAccessToken = null; // Track access token explicitly
51
53
 
52
54
  // Action execution timeout (5 minutes)
@@ -60,6 +62,13 @@ export class LinkedInAutomation {
60
62
 
61
63
  // Token re-sync callback (set externally if available)
62
64
  this.tokenResyncCallback = null;
65
+
66
+ // Token sync-back callback (to persist refreshed tokens to session store)
67
+ this.tokenSyncBackCallback = null;
68
+
69
+ // Proactive token refresh timer
70
+ this._tokenRefreshInterval = null;
71
+ this._tokenRefreshIntervalMs = 45 * 60 * 1000; // 45 minutes
63
72
 
64
73
  // Lifecycle phases
65
74
  this.PHASE_IDLE = 'idle';
@@ -363,6 +372,7 @@ export class LinkedInAutomation {
363
372
  this.setupPageListeners();
364
373
  this.startAmbientMouseMovements();
365
374
  this.startScreenshotCapture();
375
+ this.startTokenRefreshTimer();
366
376
 
367
377
  logger.log('[LinkedInAutomation] Automation fully started and running');
368
378
 
@@ -638,6 +648,7 @@ export class LinkedInAutomation {
638
648
  this.stopScreenshotCapture();
639
649
  this.stopActionPolling();
640
650
  this.stopAmbientMouseMovements();
651
+ this.stopTokenRefreshTimer();
641
652
 
642
653
  await this.closeBrowser();
643
654
 
@@ -697,6 +708,19 @@ export class LinkedInAutomation {
697
708
  }
698
709
  }
699
710
 
711
+ _trackPageActivity() {
712
+ if (!this.page) return;
713
+ const mark = () => { this._lastPageActivityAt = Date.now(); };
714
+ for (const method of ['move', 'click', 'down', 'up']) {
715
+ const orig = this.page.mouse[method].bind(this.page.mouse);
716
+ this.page.mouse[method] = (...args) => { mark(); return orig(...args); };
717
+ }
718
+ for (const method of ['type', 'press', 'down', 'up', 'insertText']) {
719
+ const orig = this.page.keyboard[method].bind(this.page.keyboard);
720
+ this.page.keyboard[method] = (...args) => { mark(); return orig(...args); };
721
+ }
722
+ }
723
+
700
724
  async captureScreenshot() {
701
725
  if (!this.ensureBrowserAvailable('captureScreenshot')) {
702
726
  return;
@@ -719,8 +743,11 @@ export class LinkedInAutomation {
719
743
  timestamp: new Date().toISOString()
720
744
  });
721
745
 
722
- // Fire-and-forget upload to S3
723
- this.uploadScreenshotToS3(base64Screenshot, this.currentAction?.action_id);
746
+ // Only upload to S3 when an action is in progress and there was page activity since last upload
747
+ if (this.currentAction && this._lastPageActivityAt > this._lastScreenshotUploadAt) {
748
+ this._lastScreenshotUploadAt = Date.now();
749
+ this.uploadScreenshotToS3(base64Screenshot, this.currentAction.action_id);
750
+ }
724
751
 
725
752
  } catch (error) {
726
753
  logger.error('[LinkedInAutomation] Failed to capture screenshot:', error.message);
@@ -730,12 +757,15 @@ export class LinkedInAutomation {
730
757
  }
731
758
  }
732
759
 
733
- async uploadScreenshotToS3(base64Screenshot, actionId) {
760
+ async uploadScreenshotToS3(base64Screenshot, actionId, retryCount = 0) {
734
761
  try {
735
762
  const idToken = await this.getAccessToken();
736
- if (!idToken) return;
763
+ if (!idToken) {
764
+ logger.warn('[LinkedInAutomation] Screenshot upload skipped - no valid token');
765
+ return;
766
+ }
737
767
 
738
- fetch('https://api.coffeeinabit.com/app/screenshots', {
768
+ const response = await fetch('https://api.coffeeinabit.com/app/screenshots', {
739
769
  method: 'POST',
740
770
  headers: {
741
771
  'Content-Type': 'application/json',
@@ -746,11 +776,21 @@ export class LinkedInAutomation {
746
776
  action_id: actionId || null,
747
777
  timestamp: Date.now()
748
778
  })
749
- }).catch(err => {
750
- logger.error('[LinkedInAutomation] Screenshot upload failed:', err.message);
751
779
  });
780
+
781
+ if (response.status === 401 && retryCount < 2) {
782
+ logger.warn('[LinkedInAutomation] Screenshot upload got 401, refreshing token and retrying...');
783
+ this._currentAccessToken = null;
784
+ return this.uploadScreenshotToS3(base64Screenshot, actionId, retryCount + 1);
785
+ }
752
786
  } catch (error) {
753
- logger.error('[LinkedInAutomation] Screenshot upload error:', error.message);
787
+ if (retryCount < 2) {
788
+ const delay = 2000 * (retryCount + 1);
789
+ logger.warn(`[LinkedInAutomation] Screenshot upload failed (retry ${retryCount + 1}/2 in ${delay / 1000}s):`, error.message);
790
+ await new Promise(resolve => setTimeout(resolve, delay));
791
+ return this.uploadScreenshotToS3(base64Screenshot, actionId, retryCount + 1);
792
+ }
793
+ logger.error('[LinkedInAutomation] Screenshot upload failed after retries:', error.message);
754
794
  }
755
795
  }
756
796
 
@@ -940,6 +980,7 @@ export class LinkedInAutomation {
940
980
  if (refreshResult.tokens.refreshToken) {
941
981
  this._currentRefreshToken = refreshResult.tokens.refreshToken;
942
982
  }
983
+ this.syncTokensBack();
943
984
  logger.log('[LinkedInAutomation] Token re-sync and refresh succeeded after 401, resetting failure counter');
944
985
  this.consecutiveAuthFailures = 0;
945
986
  this.authFailurePaused = false;
@@ -1040,7 +1081,11 @@ export class LinkedInAutomation {
1040
1081
  originalConsoleWarn.apply(console, args);
1041
1082
  };
1042
1083
  await closeAllConversationBubbles(this.page);
1043
-
1084
+
1085
+ // Pause background page operations so they don't compete with the action
1086
+ this.stopScreenshotCapture();
1087
+ this.stopAmbientMouseMovements();
1088
+
1044
1089
  try {
1045
1090
  logger.log('[LinkedInAutomation] Starting execution of action:', action.action, 'ID:', action.action_id, 'Local time:', new Date().toLocaleString());
1046
1091
  logger.log('[LinkedInAutomation] Action parameters:', JSON.stringify(action.parameters, null, 2));
@@ -1150,24 +1195,31 @@ export class LinkedInAutomation {
1150
1195
  console.log = originalConsoleLog;
1151
1196
  console.error = originalConsoleError;
1152
1197
  console.warn = originalConsoleWarn;
1153
-
1198
+
1154
1199
  this.currentAction = null;
1155
1200
  this.currentActionStartedAt = null;
1156
1201
  this.emitActionUpdate();
1202
+
1203
+ // Resume background page operations after action completes
1204
+ if (this.isRunning && this.hasActiveBrowser()) {
1205
+ this.startScreenshotCapture();
1206
+ this.startAmbientMouseMovements();
1207
+ }
1157
1208
  }
1158
1209
  }
1159
1210
 
1160
1211
 
1161
- async reportActionResult(actionId, result, action) {
1212
+ async reportActionResult(actionId, result, action, retryCount = 0) {
1213
+ const maxRetries = 3;
1162
1214
  try {
1163
- logger.log(`[LinkedInAutomation] Reporting action result for ID: ${actionId}`);
1164
-
1215
+ logger.log(`[LinkedInAutomation] Reporting action result for ID: ${actionId}${retryCount > 0 ? ` (retry ${retryCount}/${maxRetries})` : ''}`);
1216
+
1165
1217
  const idToken = await this.getAccessToken();
1166
1218
  if (!idToken) {
1167
1219
  logger.error('[LinkedInAutomation] No id_token available for reporting result');
1168
1220
  return;
1169
1221
  }
1170
-
1222
+
1171
1223
  const response = await fetch('https://api.coffeeinabit.com/app/actions', {
1172
1224
  method: 'POST',
1173
1225
  headers: {
@@ -1179,11 +1231,11 @@ export class LinkedInAutomation {
1179
1231
  result: result
1180
1232
  })
1181
1233
  });
1182
-
1234
+
1183
1235
  if (response.ok) {
1184
1236
  const responseData = await response.json();
1185
1237
  logger.log(`[LinkedInAutomation] Successfully reported action ${action.action} result. Response:`, responseData);
1186
-
1238
+
1187
1239
  const { task_id, status } = responseData;
1188
1240
  if (task_id) {
1189
1241
  logger.log('[LinkedInAutomation] Received task_id:', task_id);
@@ -1197,7 +1249,13 @@ export class LinkedInAutomation {
1197
1249
  logger.error('[LinkedInAutomation] Error response:', errorText);
1198
1250
  }
1199
1251
  } catch (error) {
1200
- logger.error('[LinkedInAutomation] Error reporting action result:', error);
1252
+ if (retryCount < maxRetries) {
1253
+ const delay = Math.min(2000 * Math.pow(2, retryCount), 15000);
1254
+ logger.warn(`[LinkedInAutomation] Error reporting action result (retry ${retryCount + 1}/${maxRetries} in ${delay / 1000}s):`, error.message);
1255
+ await new Promise(resolve => setTimeout(resolve, delay));
1256
+ return this.reportActionResult(actionId, result, action, retryCount + 1);
1257
+ }
1258
+ logger.error('[LinkedInAutomation] Error reporting action result after all retries:', error);
1201
1259
  }
1202
1260
  }
1203
1261
 
@@ -1313,6 +1371,58 @@ export class LinkedInAutomation {
1313
1371
  this.tokenResyncCallback = callback;
1314
1372
  }
1315
1373
 
1374
+ /**
1375
+ * Set a callback to persist refreshed tokens back to the session store
1376
+ * @param {Function} callback - Function(idToken, refreshToken) that saves tokens to session
1377
+ */
1378
+ setTokenSyncBackCallback(callback) {
1379
+ this.tokenSyncBackCallback = callback;
1380
+ }
1381
+
1382
+ /**
1383
+ * Persist refreshed tokens back to the session store so future resyncs get fresh tokens
1384
+ */
1385
+ async syncTokensBack() {
1386
+ if (!this.tokenSyncBackCallback) return;
1387
+ try {
1388
+ await this.tokenSyncBackCallback(this._currentAccessToken, this._currentRefreshToken);
1389
+ } catch (error) {
1390
+ logger.error('[LinkedInAutomation] Error syncing tokens back to session:', error.message);
1391
+ }
1392
+ }
1393
+
1394
+ /**
1395
+ * Start a periodic timer that proactively refreshes the JWT token before it expires.
1396
+ * This prevents stale tokens from accumulating during long-running automation.
1397
+ */
1398
+ startTokenRefreshTimer() {
1399
+ this.stopTokenRefreshTimer();
1400
+ logger.log(`[LinkedInAutomation] Starting proactive token refresh timer (every ${this._tokenRefreshIntervalMs / 60000} min)`);
1401
+
1402
+ this._tokenRefreshInterval = setInterval(async () => {
1403
+ if (!this.isRunning) return;
1404
+ try {
1405
+ logger.log('[LinkedInAutomation] Proactive token refresh triggered');
1406
+ const token = await this.getAccessToken();
1407
+ if (token) {
1408
+ logger.log('[LinkedInAutomation] Proactive token refresh succeeded');
1409
+ } else {
1410
+ logger.error('[LinkedInAutomation] Proactive token refresh failed - no valid token');
1411
+ }
1412
+ } catch (error) {
1413
+ logger.error('[LinkedInAutomation] Proactive token refresh error:', error.message);
1414
+ }
1415
+ }, this._tokenRefreshIntervalMs);
1416
+ }
1417
+
1418
+ stopTokenRefreshTimer() {
1419
+ if (this._tokenRefreshInterval) {
1420
+ clearInterval(this._tokenRefreshInterval);
1421
+ this._tokenRefreshInterval = null;
1422
+ logger.log('[LinkedInAutomation] Token refresh timer stopped');
1423
+ }
1424
+ }
1425
+
1316
1426
  /**
1317
1427
  * Attempt to re-sync tokens from external source (session, etc.)
1318
1428
  * @returns {Promise<boolean>} true if tokens were successfully re-synced
@@ -1338,12 +1448,13 @@ export class LinkedInAutomation {
1338
1448
  if (result.tokens.refreshToken) {
1339
1449
  this._currentRefreshToken = result.tokens.refreshToken;
1340
1450
  }
1451
+ this.syncTokensBack();
1341
1452
  } else {
1342
1453
  logger.error('[LinkedInAutomation] Failed to refresh re-synced tokens:', result.error);
1343
1454
  return false;
1344
1455
  }
1345
1456
  }
1346
-
1457
+
1347
1458
  return true;
1348
1459
  }
1349
1460
  } catch (error) {
@@ -1390,37 +1501,41 @@ export class LinkedInAutomation {
1390
1501
  if (result.tokens.refreshToken) {
1391
1502
  this._currentRefreshToken = result.tokens.refreshToken;
1392
1503
  }
1504
+ // Persist refreshed tokens back to session store
1505
+ this.syncTokensBack();
1393
1506
  // Reset failure counter on successful refresh
1394
1507
  this.consecutiveAuthFailures = 0;
1395
1508
  this.authFailurePaused = false;
1396
1509
  return this._currentAccessToken;
1397
1510
  } else {
1398
1511
  logger.error('[LinkedInAutomation] Token refresh failed:', result.error);
1399
-
1512
+
1400
1513
  // Try one more time to re-sync before giving up
1401
1514
  logger.log('[LinkedInAutomation] Attempting token re-sync after refresh failure...');
1402
1515
  const resynced = await this.attemptTokenResync();
1403
1516
  if (resynced && !this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
1404
1517
  logger.log('[LinkedInAutomation] Token re-synced successfully after refresh failure');
1518
+ this.syncTokensBack();
1405
1519
  this.consecutiveAuthFailures = 0;
1406
1520
  this.authFailurePaused = false;
1407
1521
  return this._currentAccessToken;
1408
1522
  }
1409
-
1523
+
1410
1524
  return null;
1411
1525
  }
1412
1526
  } catch (error) {
1413
1527
  logger.error('[LinkedInAutomation] Error during token refresh:', error.message);
1414
-
1528
+
1415
1529
  // Try re-sync one more time
1416
1530
  const resynced = await this.attemptTokenResync();
1417
1531
  if (resynced && !this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
1418
1532
  logger.log('[LinkedInAutomation] Token re-synced successfully after refresh error');
1533
+ this.syncTokensBack();
1419
1534
  this.consecutiveAuthFailures = 0;
1420
1535
  this.authFailurePaused = false;
1421
1536
  return this._currentAccessToken;
1422
1537
  }
1423
-
1538
+
1424
1539
  return null;
1425
1540
  }
1426
1541
  }
@@ -1574,7 +1689,8 @@ export class LinkedInAutomation {
1574
1689
  }
1575
1690
 
1576
1691
  resetHumanMouseState();
1577
-
1692
+ this._trackPageActivity();
1693
+
1578
1694
  // Transition to BROWSER_READY phase
1579
1695
  this.transitionToPhase(this.PHASE_BROWSER_READY);
1580
1696
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coffeeinabit",
3
- "version": "0.0.59",
3
+ "version": "0.0.63",
4
4
  "description": "coffeeinabit app",
5
5
  "main": "server.js",
6
6
  "type": "module",
package/server.js CHANGED
@@ -95,6 +95,37 @@ const sessionStore = new FileStoreSession({
95
95
  retries: 0
96
96
  });
97
97
 
98
+ // Set up token sync-back callback: when automation refreshes tokens, persist them to the session store
99
+ linkedinAutomation.setTokenSyncBackCallback(async (idToken, refreshToken) => {
100
+ if (!linkedinAutomation._currentUserEmail || !idToken) return;
101
+
102
+ if (typeof sessionStore.all !== 'function') return;
103
+
104
+ return new Promise((resolve) => {
105
+ sessionStore.all((err, sessions) => {
106
+ if (err || !sessions) { resolve(); return; }
107
+
108
+ for (const sessionId in sessions) {
109
+ const sess = sessions[sessionId];
110
+ if (sess?.user?.email === linkedinAutomation._currentUserEmail && sess.tokens) {
111
+ sess.tokens.idToken = idToken;
112
+ if (refreshToken) sess.tokens.refreshToken = refreshToken;
113
+ sessionStore.set(sessionId, sess, (setErr) => {
114
+ if (setErr) {
115
+ logger.error('[Server] Failed to sync tokens back to session store:', setErr.message);
116
+ } else {
117
+ logger.log('[Server] Tokens synced back to session store for user:', sess.user.email);
118
+ }
119
+ resolve();
120
+ });
121
+ return;
122
+ }
123
+ }
124
+ resolve();
125
+ });
126
+ });
127
+ });
128
+
98
129
  // Configure Express middleware
99
130
  app.use(express.json());
100
131
  app.use(express.static(path.join(__dirname, 'public')));
@@ -1,7 +1,125 @@
1
1
  {
2
- "threads": [],
3
- "thread_count": 0,
4
- "total_messages": 0,
5
- "status": "no_history",
6
- "message": "No previous messages found with natalie-grigorchuk-48277811. Conversation overlay shows \"New message\"."
2
+ "threads": [
3
+ {
4
+ "thread_id": "urn:li:messagingThread:2-ZGU3NTUyMzMtMTJkNy00NDIxLTk4MDgtYmYxNGIwOTQ5YjFmXzEwMA==",
5
+ "participants": [
6
+ {
7
+ "name": "Alena Borikova",
8
+ "uniq_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
9
+ "is_self": true
10
+ },
11
+ {
12
+ "name": "Kate Yanchenko",
13
+ "uniq_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
14
+ "is_self": false
15
+ }
16
+ ],
17
+ "last_activity_at": 1770139934759,
18
+ "last_activity_at_iso": "2026-02-03T17:32:14.759Z",
19
+ "messages": [
20
+ {
21
+ "author": "Kate Yanchenko",
22
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
23
+ "is_self": false,
24
+ "text": "Hello!",
25
+ "timestamp": 1765865708197,
26
+ "timestamp_iso": "2025-12-16T06:15:08.197Z",
27
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
28
+ },
29
+ {
30
+ "author": "Alena Borikova",
31
+ "author_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
32
+ "is_self": true,
33
+ "text": "Hi hi ",
34
+ "timestamp": 1765867625215,
35
+ "timestamp_iso": "2025-12-16T06:47:05.215Z",
36
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
37
+ },
38
+ {
39
+ "author": "Kate Yanchenko",
40
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
41
+ "is_self": false,
42
+ "text": "Hello!",
43
+ "timestamp": 1765872088845,
44
+ "timestamp_iso": "2025-12-16T08:01:28.845Z",
45
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
46
+ },
47
+ {
48
+ "author": "Kate Yanchenko",
49
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
50
+ "is_self": false,
51
+ "text": "just testing",
52
+ "timestamp": 1765873430497,
53
+ "timestamp_iso": "2025-12-16T08:23:50.497Z",
54
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
55
+ },
56
+ {
57
+ "author": "Kate Yanchenko",
58
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
59
+ "is_self": false,
60
+ "text": "Hey, how are you?",
61
+ "timestamp": 1770138675922,
62
+ "timestamp_iso": "2026-02-03T17:11:15.922Z",
63
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
64
+ },
65
+ {
66
+ "author": "Alena Borikova",
67
+ "author_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
68
+ "is_self": true,
69
+ "text": "Fine, you? ❤️",
70
+ "timestamp": 1770138705220,
71
+ "timestamp_iso": "2026-02-03T17:11:45.220Z",
72
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
73
+ },
74
+ {
75
+ "author": "Kate Yanchenko",
76
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
77
+ "is_self": false,
78
+ "text": "Hey, how are you?",
79
+ "timestamp": 1770139276918,
80
+ "timestamp_iso": "2026-02-03T17:21:16.918Z",
81
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
82
+ },
83
+ {
84
+ "author": "Alena Borikova",
85
+ "author_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
86
+ "is_self": true,
87
+ "text": "I'm good",
88
+ "timestamp": 1770139300739,
89
+ "timestamp_iso": "2026-02-03T17:21:40.739Z",
90
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
91
+ },
92
+ {
93
+ "author": "Alena Borikova",
94
+ "author_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
95
+ "is_self": true,
96
+ "text": "Thanks for asking 😁❤️",
97
+ "timestamp": 1770139311452,
98
+ "timestamp_iso": "2026-02-03T17:21:51.452Z",
99
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
100
+ },
101
+ {
102
+ "author": "Kate Yanchenko",
103
+ "author_id": "ACoAABIyz2sB-O7ZtfsS5d8myngQBtR9JEKz_sY",
104
+ "is_self": false,
105
+ "text": "Hey, how are you?",
106
+ "timestamp": 1770139531176,
107
+ "timestamp_iso": "2026-02-03T17:25:31.176Z",
108
+ "network_intercepted_at": "2026-02-21T02:53:50.372Z"
109
+ },
110
+ {
111
+ "author": "Alena Borikova",
112
+ "author_id": "ACoAAB3lzOkB3_fTfG3OZRQMLMVLvIiQgowXmPk",
113
+ "is_self": true,
114
+ "text": "🐥❤️",
115
+ "timestamp": 1770139934759,
116
+ "timestamp_iso": "2026-02-03T17:32:14.759Z",
117
+ "network_intercepted_at": "2026-02-21T02:53:40.617Z"
118
+ }
119
+ ],
120
+ "message_count": 11
121
+ }
122
+ ],
123
+ "thread_count": 1,
124
+ "total_messages": 11
7
125
  }
@@ -148,9 +148,17 @@ function createRouteHandler(threadsMap, processedMessageKeys) {
148
148
  const url = route.request().url();
149
149
 
150
150
  if (url.includes('messengerConversations')) {
151
- const response = await route.fetch();
151
+ let response;
152
152
  try {
153
- const body = await response.text();
153
+ response = await route.fetch();
154
+ } catch (e) {
155
+ console.error('[GetMessageThreads] Fetch failed (messengerConversations):', e.message);
156
+ try { await route.continue(); } catch (_) { /* ignore */ }
157
+ return;
158
+ }
159
+ let body;
160
+ try {
161
+ body = await response.text();
154
162
  const data = JSON.parse(body);
155
163
  const dataKeys = Object.keys(data?.data || {});
156
164
  const prevSize = threadsMap.size;
@@ -159,21 +167,41 @@ function createRouteHandler(threadsMap, processedMessageKeys) {
159
167
  } catch (error) {
160
168
  console.error('[GetMessageThreads] Error parsing conversations:', error.message);
161
169
  }
162
- try { await route.fulfill({ response }); } catch (e) { /* ignore */ }
170
+ try {
171
+ await route.fulfill({
172
+ status: response.status(),
173
+ headers: response.headers(),
174
+ body: body || ''
175
+ });
176
+ } catch (e) { /* ignore */ }
163
177
  return;
164
178
  }
165
179
 
166
180
  if (url.includes('messengerMessages')) {
167
- const response = await route.fetch();
181
+ let response;
168
182
  try {
169
- const body = await response.text();
183
+ response = await route.fetch();
184
+ } catch (e) {
185
+ console.error('[GetMessageThreads] Fetch failed (messengerMessages):', e.message);
186
+ try { await route.continue(); } catch (_) { /* ignore */ }
187
+ return;
188
+ }
189
+ let body;
190
+ try {
191
+ body = await response.text();
170
192
  const data = JSON.parse(body);
171
193
  const added = parseMessages(data, threadsMap, processedMessageKeys);
172
194
  console.log(`[GetMessageThreads] [${new Date().toISOString()}] Parsed ${added} messages`);
173
195
  } catch (error) {
174
196
  console.error('[GetMessageThreads] Error parsing messages:', error.message);
175
197
  }
176
- try { await route.fulfill({ response }); } catch (e) { /* ignore */ }
198
+ try {
199
+ await route.fulfill({
200
+ status: response.status(),
201
+ headers: response.headers(),
202
+ body: body || ''
203
+ });
204
+ } catch (e) { /* ignore */ }
177
205
  return;
178
206
  }
179
207
 
@@ -188,9 +216,17 @@ function createRouteHandler(threadsMap, processedMessageKeys) {
188
216
  */
189
217
  async function clickConversationsToLoadMessages(page, threadsMap, cutoffTimestamp) {
190
218
  const maxThreads = 50;
219
+ const overallTimeout = 10 * 60 * 1000; // 10 minutes max for entire clicking phase
220
+ const perClickTimeout = 30000; // 30s max per conversation click
221
+ const startTime = Date.now();
191
222
  let clicked = 0;
192
223
 
193
224
  while (clicked < maxThreads) {
225
+ if (Date.now() - startTime > overallTimeout) {
226
+ console.log(`[GetMessageThreads] Overall timeout reached (${overallTimeout / 1000}s), stopping`);
227
+ break;
228
+ }
229
+
194
230
  const items = await page.locator('li.msg-conversation-listitem .msg-conversation-listitem__link').all();
195
231
 
196
232
  if (clicked === 0) {
@@ -215,38 +251,43 @@ async function clickConversationsToLoadMessages(page, threadsMap, cutoffTimestam
215
251
  const messagesBefore = countAllMessages(threadsMap);
216
252
 
217
253
  try {
218
- await item.scrollIntoViewIfNeeded();
219
- await waitRandom(200, 400);
220
-
221
- const box = await item.boundingBox();
222
- if (!box) {
223
- console.log(`[GetMessageThreads] No bounding box for conversation ${clicked + 1}, skipping`);
224
- clicked++;
225
- continue;
226
- }
227
- // Click center-right of the item to avoid the checkbox on the left edge
228
- const safeLeft = box.x + box.width * 0.4;
229
- const safeRight = box.x + box.width * 0.9;
230
- const targetX = safeLeft + Math.random() * (safeRight - safeLeft);
231
- const targetY = box.y + box.height * 0.25 + Math.random() * (box.height * 0.5);
232
- await page.mouse.click(targetX, targetY);
233
- await waitRandom(800, 1500);
234
-
235
- await new Promise(resolve => setTimeout(resolve, 2000));
236
-
237
- // Dismiss bulk selection panel if it appeared
238
- try {
239
- const dismissBtn = page.locator('#msg-bulk-actions-panel-presenter-close-button');
240
- if (await dismissBtn.count() > 0) {
241
- console.log(`[GetMessageThreads] Bulk selection panel detected, dismissing...`);
242
- await waitRandom(300, 700);
243
- await humanLikeClick(page, dismissBtn, {
244
- timeout: 5000,
245
- postClickDelay: [200, 500],
246
- fallback: { force: true }
247
- });
248
- }
249
- } catch (e) { /* non-critical */ }
254
+ // Wrap the entire click operation in a timeout to prevent hanging
255
+ await Promise.race([
256
+ (async () => {
257
+ await item.scrollIntoViewIfNeeded();
258
+ await waitRandom(200, 400);
259
+
260
+ const box = await item.boundingBox();
261
+ if (!box) {
262
+ console.log(`[GetMessageThreads] No bounding box for conversation ${clicked + 1}, skipping`);
263
+ return;
264
+ }
265
+ // Click center-right of the item to avoid the checkbox on the left edge
266
+ const safeLeft = box.x + box.width * 0.4;
267
+ const safeRight = box.x + box.width * 0.9;
268
+ const targetX = safeLeft + Math.random() * (safeRight - safeLeft);
269
+ const targetY = box.y + box.height * 0.25 + Math.random() * (box.height * 0.5);
270
+ await page.mouse.click(targetX, targetY);
271
+ await waitRandom(800, 1500);
272
+
273
+ await new Promise(resolve => setTimeout(resolve, 2000));
274
+
275
+ // Dismiss bulk selection panel if it appeared
276
+ try {
277
+ const dismissBtn = page.locator('#msg-bulk-actions-panel-presenter-close-button');
278
+ if (await dismissBtn.count() > 0) {
279
+ console.log(`[GetMessageThreads] Bulk selection panel detected, dismissing...`);
280
+ await waitRandom(300, 700);
281
+ await humanLikeClick(page, dismissBtn, {
282
+ timeout: 5000,
283
+ postClickDelay: [200, 500],
284
+ fallback: { force: true }
285
+ });
286
+ }
287
+ } catch (e) { /* non-critical */ }
288
+ })(),
289
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Click operation timed out')), perClickTimeout))
290
+ ]);
250
291
 
251
292
  const messagesAfter = countAllMessages(threadsMap);
252
293
  const newMessages = messagesAfter - messagesBefore;
@@ -268,7 +309,7 @@ async function clickConversationsToLoadMessages(page, threadsMap, cutoffTimestam
268
309
  }
269
310
  }
270
311
 
271
- console.log(`[GetMessageThreads] Clicked ${clicked} conversations total`);
312
+ console.log(`[GetMessageThreads] Clicked ${clicked} conversations total (elapsed: ${Math.round((Date.now() - startTime) / 1000)}s)`);
272
313
  }
273
314
 
274
315
  function countAllMessages(threadsMap) {
@@ -295,10 +336,17 @@ function getNewestMessageTimestamp(threadsMap, messagesBefore) {
295
336
  * Filter threads to only include messages from the last 24 hours
296
337
  * and build the final result structure
297
338
  */
298
- export function buildResult(threadsMap, cutoffTimestamp = 0) {
339
+ export function buildResult(threadsMap, cutoffTimestamp = 0, targetUniqIds = null) {
299
340
  const result = [];
341
+ const targetSet = targetUniqIds && targetUniqIds.length > 0 ? new Set(targetUniqIds) : null;
300
342
 
301
343
  for (const [, thread] of threadsMap) {
344
+ // Filter to only threads with target participants
345
+ if (targetSet) {
346
+ const hasTarget = thread.participants.some(p => !p.isSelf && targetSet.has(p.uniqId));
347
+ if (!hasTarget) continue;
348
+ }
349
+
302
350
  const recentMessages = thread.messages
303
351
  .filter(m => m.deliveredAt >= cutoffTimestamp)
304
352
  .sort((a, b) => a.deliveredAt - b.deliveredAt);
@@ -352,7 +400,9 @@ export async function executeGetMessageThreads(page, action) {
352
400
  } else {
353
401
  cutoffTimestamp = Date.now() - (14 * 24 * 60 * 60 * 1000);
354
402
  }
403
+ const targetUniqIds = action.parameters?.target_uniq_ids || null;
355
404
  console.log(`[GetMessageThreads] Cutoff timestamp: ${cutoffTimestamp} (${new Date(cutoffTimestamp).toLocaleString()})`);
405
+ console.log(`[GetMessageThreads] Target uniq_ids: ${targetUniqIds ? targetUniqIds.length + ' users' : 'all (no filter)'}`);
356
406
 
357
407
  const threadsMap = new Map();
358
408
  const processedMessageKeys = new Set();
@@ -397,8 +447,8 @@ export async function executeGetMessageThreads(page, action) {
397
447
  // Cleanup route handler
398
448
  await cleanupRoute(page, routeHandler);
399
449
 
400
- // Build result
401
- const threads = buildResult(threadsMap, cutoffTimestamp);
450
+ // Build result (filtered to target users if provided)
451
+ const threads = buildResult(threadsMap, cutoffTimestamp, targetUniqIds);
402
452
 
403
453
  let totalMessages = 0;
404
454
  for (const t of threads) totalMessages += t.message_count;
@@ -91,7 +91,13 @@ export async function insertMessageText(messageInput, message, page) {
91
91
  export async function sendMessageInConversation(page, message, dryRun = false) {
92
92
  try {
93
93
  console.log('[send_linkedin_message] Finding message input...');
94
- const messageInput = await page.waitForSelector('div[role="textbox"]', { timeout: 3000 });
94
+ let messageInput;
95
+ try {
96
+ messageInput = await page.waitForSelector('div[role="textbox"]', { timeout: 10000 });
97
+ } catch {
98
+ // Fallback: try the aria-label selector
99
+ messageInput = await page.waitForSelector('[aria-label="Write a message…"]', { timeout: 5000 }).catch(() => null);
100
+ }
95
101
  if (!messageInput) {
96
102
  throw new Error('Message input not found');
97
103
  }