MediaWiki:Gadget-Mastodon.js

Version vom 19. September 2026, 20:12 Uhr von Admin (Diskussion | Beiträge) (Die Seite wurde neu angelegt: „/** * Gadget-Mastodon.js * ============================================================================ * Bettet Mastodon-Beiträge (samt Video oder anderen Medien) in Wiki-Seiten * ein – instanzunabhängig, also mastodon.social genauso wie jede andere * Instanz. Verwendet die eingebaute /embed-Ansicht von Mastodon. * * KEIN ZUSATZSKRIPT DES ANBIETERS * Die offizielle Einbindung lädt embed.js der Instanz nach, nur um die Höhe * des Rahmens an…“)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)

Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.

  • Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
  • Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
  • Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
/**
 * Gadget-Mastodon.js
 * ============================================================================
 * Bettet Mastodon-Beiträge (samt Video oder anderen Medien) in Wiki-Seiten
 * ein – instanzunabhängig, also mastodon.social genauso wie jede andere
 * Instanz. Verwendet die eingebaute /embed-Ansicht von Mastodon.
 *
 * KEIN ZUSATZSKRIPT DES ANBIETERS
 * Die offizielle Einbindung lädt embed.js der Instanz nach, nur um die Höhe
 * des Rahmens anzupassen. Das macht dieses Gadget selbst: Mastodon beantwortet
 * eine postMessage mit seiner tatsächlichen Höhe. Es wird also genau eine
 * Adresse der Instanz abgerufen und sonst nichts.
 *
 * KEINE ZWEI-KLICK-SPERRE – warum nicht?
 * Die /embed-Ansicht ist eine öffentliche, cachebare Seite ohne Werbung,
 * ohne Reichweitenmessung und ohne Profilbildung; für anonyme Besucher
 * setzt sie keine Cookies. Damit greift die Einwilligungspflicht des
 * §165 TKG nicht, und für die IP-Übertragung beim Abruf trägt das
 * berechtigte Interesse – wie beim CCC-Gadget. Anders bei fahrplan.oebb.at
 * und makertube.net, die ihre Vorschaubox behalten haben.
 *
 * NOTAUSGANG: data-sperre="ja" schaltet für eine einzelne Einbettung doch
 * eine Vorschaubox mit Ladeknopf davor – gedacht für Instanzen, deren
 * Betreiber du nicht einschätzen kannst.
 *
 * EINBINDUNG AUF EINER SEITE
 *   <div class="mastodon-embed"
 *        data-post="https://mastodon.social/@benutzer/123456789012345678"></div>
 *
 * Alternativ getrennt:
 *   <div class="mastodon-embed" data-instanz="mastodon.social"
 *        data-benutzer="benutzer" data-id="123456789012345678"></div>
 *
 * data-Attribute:
 *   data-post      Vollständige Adresse des Beitrags (empfohlen).
 *   data-instanz   Hostname der Instanz, falls data-post nicht genutzt wird.
 *   data-benutzer  Benutzername ohne @, zusammen mit data-instanz/data-id.
 *   data-id        Beitrags-ID, zusammen mit data-instanz/data-benutzer.
 *   data-width     Breite (Standard 100%, höchstens 42em).
 *   data-height    Starthöhe in Pixeln (Standard 600). Die endgültige Höhe
 *                  meldet Mastodon selbst, sobald der Beitrag geladen ist.
 *   data-titel     Titelzeile des Rahmens, für Screenreader
 *                  (Standard: "Mastodon-Beitrag").
 *   data-sperre    "ja" => Vorschaubox mit Ladeknopf davorschalten.
 * ============================================================================
 */
( function () {
	'use strict';

	var STD_HOEHE = 600;

	/* Laufende Rahmen für die Höhenmeldung: Kennnummer -> iframe */
	var rahmenListe = {};
	var naechsteNummer = 0;
	var hoertZu = false;

	function stil( el, regeln ) {
		var name;
		for ( name in regeln ) {
			el.style[ name ] = regeln[ name ];
		}
		return el;
	}

	function masszahl( wert, standard ) {
		if ( wert === undefined || wert === null || wert === '' ) {
			return standard;
		}
		var text = String( wert ).trim();
		return /^\d+$/.test( text ) ? text + 'px' : text;
	}

	/* ------------------------------------------------------------------ *
	 * Adresse des Beitrags auswerten.
	 * Erlaubt sind die üblichen Mastodon-Formen:
	 *   https://instanz/@benutzer/123…
	 *   https://instanz/@benutzer@andereinstanz/123…
	 *   https://instanz/users/benutzer/statuses/123…
	 * Zurück kommt { host, seite, embed } oder null.
	 * ------------------------------------------------------------------ */
	function leseAdresse( roh ) {
		var text = String( roh || '' ).trim();
		if ( !/^https:\/\//i.test( text ) ) {
			return null;
		}
		var a = document.createElement( 'a' );
		a.href = text;
		if ( !a.hostname || a.protocol !== 'https:' ) {
			return null;
		}
		var pfad = a.pathname.replace( /\/+$/, '' ).replace( /\/embed$/, '' );
		if ( !/^\/(@[^/]+\/\d+|users\/[^/]+\/statuses\/\d+)$/.test( pfad ) ) {
			return null;
		}
		var seite = 'https://' + a.hostname + pfad;
		return { host: a.hostname, seite: seite, embed: seite + '/embed' };
	}

	/* ------------------------------------------------------------------ *
	 * Höhenmeldung von Mastodon entgegennehmen. Das eingebettete Mastodon
	 * antwortet auf { type: "setHeight", id: … } mit derselben Nachricht
	 * samt height – so sparen wir uns das embed.js des Anbieters.
	 * ------------------------------------------------------------------ */
	function starteZuhoerer() {
		if ( hoertZu ) {
			return;
		}
		hoertZu = true;
		window.addEventListener( 'message', function ( e ) {
			var daten = e.data || {};
			if ( daten.type !== 'setHeight' || !rahmenListe[ daten.id ] ) {
				return;
			}
			var hoehe = parseInt( daten.height, 10 );
			if ( isFinite( hoehe ) && hoehe > 0 ) {
				rahmenListe[ daten.id ].style.height = hoehe + 'px';
			}
		} );
	}

	/* --- Beitrag einhängen ------------------------------------------------ */
	function lade( container, cfg ) {
		container.textContent = '';
		starteZuhoerer();

		var nummer = 'wiki-mastodon-' + ( naechsteNummer++ );

		var iframe = document.createElement( 'iframe' );
		iframe.src = cfg.embed;
		iframe.title = cfg.titel + ' (' + cfg.host + ')';
		iframe.setAttribute( 'frameborder', '0' );
		iframe.setAttribute( 'scrolling', 'no' );
		iframe.setAttribute( 'allowfullscreen', 'allowfullscreen' );
		iframe.setAttribute( 'allow', 'fullscreen' );
		iframe.setAttribute( 'sandbox',
			'allow-same-origin allow-scripts allow-popups' );
		iframe.setAttribute( 'loading', 'lazy' );
		iframe.setAttribute( 'referrerpolicy', 'no-referrer' );
		stil( iframe, {
			width: cfg.breite,
			maxWidth: '42em',
			height: cfg.hoehe,
			border: '0'
		} );

		rahmenListe[ nummer ] = iframe;
		iframe.addEventListener( 'load', function () {
			try {
				iframe.contentWindow.postMessage(
					{ type: 'setHeight', id: nummer }, 'https://' + cfg.host
				);
			} catch ( e ) {}
		} );

		container.appendChild( iframe );

		// Rückfallebene: wer das iframe blockiert, bekommt wenigstens den Weg
		// zum Beitrag.
		var fuss = stil( document.createElement( 'div' ), {
			fontSize: '85%',
			color: '#54595d',
			marginTop: '0.4em'
		} );
		var link = document.createElement( 'a' );
		link.href = cfg.seite;
		link.target = '_blank';
		link.rel = 'noopener';
		link.textContent = 'Beitrag auf ' + cfg.host + ' öffnen';
		fuss.appendChild( link );
		container.appendChild( fuss );
	}

	/* --- Notausgang: Vorschaubox statt sofortigem Laden -------------------- */
	function sperre( container, cfg ) {
		container.textContent = '';

		var box = stil( document.createElement( 'div' ), {
			border: '1px solid #a2a9b1',
			borderRadius: '2px',
			background: '#f8f9fa',
			padding: '1em',
			maxWidth: '42em',
			boxSizing: 'border-box'
		} );
		box.className = 'mastodon-embed-sperre';

		var kopf = stil( document.createElement( 'div' ), {
			fontWeight: 'bold',
			fontSize: '108%',
			marginBottom: '0.4em'
		} );
		kopf.textContent = cfg.titel + ' von ' + cfg.host;
		box.appendChild( kopf );

		var text = stil( document.createElement( 'p' ), { margin: '0.4em 0' } );
		text.textContent = 'Der Beitrag wird direkt von der Mastodon-Instanz ' +
			cfg.host + ' geladen – einem Server, der nicht uns gehört. Dabei ' +
			'erfährt dessen Betreiber deine IP-Adresse und welchen Beitrag du ' +
			'ansiehst. Vor deinem Klick geht keine Anfrage dorthin.';
		box.appendChild( text );

		var knopf = stil( document.createElement( 'button' ), {
			cursor: 'pointer',
			padding: '0.4em 0.9em',
			fontWeight: 'bold'
		} );
		knopf.type = 'button';
		knopf.textContent = 'Beitrag laden';
		knopf.addEventListener( 'click', function () {
			lade( container, cfg );
		} );
		box.appendChild( stil( document.createElement( 'div' ),
			{ margin: '0.7em 0' } ) ).appendChild( knopf );

		var fuss = stil( document.createElement( 'div' ), {
			fontSize: '85%',
			color: '#54595d',
			marginTop: '0.6em'
		} );
		var link = document.createElement( 'a' );
		link.href = cfg.seite;
		link.target = '_blank';
		link.rel = 'noopener';
		link.textContent = 'Stattdessen direkt auf ' + cfg.host + ' ansehen';
		fuss.appendChild( link );
		box.appendChild( fuss );

		container.appendChild( box );
	}

	/* --- Einstieg ---------------------------------------------------------- */
	function baueEinbettung( container ) {
		var $c = $( container );
		var roh = $.trim( String( $c.data( 'post' ) || '' ) );

		if ( !roh ) {
			var instanz = $.trim( String( $c.data( 'instanz' ) || '' ) )
				.replace( /^https?:\/\//i, '' ).replace( /\/+$/, '' );
			var benutzer = $.trim( String( $c.data( 'benutzer' ) || '' ) )
				.replace( /^@/, '' );
			var id = $.trim( String( $c.data( 'id' ) || '' ) );
			if ( instanz && benutzer && id ) {
				roh = 'https://' + instanz + '/@' + benutzer + '/' + id;
			}
		}

		var adresse = leseAdresse( roh );
		if ( !adresse ) {
			container.textContent = 'Mastodon-Einbettung: keine gültige ' +
				'Beitragsadresse angegeben (erwartet z. B. ' +
				'https://mastodon.social/@benutzer/123456789012345678).';
			return;
		}

		var cfg = {
			host: adresse.host,
			seite: adresse.seite,
			embed: adresse.embed,
			titel: $.trim( String( $c.data( 'titel' ) || '' ) ) || 'Mastodon-Beitrag',
			breite: masszahl( $c.data( 'width' ), '100%' ),
			hoehe: masszahl( $c.data( 'height' ), STD_HOEHE + 'px' )
		};

		if ( String( $c.data( 'sperre' ) || '' ) === 'ja' ) {
			sperre( container, cfg );
		} else {
			lade( container, cfg );
		}
	}

	$( function () {
		$( '.mastodon-embed' ).each( function () {
			try {
				baueEinbettung( this );
			} catch ( e ) {
				this.textContent = 'Mastodon-Beitrag konnte nicht geladen werden.';
				if ( window.mw && mw.log ) {
					mw.log.error( e );
				}
			}
		} );
	} );

}() );