coffeeinabit 0.0.34 → 0.0.45
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/Makefile +10 -1
- package/linkedin_automation.js +221 -56
- package/package.json +2 -2
- package/public/dashboard.html +3 -3
- package/public/login.html +4 -4
- package/server.js +1 -0
- package/tools/get_linkedin_updates.js +14 -1
- package/tools/get_new_messages.js +30 -5
- package/tools/get_profile.js +442 -246
- package/tools/human_typing.js +34 -18
- package/tools/send_connection_request.js +92 -77
- package/tools/send_messages.js +53 -20
package/Makefile
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
patch-and-publish:
|
|
2
|
+
@echo "Checking npm authentication..."
|
|
3
|
+
@npm whoami || (echo "Error: Not logged in to npm. Run 'npm login' first." && exit 1)
|
|
4
|
+
@echo "Authenticated as: $$(npm whoami)"
|
|
5
|
+
@echo "Verifying npm token in ~/.npmrc..."
|
|
6
|
+
@grep -q "//registry.npmjs.org/:_authToken=" ~/.npmrc || (echo "Error: No npm token found in ~/.npmrc" && exit 1)
|
|
7
|
+
@echo "Token found. Publishing..."
|
|
2
8
|
npm version patch
|
|
3
|
-
npm publish
|
|
9
|
+
npm publish --access public || (echo "" && echo "❌ Publish failed! If you see a 2FA error:" && echo " 1. Go to: https://www.npmjs.com/settings/kate_yan/tokens" && echo " 2. Create a 'Granular Access Token'" && echo " 3. Enable 'Bypass 2FA' permission" && echo " 4. Update ~/.npmrc with: //registry.npmjs.org/:_authToken=YOUR_NEW_TOKEN" && exit 1)
|
|
10
|
+
|
|
11
|
+
stop:
|
|
12
|
+
pm2 stop "npx coffeeinabit" && pm2 delete "npx coffeeinabit" && pkill -9 -f "npm exec coffeeinabit"; pkill -9 -f "npx.*coffeeinabit"; pkill -9 -f "node.*coffeeinabit"
|
package/linkedin_automation.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'fs';
|
|
|
4
4
|
import { getContextDirectory } from './tools/context_paths.js';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { humanLikeType, readLinkedInCredentials } from './tools/human_typing.js';
|
|
7
|
+
import { CloudAuth } from './cloud_auth.js';
|
|
7
8
|
import { executeGetProfile } from './tools/get_profile.js';
|
|
8
9
|
import { executeLikePost } from './tools/like_post.js';
|
|
9
10
|
import { executeSendConnectionRequest } from './tools/send_connection_request.js';
|
|
@@ -39,6 +40,28 @@ export class LinkedInAutomation {
|
|
|
39
40
|
this.isActionPollingActive = false;
|
|
40
41
|
this.linkedInSessionDir = getContextDirectory();
|
|
41
42
|
this.ambientMouseTimeout = null;
|
|
43
|
+
this.cloudAuth = new CloudAuth();
|
|
44
|
+
this._currentRefreshToken = null;
|
|
45
|
+
|
|
46
|
+
// Lifecycle phases
|
|
47
|
+
this.PHASE_IDLE = 'idle';
|
|
48
|
+
this.PHASE_INITIALIZING = 'initializing';
|
|
49
|
+
this.PHASE_BROWSER_READY = 'browser_ready';
|
|
50
|
+
this.PHASE_AUTHENTICATING = 'authenticating';
|
|
51
|
+
this.PHASE_AUTHENTICATED = 'authenticated';
|
|
52
|
+
this.PHASE_RUNNING = 'running';
|
|
53
|
+
this.PHASE_STOPPING = 'stopping';
|
|
54
|
+
this.PHASE_ERROR = 'error';
|
|
55
|
+
|
|
56
|
+
this.currentPhase = this.PHASE_IDLE;
|
|
57
|
+
this.authenticationPromise = null;
|
|
58
|
+
this.authenticationResolve = null;
|
|
59
|
+
this.authenticationReject = null;
|
|
60
|
+
|
|
61
|
+
// Polling/backoff configuration
|
|
62
|
+
this.pollIntervalMs = 2000; // starting interval (2s)
|
|
63
|
+
this.pollBackoffFactor = 1.5; // multiply each time when no work found
|
|
64
|
+
this.pollMaxMs = 5 * 60 * 1000; // 5 minutes max
|
|
42
65
|
}
|
|
43
66
|
|
|
44
67
|
randomBetween(min, max) {
|
|
@@ -51,6 +74,61 @@ export class LinkedInAutomation {
|
|
|
51
74
|
return delay;
|
|
52
75
|
}
|
|
53
76
|
|
|
77
|
+
transitionToPhase(newPhase) {
|
|
78
|
+
const oldPhase = this.currentPhase;
|
|
79
|
+
this.currentPhase = newPhase;
|
|
80
|
+
console.log(`[LinkedInAutomation] Phase transition: ${oldPhase} → ${newPhase}`);
|
|
81
|
+
|
|
82
|
+
// Update status to match phase (for backward compatibility)
|
|
83
|
+
if (newPhase === this.PHASE_RUNNING) {
|
|
84
|
+
this.status = 'running';
|
|
85
|
+
} else if (newPhase === this.PHASE_AUTHENTICATED) {
|
|
86
|
+
this.status = 'linkedin_logged_in';
|
|
87
|
+
} else if (newPhase === this.PHASE_AUTHENTICATING) {
|
|
88
|
+
this.status = 'waiting_for_manual_login';
|
|
89
|
+
} else if (newPhase === this.PHASE_BROWSER_READY) {
|
|
90
|
+
this.status = 'browser_ready';
|
|
91
|
+
} else if (newPhase === this.PHASE_STOPPING) {
|
|
92
|
+
this.status = 'stopping';
|
|
93
|
+
} else if (newPhase === this.PHASE_IDLE) {
|
|
94
|
+
this.status = 'stopped';
|
|
95
|
+
} else if (newPhase === this.PHASE_ERROR) {
|
|
96
|
+
this.status = 'error';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.emitStatus();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async waitForPhase(targetPhase, timeoutMs = 120000) {
|
|
103
|
+
if (this.currentPhase === targetPhase) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`[LinkedInAutomation] Waiting for phase: ${targetPhase} (current: ${this.currentPhase})`);
|
|
108
|
+
|
|
109
|
+
const startTime = Date.now();
|
|
110
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
111
|
+
if (this.currentPhase === targetPhase) {
|
|
112
|
+
console.log(`[LinkedInAutomation] Reached target phase: ${targetPhase}`);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (this.currentPhase === this.PHASE_ERROR || this.currentPhase === this.PHASE_IDLE) {
|
|
117
|
+
console.log(`[LinkedInAutomation] Phase wait interrupted by ${this.currentPhase}`);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.error(`[LinkedInAutomation] Timeout waiting for phase ${targetPhase}`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
isInPhaseOrLater(...phases) {
|
|
129
|
+
return phases.includes(this.currentPhase);
|
|
130
|
+
}
|
|
131
|
+
|
|
54
132
|
hasActiveBrowser() {
|
|
55
133
|
const contextOpen = this.context && typeof this.context.isClosed === 'function' ? !this.context.isClosed() : Boolean(this.context);
|
|
56
134
|
const pageOpen = this.page && typeof this.page.isClosed === 'function' ? !this.page.isClosed() : Boolean(this.page);
|
|
@@ -211,32 +289,55 @@ export class LinkedInAutomation {
|
|
|
211
289
|
async startAutomation(headless = true) {
|
|
212
290
|
try {
|
|
213
291
|
console.log('[LinkedInAutomation] Starting automation...');
|
|
292
|
+
|
|
293
|
+
// Reset authentication promise
|
|
294
|
+
this.authenticationPromise = new Promise((resolve, reject) => {
|
|
295
|
+
this.authenticationResolve = resolve;
|
|
296
|
+
this.authenticationReject = reject;
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
this.transitionToPhase(this.PHASE_INITIALIZING);
|
|
214
300
|
this.headless = headless;
|
|
215
301
|
this.originalHeadless = headless;
|
|
216
302
|
this.needsManualLogin = false;
|
|
217
303
|
console.log(`[LinkedInAutomation] Browser mode: ${headless ? 'headless' : 'visible'}`);
|
|
218
|
-
this.status = 'starting';
|
|
219
|
-
this.emitStatus();
|
|
220
304
|
|
|
221
305
|
this.userEmail = this._currentUserEmail || 'default_user';
|
|
222
306
|
console.log('[LinkedInAutomation] User email:', this.userEmail);
|
|
223
307
|
|
|
224
308
|
console.log('[LinkedInAutomation] Launching browser for login...');
|
|
225
309
|
await this.launchBrowserAndLogin();
|
|
226
|
-
|
|
310
|
+
|
|
311
|
+
// launchBrowserAndLogin now ensures we're at BROWSER_READY phase
|
|
312
|
+
console.log('[LinkedInAutomation] Browser launched, waiting for authentication...');
|
|
313
|
+
|
|
314
|
+
// Wait for authentication to complete (resolves when checkAndVerifySession succeeds)
|
|
315
|
+
const authSuccess = await Promise.race([
|
|
316
|
+
this.authenticationPromise,
|
|
317
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Authentication timeout')), 300000)) // 5 min timeout
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
if (!authSuccess) {
|
|
321
|
+
throw new Error('Authentication failed');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
console.log('[LinkedInAutomation] Authentication successful, starting background activities...');
|
|
325
|
+
|
|
326
|
+
// Now we're authenticated, mark as running and start background activities
|
|
327
|
+
this.isRunning = true;
|
|
328
|
+
this.transitionToPhase(this.PHASE_RUNNING);
|
|
329
|
+
|
|
330
|
+
// Safe to start background activities now
|
|
227
331
|
this.setupPageListeners();
|
|
228
332
|
this.startAmbientMouseMovements();
|
|
229
|
-
|
|
230
|
-
this.isRunning = true;
|
|
231
|
-
this.status = 'running';
|
|
232
|
-
this.emitStatus();
|
|
233
|
-
|
|
234
333
|
this.startScreenshotCapture();
|
|
334
|
+
|
|
335
|
+
console.log('[LinkedInAutomation] Automation fully started and running');
|
|
235
336
|
|
|
236
337
|
} catch (error) {
|
|
237
338
|
console.error('[LinkedInAutomation] Failed to start automation:', error);
|
|
238
|
-
this.
|
|
239
|
-
this.
|
|
339
|
+
this.transitionToPhase(this.PHASE_ERROR);
|
|
340
|
+
this.isRunning = false;
|
|
240
341
|
throw error;
|
|
241
342
|
}
|
|
242
343
|
}
|
|
@@ -406,6 +507,12 @@ export class LinkedInAutomation {
|
|
|
406
507
|
console.log('[LinkedInAutomation] Starting action polling after successful login...');
|
|
407
508
|
this.startActionPolling();
|
|
408
509
|
|
|
510
|
+
// Transition to AUTHENTICATED phase and resolve promise
|
|
511
|
+
this.transitionToPhase(this.PHASE_AUTHENTICATED);
|
|
512
|
+
if (this.authenticationResolve) {
|
|
513
|
+
this.authenticationResolve(true);
|
|
514
|
+
}
|
|
515
|
+
|
|
409
516
|
clearInterval(pollInterval);
|
|
410
517
|
} else {
|
|
411
518
|
console.log('[LinkedInAutomation] Second check failed - not on /feed/ anymore, continuing to wait...');
|
|
@@ -467,8 +574,15 @@ export class LinkedInAutomation {
|
|
|
467
574
|
async stopAutomation() {
|
|
468
575
|
try {
|
|
469
576
|
console.log('[LinkedInAutomation] Stopping automation...');
|
|
470
|
-
this.
|
|
471
|
-
this.
|
|
577
|
+
this.transitionToPhase(this.PHASE_STOPPING);
|
|
578
|
+
this.isRunning = false;
|
|
579
|
+
|
|
580
|
+
// Reject any pending authentication promises
|
|
581
|
+
if (this.authenticationReject) {
|
|
582
|
+
this.authenticationReject(new Error('Automation stopped'));
|
|
583
|
+
this.authenticationResolve = null;
|
|
584
|
+
this.authenticationReject = null;
|
|
585
|
+
}
|
|
472
586
|
|
|
473
587
|
this.stopScreenshotCapture();
|
|
474
588
|
this.stopActionPolling();
|
|
@@ -476,13 +590,11 @@ export class LinkedInAutomation {
|
|
|
476
590
|
|
|
477
591
|
await this.closeBrowser();
|
|
478
592
|
|
|
479
|
-
this.
|
|
480
|
-
this.emitStatus();
|
|
593
|
+
this.transitionToPhase(this.PHASE_IDLE);
|
|
481
594
|
|
|
482
595
|
} catch (error) {
|
|
483
596
|
console.error('[LinkedInAutomation] Error stopping automation:', error);
|
|
484
|
-
this.
|
|
485
|
-
this.emitStatus();
|
|
597
|
+
this.transitionToPhase(this.PHASE_ERROR);
|
|
486
598
|
}
|
|
487
599
|
}
|
|
488
600
|
|
|
@@ -571,11 +683,13 @@ export class LinkedInAutomation {
|
|
|
571
683
|
if (!this.ensureBrowserAvailable('startActionPolling')) {
|
|
572
684
|
return;
|
|
573
685
|
}
|
|
574
|
-
|
|
575
|
-
console.log('[LinkedInAutomation] Starting action polling with random intervals (20-40 seconds)...');
|
|
686
|
+
console.log('[LinkedInAutomation] Starting action polling with backoff (max 5 minutes)...');
|
|
576
687
|
this.isActionPollingActive = true;
|
|
577
|
-
|
|
578
|
-
|
|
688
|
+
|
|
689
|
+
// initialize a local current interval so we can grow it on each empty poll
|
|
690
|
+
let currentInterval = this.pollIntervalMs;
|
|
691
|
+
|
|
692
|
+
const pollWithBackoff = async () => {
|
|
579
693
|
if (!this.isActionPollingActive) {
|
|
580
694
|
console.log('[LinkedInAutomation] Action polling stopped, not scheduling next poll');
|
|
581
695
|
return;
|
|
@@ -583,20 +697,28 @@ export class LinkedInAutomation {
|
|
|
583
697
|
|
|
584
698
|
if (this.currentAction) {
|
|
585
699
|
console.log('[LinkedInAutomation] Action in progress, skipping polling...');
|
|
586
|
-
|
|
587
|
-
setTimeout(
|
|
700
|
+
console.log('[LinkedInAutomation] Next poll in', Math.round(currentInterval / 1000), 'seconds');
|
|
701
|
+
setTimeout(pollWithBackoff, currentInterval);
|
|
588
702
|
return;
|
|
589
703
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
704
|
+
|
|
705
|
+
// pollForActions now returns true if actions were found and processed
|
|
706
|
+
const foundActions = await this.pollForActions();
|
|
707
|
+
|
|
708
|
+
if (foundActions) {
|
|
709
|
+
// reset backoff when we processed work
|
|
710
|
+
currentInterval = this.pollIntervalMs;
|
|
711
|
+
} else {
|
|
712
|
+
// increase interval (backoff) with slight jitter, cap to pollMaxMs
|
|
713
|
+
const jitter = Math.floor(Math.random() * 2000);
|
|
714
|
+
currentInterval = Math.min(this.pollMaxMs, Math.floor(currentInterval * this.pollBackoffFactor) + jitter);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
console.log('[LinkedInAutomation] Next poll in', Math.round(currentInterval / 1000), 'seconds');
|
|
718
|
+
setTimeout(pollWithBackoff, currentInterval);
|
|
597
719
|
};
|
|
598
|
-
|
|
599
|
-
|
|
720
|
+
|
|
721
|
+
pollWithBackoff();
|
|
600
722
|
}
|
|
601
723
|
|
|
602
724
|
stopActionPolling() {
|
|
@@ -611,7 +733,7 @@ export class LinkedInAutomation {
|
|
|
611
733
|
try {
|
|
612
734
|
console.log('[LinkedInAutomation] Polling for actions from backend...');
|
|
613
735
|
|
|
614
|
-
const idToken = this.getAccessToken();
|
|
736
|
+
const idToken = await this.getAccessToken();
|
|
615
737
|
if (!idToken) {
|
|
616
738
|
console.error('[LinkedInAutomation] No id_token available for authentication');
|
|
617
739
|
return;
|
|
@@ -638,19 +760,22 @@ export class LinkedInAutomation {
|
|
|
638
760
|
for (const action of actions) {
|
|
639
761
|
await this.executeAction(action);
|
|
640
762
|
}
|
|
641
|
-
|
|
642
|
-
|
|
763
|
+
// Indicate there was work
|
|
764
|
+
return true;
|
|
643
765
|
} else {
|
|
644
766
|
console.log('[LinkedInAutomation] No actions to execute, continuing to poll...');
|
|
767
|
+
return false;
|
|
645
768
|
}
|
|
646
769
|
} else {
|
|
647
770
|
console.error('[LinkedInAutomation] Failed to fetch actions. Status:', response.status, response.statusText);
|
|
648
771
|
const errorText = await response.text();
|
|
649
772
|
console.error('[LinkedInAutomation] Error response:', errorText);
|
|
773
|
+
return false;
|
|
650
774
|
}
|
|
651
775
|
} catch (error) {
|
|
652
776
|
console.error('[LinkedInAutomation] Error polling for actions:', error.message);
|
|
653
777
|
console.error('[LinkedInAutomation] Full error:', error);
|
|
778
|
+
return false;
|
|
654
779
|
}
|
|
655
780
|
}
|
|
656
781
|
|
|
@@ -700,63 +825,56 @@ export class LinkedInAutomation {
|
|
|
700
825
|
|
|
701
826
|
let result = null;
|
|
702
827
|
|
|
828
|
+
// add esc to close any modals that might interfere
|
|
829
|
+
await this.page.keyboard.press('Escape');
|
|
830
|
+
await this.waitRandom(100, 2000);
|
|
831
|
+
|
|
703
832
|
switch (action.action) {
|
|
704
833
|
case 'get_profile':
|
|
705
834
|
console.log('[LinkedInAutomation] Executing get_profile action...');
|
|
706
835
|
result = await executeGetProfile(this.page, action);
|
|
707
|
-
console.log('[LinkedInAutomation] get_profile result:', result);
|
|
708
836
|
break;
|
|
709
837
|
case 'like_post':
|
|
710
838
|
console.log('[LinkedInAutomation] Executing like_post action...');
|
|
711
839
|
result = await executeLikePost(this.page, action);
|
|
712
|
-
console.log('[LinkedInAutomation] like_post result:', result);
|
|
713
840
|
break;
|
|
714
841
|
case 'send_connection_request':
|
|
715
842
|
console.log('[LinkedInAutomation] Executing send_connection_request action...');
|
|
716
843
|
result = await executeSendConnectionRequest(this.page, action);
|
|
717
|
-
console.log('[LinkedInAutomation] send_connection_request result:', result);
|
|
718
844
|
break;
|
|
719
845
|
case 'send_messages':
|
|
720
846
|
console.log('[LinkedInAutomation] Executing send_messages action...');
|
|
721
847
|
result = await executeSendMessages(this.page, action);
|
|
722
|
-
console.log('[LinkedInAutomation] send_messages result:', result);
|
|
723
848
|
break;
|
|
724
849
|
case 'comment_post':
|
|
725
850
|
console.log('[LinkedInAutomation] Executing comment_post action...');
|
|
726
851
|
result = await executeCommentPost(this.page, action);
|
|
727
|
-
console.log('[LinkedInAutomation] comment_post result:', result);
|
|
728
852
|
break;
|
|
729
853
|
case 'get_messages':
|
|
730
854
|
console.log('[LinkedInAutomation] Executing get_messages action...');
|
|
731
855
|
result = await executeGetMessages(this.page, action);
|
|
732
|
-
console.log('[LinkedInAutomation] get_messages result:', result);
|
|
733
856
|
break;
|
|
734
857
|
case 'get_linkedin_search_results':
|
|
735
858
|
console.log('[LinkedInAutomation] Executing get_linkedin_search_results action...');
|
|
736
859
|
result = await executeLinkedInSearch(this.page, action);
|
|
737
|
-
console.log('[LinkedInAutomation] get_linkedin_search_results result:', result);
|
|
738
860
|
break;
|
|
739
861
|
case 'get_daily_linkedin_connections':
|
|
740
862
|
console.log('[LinkedInAutomation] Executing get_daily_linkedin_connections action...');
|
|
741
863
|
result = await executeGetDailyLinkedInConnections(this.page, action);
|
|
742
|
-
console.log('[LinkedInAutomation] get_daily_linkedin_connections result:', result);
|
|
743
864
|
break;
|
|
744
865
|
case 'get_new_messages':
|
|
745
866
|
console.log('[LinkedInAutomation] Executing get_new_messages action...');
|
|
746
|
-
result = await executeGetNewMessages(this.page, action, this.getAccessToken());
|
|
747
|
-
console.log('[LinkedInAutomation] get_new_messages result:', result);
|
|
867
|
+
result = await executeGetNewMessages(this.page, action, await this.getAccessToken());
|
|
748
868
|
break;
|
|
749
869
|
case 'get_linkedin_updates':
|
|
750
870
|
console.log('[LinkedInAutomation] Executing get_linkedin_updates action...');
|
|
751
|
-
result = await executeGetLinkedInUpdates(this.page, action, this.getAccessToken());
|
|
752
|
-
console.log('[LinkedInAutomation] get_linkedin_updates result:', result);
|
|
871
|
+
result = await executeGetLinkedInUpdates(this.page, action, await this.getAccessToken());
|
|
753
872
|
break;
|
|
754
873
|
default:
|
|
755
874
|
console.log('[LinkedInAutomation] Unknown action type:', action.action);
|
|
756
875
|
result = { error: `Unknown action type: ${action.action}` };
|
|
757
876
|
}
|
|
758
|
-
|
|
759
|
-
console.log('[LinkedInAutomation] Reporting action result for ID:', action.action_id);
|
|
877
|
+
console.log(`[LinkedInAutomation] ${action.action}: ${action.action_id} result: ${JSON.stringify(result)}`);
|
|
760
878
|
|
|
761
879
|
if (result && typeof result === 'object') {
|
|
762
880
|
result.action_logs = actionLogs;
|
|
@@ -790,10 +908,9 @@ export class LinkedInAutomation {
|
|
|
790
908
|
|
|
791
909
|
async reportActionResult(actionId, result, action) {
|
|
792
910
|
try {
|
|
793
|
-
console.log(
|
|
794
|
-
console.log('[LinkedInAutomation] Result data:', JSON.stringify(result, null, 2));
|
|
911
|
+
console.log(`[LinkedInAutomation] Reporting action result for ID: ${actionId}`);
|
|
795
912
|
|
|
796
|
-
const idToken = this.getAccessToken();
|
|
913
|
+
const idToken = await this.getAccessToken();
|
|
797
914
|
if (!idToken) {
|
|
798
915
|
console.error('[LinkedInAutomation] No id_token available for reporting result');
|
|
799
916
|
return;
|
|
@@ -841,7 +958,7 @@ export class LinkedInAutomation {
|
|
|
841
958
|
|
|
842
959
|
while (Date.now() - startTime < maxWaitTime) {
|
|
843
960
|
try {
|
|
844
|
-
const idToken = this.getAccessToken();
|
|
961
|
+
const idToken = await this.getAccessToken();
|
|
845
962
|
if (!idToken) {
|
|
846
963
|
console.error('[LinkedInAutomation] No id_token available for polling');
|
|
847
964
|
return;
|
|
@@ -936,8 +1053,46 @@ export class LinkedInAutomation {
|
|
|
936
1053
|
}
|
|
937
1054
|
}
|
|
938
1055
|
|
|
939
|
-
getAccessToken() {
|
|
940
|
-
|
|
1056
|
+
async getAccessToken() {
|
|
1057
|
+
// Check if token exists
|
|
1058
|
+
if (!this._currentAccessToken) {
|
|
1059
|
+
console.error('[LinkedInAutomation] No access token available');
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Check if token is expired or expiring soon (within 5 minutes)
|
|
1064
|
+
if (this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
|
|
1065
|
+
console.log('[LinkedInAutomation] Token expired or expiring soon, attempting refresh...');
|
|
1066
|
+
|
|
1067
|
+
// Check if refresh token is available
|
|
1068
|
+
if (!this._currentRefreshToken) {
|
|
1069
|
+
console.error('[LinkedInAutomation] No refresh token available, cannot refresh');
|
|
1070
|
+
return null;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Attempt to refresh the token
|
|
1074
|
+
try {
|
|
1075
|
+
const result = await this.cloudAuth.refreshTokens(this._currentRefreshToken);
|
|
1076
|
+
|
|
1077
|
+
if (result.success) {
|
|
1078
|
+
console.log('[LinkedInAutomation] Token refreshed successfully');
|
|
1079
|
+
this._currentAccessToken = result.tokens.idToken;
|
|
1080
|
+
// Update refresh token if a new one was provided
|
|
1081
|
+
if (result.tokens.refreshToken) {
|
|
1082
|
+
this._currentRefreshToken = result.tokens.refreshToken;
|
|
1083
|
+
}
|
|
1084
|
+
return this._currentAccessToken;
|
|
1085
|
+
} else {
|
|
1086
|
+
console.error('[LinkedInAutomation] Token refresh failed:', result.error);
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
} catch (error) {
|
|
1090
|
+
console.error('[LinkedInAutomation] Error during token refresh:', error.message);
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return this._currentAccessToken;
|
|
941
1096
|
}
|
|
942
1097
|
|
|
943
1098
|
getStorageStatePath() {
|
|
@@ -1037,6 +1192,9 @@ export class LinkedInAutomation {
|
|
|
1037
1192
|
}
|
|
1038
1193
|
|
|
1039
1194
|
resetHumanMouseState();
|
|
1195
|
+
|
|
1196
|
+
// Transition to BROWSER_READY phase
|
|
1197
|
+
this.transitionToPhase(this.PHASE_BROWSER_READY);
|
|
1040
1198
|
}
|
|
1041
1199
|
|
|
1042
1200
|
async updateBrowserVisibility(keepBrowser) {
|
|
@@ -1129,6 +1287,8 @@ export class LinkedInAutomation {
|
|
|
1129
1287
|
|
|
1130
1288
|
async checkAndVerifySession() {
|
|
1131
1289
|
try {
|
|
1290
|
+
this.transitionToPhase(this.PHASE_AUTHENTICATING);
|
|
1291
|
+
|
|
1132
1292
|
console.log('[LinkedInAutomation] Navigating to LinkedIn feed to verify persistent session...');
|
|
1133
1293
|
await safeGoto(this.page, 'https://www.linkedin.com/feed/', {
|
|
1134
1294
|
waitUntil: 'domcontentloaded',
|
|
@@ -1146,11 +1306,16 @@ export class LinkedInAutomation {
|
|
|
1146
1306
|
|
|
1147
1307
|
if (this.currentUrl.includes('/feed')) {
|
|
1148
1308
|
console.log('[LinkedInAutomation] Session verified! Already logged in via persistent context.');
|
|
1149
|
-
this.
|
|
1150
|
-
this.emitStatus();
|
|
1309
|
+
this.transitionToPhase(this.PHASE_AUTHENTICATED);
|
|
1151
1310
|
|
|
1152
1311
|
console.log('[LinkedInAutomation] Starting action polling...');
|
|
1153
1312
|
this.startActionPolling();
|
|
1313
|
+
|
|
1314
|
+
// Resolve authentication promise to allow startAutomation to proceed
|
|
1315
|
+
if (this.authenticationResolve) {
|
|
1316
|
+
this.authenticationResolve(true);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1154
1319
|
return true;
|
|
1155
1320
|
}
|
|
1156
1321
|
|
package/package.json
CHANGED
package/public/dashboard.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>
|
|
6
|
+
<title>coffeeinabit - Dashboard</title>
|
|
7
7
|
<link rel="icon" type="image/png" href="/images/favicon.png">
|
|
8
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
9
9
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
<nav class="navbar navbar-expand-lg glass-navbar">
|
|
14
14
|
<div class="container-fluid">
|
|
15
15
|
<a class="navbar-brand fw-bold" href="#">
|
|
16
|
-
<img src="/images/fav_clear.png" alt="
|
|
17
|
-
|
|
16
|
+
<img src="/images/fav_clear.png" alt="coffeeinabit" style="width: 35px; height: 35px; margin-bottom: 7px; vertical-align: middle;">
|
|
17
|
+
coffeeinabit
|
|
18
18
|
</a>
|
|
19
19
|
<div class="navbar-nav ms-auto">
|
|
20
20
|
<div class="nav-item dropdown">
|
package/public/login.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>
|
|
6
|
+
<title>coffeeinabit - Login</title>
|
|
7
7
|
<link rel="icon" type="image/png" href="/images/favicon.png">
|
|
8
8
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
9
9
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
<div class="glass-card">
|
|
17
17
|
<div class="text-center mb-4">
|
|
18
18
|
<h1 class="h3 mb-3 fw-bold" style="color: #ffffff;">
|
|
19
|
-
<img src="/images/fav_clear.png" alt="
|
|
20
|
-
|
|
19
|
+
<img src="/images/fav_clear.png" alt="coffeeinabit" style="width: 75px; height: 75px; margin-bottom: 8px; vertical-align: middle;">
|
|
20
|
+
coffeeinabit
|
|
21
21
|
</h1>
|
|
22
22
|
<p class="text-muted">Sign in to your account to continue</p>
|
|
23
23
|
</div>
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
<div class="d-grid gap-2">
|
|
26
26
|
<button id="loginButton" class="glass-button btn-primary" onclick="loginToCloud()">
|
|
27
27
|
<i class="bi bi-cloud-arrow-up me-2"></i>
|
|
28
|
-
Login to
|
|
28
|
+
Login to coffeeinabit Cloud
|
|
29
29
|
</button>
|
|
30
30
|
</div>
|
|
31
31
|
</div>
|
package/server.js
CHANGED
|
@@ -145,6 +145,7 @@ app.post('/api/automation/start', async (req, res) => {
|
|
|
145
145
|
try {
|
|
146
146
|
const { headless = false } = req.body;
|
|
147
147
|
linkedinAutomation._currentAccessToken = req.session.tokens.idToken;
|
|
148
|
+
linkedinAutomation._currentRefreshToken = req.session.tokens.refreshToken;
|
|
148
149
|
linkedinAutomation._currentUserEmail = req.session.user.email;
|
|
149
150
|
await linkedinAutomation.startAutomation(headless);
|
|
150
151
|
res.json({ success: true, message: 'Automation started' });
|
|
@@ -8,7 +8,20 @@ export async function executeGetLinkedInUpdates(page, action, accessToken) {
|
|
|
8
8
|
const lastChecked = action.parameters?.last_checked || 0;
|
|
9
9
|
|
|
10
10
|
console.log('[GetLinkedInUpdates] Fetching new messages...');
|
|
11
|
-
|
|
11
|
+
let messagesResult;
|
|
12
|
+
try {
|
|
13
|
+
const messagesTimeoutMs = 5 * 60 * 1000;
|
|
14
|
+
const timeoutPromise = new Promise((_, reject) =>
|
|
15
|
+
setTimeout(() => reject(new Error('GetNewMessages timeout')), messagesTimeoutMs)
|
|
16
|
+
);
|
|
17
|
+
messagesResult = await Promise.race([
|
|
18
|
+
executeGetNewMessages(page, action, accessToken),
|
|
19
|
+
timeoutPromise
|
|
20
|
+
]);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.error('[GetLinkedInUpdates] get_new_messages failed or timed out:', e.message);
|
|
23
|
+
messagesResult = { newMessagesCount: 0, messages: [] };
|
|
24
|
+
}
|
|
12
25
|
|
|
13
26
|
console.log('[GetLinkedInUpdates] Waiting before fetching connections...');
|
|
14
27
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
@@ -239,7 +239,7 @@ export async function executeGetNewMessages(page, action, accessToken) {
|
|
|
239
239
|
|
|
240
240
|
|
|
241
241
|
for (let i = 0; i < 5; i++) {
|
|
242
|
-
await messageContainer.evaluate(
|
|
242
|
+
await messageContainer.evaluate((el) => {
|
|
243
243
|
el.scrollBy({ top: -2000, behavior: 'smooth' });
|
|
244
244
|
});
|
|
245
245
|
console.log(`[GetNewMessages] Scrolled up -2000px (${i + 1}/5)`);
|
|
@@ -301,10 +301,13 @@ export async function executeGetNewMessages(page, action, accessToken) {
|
|
|
301
301
|
}
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
console.log('[GetNewMessages] Finished processing all conversations, waiting before cleanup...');
|
|
304
305
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
305
306
|
|
|
307
|
+
console.log('[GetNewMessages] Unrouting message handlers...');
|
|
306
308
|
try {
|
|
307
309
|
await page.unroute('**/voyagerMessagingGraphQL/**', routeHandler);
|
|
310
|
+
console.log('[GetNewMessages] Successfully unrouted message handlers');
|
|
308
311
|
} catch (error) {
|
|
309
312
|
console.log('[GetNewMessages] Error unrouting (non-critical):', error.message);
|
|
310
313
|
}
|
|
@@ -338,25 +341,47 @@ export async function executeGetNewMessages(page, action, accessToken) {
|
|
|
338
341
|
console.log('[GetNewMessages] Unique profiles to decode:', encodedUrlMap.size);
|
|
339
342
|
|
|
340
343
|
const decodedUrlMap = new Map();
|
|
344
|
+
const maxDecodeAttempts = Math.min(encodedUrlMap.size, 50);
|
|
345
|
+
let decodeCount = 0;
|
|
346
|
+
|
|
341
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
|
+
|
|
342
353
|
try {
|
|
343
|
-
|
|
354
|
+
console.log(`[GetNewMessages] Decoding profile ${decodeCount + 1}/${encodedUrlMap.size}: ${encodedUrl}`);
|
|
355
|
+
|
|
356
|
+
const decodePromise = safeGoto(page, `https://www.linkedin.com/in/${encodedUrl}`, {
|
|
344
357
|
waitUntil: 'domcontentloaded',
|
|
345
358
|
timeout: 30000
|
|
346
359
|
});
|
|
347
|
-
|
|
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 });
|
|
348
367
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
349
368
|
|
|
350
369
|
const currentUrl = page.url();
|
|
351
370
|
const match = currentUrl.match(/linkedin\.com\/in\/([^\/\?]+)/);
|
|
352
371
|
if (match && match[1]) {
|
|
353
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}`);
|
|
354
376
|
}
|
|
377
|
+
decodeCount++;
|
|
355
378
|
} catch (error) {
|
|
356
|
-
console.error(`[GetNewMessages] Failed to decode URL:`, error.message);
|
|
379
|
+
console.error(`[GetNewMessages] Failed to decode URL ${encodedUrl}:`, error.message);
|
|
380
|
+
decodeCount++;
|
|
381
|
+
continue;
|
|
357
382
|
}
|
|
358
383
|
}
|
|
359
|
-
|
|
384
|
+
|
|
360
385
|
let sentCount = 0;
|
|
361
386
|
let failedCount = 0;
|
|
362
387
|
let totalToSend = 0;
|