// ==UserScript== // @name netzradar-linker // @source https://gitlab.ard.de/ida/netzradar-linker // @description extrahiert Metadaten (Headline, Hostnamen, URL) des gegenwärtigen Dokuments, generiert Kurz-URL, übersetzt englischsprachigen Titel ins Deutsch, und kopiert Metadaten und Kurz-URL in die Zwischenablage. // @version 2 // @grant GM_setClipboard // @match *://*/* // ==/UserScript== (function () { "use strict"; /* ===================== Einstellungen ===================== */ /* Status-Meldungen */ /** Zeige diesen Text im Button an */ const STATUS_START_TEXT = "Link kürzen"; const STATUS_START_COLOR = "#e52600"; /** Zeige diesen Text im Button an, wenn kopiert wurde */ const STATUS_COPIED_TEXT = "Link in 📋 🥳"; const STATUS_COPIED_COLOR = "black"; /** Zeige diesen Text im Button an, wenn Yourls-Abruf nicht klappt */ const STATUS_YOURLS_ERROR_TEXT = "❌ Yourls-Fehler ❌"; /** Setze STATUS_COPIED zurück auf STATUS_START zurück (nach Sekunden) */ const STATUS_RESET_AFTER = 2; /* Ausgabe */ /** Kopiere diese Werte in dieser Form in die Zwischenablage */ const OUTPUT_TEMPLATE = "{{domain}}: {{headline}}\n{{url}}"; /* Headline */ /** Nutze diese Tags (in dieser Reihenfolge), um eine Headline zu finden */ const HEADLINE_TAGS = ["h1", "title"]; /** Nutze diesen Text, wenn keine Headline gefunden wird */ const HEADLINE_FALLBACK = "<Titel bitte manuell eintragen>"; /* Yourls-API */ /** URL unter der die Yourls-API erreichbar */ const YOURLS_API_URL = new URL('https://x.swr.de/a/yourls-api.php'); /** * @typedef {Object} YourlsApiParams * @property {string} username Benutzername * @property {string} password Passwort * @property {'shorturl'} action durchzuführende Aktion * @property {'json'} format Rückgabe-Format */ /** * Parameter für Yourls-API. Details können unter http://x.swr.de/a/ eingesehen werden bzw. im PWDSafe. * @type {YourlsApiParams} */ const YOURLS_API_PARAMS = { username: "<User>", password: "<Password>", action: "shorturl", format: "json", }; /* Google-Translate-API */ /** URL unter der Google-Translate erreichbar ist */ const GOOGLE_TRANSLATE_API_URL = "https://www.googleapis.com/language/translate/v2"; /** * Parameter für Google-Translate-API. * @typedef {Object} GoogleTranslateParams * @property {string} key Google-API-Key * @property {string} target Code für die Zielsprache. * @property {string} source Code für die Ursprungssprache (wird automatisch erkannt, wenn leerer String). */ /** * Google Translate-Parameter. * @type {GoogleTranslateParams} */ const GOOGLE_TRANSLATE_PARAMS = { key: "YOUR_GOOGLE_API_KEY", target: "de", source: "", }; /* ===================== Funktionen ===================== */ /* Extraktoren */ /** * Erhalte den Domainname (ohne WWW) von einer URL. * @param {URL} targetURL URL deren Domainname ausgegeben werden soll * @returns {string} Domainname (ohne WWW) */ const extractDomainName = (targetURL) => targetURL.hostname.toLowerCase().startsWith("www.") ? targetURL.hostname.slice(4) : targetURL.hostname; /** * Extrahiere die Headline aus einem Dokument. * @param {Document} document Dokument aus dem die Headline ausgelesen werden soll * @param {Array.<string>} headlineTags Suche in diesen Tags in dieser Reihenfolge nach der Headline * @returns {string | undefined} Headline (wenn gefunden), sonst undefined */ const extractHeadline = (document, headlineTags) => headlineTags .map((tag) => document.querySelector(tag)?.innerText) .filter((mayBeTitle) => typeof mayBeTitle === "string")[0] ?.trim(); /* API-Calls */ /** * Erhalte eine Kurz-URL von `targetURL` von der Yourls-API. * @param {URL} apiURL URL der Yourls-API * @param {YourlsApiParams} apiParams Paramater der der Yourls-API * @param {URL} targetURL URL die gekürzt werden soll * @returns {Promise<string>} Die gekürzte URL * @throws {Error} Es konnte keine Kurz-URL abgeholt werden */ const fetchShortURL = async (apiURL, apiParams, targetURL) => { // GET-URL bauen für Yourls-API const params = { ...apiParams, url: targetURL.toString() }; Object.entries(params).forEach(([name, value]) => apiURL.searchParams.append(name, value) ); // Request durchführen const request = await fetch(apiURL); if (!request.ok) throw new Error(request); const data = await request.json(); return data.shorturl; }; /** * Übersetze `text` via Google-Translate-API. * @param {URL} apiURL URL der Google-Translate-API * @param {GoogleTranslateParams} apiParams - API-Parameter. * @param {string} text Zu übersetzender Text. * @returns {Promise<string>} Der übersetzte Text. * @throws {Error} Request ist fehlgeschlagen. */ const fetchTranslation = async (apiURL, apiParams, text) => { // GET-URL bauen für Google-Translate-API const params = { ...apiParams, q: text }; Object.entries(params).forEach(([name, value]) => apiURL.searchParams.append(name, value) ); // Request durchführen const request = await fetch(apiURL); if (!request.ok) throw new Error(request); const data = await request.json(); // Übersetzung zurückgeben (wenn nicht vorhanden, den Originaltext) return data?.data?.translations[0]?.translatedText || text; }; /* Button */ /** * Füge Interaktions-Button ins Dokument ein. * @param {Document} document Dokument dem der Button hinzufgefügt werden soll * @param {(event: MouseEvent, button: HTMLButtonElement) => Promise<void>} handleClick Löse diese Funktion bei Klick aus * @returns {HTMLButtonElement} Das erstellte HTML-Button-Element */ const addButton = (document, handleClick) => { const button = document.createElement("button"); button.setAttribute("id", "myshortenLink"); button.addEventListener("click", (e) => handleClick(e, button), false); button.innerHTML = STATUS_START_TEXT; // Button stylen button.style.bottom = "20px"; button.style.right = "50px"; button.style.height = "50px"; button.style.width = "200px"; button.style.position = "fixed"; button.style.color = "white"; button.style.fontWeight = "700"; button.style.fontFamily = "sans-serif"; button.style.fontSize = "14px"; button.style.padding = "3px"; button.style.borderStyle = "none"; button.style.borderRadius = "25px"; button.style.zIndex = 99999999; button.style.cursor = "pointer"; button.style.backgroundColor = STATUS_START_COLOR; button.style.transition = "background-color 0.7s ease"; // Button dem Dokument hinzufügen document.body.appendChild(button); return button; }; /** wurde der Button gedrückt? */ let buttonIsPressed = false; /** * Manage den Anzeige-Text und Anzeige-Stil des Buttons * (vgl. für Status: `buttonIsPressed`). * @param {HTMLButtonElement} button Der Knopf */ const toggleButton = (button) => { buttonIsPressed = !buttonIsPressed; // Löse Reflow aus, um sicherzustellen, dass die Transition startet void button.offsetWidth; if (buttonIsPressed) { button.style.backgroundColor = STATUS_COPIED_COLOR; button.innerHTML = STATUS_COPIED_TEXT; return; } button.innerHTML = STATUS_START_TEXT; button.style.backgroundColor = STATUS_START_COLOR; }; /* ===================== Programmablauf ===================== */ /** * Extrahiere Metadaten (Headline, Hostnamen, URL) des aktuellen Dokuments, * generiere Kurz-URL, * übersetze fremdsprachige Headline ins Deutsch * und kopiere Metadaten und Kurz-URL in die Zwischenablage. * @param {MouseEvent} event Das auslösende Klick-Event * @param {HTMLButtonElement} button Der gedrückte Knopf */ const handleClick = async (event, button) => { event.preventDefault(); /* Extrahiere Metadaten */ /** URL der Website, die verlinkt werden soll */ const targetURL = new URL(window.location.href); const domainName = extractDomainName(targetURL); // Wenn die extrahierte falsy ist (hier relevant: leerer String oder undefined), // soll HEADLINE_FALLBACK genutzt werden const headline = extractHeadline(document, HEADLINE_TAGS) || HEADLINE_FALLBACK; /* Hole Short-URL und breche ab, falls nicht möglich */ let shortURL = ""; try { shortURL = await fetchShortURL( YOURLS_API_URL, YOURLS_API_PARAMS, targetURL ); } catch (error) { button.innerHTML = STATUS_YOURLS_ERROR_TEXT; const msg = `Fataler Fehler, konnte short-URL nicht generieren ${JSON.stringify( error )}`; throw new Error(msg); } /* Wenn nötig: Versuche Headline zu übersetzen */ let translatedHeadline = headline; if (headline !== HEADLINE_FALLBACK) { try { translatedHeadline = await fetchTranslation( GOOGLE_TRANSLATE_API_URL, GOOGLE_TRANSLATE_PARAMS ); } catch (error) { console.error( `Fehler, konnte nicht übersetzen -- fahre ohne Übersetzung fort: ${JSON.stringify( error )}` ); } } /* Formatiere Ausgabe */ const output = OUTPUT_TEMPLATE.replace("{{domain}}", domainName) .replace("{{headline}}", translatedHeadline) .replace("{{url}}", shortURL); // Kopiere Ausgabe in den Zwischenspeicher // vgl. https://www.tampermonkey.net/documentation.php?locale=en#api:GM_setClipboard GM_setClipboard(output); /* Informiere Userin, dass kopiert */ toggleButton(button); // Setze Text zurück (falls Userin noch einmal kopieren möchte) let buttonPressedTimeoutID = undefined; // Lösche ggf. existierendes Timeout clearTimeout(buttonPressedTimeoutID); buttonPressedTimeoutID = setTimeout( () => toggleButton(button), STATUS_RESET_AFTER * 1000 ); }; // Button hinzufügen addButton(document, handleClick); })();