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.
- package/linkedin_automation.js +139 -23
- package/package.json +1 -1
- package/server.js +31 -0
- package/test/test_output_get_messages.json +123 -5
- package/tools/get_message_threads.js +92 -42
- package/tools/send_linkedin_message.js +7 -1
package/linkedin_automation.js
CHANGED
|
@@ -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
|
-
//
|
|
723
|
-
this.
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
151
|
+
let response;
|
|
152
152
|
try {
|
|
153
|
-
|
|
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 {
|
|
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
|
-
|
|
181
|
+
let response;
|
|
168
182
|
try {
|
|
169
|
-
|
|
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 {
|
|
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
|
-
|
|
219
|
-
await
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
}
|