coffeeinabit 0.0.59 → 0.0.61
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 +90 -11
- package/package.json +1 -1
- package/server.js +31 -0
- package/test/test_output_get_messages.json +123 -5
package/linkedin_automation.js
CHANGED
|
@@ -60,6 +60,13 @@ export class LinkedInAutomation {
|
|
|
60
60
|
|
|
61
61
|
// Token re-sync callback (set externally if available)
|
|
62
62
|
this.tokenResyncCallback = null;
|
|
63
|
+
|
|
64
|
+
// Token sync-back callback (to persist refreshed tokens to session store)
|
|
65
|
+
this.tokenSyncBackCallback = null;
|
|
66
|
+
|
|
67
|
+
// Proactive token refresh timer
|
|
68
|
+
this._tokenRefreshInterval = null;
|
|
69
|
+
this._tokenRefreshIntervalMs = 45 * 60 * 1000; // 45 minutes
|
|
63
70
|
|
|
64
71
|
// Lifecycle phases
|
|
65
72
|
this.PHASE_IDLE = 'idle';
|
|
@@ -363,6 +370,7 @@ export class LinkedInAutomation {
|
|
|
363
370
|
this.setupPageListeners();
|
|
364
371
|
this.startAmbientMouseMovements();
|
|
365
372
|
this.startScreenshotCapture();
|
|
373
|
+
this.startTokenRefreshTimer();
|
|
366
374
|
|
|
367
375
|
logger.log('[LinkedInAutomation] Automation fully started and running');
|
|
368
376
|
|
|
@@ -638,6 +646,7 @@ export class LinkedInAutomation {
|
|
|
638
646
|
this.stopScreenshotCapture();
|
|
639
647
|
this.stopActionPolling();
|
|
640
648
|
this.stopAmbientMouseMovements();
|
|
649
|
+
this.stopTokenRefreshTimer();
|
|
641
650
|
|
|
642
651
|
await this.closeBrowser();
|
|
643
652
|
|
|
@@ -730,12 +739,15 @@ export class LinkedInAutomation {
|
|
|
730
739
|
}
|
|
731
740
|
}
|
|
732
741
|
|
|
733
|
-
async uploadScreenshotToS3(base64Screenshot, actionId) {
|
|
742
|
+
async uploadScreenshotToS3(base64Screenshot, actionId, retryCount = 0) {
|
|
734
743
|
try {
|
|
735
744
|
const idToken = await this.getAccessToken();
|
|
736
|
-
if (!idToken)
|
|
745
|
+
if (!idToken) {
|
|
746
|
+
logger.warn('[LinkedInAutomation] Screenshot upload skipped - no valid token');
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
737
749
|
|
|
738
|
-
fetch('https://api.coffeeinabit.com/app/screenshots', {
|
|
750
|
+
const response = await fetch('https://api.coffeeinabit.com/app/screenshots', {
|
|
739
751
|
method: 'POST',
|
|
740
752
|
headers: {
|
|
741
753
|
'Content-Type': 'application/json',
|
|
@@ -746,11 +758,20 @@ export class LinkedInAutomation {
|
|
|
746
758
|
action_id: actionId || null,
|
|
747
759
|
timestamp: Date.now()
|
|
748
760
|
})
|
|
749
|
-
}).catch(err => {
|
|
750
|
-
logger.error('[LinkedInAutomation] Screenshot upload failed:', err.message);
|
|
751
761
|
});
|
|
762
|
+
|
|
763
|
+
if (response.status === 401 && retryCount < 1) {
|
|
764
|
+
logger.warn('[LinkedInAutomation] Screenshot upload got 401, refreshing token and retrying...');
|
|
765
|
+
this._currentAccessToken = null;
|
|
766
|
+
return this.uploadScreenshotToS3(base64Screenshot, actionId, retryCount + 1);
|
|
767
|
+
}
|
|
752
768
|
} catch (error) {
|
|
753
|
-
|
|
769
|
+
if (retryCount < 1) {
|
|
770
|
+
logger.warn('[LinkedInAutomation] Screenshot upload failed, retrying in 2s:', error.message);
|
|
771
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
772
|
+
return this.uploadScreenshotToS3(base64Screenshot, actionId, retryCount + 1);
|
|
773
|
+
}
|
|
774
|
+
logger.error('[LinkedInAutomation] Screenshot upload failed after retry:', error.message);
|
|
754
775
|
}
|
|
755
776
|
}
|
|
756
777
|
|
|
@@ -940,6 +961,7 @@ export class LinkedInAutomation {
|
|
|
940
961
|
if (refreshResult.tokens.refreshToken) {
|
|
941
962
|
this._currentRefreshToken = refreshResult.tokens.refreshToken;
|
|
942
963
|
}
|
|
964
|
+
this.syncTokensBack();
|
|
943
965
|
logger.log('[LinkedInAutomation] Token re-sync and refresh succeeded after 401, resetting failure counter');
|
|
944
966
|
this.consecutiveAuthFailures = 0;
|
|
945
967
|
this.authFailurePaused = false;
|
|
@@ -1313,6 +1335,58 @@ export class LinkedInAutomation {
|
|
|
1313
1335
|
this.tokenResyncCallback = callback;
|
|
1314
1336
|
}
|
|
1315
1337
|
|
|
1338
|
+
/**
|
|
1339
|
+
* Set a callback to persist refreshed tokens back to the session store
|
|
1340
|
+
* @param {Function} callback - Function(idToken, refreshToken) that saves tokens to session
|
|
1341
|
+
*/
|
|
1342
|
+
setTokenSyncBackCallback(callback) {
|
|
1343
|
+
this.tokenSyncBackCallback = callback;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* Persist refreshed tokens back to the session store so future resyncs get fresh tokens
|
|
1348
|
+
*/
|
|
1349
|
+
async syncTokensBack() {
|
|
1350
|
+
if (!this.tokenSyncBackCallback) return;
|
|
1351
|
+
try {
|
|
1352
|
+
await this.tokenSyncBackCallback(this._currentAccessToken, this._currentRefreshToken);
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
logger.error('[LinkedInAutomation] Error syncing tokens back to session:', error.message);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/**
|
|
1359
|
+
* Start a periodic timer that proactively refreshes the JWT token before it expires.
|
|
1360
|
+
* This prevents stale tokens from accumulating during long-running automation.
|
|
1361
|
+
*/
|
|
1362
|
+
startTokenRefreshTimer() {
|
|
1363
|
+
this.stopTokenRefreshTimer();
|
|
1364
|
+
logger.log(`[LinkedInAutomation] Starting proactive token refresh timer (every ${this._tokenRefreshIntervalMs / 60000} min)`);
|
|
1365
|
+
|
|
1366
|
+
this._tokenRefreshInterval = setInterval(async () => {
|
|
1367
|
+
if (!this.isRunning) return;
|
|
1368
|
+
try {
|
|
1369
|
+
logger.log('[LinkedInAutomation] Proactive token refresh triggered');
|
|
1370
|
+
const token = await this.getAccessToken();
|
|
1371
|
+
if (token) {
|
|
1372
|
+
logger.log('[LinkedInAutomation] Proactive token refresh succeeded');
|
|
1373
|
+
} else {
|
|
1374
|
+
logger.error('[LinkedInAutomation] Proactive token refresh failed - no valid token');
|
|
1375
|
+
}
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
logger.error('[LinkedInAutomation] Proactive token refresh error:', error.message);
|
|
1378
|
+
}
|
|
1379
|
+
}, this._tokenRefreshIntervalMs);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
stopTokenRefreshTimer() {
|
|
1383
|
+
if (this._tokenRefreshInterval) {
|
|
1384
|
+
clearInterval(this._tokenRefreshInterval);
|
|
1385
|
+
this._tokenRefreshInterval = null;
|
|
1386
|
+
logger.log('[LinkedInAutomation] Token refresh timer stopped');
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1316
1390
|
/**
|
|
1317
1391
|
* Attempt to re-sync tokens from external source (session, etc.)
|
|
1318
1392
|
* @returns {Promise<boolean>} true if tokens were successfully re-synced
|
|
@@ -1338,12 +1412,13 @@ export class LinkedInAutomation {
|
|
|
1338
1412
|
if (result.tokens.refreshToken) {
|
|
1339
1413
|
this._currentRefreshToken = result.tokens.refreshToken;
|
|
1340
1414
|
}
|
|
1415
|
+
this.syncTokensBack();
|
|
1341
1416
|
} else {
|
|
1342
1417
|
logger.error('[LinkedInAutomation] Failed to refresh re-synced tokens:', result.error);
|
|
1343
1418
|
return false;
|
|
1344
1419
|
}
|
|
1345
1420
|
}
|
|
1346
|
-
|
|
1421
|
+
|
|
1347
1422
|
return true;
|
|
1348
1423
|
}
|
|
1349
1424
|
} catch (error) {
|
|
@@ -1390,37 +1465,41 @@ export class LinkedInAutomation {
|
|
|
1390
1465
|
if (result.tokens.refreshToken) {
|
|
1391
1466
|
this._currentRefreshToken = result.tokens.refreshToken;
|
|
1392
1467
|
}
|
|
1468
|
+
// Persist refreshed tokens back to session store
|
|
1469
|
+
this.syncTokensBack();
|
|
1393
1470
|
// Reset failure counter on successful refresh
|
|
1394
1471
|
this.consecutiveAuthFailures = 0;
|
|
1395
1472
|
this.authFailurePaused = false;
|
|
1396
1473
|
return this._currentAccessToken;
|
|
1397
1474
|
} else {
|
|
1398
1475
|
logger.error('[LinkedInAutomation] Token refresh failed:', result.error);
|
|
1399
|
-
|
|
1476
|
+
|
|
1400
1477
|
// Try one more time to re-sync before giving up
|
|
1401
1478
|
logger.log('[LinkedInAutomation] Attempting token re-sync after refresh failure...');
|
|
1402
1479
|
const resynced = await this.attemptTokenResync();
|
|
1403
1480
|
if (resynced && !this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
|
|
1404
1481
|
logger.log('[LinkedInAutomation] Token re-synced successfully after refresh failure');
|
|
1482
|
+
this.syncTokensBack();
|
|
1405
1483
|
this.consecutiveAuthFailures = 0;
|
|
1406
1484
|
this.authFailurePaused = false;
|
|
1407
1485
|
return this._currentAccessToken;
|
|
1408
1486
|
}
|
|
1409
|
-
|
|
1487
|
+
|
|
1410
1488
|
return null;
|
|
1411
1489
|
}
|
|
1412
1490
|
} catch (error) {
|
|
1413
1491
|
logger.error('[LinkedInAutomation] Error during token refresh:', error.message);
|
|
1414
|
-
|
|
1492
|
+
|
|
1415
1493
|
// Try re-sync one more time
|
|
1416
1494
|
const resynced = await this.attemptTokenResync();
|
|
1417
1495
|
if (resynced && !this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
|
|
1418
1496
|
logger.log('[LinkedInAutomation] Token re-synced successfully after refresh error');
|
|
1497
|
+
this.syncTokensBack();
|
|
1419
1498
|
this.consecutiveAuthFailures = 0;
|
|
1420
1499
|
this.authFailurePaused = false;
|
|
1421
1500
|
return this._currentAccessToken;
|
|
1422
1501
|
}
|
|
1423
|
-
|
|
1502
|
+
|
|
1424
1503
|
return null;
|
|
1425
1504
|
}
|
|
1426
1505
|
}
|
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
|
}
|