coffeeinabit 0.0.46 → 0.0.48
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/helpers/version_check.js +172 -0
- package/linkedin_automation.js +225 -11
- package/package.json +2 -1
- package/server.js +56 -3
- package/state/app_state.js +62 -3
- package/tools/get_daily_linkedin_connections.js +144 -83
- package/tools/get_new_messages.js +468 -323
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import { exec } from 'child_process';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, join } from 'path';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Version Check Helper
|
|
15
|
+
*
|
|
16
|
+
* Checks npm registry for newer version and automatically installs it if available.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get current version from package.json
|
|
21
|
+
* @returns {string} Current version
|
|
22
|
+
*/
|
|
23
|
+
function getCurrentVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const packagePath = join(__dirname, '..', 'package.json');
|
|
26
|
+
const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
27
|
+
return packageJson.version;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('[VersionCheck] Error reading package.json:', error.message);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Query npm registry for latest version
|
|
36
|
+
* @param {string} packageName - Package name to check
|
|
37
|
+
* @returns {Promise<string|null>} Latest version or null if error
|
|
38
|
+
*/
|
|
39
|
+
async function getLatestVersion(packageName) {
|
|
40
|
+
try {
|
|
41
|
+
const registryUrl = `https://registry.npmjs.org/${packageName}/latest`;
|
|
42
|
+
const response = await axios.get(registryUrl, {
|
|
43
|
+
timeout: 5000,
|
|
44
|
+
headers: {
|
|
45
|
+
'Accept': 'application/json'
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return response.data.version;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('[VersionCheck] Error querying npm registry:', error.message);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compare two semantic versions
|
|
57
|
+
* @param {string} current - Current version
|
|
58
|
+
* @param {string} latest - Latest version
|
|
59
|
+
* @returns {boolean} True if latest is newer than current
|
|
60
|
+
*/
|
|
61
|
+
function isNewerVersion(current, latest) {
|
|
62
|
+
if (!current || !latest) return false;
|
|
63
|
+
|
|
64
|
+
const currentParts = current.split('.').map(Number);
|
|
65
|
+
const latestParts = latest.split('.').map(Number);
|
|
66
|
+
|
|
67
|
+
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
|
|
68
|
+
const currentPart = currentParts[i] || 0;
|
|
69
|
+
const latestPart = latestParts[i] || 0;
|
|
70
|
+
|
|
71
|
+
if (latestPart > currentPart) return true;
|
|
72
|
+
if (latestPart < currentPart) return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Install the latest version globally
|
|
80
|
+
* @param {string} packageName - Package name to install
|
|
81
|
+
* @returns {Promise<boolean>} True if installation succeeded
|
|
82
|
+
*/
|
|
83
|
+
async function installLatestVersion(packageName) {
|
|
84
|
+
try {
|
|
85
|
+
console.log(`[VersionCheck] Installing ${packageName}@latest...`);
|
|
86
|
+
const { stdout, stderr } = await execAsync(`npm install -g ${packageName}@latest`, {
|
|
87
|
+
timeout: 120000 // 2 minutes timeout
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (stderr && !stderr.includes('npm WARN')) {
|
|
91
|
+
console.error('[VersionCheck] Installation warnings:', stderr);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log('[VersionCheck] ✅ Successfully installed latest version');
|
|
95
|
+
console.log('[VersionCheck] Please restart the application to use the new version');
|
|
96
|
+
return true;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('[VersionCheck] ❌ Failed to install latest version:', error.message);
|
|
99
|
+
if (error.stdout) console.log('[VersionCheck] stdout:', error.stdout);
|
|
100
|
+
if (error.stderr) console.error('[VersionCheck] stderr:', error.stderr);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check for updates and install if newer version is available
|
|
107
|
+
* @param {string} packageName - Package name to check (default: 'coffeeinabit')
|
|
108
|
+
* @param {boolean} autoInstall - Whether to automatically install (default: true)
|
|
109
|
+
* @returns {Promise<object>} Update check result
|
|
110
|
+
*/
|
|
111
|
+
export async function checkAndUpdateVersion(packageName = 'coffeeinabit', autoInstall = true) {
|
|
112
|
+
try {
|
|
113
|
+
console.log('[VersionCheck] Checking for updates...');
|
|
114
|
+
|
|
115
|
+
const currentVersion = getCurrentVersion();
|
|
116
|
+
if (!currentVersion) {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
error: 'Could not read current version'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`[VersionCheck] Current version: ${currentVersion}`);
|
|
124
|
+
|
|
125
|
+
const latestVersion = await getLatestVersion(packageName);
|
|
126
|
+
if (!latestVersion) {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
error: 'Could not query npm registry'
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`[VersionCheck] Latest version: ${latestVersion}`);
|
|
134
|
+
|
|
135
|
+
if (!isNewerVersion(currentVersion, latestVersion)) {
|
|
136
|
+
console.log('[VersionCheck] ✅ You are running the latest version');
|
|
137
|
+
return {
|
|
138
|
+
success: true,
|
|
139
|
+
currentVersion,
|
|
140
|
+
latestVersion,
|
|
141
|
+
updateAvailable: false
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log(`[VersionCheck] ⚠️ New version available: ${latestVersion} (current: ${currentVersion})`);
|
|
146
|
+
|
|
147
|
+
if (autoInstall) {
|
|
148
|
+
const installed = await installLatestVersion(packageName);
|
|
149
|
+
return {
|
|
150
|
+
success: installed,
|
|
151
|
+
currentVersion,
|
|
152
|
+
latestVersion,
|
|
153
|
+
updateAvailable: true,
|
|
154
|
+
installed
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
currentVersion,
|
|
161
|
+
latestVersion,
|
|
162
|
+
updateAvailable: true,
|
|
163
|
+
installed: false
|
|
164
|
+
};
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('[VersionCheck] Error during version check:', error.message);
|
|
167
|
+
return {
|
|
168
|
+
success: false,
|
|
169
|
+
error: error.message
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
package/linkedin_automation.js
CHANGED
|
@@ -12,7 +12,6 @@ import { executeSendMessages } from './tools/send_messages.js';
|
|
|
12
12
|
import { executeCommentPost } from './tools/comment_post.js';
|
|
13
13
|
import { executeGetMessages, closeAllConversationBubbles } from './tools/get_messages.js';
|
|
14
14
|
import { executeLinkedInSearch } from './tools/get_linkedin_search_results.js';
|
|
15
|
-
import { executeGetDailyLinkedInConnections } from './tools/get_daily_linkedin_connections.js';
|
|
16
15
|
import { executeGetNewMessages } from './tools/get_new_messages.js';
|
|
17
16
|
import { executeGetLinkedInUpdates } from './tools/get_linkedin_updates.js';
|
|
18
17
|
import { safeGoto } from './tools/navigation.js';
|
|
@@ -42,6 +41,16 @@ export class LinkedInAutomation {
|
|
|
42
41
|
this.ambientMouseTimeout = null;
|
|
43
42
|
this.cloudAuth = new CloudAuth();
|
|
44
43
|
this._currentRefreshToken = null;
|
|
44
|
+
this._currentAccessToken = null; // Track access token explicitly
|
|
45
|
+
|
|
46
|
+
// Authentication failure tracking
|
|
47
|
+
this.consecutiveAuthFailures = 0;
|
|
48
|
+
this.maxAuthFailures = 3; // Stop polling after 3 consecutive failures
|
|
49
|
+
this.authFailurePaused = false;
|
|
50
|
+
this.lastAuthFailureTime = null;
|
|
51
|
+
|
|
52
|
+
// Token re-sync callback (set externally if available)
|
|
53
|
+
this.tokenResyncCallback = null;
|
|
45
54
|
|
|
46
55
|
// Lifecycle phases
|
|
47
56
|
this.PHASE_IDLE = 'idle';
|
|
@@ -730,13 +739,43 @@ export class LinkedInAutomation {
|
|
|
730
739
|
if (!this.ensureBrowserAvailable('pollForActions')) {
|
|
731
740
|
return;
|
|
732
741
|
}
|
|
742
|
+
|
|
743
|
+
// Check if polling is paused due to auth failures
|
|
744
|
+
if (this.authFailurePaused) {
|
|
745
|
+
console.log('[LinkedInAutomation] Polling paused due to authentication failures. Attempting recovery...');
|
|
746
|
+
|
|
747
|
+
// Try to re-sync tokens and recover
|
|
748
|
+
const idToken = await this.getAccessToken();
|
|
749
|
+
if (idToken && !this.cloudAuth.isTokenExpired(idToken)) {
|
|
750
|
+
console.log('[LinkedInAutomation] Authentication recovered, resuming polling');
|
|
751
|
+
this.authFailurePaused = false;
|
|
752
|
+
this.consecutiveAuthFailures = 0;
|
|
753
|
+
// Continue with normal polling below
|
|
754
|
+
} else {
|
|
755
|
+
console.log('[LinkedInAutomation] Still unable to authenticate, skipping poll');
|
|
756
|
+
|
|
757
|
+
// Emit event to notify UI
|
|
758
|
+
if (this.io) {
|
|
759
|
+
this.io.emit('automation:auth-failure', {
|
|
760
|
+
message: 'Authentication failed. Please re-authenticate.',
|
|
761
|
+
paused: true,
|
|
762
|
+
consecutiveFailures: this.consecutiveAuthFailures,
|
|
763
|
+
lastFailureTime: this.lastAuthFailureTime
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
733
771
|
try {
|
|
734
772
|
console.log('[LinkedInAutomation] Polling for actions from backend...');
|
|
735
773
|
|
|
736
774
|
const idToken = await this.getAccessToken();
|
|
737
775
|
if (!idToken) {
|
|
738
776
|
console.error('[LinkedInAutomation] No id_token available for authentication');
|
|
739
|
-
|
|
777
|
+
this.handleAuthFailure('No token available');
|
|
778
|
+
return false;
|
|
740
779
|
}
|
|
741
780
|
|
|
742
781
|
console.log('[LinkedInAutomation] Using id_token for authentication:', idToken.substring(0, 20) + '...');
|
|
@@ -760,25 +799,116 @@ export class LinkedInAutomation {
|
|
|
760
799
|
for (const action of actions) {
|
|
761
800
|
await this.executeAction(action);
|
|
762
801
|
}
|
|
802
|
+
// Reset failure counter on successful request
|
|
803
|
+
this.consecutiveAuthFailures = 0;
|
|
804
|
+
this.authFailurePaused = false;
|
|
763
805
|
// Indicate there was work
|
|
764
806
|
return true;
|
|
765
807
|
} else {
|
|
766
808
|
console.log('[LinkedInAutomation] No actions to execute, continuing to poll...');
|
|
809
|
+
// Reset failure counter on successful request (even with no actions)
|
|
810
|
+
this.consecutiveAuthFailures = 0;
|
|
811
|
+
this.authFailurePaused = false;
|
|
767
812
|
return false;
|
|
768
813
|
}
|
|
814
|
+
} else if (response.status === 401) {
|
|
815
|
+
// Handle 401 Unauthorized specifically
|
|
816
|
+
const errorText = await response.text();
|
|
817
|
+
let errorMessage;
|
|
818
|
+
try {
|
|
819
|
+
const errorJson = JSON.parse(errorText);
|
|
820
|
+
errorMessage = errorJson.message || 'Unauthorized';
|
|
821
|
+
} catch {
|
|
822
|
+
errorMessage = errorText || 'Unauthorized';
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
console.error('[LinkedInAutomation] Authentication failed (401 Unauthorized):', errorMessage);
|
|
826
|
+
|
|
827
|
+
// Clear current token since it's invalid
|
|
828
|
+
this._currentAccessToken = null;
|
|
829
|
+
|
|
830
|
+
// Try to re-sync tokens one more time
|
|
831
|
+
console.log('[LinkedInAutomation] Attempting token re-sync after 401 error...');
|
|
832
|
+
const resynced = await this.attemptTokenResync();
|
|
833
|
+
|
|
834
|
+
if (!resynced) {
|
|
835
|
+
this.handleAuthFailure(`401 Unauthorized: ${errorMessage}`);
|
|
836
|
+
} else {
|
|
837
|
+
// If re-sync worked, try to refresh the token
|
|
838
|
+
if (this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
|
|
839
|
+
const refreshResult = await this.cloudAuth.refreshTokens(this._currentRefreshToken);
|
|
840
|
+
if (!refreshResult.success) {
|
|
841
|
+
this.handleAuthFailure(`401 Unauthorized: Token re-sync succeeded but refresh failed`);
|
|
842
|
+
} else {
|
|
843
|
+
this._currentAccessToken = refreshResult.tokens.idToken;
|
|
844
|
+
if (refreshResult.tokens.refreshToken) {
|
|
845
|
+
this._currentRefreshToken = refreshResult.tokens.refreshToken;
|
|
846
|
+
}
|
|
847
|
+
console.log('[LinkedInAutomation] Token re-sync and refresh succeeded after 401, resetting failure counter');
|
|
848
|
+
this.consecutiveAuthFailures = 0;
|
|
849
|
+
this.authFailurePaused = false;
|
|
850
|
+
}
|
|
851
|
+
} else {
|
|
852
|
+
console.log('[LinkedInAutomation] Token re-sync succeeded after 401, resetting failure counter');
|
|
853
|
+
this.consecutiveAuthFailures = 0;
|
|
854
|
+
this.authFailurePaused = false;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return false;
|
|
769
859
|
} else {
|
|
770
860
|
console.error('[LinkedInAutomation] Failed to fetch actions. Status:', response.status, response.statusText);
|
|
771
861
|
const errorText = await response.text();
|
|
772
862
|
console.error('[LinkedInAutomation] Error response:', errorText);
|
|
863
|
+
// Non-auth errors don't count as auth failures
|
|
773
864
|
return false;
|
|
774
865
|
}
|
|
775
866
|
} catch (error) {
|
|
776
867
|
console.error('[LinkedInAutomation] Error polling for actions:', error.message);
|
|
777
868
|
console.error('[LinkedInAutomation] Full error:', error);
|
|
869
|
+
|
|
870
|
+
// Check if error is auth-related
|
|
871
|
+
if (error.message.includes('401') || error.message.includes('Unauthorized') || error.message.includes('token')) {
|
|
872
|
+
this.handleAuthFailure(`Network error: ${error.message}`);
|
|
873
|
+
}
|
|
874
|
+
|
|
778
875
|
return false;
|
|
779
876
|
}
|
|
780
877
|
}
|
|
781
878
|
|
|
879
|
+
/**
|
|
880
|
+
* Handle authentication failure - track consecutive failures and pause polling if threshold reached
|
|
881
|
+
* @param {string} reason - Reason for the auth failure
|
|
882
|
+
*/
|
|
883
|
+
handleAuthFailure(reason) {
|
|
884
|
+
this.consecutiveAuthFailures++;
|
|
885
|
+
this.lastAuthFailureTime = new Date().toISOString();
|
|
886
|
+
|
|
887
|
+
console.warn(`[LinkedInAutomation] Authentication failure #${this.consecutiveAuthFailures}: ${reason}`);
|
|
888
|
+
|
|
889
|
+
if (this.consecutiveAuthFailures >= this.maxAuthFailures) {
|
|
890
|
+
if (!this.authFailurePaused) {
|
|
891
|
+
this.authFailurePaused = true;
|
|
892
|
+
console.error(`[LinkedInAutomation] ⚠️ STOPPING polling after ${this.consecutiveAuthFailures} consecutive authentication failures`);
|
|
893
|
+
console.error('[LinkedInAutomation] Polling will resume when authentication is restored.');
|
|
894
|
+
console.error('[LinkedInAutomation] Please re-authenticate or check your session.');
|
|
895
|
+
|
|
896
|
+
// Emit event to notify UI
|
|
897
|
+
if (this.io) {
|
|
898
|
+
this.io.emit('automation:auth-failure', {
|
|
899
|
+
message: `Authentication failed after ${this.consecutiveAuthFailures} attempts. Polling paused.`,
|
|
900
|
+
paused: true,
|
|
901
|
+
consecutiveFailures: this.consecutiveAuthFailures,
|
|
902
|
+
lastFailureTime: this.lastAuthFailureTime,
|
|
903
|
+
reason: reason
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
} else {
|
|
908
|
+
console.warn(`[LinkedInAutomation] Will pause polling after ${this.maxAuthFailures - this.consecutiveAuthFailures} more failures`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
782
912
|
async executeAction(action) {
|
|
783
913
|
const actionLogs = [];
|
|
784
914
|
const originalConsoleLog = console.log;
|
|
@@ -858,10 +988,6 @@ export class LinkedInAutomation {
|
|
|
858
988
|
console.log('[LinkedInAutomation] Executing get_linkedin_search_results action...');
|
|
859
989
|
result = await executeLinkedInSearch(this.page, action);
|
|
860
990
|
break;
|
|
861
|
-
case 'get_daily_linkedin_connections':
|
|
862
|
-
console.log('[LinkedInAutomation] Executing get_daily_linkedin_connections action...');
|
|
863
|
-
result = await executeGetDailyLinkedInConnections(this.page, action);
|
|
864
|
-
break;
|
|
865
991
|
case 'get_new_messages':
|
|
866
992
|
console.log('[LinkedInAutomation] Executing get_new_messages action...');
|
|
867
993
|
result = await executeGetNewMessages(this.page, action, await this.getAccessToken());
|
|
@@ -1053,11 +1179,64 @@ export class LinkedInAutomation {
|
|
|
1053
1179
|
}
|
|
1054
1180
|
}
|
|
1055
1181
|
|
|
1182
|
+
/**
|
|
1183
|
+
* Set a callback function to re-sync tokens from external source (e.g., session store)
|
|
1184
|
+
* @param {Function} callback - Function that returns Promise<{idToken, refreshToken}> or null
|
|
1185
|
+
*/
|
|
1186
|
+
setTokenResyncCallback(callback) {
|
|
1187
|
+
this.tokenResyncCallback = callback;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* Attempt to re-sync tokens from external source (session, etc.)
|
|
1192
|
+
* @returns {Promise<boolean>} true if tokens were successfully re-synced
|
|
1193
|
+
*/
|
|
1194
|
+
async attemptTokenResync() {
|
|
1195
|
+
if (!this.tokenResyncCallback) {
|
|
1196
|
+
return false;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
try {
|
|
1200
|
+
const tokens = await this.tokenResyncCallback();
|
|
1201
|
+
if (tokens && tokens.idToken && tokens.refreshToken) {
|
|
1202
|
+
console.log('[LinkedInAutomation] Tokens re-synced from external source');
|
|
1203
|
+
this._currentAccessToken = tokens.idToken;
|
|
1204
|
+
this._currentRefreshToken = tokens.refreshToken;
|
|
1205
|
+
|
|
1206
|
+
// If tokens are expired, try to refresh them
|
|
1207
|
+
if (this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
|
|
1208
|
+
console.log('[LinkedInAutomation] Re-synced tokens are expired, attempting refresh...');
|
|
1209
|
+
const result = await this.cloudAuth.refreshTokens(this._currentRefreshToken);
|
|
1210
|
+
if (result.success) {
|
|
1211
|
+
this._currentAccessToken = result.tokens.idToken;
|
|
1212
|
+
if (result.tokens.refreshToken) {
|
|
1213
|
+
this._currentRefreshToken = result.tokens.refreshToken;
|
|
1214
|
+
}
|
|
1215
|
+
} else {
|
|
1216
|
+
console.error('[LinkedInAutomation] Failed to refresh re-synced tokens:', result.error);
|
|
1217
|
+
return false;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return true;
|
|
1222
|
+
}
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
console.error('[LinkedInAutomation] Error during token re-sync:', error.message);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
return false;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1056
1230
|
async getAccessToken() {
|
|
1057
|
-
//
|
|
1231
|
+
// First, try to get current token
|
|
1058
1232
|
if (!this._currentAccessToken) {
|
|
1059
|
-
console.
|
|
1060
|
-
|
|
1233
|
+
console.log('[LinkedInAutomation] No access token available, attempting re-sync...');
|
|
1234
|
+
const resynced = await this.attemptTokenResync();
|
|
1235
|
+
if (!resynced) {
|
|
1236
|
+
console.error('[LinkedInAutomation] No access token available and re-sync failed');
|
|
1237
|
+
return null;
|
|
1238
|
+
}
|
|
1239
|
+
// Token was re-synced, continue to validation below
|
|
1061
1240
|
}
|
|
1062
1241
|
|
|
1063
1242
|
// Check if token is expired or expiring soon (within 5 minutes)
|
|
@@ -1066,8 +1245,12 @@ export class LinkedInAutomation {
|
|
|
1066
1245
|
|
|
1067
1246
|
// Check if refresh token is available
|
|
1068
1247
|
if (!this._currentRefreshToken) {
|
|
1069
|
-
console.
|
|
1070
|
-
|
|
1248
|
+
console.log('[LinkedInAutomation] No refresh token available, attempting re-sync...');
|
|
1249
|
+
const resynced = await this.attemptTokenResync();
|
|
1250
|
+
if (!resynced || !this._currentRefreshToken) {
|
|
1251
|
+
console.error('[LinkedInAutomation] No refresh token available and re-sync failed');
|
|
1252
|
+
return null;
|
|
1253
|
+
}
|
|
1071
1254
|
}
|
|
1072
1255
|
|
|
1073
1256
|
// Attempt to refresh the token
|
|
@@ -1081,17 +1264,48 @@ export class LinkedInAutomation {
|
|
|
1081
1264
|
if (result.tokens.refreshToken) {
|
|
1082
1265
|
this._currentRefreshToken = result.tokens.refreshToken;
|
|
1083
1266
|
}
|
|
1267
|
+
// Reset failure counter on successful refresh
|
|
1268
|
+
this.consecutiveAuthFailures = 0;
|
|
1269
|
+
this.authFailurePaused = false;
|
|
1084
1270
|
return this._currentAccessToken;
|
|
1085
1271
|
} else {
|
|
1086
1272
|
console.error('[LinkedInAutomation] Token refresh failed:', result.error);
|
|
1273
|
+
|
|
1274
|
+
// Try one more time to re-sync before giving up
|
|
1275
|
+
console.log('[LinkedInAutomation] Attempting token re-sync after refresh failure...');
|
|
1276
|
+
const resynced = await this.attemptTokenResync();
|
|
1277
|
+
if (resynced && !this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
|
|
1278
|
+
console.log('[LinkedInAutomation] Token re-synced successfully after refresh failure');
|
|
1279
|
+
this.consecutiveAuthFailures = 0;
|
|
1280
|
+
this.authFailurePaused = false;
|
|
1281
|
+
return this._currentAccessToken;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1087
1284
|
return null;
|
|
1088
1285
|
}
|
|
1089
1286
|
} catch (error) {
|
|
1090
1287
|
console.error('[LinkedInAutomation] Error during token refresh:', error.message);
|
|
1288
|
+
|
|
1289
|
+
// Try re-sync one more time
|
|
1290
|
+
const resynced = await this.attemptTokenResync();
|
|
1291
|
+
if (resynced && !this.cloudAuth.isTokenExpired(this._currentAccessToken)) {
|
|
1292
|
+
console.log('[LinkedInAutomation] Token re-synced successfully after refresh error');
|
|
1293
|
+
this.consecutiveAuthFailures = 0;
|
|
1294
|
+
this.authFailurePaused = false;
|
|
1295
|
+
return this._currentAccessToken;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1091
1298
|
return null;
|
|
1092
1299
|
}
|
|
1093
1300
|
}
|
|
1094
1301
|
|
|
1302
|
+
// Token is valid, reset failure counter
|
|
1303
|
+
if (this.consecutiveAuthFailures > 0) {
|
|
1304
|
+
console.log('[LinkedInAutomation] Authentication recovered, resetting failure counter');
|
|
1305
|
+
this.consecutiveAuthFailures = 0;
|
|
1306
|
+
this.authFailurePaused = false;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1095
1309
|
return this._currentAccessToken;
|
|
1096
1310
|
}
|
|
1097
1311
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coffeeinabit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.48",
|
|
4
4
|
"description": "coffeeinabit app",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"axios": "^1.6.0",
|
|
25
|
+
"coffeeinabit": "^0.0.47",
|
|
25
26
|
"dotenv": "^16.3.1",
|
|
26
27
|
"express": "^4.18.2",
|
|
27
28
|
"express-session": "^1.17.3",
|
package/server.js
CHANGED
|
@@ -17,6 +17,7 @@ import { createAuthRoutes } from './routes/auth.js';
|
|
|
17
17
|
import { createAutomationRoutes, createStateRoutes } from './routes/automation.js';
|
|
18
18
|
import { setupSocketHandlers } from './socket/handlers.js';
|
|
19
19
|
import { tryAutoStartAutomation } from './helpers/auto_start.js';
|
|
20
|
+
import { checkAndUpdateVersion } from './helpers/version_check.js';
|
|
20
21
|
|
|
21
22
|
dotenv.config();
|
|
22
23
|
|
|
@@ -36,9 +37,53 @@ const appState = new AppState();
|
|
|
36
37
|
// Legacy single automation instance (for backward compatibility during migration)
|
|
37
38
|
// TODO: Remove after full migration to multi-process state
|
|
38
39
|
const linkedinAutomation = new LinkedInAutomation(io);
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
|
|
41
|
+
// Set up token re-sync callback for legacy automation instance
|
|
42
|
+
// This allows the automation to re-sync tokens from the session store when they expire
|
|
43
|
+
linkedinAutomation.setTokenResyncCallback(async () => {
|
|
44
|
+
try {
|
|
45
|
+
// Try to get tokens from session store for the user
|
|
46
|
+
if (!linkedinAutomation._currentUserEmail) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Get all sessions and find one for this user
|
|
51
|
+
if (typeof sessionStore.all === 'function') {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
sessionStore.all((err, sessions) => {
|
|
54
|
+
if (err || !sessions) {
|
|
55
|
+
resolve(null);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Find a session for this user
|
|
60
|
+
for (const sessionId in sessions) {
|
|
61
|
+
const session = sessions[sessionId];
|
|
62
|
+
if (session && session.user && session.user.email === linkedinAutomation._currentUserEmail) {
|
|
63
|
+
// Ensure tokens are valid
|
|
64
|
+
cloudAuth.ensureValidToken(session).then(result => {
|
|
65
|
+
if (result.valid && session.tokens) {
|
|
66
|
+
resolve({
|
|
67
|
+
idToken: session.tokens.idToken,
|
|
68
|
+
refreshToken: session.tokens.refreshToken
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
resolve(null);
|
|
72
|
+
}
|
|
73
|
+
}).catch(() => resolve(null));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
resolve(null);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('[Server] Error in token re-sync callback:', error.message);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
});
|
|
42
87
|
|
|
43
88
|
// Initialize session store
|
|
44
89
|
const FileStoreSession = FileStore(session);
|
|
@@ -94,6 +139,9 @@ app.use('/auth', createAuthRoutes(cloudAuth, PORT, appState, linkedinAutomation)
|
|
|
94
139
|
app.use('/api/automation', createAutomationRoutes(cloudAuth, appState, linkedinAutomation, io));
|
|
95
140
|
app.use('/api/state', createStateRoutes(cloudAuth, appState));
|
|
96
141
|
|
|
142
|
+
// Set up token re-sync dependencies for AppState
|
|
143
|
+
appState.setTokenResyncDependencies(cloudAuth, sessionStore);
|
|
144
|
+
|
|
97
145
|
// Setup WebSocket handlers
|
|
98
146
|
setupSocketHandlers(io, appState);
|
|
99
147
|
|
|
@@ -104,6 +152,11 @@ server.listen(PORT, async () => {
|
|
|
104
152
|
console.log(' Press Ctrl+C to stop');
|
|
105
153
|
console.log('');
|
|
106
154
|
|
|
155
|
+
// Check for updates on startup (non-blocking)
|
|
156
|
+
checkAndUpdateVersion('coffeeinabit', true).catch(err => {
|
|
157
|
+
console.error('[Server] Version check failed:', err.message);
|
|
158
|
+
});
|
|
159
|
+
|
|
107
160
|
// Wait a bit for everything to initialize, then try to auto-start automation
|
|
108
161
|
setTimeout(() => {
|
|
109
162
|
tryAutoStartAutomation(sessionStore, contextDir, cloudAuth, linkedinAutomation);
|
package/state/app_state.js
CHANGED
|
@@ -18,6 +18,10 @@ export class AppState {
|
|
|
18
18
|
// Private state: Map of socketId -> process state
|
|
19
19
|
this._processes = new Map();
|
|
20
20
|
|
|
21
|
+
// Dependencies for token re-sync (set externally)
|
|
22
|
+
this._cloudAuth = null;
|
|
23
|
+
this._sessionStore = null;
|
|
24
|
+
|
|
21
25
|
// Statistics
|
|
22
26
|
this._stats = {
|
|
23
27
|
totalProcessesCreated: 0,
|
|
@@ -26,6 +30,16 @@ export class AppState {
|
|
|
26
30
|
};
|
|
27
31
|
}
|
|
28
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Set dependencies for token re-sync
|
|
35
|
+
* @param {object} cloudAuth - CloudAuth instance
|
|
36
|
+
* @param {object} sessionStore - Session store instance
|
|
37
|
+
*/
|
|
38
|
+
setTokenResyncDependencies(cloudAuth, sessionStore) {
|
|
39
|
+
this._cloudAuth = cloudAuth;
|
|
40
|
+
this._sessionStore = sessionStore;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
/**
|
|
30
44
|
* Create a new automation process for a socket connection
|
|
31
45
|
* @param {string} socketId - WebSocket connection ID
|
|
@@ -40,9 +54,54 @@ export class AppState {
|
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
const automation = new LinkedInAutomation(io);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
57
|
+
|
|
58
|
+
// Set up token re-sync callback if dependencies are available
|
|
59
|
+
if (this._cloudAuth && this._sessionStore) {
|
|
60
|
+
const userEmail = session?.user?.email;
|
|
61
|
+
automation.setTokenResyncCallback(async () => {
|
|
62
|
+
try {
|
|
63
|
+
if (!userEmail) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Try to get tokens from session store for this user
|
|
68
|
+
if (typeof this._sessionStore.all === 'function') {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
this._sessionStore.all((err, sessions) => {
|
|
71
|
+
if (err || !sessions) {
|
|
72
|
+
resolve(null);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Find a session for this user
|
|
77
|
+
for (const sessionId in sessions) {
|
|
78
|
+
const sess = sessions[sessionId];
|
|
79
|
+
if (sess && sess.user && sess.user.email === userEmail) {
|
|
80
|
+
// Ensure tokens are valid
|
|
81
|
+
this._cloudAuth.ensureValidToken(sess).then(result => {
|
|
82
|
+
if (result.valid && sess.tokens) {
|
|
83
|
+
resolve({
|
|
84
|
+
idToken: sess.tokens.idToken,
|
|
85
|
+
refreshToken: sess.tokens.refreshToken
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
resolve(null);
|
|
89
|
+
}
|
|
90
|
+
}).catch(() => resolve(null));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
resolve(null);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('[AppState] Error in token re-sync callback:', error.message);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
46
105
|
|
|
47
106
|
const processState = {
|
|
48
107
|
socketId,
|