From 939ddba73f33dc4e490e1a9040729e8c597c6def Mon Sep 17 00:00:00 2001 From: Kristoph Sachsenweger <kristoph.sachsenweger@swr.de> Date: Wed, 27 Mar 2024 11:00:16 +0000 Subject: [PATCH] Add new file --- netzradar-linker.js | 318 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 netzradar-linker.js diff --git a/netzradar-linker.js b/netzradar-linker.js new file mode 100644 index 0000000..eadff6e --- /dev/null +++ b/netzradar-linker.js @@ -0,0 +1,318 @@ +// ==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); +})(); -- GitLab