linkedin-automation-cli 1.0.0
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/.env.example +12 -0
- package/.github/workflows/ci.yml +66 -0
- package/.github/workflows/publish.yml +48 -0
- package/.husky/pre-commit +6 -0
- package/.prettierignore +4 -0
- package/.prettierrc +10 -0
- package/AGENTS.md +294 -0
- package/CHANGELOG.md +40 -0
- package/GIT_RELEASE.md +167 -0
- package/LICENSE +21 -0
- package/Makefile +30 -0
- package/NPM_PUBLISHING.md +230 -0
- package/PYEOF +0 -0
- package/README.md +295 -0
- package/TESTING-GUIDE.md +151 -0
- package/cmd/linkedin/main.go +9 -0
- package/dist/agent/action-executor.d.ts +81 -0
- package/dist/agent/action-executor.d.ts.map +1 -0
- package/dist/agent/action-executor.js +170 -0
- package/dist/agent/action-executor.js.map +1 -0
- package/dist/agent/action-executor.test.d.ts +2 -0
- package/dist/agent/action-executor.test.d.ts.map +1 -0
- package/dist/agent/action-executor.test.js +366 -0
- package/dist/agent/action-executor.test.js.map +1 -0
- package/dist/agent/claude-client.d.ts +74 -0
- package/dist/agent/claude-client.d.ts.map +1 -0
- package/dist/agent/claude-client.js +314 -0
- package/dist/agent/claude-client.js.map +1 -0
- package/dist/agent/claude-client.test.d.ts +2 -0
- package/dist/agent/claude-client.test.d.ts.map +1 -0
- package/dist/agent/claude-client.test.js +590 -0
- package/dist/agent/claude-client.test.js.map +1 -0
- package/dist/agent/dom-extractor.d.ts +50 -0
- package/dist/agent/dom-extractor.d.ts.map +1 -0
- package/dist/agent/dom-extractor.js +374 -0
- package/dist/agent/dom-extractor.js.map +1 -0
- package/dist/agent/dom-extractor.test.d.ts +7 -0
- package/dist/agent/dom-extractor.test.d.ts.map +1 -0
- package/dist/agent/dom-extractor.test.js +504 -0
- package/dist/agent/dom-extractor.test.js.map +1 -0
- package/dist/agent/extension-client.d.ts +75 -0
- package/dist/agent/extension-client.d.ts.map +1 -0
- package/dist/agent/extension-client.js +245 -0
- package/dist/agent/extension-client.js.map +1 -0
- package/dist/agent/index.d.ts +8 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +16 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/agent/page-agent.d.ts +76 -0
- package/dist/agent/page-agent.d.ts.map +1 -0
- package/dist/agent/page-agent.js +236 -0
- package/dist/agent/page-agent.js.map +1 -0
- package/dist/agent/types.d.ts +236 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +37 -0
- package/dist/agent/types.js.map +1 -0
- package/dist/cli/agent-commands.d.ts +3 -0
- package/dist/cli/agent-commands.d.ts.map +1 -0
- package/dist/cli/agent-commands.js +250 -0
- package/dist/cli/agent-commands.js.map +1 -0
- package/dist/cli/auth.d.ts +3 -0
- package/dist/cli/auth.d.ts.map +1 -0
- package/dist/cli/auth.js +288 -0
- package/dist/cli/auth.js.map +1 -0
- package/dist/cli/company.d.ts +3 -0
- package/dist/cli/company.d.ts.map +1 -0
- package/dist/cli/company.js +55 -0
- package/dist/cli/company.js.map +1 -0
- package/dist/cli/connection.d.ts +3 -0
- package/dist/cli/connection.d.ts.map +1 -0
- package/dist/cli/connection.js +79 -0
- package/dist/cli/connection.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +17 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/messages.d.ts +3 -0
- package/dist/cli/messages.d.ts.map +1 -0
- package/dist/cli/messages.js +268 -0
- package/dist/cli/messages.js.map +1 -0
- package/dist/cli/profile.d.ts +3 -0
- package/dist/cli/profile.d.ts.map +1 -0
- package/dist/cli/profile.js +81 -0
- package/dist/cli/profile.js.map +1 -0
- package/dist/cli/profile.test.d.ts +2 -0
- package/dist/cli/profile.test.d.ts.map +1 -0
- package/dist/cli/profile.test.js +15 -0
- package/dist/cli/profile.test.js.map +1 -0
- package/dist/cli/reply.d.ts +3 -0
- package/dist/cli/reply.d.ts.map +1 -0
- package/dist/cli/reply.js +129 -0
- package/dist/cli/reply.js.map +1 -0
- package/dist/core/audit.d.ts +17 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +121 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/audit.test.d.ts +2 -0
- package/dist/core/audit.test.d.ts.map +1 -0
- package/dist/core/audit.test.js +142 -0
- package/dist/core/audit.test.js.map +1 -0
- package/dist/core/browser-cookies.d.ts +19 -0
- package/dist/core/browser-cookies.d.ts.map +1 -0
- package/dist/core/browser-cookies.js +181 -0
- package/dist/core/browser-cookies.js.map +1 -0
- package/dist/core/browser.d.ts +50 -0
- package/dist/core/browser.d.ts.map +1 -0
- package/dist/core/browser.js +318 -0
- package/dist/core/browser.js.map +1 -0
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +103 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/config.test.d.ts +2 -0
- package/dist/core/config.test.d.ts.map +1 -0
- package/dist/core/config.test.js +111 -0
- package/dist/core/config.test.js.map +1 -0
- package/dist/core/storage.d.ts +19 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +124 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/storage.test.d.ts +2 -0
- package/dist/core/storage.test.d.ts.map +1 -0
- package/dist/core/storage.test.js +142 -0
- package/dist/core/storage.test.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/linkedin/auth.d.ts +22 -0
- package/dist/linkedin/auth.d.ts.map +1 -0
- package/dist/linkedin/auth.js +167 -0
- package/dist/linkedin/auth.js.map +1 -0
- package/dist/linkedin/company-extractor.d.ts +36 -0
- package/dist/linkedin/company-extractor.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.js +211 -0
- package/dist/linkedin/company-extractor.js.map +1 -0
- package/dist/linkedin/company-extractor.test.d.ts +2 -0
- package/dist/linkedin/company-extractor.test.d.ts.map +1 -0
- package/dist/linkedin/company-extractor.test.js +52 -0
- package/dist/linkedin/company-extractor.test.js.map +1 -0
- package/dist/linkedin/connector.d.ts +45 -0
- package/dist/linkedin/connector.d.ts.map +1 -0
- package/dist/linkedin/connector.js +245 -0
- package/dist/linkedin/connector.js.map +1 -0
- package/dist/linkedin/message-sender.d.ts +32 -0
- package/dist/linkedin/message-sender.d.ts.map +1 -0
- package/dist/linkedin/message-sender.js +112 -0
- package/dist/linkedin/message-sender.js.map +1 -0
- package/dist/linkedin/messages.d.ts +78 -0
- package/dist/linkedin/messages.d.ts.map +1 -0
- package/dist/linkedin/messages.js +745 -0
- package/dist/linkedin/messages.js.map +1 -0
- package/dist/linkedin/profile.d.ts +37 -0
- package/dist/linkedin/profile.d.ts.map +1 -0
- package/dist/linkedin/profile.js +268 -0
- package/dist/linkedin/profile.js.map +1 -0
- package/dist/linkedin/profile.test.d.ts +2 -0
- package/dist/linkedin/profile.test.d.ts.map +1 -0
- package/dist/linkedin/profile.test.js +68 -0
- package/dist/linkedin/profile.test.js.map +1 -0
- package/dist/linkedin/reply.d.ts +21 -0
- package/dist/linkedin/reply.d.ts.map +1 -0
- package/dist/linkedin/reply.js +76 -0
- package/dist/linkedin/reply.js.map +1 -0
- package/dist/linkedin/selector-engine.d.ts +69 -0
- package/dist/linkedin/selector-engine.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.js +339 -0
- package/dist/linkedin/selector-engine.js.map +1 -0
- package/dist/linkedin/selector-engine.test.d.ts +2 -0
- package/dist/linkedin/selector-engine.test.d.ts.map +1 -0
- package/dist/linkedin/selector-engine.test.js +135 -0
- package/dist/linkedin/selector-engine.test.js.map +1 -0
- package/dist/linkedin/selectors.d.ts +65 -0
- package/dist/linkedin/selectors.d.ts.map +1 -0
- package/dist/linkedin/selectors.js +261 -0
- package/dist/linkedin/selectors.js.map +1 -0
- package/dist/templates/engine.d.ts +37 -0
- package/dist/templates/engine.d.ts.map +1 -0
- package/dist/templates/engine.js +215 -0
- package/dist/templates/engine.js.map +1 -0
- package/dist/templates/engine.test.d.ts +2 -0
- package/dist/templates/engine.test.d.ts.map +1 -0
- package/dist/templates/engine.test.js +212 -0
- package/dist/templates/engine.test.js.map +1 -0
- package/dist/templates/index.d.ts +2 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +7 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/types/index.d.ts +113 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/index.test.d.ts +2 -0
- package/dist/types/index.test.d.ts.map +1 -0
- package/dist/types/index.test.js +90 -0
- package/dist/types/index.test.js.map +1 -0
- package/dist/utils/paths.d.ts +8 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +68 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +22 -0
- package/dist/utils/rate-limiter.d.ts.map +1 -0
- package/dist/utils/rate-limiter.js +57 -0
- package/dist/utils/rate-limiter.js.map +1 -0
- package/dist/utils/retry.d.ts +18 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +49 -0
- package/dist/utils/retry.js.map +1 -0
- package/docs/connection-command.md +52 -0
- package/docs/plans/2025-03-03-linkedin-cli-design.md +280 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation-plan.md +2087 -0
- package/docs/plans/2025-03-03-linkedin-cli-implementation.md +2420 -0
- package/docs/plans/2026-02-19-linkedin-connection-feature.md +596 -0
- package/docs/plans/2026-02-28-messages-send-feature.md +480 -0
- package/docs/plans/2026-02-28-messages-show-design.md +243 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-design.md +394 -0
- package/docs/plans/2026-03-03-linkedin-cli-oss-publishing-plan.md +1592 -0
- package/docs/superpowers/plans/2026-03-13-linkedin-automation-resilience-migration.md +425 -0
- package/docs/superpowers/plans/2026-03-13-playwright-fara-migration.md +1112 -0
- package/docs/superpowers/plans/2026-03-14-page-agent-plan.md +1598 -0
- package/docs/superpowers/plans/2026-03-15-company-profile-extraction.md +591 -0
- package/docs/superpowers/plans/2026-03-15-profile-extraction-plan.md +943 -0
- package/docs/superpowers/specs/2026-03-14-company-profile-extraction-design.md +371 -0
- package/docs/superpowers/specs/2026-03-14-page-agent-design.md +385 -0
- package/docs/superpowers/specs/2026-03-15-profile-extraction-design.md +409 -0
- package/eslint.config.mjs +58 -0
- package/go.mod +9 -0
- package/go.sum +10 -0
- package/import-cookies.js +376 -0
- package/internal/cmd/actions.go +123 -0
- package/internal/cmd/auth.go +108 -0
- package/internal/cmd/connect.go +42 -0
- package/internal/cmd/message.go +44 -0
- package/internal/cmd/people.go +454 -0
- package/internal/cmd/profiles.go +121 -0
- package/internal/cmd/root.go +89 -0
- package/internal/cmd/sequence.go +192 -0
- package/internal/config/config.go +187 -0
- package/internal/config/config_test.go +121 -0
- package/internal/config/profile.go +65 -0
- package/internal/linkedin/navigator.go +195 -0
- package/internal/linkedin/selectors.go +39 -0
- package/internal/linkedin/validator.go +69 -0
- package/internal/pinchtab/client.go +183 -0
- package/internal/pinchtab/client_test.go +67 -0
- package/internal/pinchtab/types.go +50 -0
- package/internal/ratelimit/limiter.go +115 -0
- package/internal/ratelimit/limits.go +32 -0
- package/package.json +67 -0
- package/release.sh +66 -0
- package/scripts/debug-linkedin.js +156 -0
- package/scripts/debug-login.js +193 -0
- package/scripts/extract-from-edge.js +96 -0
- package/scripts/import-cookies.js +101 -0
- package/scripts/poc-show-data.js +205 -0
- package/scripts/proof-of-access.js +87 -0
- package/scripts/prove-connection.js +110 -0
- package/scripts/show-linkedin-data.js +173 -0
- package/src/agent/action-executor.test.ts +464 -0
- package/src/agent/action-executor.ts +203 -0
- package/src/agent/claude-client.test.ts +707 -0
- package/src/agent/claude-client.ts +422 -0
- package/src/agent/dom-extractor.test.ts +574 -0
- package/src/agent/dom-extractor.ts +437 -0
- package/src/agent/extension-client.ts +306 -0
- package/src/agent/index.ts +28 -0
- package/src/agent/page-agent.ts +292 -0
- package/src/agent/types.ts +288 -0
- package/src/cli/agent-commands.ts +274 -0
- package/src/cli/auth.ts +343 -0
- package/src/cli/company.ts +66 -0
- package/src/cli/connection.ts +89 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/messages.ts +338 -0
- package/src/cli/profile.test.ts +14 -0
- package/src/cli/profile.ts +95 -0
- package/src/cli/reply.ts +110 -0
- package/src/core/audit.test.ts +134 -0
- package/src/core/audit.ts +98 -0
- package/src/core/browser-cookies.ts +203 -0
- package/src/core/browser.ts +304 -0
- package/src/core/config.test.ts +90 -0
- package/src/core/config.ts +81 -0
- package/src/core/storage.test.ts +129 -0
- package/src/core/storage.ts +100 -0
- package/src/index.ts +70 -0
- package/src/linkedin/auth.ts +218 -0
- package/src/linkedin/company-extractor.test.ts +58 -0
- package/src/linkedin/company-extractor.ts +222 -0
- package/src/linkedin/connector.ts +336 -0
- package/src/linkedin/message-sender.ts +141 -0
- package/src/linkedin/messages.ts +894 -0
- package/src/linkedin/profile.test.ts +79 -0
- package/src/linkedin/profile.ts +314 -0
- package/src/linkedin/reply.ts +96 -0
- package/src/linkedin/selector-engine.test.ts +167 -0
- package/src/linkedin/selector-engine.ts +393 -0
- package/src/linkedin/selectors.ts +268 -0
- package/src/templates/defaults/followup.txt +14 -0
- package/src/templates/defaults/meeting.txt +16 -0
- package/src/templates/defaults/welcome.txt +14 -0
- package/src/templates/engine.test.ts +228 -0
- package/src/templates/engine.ts +208 -0
- package/src/templates/index.ts +1 -0
- package/src/types/index.test.ts +94 -0
- package/src/types/index.ts +143 -0
- package/src/types/sql.js.d.ts +23 -0
- package/src/utils/paths.ts +33 -0
- package/src/utils/rate-limiter.ts +75 -0
- package/src/utils/retry.ts +78 -0
- package/test-cli.sh +85 -0
- package/test-real-data.sh +97 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,745 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LinkedInMessages = void 0;
|
|
4
|
+
const selector_engine_1 = require("./selector-engine");
|
|
5
|
+
const selectors_1 = require("./selectors");
|
|
6
|
+
class LinkedInMessages {
|
|
7
|
+
page;
|
|
8
|
+
selectorEngine;
|
|
9
|
+
constructor(page, _browser) {
|
|
10
|
+
this.page = page;
|
|
11
|
+
this.selectorEngine = new selector_engine_1.SelectorEngine(page);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Navigate to LinkedIn messaging page
|
|
15
|
+
*/
|
|
16
|
+
async navigateToMessages() {
|
|
17
|
+
await this.page.goto('https://www.linkedin.com/messaging/', {
|
|
18
|
+
waitUntil: 'domcontentloaded',
|
|
19
|
+
timeout: 10000,
|
|
20
|
+
});
|
|
21
|
+
await this.page.waitForTimeout(1000);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get list of conversation threads
|
|
25
|
+
*/
|
|
26
|
+
async getThreads(options = {}) {
|
|
27
|
+
// Navigation is now handled by the caller - we're already on the messaging page
|
|
28
|
+
const limit = options.limit ?? 20;
|
|
29
|
+
const threads = [];
|
|
30
|
+
// Wait for conversation list to load
|
|
31
|
+
const listResult = await this.selectorEngine.findElement('messages', 'conversationList', {
|
|
32
|
+
timeout: 10000,
|
|
33
|
+
});
|
|
34
|
+
if (!listResult.element) {
|
|
35
|
+
console.warn('Could not find conversation list');
|
|
36
|
+
return threads;
|
|
37
|
+
}
|
|
38
|
+
// Get all conversation items using Playwright locators (more reliable than CSS selectors)
|
|
39
|
+
// LinkedIn uses dynamic class names, so we use multiple strategies
|
|
40
|
+
// Strategy 1: Look for li elements with msg-conversation-listitem class (the actual list items)
|
|
41
|
+
const listItems = await this.page.$$('[class*="msg-conversation-listitem"]');
|
|
42
|
+
// Get the conversation card div inside each list item
|
|
43
|
+
let conversationItems = [];
|
|
44
|
+
for (const listItem of listItems) {
|
|
45
|
+
const card = await listItem.$('.msg-conversation-card');
|
|
46
|
+
if (card) {
|
|
47
|
+
conversationItems.push(card);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Strategy 2: Try data-testid
|
|
51
|
+
if (conversationItems.length === 0) {
|
|
52
|
+
conversationItems = await this.page.$$('[data-testid="conversation-card"]');
|
|
53
|
+
}
|
|
54
|
+
// Strategy 3: Try by ID pattern (conversation-card-ember*)
|
|
55
|
+
if (conversationItems.length === 0) {
|
|
56
|
+
conversationItems = await this.page.$$('[id^="conversation-card-"]');
|
|
57
|
+
}
|
|
58
|
+
// Strategy 4: Try role-based listitem query
|
|
59
|
+
if (conversationItems.length === 0) {
|
|
60
|
+
const allListItems = await this.page.$$('[role="listitem"]');
|
|
61
|
+
for (const item of allListItems) {
|
|
62
|
+
const text = await item.evaluate((el) => el.textContent || '');
|
|
63
|
+
// Conversation cards typically have participant names and message previews
|
|
64
|
+
if (text.length > 5 && (text.includes(':') || text.match(/\d+[mhd]/))) {
|
|
65
|
+
conversationItems.push(item);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Final fallback: get all children of the conversation list that look like cards
|
|
70
|
+
if (conversationItems.length === 0 && listResult.element) {
|
|
71
|
+
const allChildren = await listResult.element.$$('* > *');
|
|
72
|
+
for (const child of allChildren) {
|
|
73
|
+
const className = await child.evaluate((el) => {
|
|
74
|
+
// Handle both SVG and HTML elements
|
|
75
|
+
if ('className' in el && typeof el.className === 'string') {
|
|
76
|
+
return el.className;
|
|
77
|
+
}
|
|
78
|
+
// For SVG elements, className might be an object
|
|
79
|
+
if ('className' in el && el.className && 'baseVal' in el.className) {
|
|
80
|
+
return el.className.baseVal;
|
|
81
|
+
}
|
|
82
|
+
return '';
|
|
83
|
+
});
|
|
84
|
+
// LinkedIn conversation cards typically have specific patterns
|
|
85
|
+
if (className &&
|
|
86
|
+
(className.includes('msg-') ||
|
|
87
|
+
className.includes('conversation') ||
|
|
88
|
+
className.includes('artdeco'))) {
|
|
89
|
+
conversationItems.push(child);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (let i = 0; i < Math.min(conversationItems.length, limit); i++) {
|
|
94
|
+
const item = conversationItems[i];
|
|
95
|
+
const thread = await this.parseThreadItem(item);
|
|
96
|
+
if (thread) {
|
|
97
|
+
// Filter by unread if requested
|
|
98
|
+
if (options.unreadOnly && thread.unreadCount === 0) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
threads.push(thread);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return threads;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Parse a single conversation item element into a Thread
|
|
108
|
+
*/
|
|
109
|
+
async parseThreadItem(item) {
|
|
110
|
+
try {
|
|
111
|
+
// Extract thread ID from data attribute or URL
|
|
112
|
+
const threadId = await item.evaluate((el) => {
|
|
113
|
+
const element = el;
|
|
114
|
+
// Strategy 1: Try to get from data-conversation-urn attribute
|
|
115
|
+
const dataId = element.getAttribute('data-conversation-urn');
|
|
116
|
+
if (dataId)
|
|
117
|
+
return dataId;
|
|
118
|
+
// Strategy 2: Try data-urn variant
|
|
119
|
+
const dataUrn = element.getAttribute('data-urn');
|
|
120
|
+
if (dataUrn) {
|
|
121
|
+
const match = dataUrn.match(/thread:(\d+)/);
|
|
122
|
+
if (match)
|
|
123
|
+
return match[1];
|
|
124
|
+
}
|
|
125
|
+
// Strategy 3: Look for data-event-urn in descendants (contains thread ID)
|
|
126
|
+
// Format: urn:li:msg_message:(urn:li:fsd_profile:...,2-<threadId>)
|
|
127
|
+
const eventUrnEl = element.querySelector('[data-event-urn]');
|
|
128
|
+
if (eventUrnEl) {
|
|
129
|
+
const eventUrn = eventUrnEl.getAttribute('data-event-urn');
|
|
130
|
+
if (eventUrn) {
|
|
131
|
+
// Extract thread ID from format: urn:li:msg_message:(...,2-<base64id>)
|
|
132
|
+
const match = eventUrn.match(/,2-([A-Za-z0-9+/=]+)/);
|
|
133
|
+
if (match) {
|
|
134
|
+
return `2-${match[1]}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Strategy 4: Try to extract from link inside the element
|
|
139
|
+
const link = element.querySelector('a[href*="/messaging/thread/"]');
|
|
140
|
+
if (link) {
|
|
141
|
+
const href = link.getAttribute('href');
|
|
142
|
+
if (href) {
|
|
143
|
+
const match = href.match(/\/messaging\/thread\/([^/]+)/);
|
|
144
|
+
if (match)
|
|
145
|
+
return match[1];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Strategy 5: Try to get from parent link
|
|
149
|
+
const parentLink = element.closest('a[href*="/messaging/thread/"]');
|
|
150
|
+
if (parentLink) {
|
|
151
|
+
const href = parentLink.getAttribute('href');
|
|
152
|
+
if (href) {
|
|
153
|
+
const match = href.match(/\/messaging\/thread\/([^/]+)/);
|
|
154
|
+
if (match)
|
|
155
|
+
return match[1];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Strategy 6: Extract from element ID (conversation-card-ember50 -> ember50)
|
|
159
|
+
const elemId = element.id;
|
|
160
|
+
if (elemId && elemId.includes('conversation-card')) {
|
|
161
|
+
// Use the ember ID as a fallback
|
|
162
|
+
return elemId.replace('conversation-card-', '');
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
});
|
|
166
|
+
if (!threadId) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
// Extract participant names using multiple selector strategies
|
|
170
|
+
const participantNames = await this.extractTextArrayFromElement(item, [
|
|
171
|
+
'.msg-conversation-card__participant-names',
|
|
172
|
+
'.msg-conversation-listitem__participant-names',
|
|
173
|
+
'[class*="participant-name"]',
|
|
174
|
+
'[class*="participant"]',
|
|
175
|
+
]);
|
|
176
|
+
// Fallback: get participant name from image alt attribute
|
|
177
|
+
if (participantNames.length === 0) {
|
|
178
|
+
const imgAlt = await item.evaluate((el) => {
|
|
179
|
+
const img = el.querySelector('img[alt]');
|
|
180
|
+
return img?.getAttribute('alt') || '';
|
|
181
|
+
});
|
|
182
|
+
if (imgAlt) {
|
|
183
|
+
participantNames.push(imgAlt);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// Extract last message preview
|
|
187
|
+
const lastMessageText = await this.extractTextFromElementSingle(item, [
|
|
188
|
+
'.msg-conversation-card__message-snippet',
|
|
189
|
+
'.msg-conversation-card__message-preview',
|
|
190
|
+
'.msg-conversation-listitem__message-preview',
|
|
191
|
+
'[class*="message-snippet"]',
|
|
192
|
+
'[class*="message-preview"]',
|
|
193
|
+
'[class*="last-message"]',
|
|
194
|
+
]);
|
|
195
|
+
// Check for unread indicator using multiple strategies
|
|
196
|
+
const hasUnread = await this.checkForUnreadIndicator(item);
|
|
197
|
+
// Get timestamp if available
|
|
198
|
+
const timestampText = await this.extractTextFromElementSingle(item, [
|
|
199
|
+
'time',
|
|
200
|
+
'[class*="timestamp"]',
|
|
201
|
+
'[class*="time"]',
|
|
202
|
+
]);
|
|
203
|
+
// Also try to get timestamp from datetime attribute
|
|
204
|
+
let lastActivity;
|
|
205
|
+
const datetimeAttr = await item.evaluate((el) => {
|
|
206
|
+
const timeEl = el.querySelector('time');
|
|
207
|
+
return timeEl?.getAttribute('datetime') || null;
|
|
208
|
+
});
|
|
209
|
+
if (datetimeAttr) {
|
|
210
|
+
lastActivity = new Date(datetimeAttr);
|
|
211
|
+
}
|
|
212
|
+
else if (timestampText) {
|
|
213
|
+
lastActivity = this.parseRelativeTimestamp(timestampText);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
lastActivity = new Date();
|
|
217
|
+
}
|
|
218
|
+
// Create partial thread (without full messages)
|
|
219
|
+
const thread = {
|
|
220
|
+
id: threadId,
|
|
221
|
+
participants: participantNames.length > 0
|
|
222
|
+
? participantNames.map((name) => ({ name, profileUrl: undefined }))
|
|
223
|
+
: [{ name: 'Unknown', profileUrl: undefined }],
|
|
224
|
+
messages: [
|
|
225
|
+
{
|
|
226
|
+
id: `preview-${threadId}`,
|
|
227
|
+
threadId,
|
|
228
|
+
sender: { name: participantNames[0] || 'Unknown', isMe: false },
|
|
229
|
+
content: lastMessageText,
|
|
230
|
+
timestamp: lastActivity,
|
|
231
|
+
isRead: !hasUnread,
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
lastActivity,
|
|
235
|
+
unreadCount: hasUnread ? 1 : 0,
|
|
236
|
+
};
|
|
237
|
+
return thread;
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.warn('Failed to parse thread item:', error);
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Extract multiple text values from an element - returns array of strings
|
|
246
|
+
*/
|
|
247
|
+
async extractTextArrayFromElement(element, selectors) {
|
|
248
|
+
for (const selector of selectors) {
|
|
249
|
+
try {
|
|
250
|
+
const texts = await element.$$eval(selector, (els) => els.map((el) => el.textContent?.trim()).filter(Boolean));
|
|
251
|
+
if (texts.length > 0)
|
|
252
|
+
return texts;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Try next selector
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Extract single text value from an element - returns string
|
|
262
|
+
*/
|
|
263
|
+
async extractTextFromElementSingle(element, selectors) {
|
|
264
|
+
for (const selector of selectors) {
|
|
265
|
+
try {
|
|
266
|
+
const text = await element.$eval(selector, (el) => el.textContent?.trim() || '');
|
|
267
|
+
if (text)
|
|
268
|
+
return text;
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Try next selector
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Check if a conversation item has unread messages
|
|
278
|
+
*/
|
|
279
|
+
async checkForUnreadIndicator(item) {
|
|
280
|
+
// Check for unread indicator element
|
|
281
|
+
const selectors = [
|
|
282
|
+
'.msg-conversation-card__unread-indicator',
|
|
283
|
+
'.msg-conversation-listitem__unread-indicator',
|
|
284
|
+
'[class*="unread"]',
|
|
285
|
+
];
|
|
286
|
+
for (const selector of selectors) {
|
|
287
|
+
try {
|
|
288
|
+
const unreadEl = await item.$(selector);
|
|
289
|
+
if (unreadEl)
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Continue
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// Check for bold/heavy font weight (unread messages often have bold text)
|
|
297
|
+
const fontWeight = await item.evaluate((el) => {
|
|
298
|
+
const nameEl = el.querySelector('[class*="participant-name"]') ||
|
|
299
|
+
el.querySelector('[class*="conversation-card__title"]');
|
|
300
|
+
if (nameEl) {
|
|
301
|
+
const style = window.getComputedStyle(nameEl);
|
|
302
|
+
return parseInt(style.fontWeight) >= 600; // semibold or bold
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
});
|
|
306
|
+
return fontWeight;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Parse relative timestamps like "5m", "2h", "Yesterday"
|
|
310
|
+
*/
|
|
311
|
+
parseRelativeTimestamp(text) {
|
|
312
|
+
const trimmed = text.trim().toLowerCase();
|
|
313
|
+
const now = new Date();
|
|
314
|
+
// Match patterns like "5m", "2h", "3d", "1w"
|
|
315
|
+
const relativeMatch = trimmed.match(/^(\d+)([mhdw])$/);
|
|
316
|
+
if (relativeMatch) {
|
|
317
|
+
const value = parseInt(relativeMatch[1]);
|
|
318
|
+
const unit = relativeMatch[2];
|
|
319
|
+
switch (unit) {
|
|
320
|
+
case 'm':
|
|
321
|
+
return new Date(now.getTime() - value * 60 * 1000);
|
|
322
|
+
case 'h':
|
|
323
|
+
return new Date(now.getTime() - value * 60 * 60 * 1000);
|
|
324
|
+
case 'd':
|
|
325
|
+
return new Date(now.getTime() - value * 24 * 60 * 60 * 1000);
|
|
326
|
+
case 'w':
|
|
327
|
+
return new Date(now.getTime() - value * 7 * 24 * 60 * 60 * 1000);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// Check for "Yesterday"
|
|
331
|
+
if (trimmed === 'yesterday') {
|
|
332
|
+
return new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
333
|
+
}
|
|
334
|
+
// Fallback to current time
|
|
335
|
+
return now;
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Get full thread with all messages
|
|
339
|
+
*/
|
|
340
|
+
async getThread(options) {
|
|
341
|
+
const { threadId, limit = 50 } = options;
|
|
342
|
+
// Navigate to thread
|
|
343
|
+
await this.page.goto(`https://www.linkedin.com/messaging/thread/${threadId}/`, {
|
|
344
|
+
waitUntil: 'networkidle',
|
|
345
|
+
timeout: 30000,
|
|
346
|
+
});
|
|
347
|
+
// Wait for messages to load
|
|
348
|
+
const messagesResult = await this.selectorEngine.findElement('messages', 'messageBubble', {
|
|
349
|
+
timeout: 10000,
|
|
350
|
+
});
|
|
351
|
+
if (!messagesResult.element) {
|
|
352
|
+
console.warn('Could not find message bubbles');
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
// Get all message bubbles
|
|
356
|
+
const messageElements = await this.page.$$(selectors_1.SELECTORS.messages.messageBubble.join(', '));
|
|
357
|
+
const messages = [];
|
|
358
|
+
for (let i = 0; i < Math.min(messageElements.length, limit); i++) {
|
|
359
|
+
const message = await this.parseMessage(messageElements[i], threadId);
|
|
360
|
+
if (message) {
|
|
361
|
+
messages.push(message);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
// Get participant info
|
|
365
|
+
const participants = await this.getThreadParticipants();
|
|
366
|
+
return {
|
|
367
|
+
id: threadId,
|
|
368
|
+
participants,
|
|
369
|
+
messages,
|
|
370
|
+
lastActivity: messages[messages.length - 1]?.timestamp || new Date(),
|
|
371
|
+
unreadCount: messages.filter((m) => !m.isRead).length,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Show a thread by clicking on a conversation card (for ember IDs) or direct navigation
|
|
376
|
+
* Returns the thread with messages, handling both ember IDs and real thread IDs
|
|
377
|
+
*/
|
|
378
|
+
async showThread(threadId, limit = 50) {
|
|
379
|
+
// Check if this is an ember ID (e.g., "ember50") or a real thread ID
|
|
380
|
+
const isEmberId = threadId.startsWith('ember');
|
|
381
|
+
if (isEmberId) {
|
|
382
|
+
// Navigate to messaging page first
|
|
383
|
+
await this.navigateToMessages();
|
|
384
|
+
// Find and click the conversation card to get the real thread ID
|
|
385
|
+
const realThreadId = await this.clickAndGetThreadId(threadId);
|
|
386
|
+
if (!realThreadId) {
|
|
387
|
+
console.warn(`Could not find conversation card with ID: ${threadId}`);
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
threadId = realThreadId;
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
// Check if already on the correct thread URL
|
|
394
|
+
const currentUrl = this.page.url();
|
|
395
|
+
const targetUrl = `https://www.linkedin.com/messaging/thread/${threadId}/`;
|
|
396
|
+
if (!currentUrl.includes(threadId)) {
|
|
397
|
+
// Navigate directly to the thread
|
|
398
|
+
await this.page.goto(targetUrl, {
|
|
399
|
+
waitUntil: 'domcontentloaded',
|
|
400
|
+
timeout: 30000,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
console.log('Already on correct thread URL');
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Wait for messages to load
|
|
408
|
+
await this.page.waitForTimeout(2000);
|
|
409
|
+
// Scrape messages
|
|
410
|
+
const messages = await this.scrapeMessages(threadId, limit);
|
|
411
|
+
const participants = await this.getThreadParticipants();
|
|
412
|
+
if (messages.length === 0) {
|
|
413
|
+
console.warn('No messages found in thread');
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
id: threadId,
|
|
418
|
+
participants,
|
|
419
|
+
messages,
|
|
420
|
+
lastActivity: messages[messages.length - 1]?.timestamp || new Date(),
|
|
421
|
+
unreadCount: messages.filter((m) => !m.isRead).length,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Click on a conversation card and extract the real thread ID from the URL
|
|
426
|
+
*/
|
|
427
|
+
async clickAndGetThreadId(emberId) {
|
|
428
|
+
try {
|
|
429
|
+
// Wait for conversation list to load
|
|
430
|
+
await this.page.waitForTimeout(3000);
|
|
431
|
+
// Strategy 1: Try to find the card by ID
|
|
432
|
+
let card = await this.page.$(`#conversation-card-${emberId}`);
|
|
433
|
+
// Strategy 2: Find by index using msg-conversation-card class
|
|
434
|
+
if (!card) {
|
|
435
|
+
const listItems = await this.page.$$('[class*="msg-conversation-listitem"]');
|
|
436
|
+
let conversationCards = [];
|
|
437
|
+
for (const listItem of listItems) {
|
|
438
|
+
const c = await listItem.$('.msg-conversation-card');
|
|
439
|
+
if (c)
|
|
440
|
+
conversationCards.push(c);
|
|
441
|
+
}
|
|
442
|
+
// Fallback to direct class match
|
|
443
|
+
if (conversationCards.length === 0) {
|
|
444
|
+
conversationCards = (await this.page.$$('.msg-conversation-card'));
|
|
445
|
+
}
|
|
446
|
+
// The ember ID number (e.g., ember50 -> 50) doesn't match DOM position
|
|
447
|
+
// We need to find the card that has this ember ID in its element ID
|
|
448
|
+
const targetEmberNumber = emberId.replace('ember', '');
|
|
449
|
+
for (let i = 0; i < conversationCards.length; i++) {
|
|
450
|
+
const cardId = await conversationCards[i].evaluate((el) => el.id);
|
|
451
|
+
if (cardId && cardId.includes(targetEmberNumber)) {
|
|
452
|
+
card = conversationCards[i];
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// If still not found, try using the first card as a test
|
|
457
|
+
if (!card && conversationCards.length > 0) {
|
|
458
|
+
card = conversationCards[0];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Strategy 3: Try data-testid
|
|
462
|
+
if (!card) {
|
|
463
|
+
const cards = await this.page.$$('[data-testid="conversation-card"]');
|
|
464
|
+
const index = parseInt(emberId.replace('ember', ''));
|
|
465
|
+
if (!isNaN(index) && cards.length > index) {
|
|
466
|
+
card = cards[index];
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!card) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
// Click the card - LinkedIn SPA doesn't trigger navigation event
|
|
473
|
+
await card.click();
|
|
474
|
+
// Wait for URL to change (SPA navigation)
|
|
475
|
+
await this.page.waitForTimeout(2000);
|
|
476
|
+
// Extract thread ID from the new URL
|
|
477
|
+
const currentUrl = this.page.url();
|
|
478
|
+
const match = currentUrl.match(/\/messaging\/thread\/([^/]+)/);
|
|
479
|
+
if (match) {
|
|
480
|
+
return match[1];
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
console.warn('Failed to click conversation card:', error);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Scrape messages from a loaded thread page
|
|
491
|
+
*/
|
|
492
|
+
async scrapeMessages(threadId, limit = 50) {
|
|
493
|
+
const messages = [];
|
|
494
|
+
// Wait for messages to load
|
|
495
|
+
await this.page.waitForTimeout(3000);
|
|
496
|
+
// Look for actual message content in the conversation
|
|
497
|
+
// LinkedIn uses event list items for messages
|
|
498
|
+
const messageElements = (await this.page.$$('.msg-s-event-listitem'));
|
|
499
|
+
for (const msgEl of messageElements) {
|
|
500
|
+
if (messages.length >= limit)
|
|
501
|
+
break;
|
|
502
|
+
// Get sender name
|
|
503
|
+
const senderName = await msgEl
|
|
504
|
+
.$eval('.msg-s-message-group__name', (el) => el.textContent?.trim() || '')
|
|
505
|
+
.catch(() => '');
|
|
506
|
+
// Get timestamp
|
|
507
|
+
let timestamp = new Date();
|
|
508
|
+
const datetimeAttr = await msgEl
|
|
509
|
+
.$eval('time', (el) => el.getAttribute('datetime'))
|
|
510
|
+
.catch(() => null);
|
|
511
|
+
if (datetimeAttr) {
|
|
512
|
+
timestamp = new Date(datetimeAttr);
|
|
513
|
+
}
|
|
514
|
+
// Get message content
|
|
515
|
+
const content = await msgEl
|
|
516
|
+
.$eval('.msg-s-event-listitem__body, [class*="event-listitem__body"]', (el) => el.textContent?.trim() || '')
|
|
517
|
+
.catch(() => '');
|
|
518
|
+
// Skip if no content
|
|
519
|
+
if (!content || content.length < 2)
|
|
520
|
+
continue;
|
|
521
|
+
// Skip UI noise
|
|
522
|
+
if (content.includes('Open Emoji Keyboard') ||
|
|
523
|
+
content.includes('Maximize compose') ||
|
|
524
|
+
content.includes('Send to') ||
|
|
525
|
+
content === 'Download') {
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
// Check if from current user
|
|
529
|
+
const isMe = await msgEl.evaluate((el) => {
|
|
530
|
+
return (el.classList.contains('msg-s-message-group--me') ||
|
|
531
|
+
el.classList.contains('sent') ||
|
|
532
|
+
el.closest('.msg-s-message-group--me') !== null);
|
|
533
|
+
});
|
|
534
|
+
messages.push({
|
|
535
|
+
id: `msg-${threadId}-${messages.length}`,
|
|
536
|
+
threadId,
|
|
537
|
+
sender: {
|
|
538
|
+
name: senderName || (isMe ? 'You' : 'Unknown'),
|
|
539
|
+
isMe,
|
|
540
|
+
},
|
|
541
|
+
content,
|
|
542
|
+
timestamp,
|
|
543
|
+
isRead: true,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
// If no messages found with event list items, try alternative approach
|
|
547
|
+
if (messages.length === 0) {
|
|
548
|
+
const messageContainers = (await this.page.$$('[class*="message"]'));
|
|
549
|
+
const seenContent = new Set();
|
|
550
|
+
for (const container of messageContainers) {
|
|
551
|
+
if (messages.length >= limit)
|
|
552
|
+
break;
|
|
553
|
+
const content = await container.evaluate((el) => {
|
|
554
|
+
// Skip meta elements
|
|
555
|
+
if (el.classList.contains('msg-s-message-group__meta') ||
|
|
556
|
+
el.classList.contains('msg-s-message-group__name') ||
|
|
557
|
+
el.classList.contains('msg-s-message-group__timestamp') ||
|
|
558
|
+
el.tagName.toLowerCase() === 'time') {
|
|
559
|
+
return '';
|
|
560
|
+
}
|
|
561
|
+
const text = el.textContent?.trim() || '';
|
|
562
|
+
// Skip short or UI text
|
|
563
|
+
if (text.length < 5 ||
|
|
564
|
+
text.includes('Open Emoji Keyboard') ||
|
|
565
|
+
text.includes('Download') ||
|
|
566
|
+
text.includes('MB') ||
|
|
567
|
+
text.includes('AM') ||
|
|
568
|
+
text.includes('PM')) {
|
|
569
|
+
return '';
|
|
570
|
+
}
|
|
571
|
+
return text;
|
|
572
|
+
});
|
|
573
|
+
if (content && !seenContent.has(content)) {
|
|
574
|
+
seenContent.add(content);
|
|
575
|
+
const isMe = await container.evaluate((el) => {
|
|
576
|
+
return el.closest('.msg-s-message-group--me') !== null;
|
|
577
|
+
});
|
|
578
|
+
messages.push({
|
|
579
|
+
id: `msg-${threadId}-${messages.length}`,
|
|
580
|
+
threadId,
|
|
581
|
+
sender: {
|
|
582
|
+
name: isMe ? 'You' : 'Other',
|
|
583
|
+
isMe,
|
|
584
|
+
},
|
|
585
|
+
content,
|
|
586
|
+
timestamp: new Date(),
|
|
587
|
+
isRead: true,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return messages;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Format thread messages as human-readable text (Style A)
|
|
596
|
+
*/
|
|
597
|
+
formatThreadText(thread) {
|
|
598
|
+
const lines = [];
|
|
599
|
+
const participantNames = thread.participants.map((p) => p.name).join(', ');
|
|
600
|
+
lines.push(`Conversations with ${participantNames}`);
|
|
601
|
+
lines.push('─'.repeat(60));
|
|
602
|
+
let lastDate = '';
|
|
603
|
+
for (const message of thread.messages) {
|
|
604
|
+
const date = message.timestamp.toLocaleDateString();
|
|
605
|
+
if (date !== lastDate) {
|
|
606
|
+
lines.push('');
|
|
607
|
+
lines.push(`─── ${date} ───`);
|
|
608
|
+
lastDate = date;
|
|
609
|
+
}
|
|
610
|
+
const time = message.timestamp.toLocaleTimeString([], {
|
|
611
|
+
hour: '2-digit',
|
|
612
|
+
minute: '2-digit',
|
|
613
|
+
});
|
|
614
|
+
const sender = message.sender.isMe ? 'You' : message.sender.name;
|
|
615
|
+
lines.push(`${time} ${sender}: ${message.content}`);
|
|
616
|
+
}
|
|
617
|
+
return lines.join('\n');
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Format thread messages as JSON for LLM processing
|
|
621
|
+
*/
|
|
622
|
+
formatThreadJson(thread) {
|
|
623
|
+
return JSON.stringify({
|
|
624
|
+
threadId: thread.id,
|
|
625
|
+
participants: thread.participants,
|
|
626
|
+
messages: thread.messages.map((m) => ({
|
|
627
|
+
id: m.id,
|
|
628
|
+
sender: {
|
|
629
|
+
name: m.sender.name,
|
|
630
|
+
isMe: m.sender.isMe,
|
|
631
|
+
},
|
|
632
|
+
content: m.content,
|
|
633
|
+
timestamp: m.timestamp.toISOString(),
|
|
634
|
+
})),
|
|
635
|
+
}, null, 2);
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Parse a single message element
|
|
639
|
+
*/
|
|
640
|
+
async parseMessage(element, threadId) {
|
|
641
|
+
try {
|
|
642
|
+
// Extract message ID
|
|
643
|
+
const messageId = await element.evaluate((el) => {
|
|
644
|
+
const element = el;
|
|
645
|
+
return (element.getAttribute('data-urn') || element.id || `msg-${Date.now()}-${Math.random()}`);
|
|
646
|
+
});
|
|
647
|
+
// Extract sender info - look for the sender name element
|
|
648
|
+
const senderInfo = await element
|
|
649
|
+
.$eval('.msg-s-message-group__name', (el) => ({
|
|
650
|
+
name: el.textContent?.trim() || '',
|
|
651
|
+
profileUrl: el.getAttribute('href') || undefined,
|
|
652
|
+
}))
|
|
653
|
+
.catch(() => ({ name: '', profileUrl: undefined }));
|
|
654
|
+
// Extract message content - look for the message body
|
|
655
|
+
let content = await element
|
|
656
|
+
.$eval('.msg-s-event-listitem__body', (el) => el.textContent?.trim() || '')
|
|
657
|
+
.catch(() => '');
|
|
658
|
+
// Fallback: try other content selectors
|
|
659
|
+
if (!content) {
|
|
660
|
+
content = await element
|
|
661
|
+
.$eval('.msg-s-message-group__message-body', (el) => el.textContent?.trim() || '')
|
|
662
|
+
.catch(() => '');
|
|
663
|
+
}
|
|
664
|
+
// Fallback: try any element that looks like message content
|
|
665
|
+
if (!content) {
|
|
666
|
+
const contentEl = await element.$('[class*="message-body"]');
|
|
667
|
+
if (contentEl) {
|
|
668
|
+
content = await contentEl.evaluate((el) => el.textContent?.trim() || '');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// Skip if no meaningful content
|
|
672
|
+
if (!content || content.length < 2) {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
// Extract timestamp
|
|
676
|
+
let timestamp = new Date();
|
|
677
|
+
const datetimeAttr = await element
|
|
678
|
+
.$eval('time', (el) => el.getAttribute('datetime'))
|
|
679
|
+
.catch(() => null);
|
|
680
|
+
if (datetimeAttr) {
|
|
681
|
+
timestamp = new Date(datetimeAttr);
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
// Try to get timestamp from text content
|
|
685
|
+
const timeText = await element
|
|
686
|
+
.$eval('.msg-s-message-group__timestamp', (el) => el.textContent?.trim() || '')
|
|
687
|
+
.catch(() => '');
|
|
688
|
+
if (timeText) {
|
|
689
|
+
// Try to parse relative time
|
|
690
|
+
const now = new Date();
|
|
691
|
+
if (timeText.toLowerCase().includes('today')) {
|
|
692
|
+
timestamp = now;
|
|
693
|
+
}
|
|
694
|
+
else if (timeText.toLowerCase().includes('yesterday')) {
|
|
695
|
+
timestamp = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
// Check if message is from current user
|
|
700
|
+
const isMe = await element.evaluate((el) => {
|
|
701
|
+
const element = el;
|
|
702
|
+
return (element.classList.contains('msg-s-message-group--me') ||
|
|
703
|
+
element.classList.contains('sent') ||
|
|
704
|
+
element.closest('.msg-s-message-group--me') !== null);
|
|
705
|
+
});
|
|
706
|
+
return {
|
|
707
|
+
id: messageId,
|
|
708
|
+
threadId,
|
|
709
|
+
sender: {
|
|
710
|
+
name: senderInfo.name || (isMe ? 'You' : 'Unknown'),
|
|
711
|
+
profileUrl: senderInfo.profileUrl,
|
|
712
|
+
isMe,
|
|
713
|
+
},
|
|
714
|
+
content,
|
|
715
|
+
timestamp,
|
|
716
|
+
isRead: true,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
catch (error) {
|
|
720
|
+
console.warn('Failed to parse message:', error);
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Get thread participant information
|
|
726
|
+
*/
|
|
727
|
+
async getThreadParticipants() {
|
|
728
|
+
try {
|
|
729
|
+
// Look for participant names in the thread header
|
|
730
|
+
const participants = await this.page.$$eval('.msg-entity-lockup__entity-title, [class*="participant-name"], [class*="thread-title"]', (elements) => elements.map((el) => ({
|
|
731
|
+
name: el.textContent?.trim() || 'Unknown',
|
|
732
|
+
profileUrl: el.getAttribute('href') || undefined,
|
|
733
|
+
})));
|
|
734
|
+
if (participants.length > 0) {
|
|
735
|
+
return participants;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
// Fall through to default
|
|
740
|
+
}
|
|
741
|
+
return [{ name: 'Unknown', profileUrl: undefined }];
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
exports.LinkedInMessages = LinkedInMessages;
|
|
745
|
+
//# sourceMappingURL=messages.js.map
|