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