Skip to content

Commit 3c5d22b

Browse files
hanziliclaude
andcommitted
fix(linkedin): clean name, profile URL and date in sent-invitations
sent-invitations emitted the person's name concatenated with their job headline ("Kyle Chang Manager at C2C Premium Seafood"), an empty profile_url on every row, a junk row from the Received/Sent nav tabs, and a date field that could capture a stray month substring (the "may" inside a surname). Rewrite the extraction: card text is no longer whitespace-collapsed before line splitting, so the name and headline stay separable; the profile link is matched by href rather than requiring visible text (the LinkedIn card link wraps only the avatar); rows are deduped by name with a merge so the profile URL is filled from whichever card carries it; the date is matched by a precise Sent/Invited relative-time pattern; and only cards with a Withdraw affordance are emitted, dropping nav tabs. Verified e2e: 10 clean rows with correct names, profile URLs and dates. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent b59897d commit 3c5d22b

1 file changed

Lines changed: 26 additions & 23 deletions

File tree

clis/linkedin/sent-invitations.js

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,46 @@ function unwrapEvaluateResult(payload) {
1111

1212
function buildSentInvitationsScript() {
1313
return String.raw`(() => {
14-
const clean = (s) => String(s || '').replace(/[\u00a0\u202f]/g, ' ').replace(/\s+/g, ' ').trim();
14+
const clean = (s) => String(s || '').replace(/[  ]/g, ' ').replace(/\s+/g, ' ').trim();
1515
const text = document.body ? (document.body.innerText || '') : '';
1616
const href = location.href;
1717
const authRequired = /\b(sign in|log in|join linkedin)\b/i.test(text)
1818
|| /linkedin\.com\/(login|checkpoint|authwall|uas)/i.test(href);
1919
const warning = /captcha|verification required|unusual activity|account restricted|temporarily restricted|security check|checkpoint/i.test(text);
20+
const cleanName = (value) => {
21+
const first = String(value || '').split(/\n+/).map(clean).filter(Boolean)[0] || '';
22+
return clean(first
23+
.replace(/^(view\s+)?profile\s+of\s+/i, '')
24+
.replace(/\s*(?:View profile|LinkedIn|Pending|Sent|Withdraw).*$/i, ''));
25+
};
2026
const cards = Array.from(document.querySelectorAll('li, div, section, article')).filter((el) => {
2127
if (!el || el.offsetParent === null) return false;
2228
const t = clean(el.innerText || el.textContent || '');
23-
return t && /withdraw|pending|sent/i.test(t) && t.length < 1200;
29+
return t && /withdraw/i.test(t) && t.length < 1200;
2430
});
25-
const rows = [];
26-
const seen = new Set();
31+
const byName = new Map();
2732
for (const card of cards) {
28-
const raw = clean(card.innerText || card.textContent || '');
29-
if (!raw) continue;
30-
const link = Array.from(card.querySelectorAll('a[href*="/in/"]')).find((a) => clean(a.innerText || a.textContent || a.getAttribute('aria-label')));
31-
const linkText = clean(link?.innerText || link?.textContent || link?.getAttribute('aria-label') || '');
33+
const raw = card.innerText || card.textContent || '';
34+
if (!/withdraw/i.test(raw)) continue;
3235
const lines = raw.split(/\n+/).map(clean).filter(Boolean);
33-
const cleanName = (value) => {
34-
const line = clean(value).split(/\n+/).map(clean).filter(Boolean)[0] || '';
35-
return clean(line
36-
.replace(/^(view\s+)?profile\s+of\s+/i, '')
37-
.replace(/\s*(?:View profile|LinkedIn|Pending|Sent|Withdraw).*$/i, ''));
38-
};
39-
let name = cleanName(linkText)
40-
|| cleanName(lines.find((line) => !/^(pending|sent|withdraw|message|view profile|invitation|invited|ago|manage)/i.test(line)) || '');
36+
const link = card.querySelector('a[href*="/in/"]');
37+
const linkName = cleanName(link ? (link.innerText || link.textContent || link.getAttribute('aria-label') || '') : '');
38+
const name = linkName
39+
|| cleanName(lines.find((line) => !/^(pending|sent|withdraw|message|view profile|invitation|invited|ago|manage|received)\b/i.test(line)) || '');
40+
if (!name) continue;
4141
const hrefAttr = link ? (link.getAttribute('href') || '') : '';
4242
const profile_url = hrefAttr ? new URL(hrefAttr, location.origin).toString().replace(/[?#].*$/, '') : '';
43-
const dateLine = lines.find((line) => /sent|invited|ago|\b\d{4}\b|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec/i.test(line)) || '';
44-
const invited_date_text = clean((dateLine.match(/(?:sent|invited)\s+(?:\d+\s+\w+\s+ago|yesterday|today|on\s+[^\n]+|[A-Z][a-z]{2,9}\s+\d{1,2},?\s+\d{4}|\d{4})/i) || [''])[0] || dateLine);
45-
const key = (profile_url || name).toLowerCase();
46-
if (name && !seen.has(key)) {
47-
seen.add(key);
48-
rows.push({ name, profile_url, invited_date_text });
43+
const invited_date_text = clean((raw.match(/(?:Sent|Invited)\s+(?:\d+\s+\w+\s+ago|yesterday|today)/i) || [''])[0]);
44+
const key = name.toLowerCase();
45+
const existing = byName.get(key);
46+
if (!existing) {
47+
byName.set(key, { name, profile_url, invited_date_text });
48+
} else {
49+
if (!existing.profile_url && profile_url) existing.profile_url = profile_url;
50+
if (!existing.invited_date_text && invited_date_text) existing.invited_date_text = invited_date_text;
4951
}
5052
}
53+
const rows = Array.from(byName.values());
5154
return { url: href, title: document.title || '', authRequired, warning, count: rows.length, rows, bodyText: text.slice(0, 1000) };
5255
})()`;
5356
}
@@ -65,7 +68,7 @@ cli({
6568
func: async (page) => {
6669
if (!page) throw new CommandExecutionError('Browser session required for linkedin sent-invitations');
6770
await page.goto(SENT_URL);
68-
await page.wait(8);
71+
await page.wait(12);
6972
let result = unwrapEvaluateResult(await page.evaluate(buildSentInvitationsScript()));
7073
if (result?.authRequired) {
7174
throw new AuthRequiredError(LINKEDIN_DOMAIN, 'LinkedIn sent invitations requires an active signed-in browser session.');

0 commit comments

Comments
 (0)