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 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"
@@ -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.status = 'error';
239
- this.emitStatus();
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.status = 'stopping';
471
- this.emitStatus();
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.status = 'stopped';
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.status = 'error';
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
- const pollWithRandomInterval = async () => {
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
- const randomInterval = Math.floor(Math.random() * 20000) + 20000;
587
- setTimeout(pollWithRandomInterval, randomInterval);
700
+ console.log('[LinkedInAutomation] Next poll in', Math.round(currentInterval / 1000), 'seconds');
701
+ setTimeout(pollWithBackoff, currentInterval);
588
702
  return;
589
703
  }
590
-
591
- await this.pollForActions();
592
-
593
- const randomInterval = Math.floor(Math.random() * 20000) + 20000;
594
- console.log('[LinkedInAutomation] Next poll in', randomInterval / 1000, 'seconds');
595
-
596
- setTimeout(pollWithRandomInterval, randomInterval);
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
- pollWithRandomInterval();
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
- } else if (actions.length > 0) {
642
- console.log('[LinkedInAutomation] Pending actions scheduled for future, continuing to poll...');
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('[LinkedInAutomation] Sending result to backend for action ID:', actionId);
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
- return this._currentAccessToken || null;
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.status = 'linkedin_logged_in';
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "coffeeinabit",
3
- "version": "0.0.34",
4
- "description": "CoffeeInABit App",
3
+ "version": "0.0.45",
4
+ "description": "coffeeinabit app",
5
5
  "main": "server.js",
6
6
  "type": "module",
7
7
  "keywords": [
@@ -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>CoffeeInABit - Dashboard</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="CoffeeInABit" style="width: 35px; height: 35px; margin-bottom: 7px; vertical-align: middle;">
17
- CoffeeInABit
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>CoffeeInABit - Login</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="CoffeeInABit" style="width: 75px; height: 75px; margin-bottom: 8px; vertical-align: middle;">
20
- CoffeeInABit
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 CoffeeInABit Cloud
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
- const messagesResult = await executeGetNewMessages(page, action, accessToken);
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(function(el) {
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
- await safeGoto(page, `https://www.linkedin.com/in/${encodedUrl}`, {
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
- await page.waitForLoadState('domcontentloaded');
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;