coffeeinabit 0.0.5 → 0.0.7
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 +87 -20
- package/package.json +7 -2
- package/public/dashboard.html +31 -7
- package/server.js +20 -0
- package/tools/get_daily_linkedin_connections.js +148 -33
- package/tools/get_linkedin_search_results.js +56 -56
- package/tools/get_messages.js +297 -31
package/linkedin_automation.js
CHANGED
|
@@ -44,6 +44,7 @@ export class LinkedInAutomation {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
async saveLinkedInSession() {
|
|
47
|
+
console.warn('[LinkedInAutomation] saveLinkedInSession is deprecated - using persistent context instead');
|
|
47
48
|
try {
|
|
48
49
|
if (!this.context || !this.page) {
|
|
49
50
|
console.error('[LinkedInAutomation] Cannot save session - no context or page');
|
|
@@ -91,6 +92,7 @@ export class LinkedInAutomation {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
async loadLinkedInSession() {
|
|
95
|
+
console.warn('[LinkedInAutomation] loadLinkedInSession is deprecated - using persistent context instead');
|
|
94
96
|
try {
|
|
95
97
|
const sessionPath = this.getLinkedInSessionPath();
|
|
96
98
|
|
|
@@ -130,6 +132,7 @@ export class LinkedInAutomation {
|
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
async restoreLocalStorage() {
|
|
135
|
+
console.warn('[LinkedInAutomation] restoreLocalStorage is deprecated - using persistent context instead');
|
|
133
136
|
try {
|
|
134
137
|
const sessionPath = this.getLinkedInSessionPath();
|
|
135
138
|
|
|
@@ -322,8 +325,7 @@ export class LinkedInAutomation {
|
|
|
322
325
|
if (secondCheck.includes('/feed')) {
|
|
323
326
|
console.log('[LinkedInAutomation] Second check confirmed - still on /feed/');
|
|
324
327
|
|
|
325
|
-
console.log('[LinkedInAutomation]
|
|
326
|
-
await this.saveLinkedInSession();
|
|
328
|
+
console.log('[LinkedInAutomation] Session will be persisted automatically by launchPersistentContext...');
|
|
327
329
|
|
|
328
330
|
this.status = 'linkedin_logged_in';
|
|
329
331
|
this.emitStatus();
|
|
@@ -384,10 +386,6 @@ export class LinkedInAutomation {
|
|
|
384
386
|
if (this.context) {
|
|
385
387
|
await this.context.close();
|
|
386
388
|
this.context = null;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (this.browser) {
|
|
390
|
-
await this.browser.close();
|
|
391
389
|
this.browser = null;
|
|
392
390
|
}
|
|
393
391
|
|
|
@@ -696,39 +694,108 @@ export class LinkedInAutomation {
|
|
|
696
694
|
}
|
|
697
695
|
|
|
698
696
|
async launchBrowserAndLogin() {
|
|
699
|
-
console.log('[LinkedInAutomation] Launching Firefox for LinkedIn login...');
|
|
697
|
+
console.log('[LinkedInAutomation] Launching Firefox with persistent context for LinkedIn login...');
|
|
698
|
+
|
|
699
|
+
this.userEmail = this._currentUserEmail || 'default_user';
|
|
700
|
+
const sanitizedEmail = this.userEmail.replace(/[^a-zA-Z0-9]/g, '_');
|
|
701
|
+
const userDataPath = path.join(this.linkedInSessionDir, `linkedin_auth_${sanitizedEmail}`);
|
|
700
702
|
|
|
701
|
-
|
|
703
|
+
console.log('[LinkedInAutomation] User data path:', userDataPath);
|
|
704
|
+
|
|
705
|
+
if (!fs.existsSync(userDataPath)) {
|
|
706
|
+
fs.mkdirSync(userDataPath, { recursive: true });
|
|
707
|
+
console.log('[LinkedInAutomation] Created user data directory:', userDataPath);
|
|
708
|
+
}
|
|
702
709
|
|
|
703
|
-
|
|
710
|
+
const launchOptions = {
|
|
711
|
+
headless: this.headless || false,
|
|
704
712
|
viewport: null,
|
|
705
713
|
ignoreHTTPSErrors: true
|
|
706
|
-
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
this.context = await firefox.launchPersistentContext(userDataPath, launchOptions);
|
|
707
717
|
|
|
718
|
+
console.log('[LinkedInAutomation] Persistent context launched successfully');
|
|
719
|
+
|
|
720
|
+
this.browser = this.context;
|
|
721
|
+
|
|
708
722
|
await this.enablePerformanceMode();
|
|
709
723
|
this.page = await this.context.newPage();
|
|
710
724
|
|
|
711
|
-
|
|
725
|
+
console.log('[LinkedInAutomation] Checking if user is already authenticated...');
|
|
712
726
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
console.log('[LinkedInAutomation] No saved session, starting fresh login...');
|
|
727
|
+
const isAuthenticated = await this.checkAndVerifySession();
|
|
728
|
+
|
|
729
|
+
if (!isAuthenticated) {
|
|
730
|
+
console.log('[LinkedInAutomation] Not authenticated, starting fresh login...');
|
|
718
731
|
await this.startLinkedInLogin();
|
|
719
732
|
}
|
|
720
733
|
}
|
|
721
734
|
|
|
735
|
+
async updateBrowserVisibility(keepBrowser) {
|
|
736
|
+
if (!this.browser || !this.isRunning) {
|
|
737
|
+
console.log('[LinkedInAutomation] Browser not running, cannot update visibility');
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
const newHeadless = !keepBrowser;
|
|
743
|
+
|
|
744
|
+
if (this.headless === newHeadless) {
|
|
745
|
+
console.log('[LinkedInAutomation] Browser visibility already matches setting');
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
console.log(`[LinkedInAutomation] Updating browser visibility: ${keepBrowser ? 'visible' : 'headless'}`);
|
|
750
|
+
|
|
751
|
+
const currentUrl = this.page.url();
|
|
752
|
+
const currentTitle = await this.page.title();
|
|
753
|
+
const sanitizedEmail = this.userEmail.replace(/[^a-zA-Z0-9]/g, '_');
|
|
754
|
+
const userDataPath = path.join(this.linkedInSessionDir, `linkedin_auth_${sanitizedEmail}`);
|
|
755
|
+
|
|
756
|
+
await this.context.close();
|
|
757
|
+
|
|
758
|
+
this.headless = newHeadless;
|
|
759
|
+
|
|
760
|
+
this.context = await firefox.launchPersistentContext(userDataPath, {
|
|
761
|
+
headless: this.headless,
|
|
762
|
+
viewport: null,
|
|
763
|
+
ignoreHTTPSErrors: true
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
this.browser = this.context;
|
|
767
|
+
|
|
768
|
+
await this.enablePerformanceMode();
|
|
769
|
+
this.page = await this.context.newPage();
|
|
770
|
+
|
|
771
|
+
await this.page.goto(currentUrl, {
|
|
772
|
+
waitUntil: 'domcontentloaded',
|
|
773
|
+
timeout: 30000
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
this.setupPageListeners();
|
|
777
|
+
|
|
778
|
+
console.log('[LinkedInAutomation] Browser visibility updated successfully');
|
|
779
|
+
|
|
780
|
+
this.io.emit('automation_status', {
|
|
781
|
+
status: 'running',
|
|
782
|
+
message: `Browser is now ${keepBrowser ? 'visible' : 'running in headless mode'}`
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
} catch (error) {
|
|
786
|
+
console.error('[LinkedInAutomation] Failed to update browser visibility:', error);
|
|
787
|
+
throw error;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
722
791
|
async checkAndVerifySession() {
|
|
723
792
|
try {
|
|
724
|
-
console.log('[LinkedInAutomation] Navigating to LinkedIn feed to verify session...');
|
|
793
|
+
console.log('[LinkedInAutomation] Navigating to LinkedIn feed to verify persistent session...');
|
|
725
794
|
await this.page.goto('https://www.linkedin.com/feed/', {
|
|
726
795
|
waitUntil: 'domcontentloaded',
|
|
727
796
|
timeout: 30000
|
|
728
797
|
});
|
|
729
798
|
|
|
730
|
-
await this.restoreLocalStorage();
|
|
731
|
-
|
|
732
799
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
733
800
|
|
|
734
801
|
let verifyAttempts = 0;
|
|
@@ -739,7 +806,7 @@ export class LinkedInAutomation {
|
|
|
739
806
|
console.log(`[LinkedInAutomation] Verification attempt ${verifyAttempts + 1}/${maxVerifyAttempts}, URL: ${this.currentUrl}`);
|
|
740
807
|
|
|
741
808
|
if (this.currentUrl.includes('/feed')) {
|
|
742
|
-
console.log('[LinkedInAutomation] Session verified! Already logged in.');
|
|
809
|
+
console.log('[LinkedInAutomation] Session verified! Already logged in via persistent context.');
|
|
743
810
|
this.status = 'linkedin_logged_in';
|
|
744
811
|
this.emitStatus();
|
|
745
812
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coffeeinabit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "CoffeeInABit App",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"keywords": [
|
|
7
|
+
"keywords": [
|
|
8
|
+
"automation",
|
|
9
|
+
"linkedin",
|
|
10
|
+
"windows",
|
|
11
|
+
"playwright"
|
|
12
|
+
],
|
|
8
13
|
"author": "Azam Alamov <azam.alamov@icloud.com>",
|
|
9
14
|
"license": "MIT",
|
|
10
15
|
"bin": {
|
package/public/dashboard.html
CHANGED
|
@@ -205,16 +205,40 @@
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
function toggleKeepBrowser() {
|
|
208
|
+
async function toggleKeepBrowser() {
|
|
209
209
|
const keepBrowserToggle = document.getElementById('keepBrowserToggle');
|
|
210
210
|
const isKeepBrowser = keepBrowserToggle.checked;
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
212
|
+
localStorage.setItem('keepBrowser', isKeepBrowser.toString());
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const response = await fetch('/api/automation/settings', {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
headers: {
|
|
218
|
+
'Content-Type': 'application/json'
|
|
219
|
+
},
|
|
220
|
+
body: JSON.stringify({
|
|
221
|
+
keepBrowser: isKeepBrowser
|
|
222
|
+
})
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const data = await response.json();
|
|
226
|
+
|
|
227
|
+
if (data.success) {
|
|
228
|
+
if (isKeepBrowser) {
|
|
229
|
+
showAlert('success', 'Browser is now visible');
|
|
230
|
+
} else {
|
|
231
|
+
showAlert('success', 'Browser is now running in headless mode');
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
showAlert('error', data.error || 'Failed to update browser setting');
|
|
235
|
+
keepBrowserToggle.checked = !isKeepBrowser;
|
|
236
|
+
localStorage.setItem('keepBrowser', (!isKeepBrowser).toString());
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
showAlert('error', 'Error updating browser setting: ' + error.message);
|
|
240
|
+
keepBrowserToggle.checked = !isKeepBrowser;
|
|
241
|
+
localStorage.setItem('keepBrowser', (!isKeepBrowser).toString());
|
|
218
242
|
}
|
|
219
243
|
}
|
|
220
244
|
|
package/server.js
CHANGED
|
@@ -173,6 +173,26 @@ app.get('/api/automation/status', (req, res) => {
|
|
|
173
173
|
res.json(linkedinAutomation.getStatus());
|
|
174
174
|
});
|
|
175
175
|
|
|
176
|
+
app.post('/api/automation/settings', async (req, res) => {
|
|
177
|
+
if (!cloudAuth.isAuthenticated(req.session)) {
|
|
178
|
+
return res.status(401).json({ error: 'Authentication required' });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const { keepBrowser } = req.body;
|
|
183
|
+
|
|
184
|
+
if (typeof keepBrowser !== 'boolean') {
|
|
185
|
+
return res.status(400).json({ error: 'keepBrowser must be a boolean' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
await linkedinAutomation.updateBrowserVisibility(keepBrowser);
|
|
189
|
+
res.json({ success: true, message: 'Browser visibility setting updated' });
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.error('[Server] Failed to update browser settings:', error);
|
|
192
|
+
res.status(500).json({ error: error.message });
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
176
196
|
io.on('connection', (socket) => {
|
|
177
197
|
console.log('[Server] Client connected:', socket.id);
|
|
178
198
|
|
|
@@ -25,38 +25,43 @@ export async function executeGetDailyLinkedInConnections(page, action) {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
const now = Date.now();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return null;
|
|
28
|
+
let allConnectionsMap = new Map();
|
|
29
|
+
let shouldStopScrolling = false;
|
|
30
|
+
let noScrollAttempts = 0;
|
|
31
|
+
const maxNoScrollAttempts = 2;
|
|
32
|
+
|
|
33
|
+
while (!shouldStopScrolling) {
|
|
34
|
+
const currentConnections = await page.evaluate(() => {
|
|
35
|
+
function parseConnectionDate(str) {
|
|
36
|
+
if (!str) return null;
|
|
37
|
+
const match = str.match(/Connected on (.+)/i);
|
|
38
|
+
if (!match) return null;
|
|
39
|
+
const dateStr = match[1].trim();
|
|
40
|
+
try {
|
|
41
|
+
const timestamp = new Date(dateStr).getTime();
|
|
42
|
+
return isNaN(timestamp) ? null : timestamp;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
39
46
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (!lastCheckTimestamp || (connectionTimestamp && connectionTimestamp >= lastCheckTimestamp)) {
|
|
47
|
+
|
|
48
|
+
const connections = [];
|
|
49
|
+
const allParagraphs = document.querySelectorAll('p');
|
|
50
|
+
|
|
51
|
+
allParagraphs.forEach(p => {
|
|
52
|
+
const text = p.textContent.trim();
|
|
53
|
+
if (text.startsWith('Connected on')) {
|
|
54
|
+
const parentDiv = p.parentElement;
|
|
55
|
+
if (parentDiv) {
|
|
56
|
+
const profileLink = parentDiv.querySelector('a[href*="/in/"]');
|
|
57
|
+
if (profileLink) {
|
|
58
|
+
const href = profileLink.getAttribute('href');
|
|
59
|
+
const usernameMatch = href.match(/\/in\/([^/?]+)/);
|
|
60
|
+
if (usernameMatch) {
|
|
61
|
+
const username = usernameMatch[1];
|
|
62
|
+
const connectionDate = text;
|
|
63
|
+
const connectionTimestamp = parseConnectionDate(connectionDate);
|
|
64
|
+
|
|
60
65
|
connections.push({
|
|
61
66
|
username: username,
|
|
62
67
|
connectionDate: connectionDate,
|
|
@@ -66,11 +71,121 @@ export async function executeGetDailyLinkedInConnections(page, action) {
|
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return connections;
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
currentConnections.forEach(conn => {
|
|
80
|
+
allConnectionsMap.set(conn.username, conn);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
console.log(`[get_daily_linkedin_connections] Current page connections: ${currentConnections.length}, Total unique: ${allConnectionsMap.size}`);
|
|
84
|
+
|
|
85
|
+
if (lastCheckTimestamp && currentConnections.length > 0) {
|
|
86
|
+
const oldestConnection = currentConnections.reduce((oldest, current) => {
|
|
87
|
+
if (!oldest.connectionTimestamp) return current;
|
|
88
|
+
if (!current.connectionTimestamp) return oldest;
|
|
89
|
+
return current.connectionTimestamp < oldest.connectionTimestamp ? current : oldest;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (oldestConnection.connectionTimestamp && oldestConnection.connectionTimestamp < lastCheckTimestamp) {
|
|
93
|
+
console.log('[get_daily_linkedin_connections] Reached connections older than last check, stopping');
|
|
94
|
+
shouldStopScrolling = true;
|
|
95
|
+
break;
|
|
69
96
|
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const scrollInfoBefore = await page.evaluate(() => {
|
|
100
|
+
const main = document.querySelector('main');
|
|
101
|
+
if (main) {
|
|
102
|
+
return {
|
|
103
|
+
scrollTop: main.scrollTop,
|
|
104
|
+
scrollHeight: main.scrollHeight,
|
|
105
|
+
clientHeight: main.clientHeight
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
scrollTop: window.pageYOffset || document.documentElement.scrollTop,
|
|
110
|
+
scrollHeight: document.documentElement.scrollHeight,
|
|
111
|
+
clientHeight: window.innerHeight
|
|
112
|
+
};
|
|
70
113
|
});
|
|
71
114
|
|
|
72
|
-
|
|
73
|
-
|
|
115
|
+
console.log(`[get_daily_linkedin_connections] Before scroll - Top: ${scrollInfoBefore.scrollTop}px, Height: ${scrollInfoBefore.scrollHeight}px`);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
console.log('[get_daily_linkedin_connections] Scrolling main element...');
|
|
119
|
+
|
|
120
|
+
const mainElement = await page.locator('main').first();
|
|
121
|
+
const containerExists = await mainElement.count();
|
|
122
|
+
|
|
123
|
+
if (containerExists > 0) {
|
|
124
|
+
await mainElement.evaluate(el => {
|
|
125
|
+
console.log('Main element found, scrolling...');
|
|
126
|
+
console.log('Main scrollHeight:', el.scrollHeight);
|
|
127
|
+
console.log('Main clientHeight:', el.clientHeight);
|
|
128
|
+
console.log('Main scrollTop before:', el.scrollTop);
|
|
129
|
+
|
|
130
|
+
el.scrollBy({ top: 2000, behavior: 'smooth' });
|
|
131
|
+
|
|
132
|
+
console.log('Main scrollTop after:', el.scrollTop);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
console.log('[get_daily_linkedin_connections] Scroll completed');
|
|
136
|
+
} else {
|
|
137
|
+
console.log('[get_daily_linkedin_connections] Main element not found');
|
|
138
|
+
}
|
|
139
|
+
console.log('[get_daily_linkedin_connections] Waiting for content to load...');
|
|
140
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
141
|
+
} catch (e) {
|
|
142
|
+
console.log(`[get_daily_linkedin_connections] Scroll failed: ${e.message}`);
|
|
143
|
+
console.log('Stack trace:', e.stack);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const scrollInfoAfter = await page.evaluate(() => {
|
|
147
|
+
const main = document.querySelector('main');
|
|
148
|
+
if (main) {
|
|
149
|
+
return {
|
|
150
|
+
scrollTop: main.scrollTop,
|
|
151
|
+
scrollHeight: main.scrollHeight,
|
|
152
|
+
clientHeight: main.clientHeight
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
scrollTop: window.pageYOffset || document.documentElement.scrollTop,
|
|
157
|
+
scrollHeight: document.documentElement.scrollHeight,
|
|
158
|
+
clientHeight: window.innerHeight
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
console.log(`[get_daily_linkedin_connections] After scroll - Top: ${scrollInfoAfter.scrollTop}px, Height: ${scrollInfoAfter.scrollHeight}px`);
|
|
163
|
+
|
|
164
|
+
const scrolled = scrollInfoAfter.scrollTop > scrollInfoBefore.scrollTop;
|
|
165
|
+
const heightIncreased = scrollInfoAfter.scrollHeight > scrollInfoBefore.scrollHeight;
|
|
166
|
+
|
|
167
|
+
if (scrolled || heightIncreased) {
|
|
168
|
+
console.log('[get_daily_linkedin_connections] Page scrolled or expanded, continuing...');
|
|
169
|
+
noScrollAttempts = 0;
|
|
170
|
+
} else {
|
|
171
|
+
noScrollAttempts++;
|
|
172
|
+
console.log(`[get_daily_linkedin_connections] No scroll detected (attempt ${noScrollAttempts}/${maxNoScrollAttempts})`);
|
|
173
|
+
|
|
174
|
+
if (noScrollAttempts < maxNoScrollAttempts) {
|
|
175
|
+
console.log('[get_daily_linkedin_connections] Waiting 5 seconds and trying again...');
|
|
176
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
177
|
+
} else {
|
|
178
|
+
console.log('[get_daily_linkedin_connections] Max attempts reached, stopping scroll');
|
|
179
|
+
shouldStopScrolling = true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const allConnections = Array.from(allConnectionsMap.values());
|
|
185
|
+
|
|
186
|
+
const newConnections = allConnections.filter(conn => {
|
|
187
|
+
return !lastCheckTimestamp || (conn.connectionTimestamp && conn.connectionTimestamp >= lastCheckTimestamp);
|
|
188
|
+
});
|
|
74
189
|
|
|
75
190
|
return {
|
|
76
191
|
action: 'get_daily_linkedin_connections',
|
|
@@ -38,19 +38,15 @@ export async function executeLinkedInSearch(page, action) {
|
|
|
38
38
|
|
|
39
39
|
try {
|
|
40
40
|
title = await page.title();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
|
|
41
|
+
|
|
42
|
+
// LinkedIn’s new structure
|
|
43
|
+
const htmlContent = await page.content();
|
|
44
|
+
|
|
45
|
+
// Look for cards directly by anchor structure
|
|
46
|
+
const hasCards = await page.locator('a[href*="/in/"]').count();
|
|
47
|
+
console.log(`[LinkedInSearch] Found ${hasCards} profile cards`);
|
|
48
|
+
|
|
52
49
|
const pageSearchResults = await parseLinkedInSearchResultsFromHTML(htmlContent);
|
|
53
|
-
|
|
54
50
|
pageResults = pageSearchResults;
|
|
55
51
|
} catch (error) {
|
|
56
52
|
pageResults = [];
|
|
@@ -65,7 +61,6 @@ export async function executeLinkedInSearch(page, action) {
|
|
|
65
61
|
|
|
66
62
|
try {
|
|
67
63
|
await page.goto(nextUrl, {
|
|
68
|
-
waitUntil: 'load',
|
|
69
64
|
timeout: Math.floor(Math.random() * 20000) + 10000
|
|
70
65
|
});
|
|
71
66
|
navigationSuccess = true;
|
|
@@ -90,52 +85,57 @@ export async function executeLinkedInSearch(page, action) {
|
|
|
90
85
|
}
|
|
91
86
|
|
|
92
87
|
async function parseLinkedInSearchResultsFromHTML(htmlContent) {
|
|
88
|
+
const results = [];
|
|
93
89
|
try {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
90
|
+
const cardPattern = /<a[^>]+href="([^"]*linkedin\.com\/in\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
91
|
+
|
|
92
|
+
let match;
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
|
|
95
|
+
while ((match = cardPattern.exec(htmlContent)) !== null) {
|
|
96
|
+
const [fullMatch, href, contentHtml] = match;
|
|
97
|
+
if (seen.has(href)) continue; // avoid duplicates
|
|
98
|
+
seen.add(href);
|
|
99
|
+
|
|
100
|
+
const usernameMatch = href.match(/\/in\/([^/?#]+)/);
|
|
101
|
+
const username = usernameMatch ? usernameMatch[1] : "";
|
|
102
|
+
|
|
103
|
+
// Extract name
|
|
104
|
+
const nameMatch = contentHtml.match(/>([^<>]{2,50})<\/span>/);
|
|
105
|
+
const name = nameMatch ? nameMatch[1].trim() : "";
|
|
106
|
+
|
|
107
|
+
// Extract headline (title, role)
|
|
108
|
+
const headlineMatch = contentHtml.match(/<div[^>]*>\s*([^<]{5,100})<\/div>/);
|
|
109
|
+
const headline = headlineMatch ? headlineMatch[1].trim() : "";
|
|
110
|
+
|
|
111
|
+
// Extract connection level
|
|
112
|
+
const connectionMatch = contentHtml.match(/\b(1st|2nd|3rd\+)\b/);
|
|
113
|
+
const connectionLevel = connectionMatch ? connectionMatch[1] : "";
|
|
114
|
+
|
|
115
|
+
// Extract location
|
|
116
|
+
const locationMatch = htmlContent
|
|
117
|
+
.slice(cardPattern.lastIndex, cardPattern.lastIndex + 800)
|
|
118
|
+
.match(/([A-Z][a-z]+(?:, [A-Z]{2})?)(?=<)/);
|
|
119
|
+
const location = locationMatch ? locationMatch[1].trim() : "";
|
|
120
|
+
|
|
121
|
+
// Unique ID (message ID-like)
|
|
122
|
+
const uniqueIdMatch = fullMatch.match(/data-view-tracking-scope=.*?contentTrackingId":"([^&]+)"/);
|
|
123
|
+
const uniqueId = uniqueIdMatch ? uniqueIdMatch[1] : "";
|
|
124
|
+
|
|
125
|
+
results.push({
|
|
126
|
+
username,
|
|
127
|
+
link: href,
|
|
128
|
+
name,
|
|
129
|
+
headline,
|
|
130
|
+
location,
|
|
131
|
+
connectionLevel,
|
|
132
|
+
uniqueId,
|
|
133
|
+
});
|
|
133
134
|
}
|
|
134
|
-
|
|
135
|
+
|
|
135
136
|
return results;
|
|
136
|
-
|
|
137
137
|
} catch (error) {
|
|
138
|
-
console.error(
|
|
138
|
+
console.error("[LinkedInSearch] Parsing failed:", error);
|
|
139
139
|
return [];
|
|
140
140
|
}
|
|
141
|
-
}
|
|
141
|
+
}
|
package/tools/get_messages.js
CHANGED
|
@@ -12,6 +12,23 @@ export async function executeGetMessages(page, action) {
|
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
await page.waitForLoadState('domcontentloaded');
|
|
15
|
+
await page.waitForLoadState('networkidle');
|
|
16
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
17
|
+
|
|
18
|
+
await page.evaluate(() => { window.scrollTo(0, 0); });
|
|
19
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
20
|
+
await page.evaluate(() => { window.scrollTo(0, 300); });
|
|
21
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await page.waitForSelector('button[aria-label^="Message"], main button.artdeco-button--primary', {
|
|
25
|
+
timeout: 5000,
|
|
26
|
+
state: 'attached'
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
15
32
|
|
|
16
33
|
const pendingButton = await getButtonByText(page, 'Pending', true);
|
|
17
34
|
if (pendingButton) {
|
|
@@ -22,20 +39,204 @@ export async function executeGetMessages(page, action) {
|
|
|
22
39
|
};
|
|
23
40
|
}
|
|
24
41
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
let clicked = false;
|
|
43
|
+
|
|
44
|
+
const checkOverlayAppeared = async (page) => {
|
|
45
|
+
const overlaySelectors = [
|
|
46
|
+
'.msg-overlay-conversation-bubble__content-wrapper',
|
|
47
|
+
'.msg-overlay',
|
|
48
|
+
'[class*="msg-overlay"]',
|
|
49
|
+
'[class*="conversation-bubble"]'
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
for (let attempt = 0; attempt < 15; attempt++) {
|
|
53
|
+
for (const selector of overlaySelectors) {
|
|
54
|
+
try {
|
|
55
|
+
const count = await page.locator(selector).count();
|
|
56
|
+
if (count > 0) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
} catch (error) {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await page.waitForSelector('.msg-overlay-conversation-bubble__content-wrapper', {
|
|
67
|
+
timeout: 5000,
|
|
68
|
+
state: 'attached'
|
|
69
|
+
});
|
|
33
70
|
return true;
|
|
71
|
+
} catch (error) {
|
|
34
72
|
}
|
|
35
|
-
|
|
73
|
+
|
|
36
74
|
return false;
|
|
37
|
-
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const clickStrategies = [
|
|
78
|
+
async () => {
|
|
79
|
+
try {
|
|
80
|
+
const buttons = await page.locator('button[aria-label^="Message"]').all();
|
|
81
|
+
for (const btn of buttons) {
|
|
82
|
+
const isVisible = await btn.isVisible().catch(() => false);
|
|
83
|
+
const ariaLabel = await btn.getAttribute('aria-label').catch(() => '');
|
|
84
|
+
if (isVisible && ariaLabel && ariaLabel.startsWith('Message')) {
|
|
85
|
+
await btn.scrollIntoViewIfNeeded();
|
|
86
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
87
|
+
await btn.click({ timeout: 5000, force: false });
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
89
|
+
return await checkOverlayAppeared(page);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
},
|
|
96
|
+
async () => {
|
|
97
|
+
try {
|
|
98
|
+
const buttons = await page.locator('main button[aria-label^="Message"]').all();
|
|
99
|
+
for (const btn of buttons) {
|
|
100
|
+
const isVisible = await btn.isVisible().catch(() => false);
|
|
101
|
+
if (isVisible) {
|
|
102
|
+
await btn.scrollIntoViewIfNeeded();
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
104
|
+
await btn.click({ timeout: 5000, force: false });
|
|
105
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
106
|
+
return await checkOverlayAppeared(page);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
},
|
|
113
|
+
async () => {
|
|
114
|
+
try {
|
|
115
|
+
const buttons = await page.locator('main button.artdeco-button--primary').all();
|
|
116
|
+
for (const btn of buttons) {
|
|
117
|
+
try {
|
|
118
|
+
const text = await btn.textContent().catch(() => '');
|
|
119
|
+
const ariaLabel = await btn.getAttribute('aria-label').catch(() => '');
|
|
120
|
+
const isVisible = await btn.isVisible().catch(() => false);
|
|
121
|
+
|
|
122
|
+
if (isVisible && text && text.trim().includes('Message') &&
|
|
123
|
+
(!text.includes('Pending')) &&
|
|
124
|
+
(!ariaLabel || !ariaLabel.includes('Pending'))) {
|
|
125
|
+
await btn.scrollIntoViewIfNeeded();
|
|
126
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
127
|
+
await btn.click({ timeout: 5000, force: false });
|
|
128
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
129
|
+
return await checkOverlayAppeared(page);
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
},
|
|
139
|
+
async () => {
|
|
140
|
+
try {
|
|
141
|
+
const allButtons = await page.locator('main button').all();
|
|
142
|
+
for (const btn of allButtons) {
|
|
143
|
+
try {
|
|
144
|
+
const text = await btn.textContent().catch(() => '');
|
|
145
|
+
const ariaLabel = await btn.getAttribute('aria-label').catch(() => '');
|
|
146
|
+
const isVisible = await btn.isVisible().catch(() => false);
|
|
147
|
+
|
|
148
|
+
if (isVisible && ((ariaLabel && ariaLabel.startsWith('Message')) ||
|
|
149
|
+
(text && text.includes('Message') && !text.includes('Pending')))) {
|
|
150
|
+
await btn.scrollIntoViewIfNeeded();
|
|
151
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
152
|
+
await btn.click({ timeout: 5000, force: false });
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
154
|
+
return await checkOverlayAppeared(page);
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
},
|
|
164
|
+
async () => {
|
|
165
|
+
try {
|
|
166
|
+
const buttonFound = await page.evaluate(() => {
|
|
167
|
+
const buttons = Array.from(document.querySelectorAll('button'));
|
|
168
|
+
for (const btn of buttons) {
|
|
169
|
+
const ariaLabel = btn.getAttribute('aria-label') || '';
|
|
170
|
+
const textContent = btn.textContent || '';
|
|
171
|
+
const isVisible = btn.offsetParent !== null &&
|
|
172
|
+
window.getComputedStyle(btn).visibility !== 'hidden' &&
|
|
173
|
+
window.getComputedStyle(btn).display !== 'none';
|
|
174
|
+
|
|
175
|
+
if (isVisible && (ariaLabel.startsWith('Message') ||
|
|
176
|
+
(textContent.includes('Message') && !textContent.includes('Pending')))) {
|
|
177
|
+
const rect = btn.getBoundingClientRect();
|
|
178
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
179
|
+
return {
|
|
180
|
+
found: true,
|
|
181
|
+
x: rect.left + rect.width / 2,
|
|
182
|
+
y: rect.top + rect.height / 2
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { found: false };
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (buttonFound.found) {
|
|
191
|
+
await page.mouse.click(buttonFound.x, buttonFound.y);
|
|
192
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
193
|
+
return await checkOverlayAppeared(page);
|
|
194
|
+
}
|
|
195
|
+
} catch (error) {
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
},
|
|
199
|
+
async () => {
|
|
200
|
+
try {
|
|
201
|
+
const messageBtn = await getButtonByText(page, 'Message', true);
|
|
202
|
+
if (messageBtn) {
|
|
203
|
+
await messageBtn.scrollIntoViewIfNeeded();
|
|
204
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
205
|
+
await messageBtn.click({ timeout: 5000, force: false });
|
|
206
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
207
|
+
return await checkOverlayAppeared(page);
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
},
|
|
213
|
+
async () => {
|
|
214
|
+
try {
|
|
215
|
+
const messageBtn = await getButtonByText(page, 'Message', false);
|
|
216
|
+
if (messageBtn) {
|
|
217
|
+
await messageBtn.scrollIntoViewIfNeeded();
|
|
218
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
219
|
+
await messageBtn.click({ timeout: 5000, force: false });
|
|
220
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
221
|
+
return await checkOverlayAppeared(page);
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
];
|
|
38
228
|
|
|
229
|
+
for (const strategy of clickStrategies) {
|
|
230
|
+
try {
|
|
231
|
+
clicked = await strategy();
|
|
232
|
+
if (clicked) {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
39
240
|
if (!clicked) {
|
|
40
241
|
return {
|
|
41
242
|
action: 'get_messages',
|
|
@@ -44,18 +245,75 @@ export async function executeGetMessages(page, action) {
|
|
|
44
245
|
};
|
|
45
246
|
}
|
|
46
247
|
|
|
47
|
-
|
|
48
|
-
|
|
248
|
+
try {
|
|
249
|
+
await page.waitForSelector('.msg-overlay-conversation-bubble__content-wrapper', { timeout: 15000 });
|
|
250
|
+
} catch (error) {
|
|
251
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
252
|
+
const overlayExists = await page.locator('.msg-overlay-conversation-bubble__content-wrapper').count();
|
|
253
|
+
if (overlayExists === 0) {
|
|
254
|
+
return {
|
|
255
|
+
action: 'get_messages',
|
|
256
|
+
result: { status: 'error', message: `Clicked Message button but overlay did not appear for ${username}` },
|
|
257
|
+
status: 'failed'
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 2000) + 1000));
|
|
49
263
|
|
|
50
264
|
await page.evaluate(() => {
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
|
|
265
|
+
const messageList = document.querySelector('.msg-s-message-list');
|
|
266
|
+
if (messageList) {
|
|
267
|
+
messageList.scrollTop = 0;
|
|
54
268
|
}
|
|
55
269
|
});
|
|
56
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
270
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
271
|
+
|
|
272
|
+
let previousScrollHeight = 0;
|
|
273
|
+
let scrollAttempts = 0;
|
|
274
|
+
const maxScrollAttempts = 10;
|
|
275
|
+
|
|
276
|
+
while (scrollAttempts < maxScrollAttempts) {
|
|
277
|
+
const scrollResult = await page.evaluate(() => {
|
|
278
|
+
const messageList = document.querySelector('.msg-s-message-list');
|
|
279
|
+
if (!messageList) return { shouldContinue: false, scrollHeight: 0 };
|
|
280
|
+
|
|
281
|
+
const currentScrollHeight = messageList.scrollHeight;
|
|
282
|
+
messageList.scrollTop = messageList.scrollHeight;
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
shouldContinue: true,
|
|
286
|
+
scrollHeight: currentScrollHeight,
|
|
287
|
+
scrollTop: messageList.scrollTop
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (!scrollResult.shouldContinue) {
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
296
|
+
|
|
297
|
+
const newScrollHeight = await page.evaluate(() => {
|
|
298
|
+
const messageList = document.querySelector('.msg-s-message-list');
|
|
299
|
+
return messageList ? messageList.scrollHeight : 0;
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
if (newScrollHeight === previousScrollHeight) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
previousScrollHeight = newScrollHeight;
|
|
307
|
+
scrollAttempts++;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
57
311
|
|
|
58
312
|
const content = await page.evaluate(() => {
|
|
313
|
+
const messageList = document.querySelector('.msg-s-message-list');
|
|
314
|
+
if (messageList) {
|
|
315
|
+
return messageList.innerText || '';
|
|
316
|
+
}
|
|
59
317
|
const wrapper = document.querySelector('.msg-overlay-conversation-bubble__content-wrapper');
|
|
60
318
|
return wrapper ? wrapper.innerText : '';
|
|
61
319
|
});
|
|
@@ -68,24 +326,32 @@ export async function executeGetMessages(page, action) {
|
|
|
68
326
|
}
|
|
69
327
|
|
|
70
328
|
async function getButtonByText(page, text, inMain = false) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
329
|
+
try {
|
|
330
|
+
let buttons = [];
|
|
331
|
+
if (inMain) {
|
|
332
|
+
buttons = await page.locator('main button').all();
|
|
333
|
+
} else {
|
|
334
|
+
buttons = await page.locator('button').all();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
for (const button of buttons) {
|
|
338
|
+
try {
|
|
339
|
+
const buttonText = await button.textContent().catch(() => '');
|
|
340
|
+
const ariaLabel = await button.getAttribute('aria-label').catch(() => '');
|
|
341
|
+
const isVisible = await button.isVisible().catch(() => false);
|
|
342
|
+
|
|
343
|
+
if (isVisible && ((buttonText && buttonText.trim().includes(text)) ||
|
|
344
|
+
(ariaLabel && ariaLabel.includes(text)))) {
|
|
345
|
+
const boundingBox = await button.boundingBox().catch(() => null);
|
|
346
|
+
if (boundingBox && boundingBox.width > 0 && boundingBox.height > 0) {
|
|
347
|
+
return button;
|
|
348
|
+
}
|
|
85
349
|
}
|
|
350
|
+
} catch (error) {
|
|
351
|
+
continue;
|
|
86
352
|
}
|
|
87
|
-
} catch (error) {
|
|
88
353
|
}
|
|
354
|
+
} catch (error) {
|
|
89
355
|
}
|
|
90
356
|
|
|
91
357
|
return null;
|