fix(kontakte): LinkedIn-Foto nur aus der Vorschau, klarere Meldung

Anlass: Bei einem Kontakt kam das Foto, beim naechsten nicht. Der Vergleich
der beiden Profilseiten zeigt warum - das zweite Profil liefert als
og:image nur LinkedIns Ersatzgrafik (static.licdn.com/aero-v1/sc/h/...),
weil die Sichtbarkeit des Fotos auf "Meine Kontakte" steht. Angemeldet
sieht man es, der Abruf hier ist es nicht.

Zwei Konsequenzen:

* Die Rueckfallebene "erste media.licdn.com-Adresse im Quelltext" faellt
  weg. Dieselbe Seite enthaelt 24 solcher Adressen: Firmenlogos,
  Beitragsbilder und die Fotos der "aehnlichen Profile" am Rand. Nur die
  Open-Graph-Vorschau gehoert nachweislich zum aufgerufenen Profil -
  alles andere haette irgendwann ein fremdes Gesicht an einen Kontakt
  gehaengt.
* Die Meldung sagt jetzt, was wirklich los ist: "nur fuer Kontakte
  sichtbar oder es gibt keines" statt "hat kein eigenes Foto". Von
  aussen sind die beiden Faelle nicht unterscheidbar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-09-10 00:30:30 +02:00
co-authored by Claude Opus 5
parent 0a2cf52e8a
commit 46983a7e2c
5 changed files with 68 additions and 24 deletions
+17 -9
View File
@@ -68,9 +68,14 @@ export function normalizeProfileUrl(input) {
}
/**
* Bild-URL aus dem HTML der Profilseite ziehen. og:image ist der
* zuverlaessigste Treffer; als Rueckfallebene wird nach einer Foto-URL des
* LinkedIn-CDN im Seitenquelltext gesucht.
* Bild-URL aus dem HTML der Profilseite ziehen - ausschliesslich aus der
* Open-Graph-Vorschau (`og:image`, ersatzweise `twitter:image`).
*
* Bewusst wird NICHT im uebrigen Quelltext nach einer Bildadresse gesucht: eine
* Profilseite enthaelt Dutzende Adressen des LinkedIn-CDN - Firmenlogos,
* Beitragsbilder und die Fotos der "aehnlichen Profile" am Rand. Die erste
* davon zu nehmen, haengt irgendwann einem Kontakt das Gesicht einer fremden
* Person an. Nur die Vorschau gehoert nachweislich zum aufgerufenen Profil.
*/
function extractImageUrl(html) {
const metaPattern = /<meta[^>]+>/gi;
@@ -80,9 +85,7 @@ function extractImageUrl(html) {
const content = tag.match(/content\s*=\s*["']([^"']+)["']/i);
if (content?.[1]) return decodeEntities(content[1]);
}
const direct = html.match(/https:\/\/media[^"'\s\\]*\.licdn\.com\/dms\/image\/[^"'\s\\]+/i);
return direct ? decodeEntities(direct[0]) : null;
return null;
}
/** Im HTML stehen die URLs mit maskierten Und-Zeichen. */
@@ -151,13 +154,18 @@ export async function fetchLinkedInPhoto(profileInput) {
}
/*
* Profile ohne eigenes Foto bekommen von LinkedIn eine graue Platzhalter-
* Silhouette als Vorschaubild. Die soll nicht als Kontaktfoto landen.
* Echte Profilfotos liegen unter media.licdn.com/dms/image/... - alles andere
* ist LinkedIns Ersatzbild: die graue Silhouette ('ghost') oder eine feste
* Grafik von static.licdn.com (/aero-v1/sc/h/...). Das bekommt zu sehen, wer
* nicht angemeldet ist und dessen Profilfoto nur fuer Kontakte sichtbar ist -
* von aussen laesst sich das nicht von "gar kein Foto" unterscheiden, deshalb
* nennt die Meldung beide Moeglichkeiten.
*/
const path = parsedImage.pathname.toLowerCase();
if (path.includes('ghost') || !path.includes('/dms/image/')) {
throw new PhotoError(
'Das Profil hat kein eigenes Foto hinterlegt.',
'LinkedIn gibt zu diesem Profil kein Foto heraus: Es ist nur für Kontakte ' +
'sichtbar oder es gibt keines. Bitte das Foto manuell hochladen.',
404
);
}