Lerne Coding
Integration des Mastodon-Feeds in die eigene Webseite

Integriere Mastodon-Toots in deine Webseite: Anleitung mit der Mastodon API und RSS-Feeds

Inhaltsverzeichnis
[[TABLE OF CONTENTS]]
access_timeGeschätzte Lesezeit ca. Minuten

In diesem Artikel stelle ich dir zwei Möglichkeiten vor, wie du Toots von Mastodon in deine Webseite integrieren kannst – die deinem Nutzerkonto oder einer anderen Person zugeordnet sind. Bei der einen Variante verwenden wir die API – sie stellt deutlich mehr und detailliertere Informationen bereit. Bei der anderen Variante verwenden wir einen RSS-Feed – er ist vom Parsing umständlicher und nicht so schön zu bedienen mittels JavaScript, aber deutlich schneller, wenn es nur um die eigenen Toots geht.

Was ist Mastodon?

Mastodon ist eine Alternative zu Twitter, wobei der Ansatz hier ein wenig anders ist. Im Gegensatz zu Twitter, das ein zentralisiertes Netzwerk ist, ist Mastodon ein dezentralisiertes Netzwerk. Es gibt viele verschiedene Mastodon-Instanzen, die alle miteinander verbunden sind, aber jede Instanz unterliegt Regeln, die von ihrem Administrator festgelegt werden. Dadurch können Benutzer eine Instanz wählen, die ihren Anforderungen entspricht. Ferner ist Mastodon ein Open-Source-Projekt, was bedeutet, dass jeder, der möchte, den Code kostenlos herunterladen und mit ihm experimentieren kann. Mastodon bietet eine Reihe von Funktionen, darunter eine mögliche 500-Zeichen-Beschränkung für Beiträge, Gruppenchats, Threads, die Möglichkeit, Beiträge zu filtern und Benachrichtigungen über bestimmte Themen zu erhalten.

Was ist die Mastodon API?

Die Mastodon API ist eine Art Programmierschnittstelle, mit der man Toots, Accounts erstellen, lesen und viele andere Dinge machen kann. Für bestimmte Endpunkt benötigt man zusätzliche Zugriffsschlüssel, die im Account unter /settings/applications erstellt werden können für eigene Anwendungen. So kann man in der Praxis eigene Anwendungen mit der API schaffen.

Eine ausführliche Dokumentation findest du unter: https://docs.joinmastodon.org/

Accounts API – für den Mastodon Feed

Ein Beispiel eines Mastodon Feeds kann wie im folgenden Screenshot aussehen. Es ist nur eine einfache Demo. Der verwendete Endpunkt stellt noch weitere Daten bereit, die implementiert werden können.

Der Account Status Endpunkt ermöglicht das Auslesen des Feeds per JSON API, was für JavaScript Anwendungen schöner ist als ein RSS Endpunkt. Dies bietet mehr Flexibilität, da nicht nur eigene Toots (Name für Tweets im Mastodon Universum), sondern auch geteilte Beiträge dort gefunden werden können.

Endpunkt: https://${instance}/api/v1/accounts/${userid}/statuses

Ich habe einmal eine kleine Web-Komponente gebaut, um sie auf einer Webseite zu verwenden. Wenn man den Endpunkt "Statuses" der API Version 2 nutzen möchte, ist das problematisch, da er eine Nutzer-ID erwartet. Diese lässt sich jedoch nicht einfach im Profil eines Nutzer finden. Deshalb verwende ich den Such-Endpunkt, um den Nutzernamen eines Mastodon-Kontos in die passende Nutzer-ID aufzulösen.

Komplette JavaScript Web-Komponente

Um das Beispiel besser nachvollziehen zu können, beginnen wir mit dem Aufruf der Web-Komponente. Dabei wird ein Element mit dem Wert "toot-feed" angelegt. Da im HTML-Tag ein Bindestrich zu finden ist, wird für den HTML-Interpreter klar, dass es sich um ein benutzerdefiniertes HTML-Element handeln muss.

<body>
    <h1>Mastodon Feed - Example</h1>

    <toot-feed>@hellocoding@troet.cafe</toot-feed>

    <script src="./tootfeed.js"></script>
</body>

Anschließende rufen wir noch die tootfeed.js ab – wo drinnen unsere Komponente für den Mastodon Feed geschrieben wird.

Zusätzlich können bei der Web-Komponente noch 2 Attribute angegeben werden:

  • refresh-rate: Dies dient dazu, zu definieren, wie lange gewartet wird, bis der Feed erneuert wird – da es sich bei Mastodon ja um ein schnelllebiges Social Media handelt, sollte der Feed nicht nur einmal initial geladen werden. Standardmäßig ist der Wert auf 30 Sekunden gesetzt.
  • host: Bei diesem Attribute kann noch einmal die korrekte Instanz angegeben werden -problem ist nämlich das, wenn die Mastodon Instanz auf einer Subdomain ist, der Instanz-Name der Domain entsprechen kann und nicht der Spezifischen Subdomain.

Schritte, die in der Web-Komponente passieren, um einen Feed darzustellen:

  • Es wird der Nutzername aus dem Text Content gelesen und entsprechende geparsed.
  • Anschließende findet eine Validierung des Nutzernamens statt, dass dieser aus zwei Segmenten besteht. Dem Nutzernamen und dem Instanz-Namen.
  • Nun werden noch die zwei zusätzlichen Parameter geprüft, ob diese einen passenden Inhalt haben und wenn ja werden diese Attribute passende definiert.
  • Anschließende wird die URL für die Suche nach dem passenden Account zusammengesetzt aus den bisherigen Werten.
  • Nun wird der connectedCallback() ausgeführt und es wird nach dem Account gesucht – wenn keiner gefunden wird, gibt es eine passende Meldung. Theoretisch könnte man auch noch ein Attribut einbauen oder eine Möglichkeit direkt die Nutzer ID zu setzen, falls man diese statisch definieren möchte.
  • Anschließend, wenn ein passender Account gefunden wurde – wird der Feed des Nutzers geladen und dargestellt. Das passiert in der renderFeed Methode und fetchFeed Methode.
  • Dort wird jeder Toot Einzelende geparst und dargestellt.
  • Ganz am Ende der Datei findest du noch eine customElements.define Methode, wo die erzeugte Klasse angegeben wird – und der Name der Custom Component in unserem Fall „toot-feed“.

Falls du nicht weiß wofür das use strict steht lese doch den folgenden Artikel: „use strict“ – der Strict Mode – welche Vorteile hat er?

Die tootfeed.js sieht wie folgt aus:

/**
 * @description Eine Webcomponent um einen Mastodon Feed darzustellen.
 * @author Felix Schürmeyer
 * @date 27/12/2022
 * 
 */
'use strict';

class TootFeed extends HTMLElement {

    constructor() {
        super()

        let content = this.textContent.split('@');

        if (content[0] == "") {
            content.shift();
        }

        if (content.length != 2) {
            this.innerHTML = `<p>Username is not Valid!</p>`;

            return;
        }

                this.username = content[0];
        this.instance = content[1];

        let refreshRate = this.getAttribute('refresh-rate');

        if (refreshRate > 0 && refreshRate != null) {
            this.refreshRate = refreshRate;
        } else {
            this.refreshRate = 30000;
        }

        let hostInstance = this.getAttribute('host');

        if (hostInstance != null) {
            this.instance = hostInstance;
        }

        this.innerHTML = "";
        this.url = `https://${this.instance}/api/v2/search?q=${this.username}&limit=40&type=accounts`;
    }

    connectedCallback() {
        this.getUserData();
    }

    getUserData() {
        fetch(this.url).then(request => request.json()).then(data => {

            let accounts = data['accounts'].filter(item => {
                return item.url == `https://${this.instance}/@${this.username}`
            });

            if (accounts.length != 1) {
                this.innerHTML = `<p>Es wurde kein passender Account gefunden.</p>`;
            }

            this.userid = accounts[0].id;

            this.fetchFeed();
        }).catch(this.renderError.bind(this))
    }

    fetchFeed() {
        if (!this.userid) {
            return;
        }

        fetch(`https://${this.instance}/api/v1/accounts/${this.userid}/statuses`).then(request => request.json()).then(this.renderFeed.bind(this)).catch(this.renderError.bind(this))
    }

    renderError(error) {
        this.innerHTML = error;
    }

    async renderFeed(jsonResponse) {
        this.innerHTML = "";

        jsonResponse.forEach(element => {
            let contentToot = document.createElement('div');

            contentToot.classList.add('tooted');

            if (element.reblog) {
                contentToot.innerHTML = `<span class="content">${element.reblog.content}</span>`

                let timeString = new Date(element.reblog.created_at).toLocaleString();

                this.addMediaAttchments(contentToot, element.reblog);

                contentToot.innerHTML += `<small>reblog <a href="${element.reblog.account.url}">${element.reblog.account.username}</a> - ${timeString} </small>`;

            } else {
                contentToot.innerHTML = `<span class="content">${element.content}</span>`;

                let timeString = new Date(element.created_at).toLocaleString();

                this.addMediaAttchments(contentToot, element);

                contentToot.innerHTML += `<small>${timeString}</small>`;
            }

            this.appendChild(contentToot);
        });

        setTimeout(e => {
            this.fetchFeed();
        }, this.refreshRate);
    }

    addMediaAttchments(contentToot, element) {
        if (element.media_attachments.length > 0) {
            element.media_attachments.forEach(media => {

                if (media.type == "image") {
                    contentToot.innerHTML += `<img loading="lazy" src="${media.url}" alt="${media.description}"></img>`
                }

            });
        }
    }
}

customElements.define('toot-feed', TootFeed);

RSS-Feed mittels JavaScript ausgeben

Nachdem wir soeben die Variante den Mastodon Feed via JavaScript darzustellen, kennengelernt haben – schauen wir uns jetzt noch einmal eine deutlich simplere Variante mittels RSS-Feed an.

Den Abruf der folgenden Variante hab ich etwas Simpler und Rudimentärer geschrieben - theoretisch könnte man auch hier eine passende Web Component für den Anwendungsfall erstellen. Theoretisch könnte man sogar eine Komponente schreiben, die beide Varianten unterstützen würde, um Toots zu laden.

<body>
    <h1>Mastodon Feed - Example</h1>

    <div id="toots" style="max-width: 600px"></div>
    <script src="./tootfeed.rss.js"></script>
</body>

Jeder Mastodon Nutzer hat auch immer ein RSS-Feed, dazu kann an den Profillink einfach die Endung „.rss“ angehangen werden. Dort drunter finden wir alle Toots, die vom Nutzer selbst geschrieben wurden, also zum Beispiel keine reblogs. Auch erhalten wir keine Auskunft über Likes und Reblogs Zähler – diese können nur bei der API abgefragt werden.

Um ein RSS-Feed parsen zu können, benötigt man die DomParser Klasse – dieses parste den Content in ein DomDocument dieses kann dann wieder wie gewohnt selektiert werden um die bereitgestellten Informationen zu lesen.

"use strict";

fetch('https://troet.cafe/@hellocoding.rss').then(response => response.text()).then(text => new DOMParser().parseFromString(text, 'text/xml')).then(data => {

    let placeContent = document.getElementById('toots');

    data.querySelectorAll('item').forEach(toot => {
        let description = toot.querySelector('description');

        if (description) {
            let element = document.createElement('div');
            element.classList.add('tooted');

            if (description.textContent) {
                let content = document.createElement('div');
                content.classList.add('content');

                content.innerHTML = description.textContent;

                element.insertAdjacentElement('beforeend', content);
            }

            let media = toot.querySelector('content');

            if (media && media.getAttribute('url') && media.getAttribute('medium') == "image") {
                let image = document.createElement('img');
                image.src = media.getAttribute('url');

                element.insertAdjacentElement('beforeend', image);
            }

            let pubDate = toot.querySelector('pubDate');

            if (pubDate && pubDate.textContent) {
                let timeString = new Date(pubDate.textContent).toLocaleString();

                let time = document.createElement('small');

                time.innerText = timeString;

                element.insertAdjacentElement('beforeend', time);
            }

            placeContent.insertAdjacentElement('beforeend', element);
        }
    })

});

Bei der Variante über den RSS-Feed erkennen wir schnell, dass deutlich weniger Logik benötigt wird als bei der ersten Variante. Denn wir müssen keine Nutzer-ID zuordnen und können auch keine Reblogs abfangen und darstellen. Daher benötigen wir dafür keine Logik.

Sicherheit von Feeds & Fazit

An sich funktioniert die Implementierung eines Feeds in die Webseite deutlich einfacher als bei zum Beispiel Twitter – was definitiv in direkter Konkurrenz zu Mastodon sich stellen kann.

Ein Punkt gibt mir zu denken, den du definitiv beachten solltest und dem du dir bewusst sein musst, wenn du einen Feed in deine Webseite implementierst. Der Content des Toots ist immer ein HTML Element, das heißt, es können Sicherheitslücken existieren, wie fremder Content für eine XSS Attacke / Cross Site Scripting – in deinen Feed platziert werden könnte, ohne dass du dieses direkt siehst. Ein Sicherheitsforscher hat bereits in die Richtung erste Informationen zu Mastodon gefunden: https://portswigger.net/daily-swig/mastodon-users-vulnerable-to-password-stealing-attacks

Also hab acht, ob du der eingebundenen Quelle und Mastodon Instanz vertrauen kannst – den theoretisch könnte der Server im Nachgang den Output vom RSS-Feed manipulieren. Da Mastodon Instanzen von jedem gehostet werden können, ist es ein Vorteil, da so nicht eine Instanz über alle Nutzer verfügt. Aber diese sehe ich gleichzeitig auch als größten Nachteil – da jeder eine Instanz machen kann – könnten auch explizit welche für bösartige Attacken erstellt werden. Und gerade dann, wenn du diese in deine Webseite implementieren möchtest, solltest du besonders vorsichtig sein.

Für die faulen unter euch noch einmal das CSS Styling, das ich für Demonstrationszwecke verwendet habe:

toot-feed {
    display: block;
    margin: 0 auto;
    max-width: 750px;
}

h1 {
    font-family: Arial, Helvetica, sans-serif;
    text-align: center;
}

div.tooted {
    font-family: Arial, Helvetica, sans-serif;
    background-color: #292929;
    box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
    color: #fff;
    border-radius: 5px;
    padding: 16px;
    margin: 16px 8px;
}

div.tooted .content {
    display: block;
    background-color: #5C5C5C33;
    border-radius: 5px;
    font-size: 16px;
    line-height: 1.25em;
    padding: 16px;
    margin: 8px 0px;
}

div.tooted a {
    color: rgb(103, 151, 235);
    word-break: break-all;
    text-decoration: none;
}

div.tooted img {
    display: block;
    max-width: 100%;
    border-radius: 5px;
}

small {
    display: block;
    margin: 6px 0;
}
Bildquellen - Vielen Dank an alle Ersteller:innen fĂĽr die Bilder
Kommentare zum Artikel
bclmnt schreibt ... Kommentar vom 24.11.2023
Danke!

Vielen Dank Felix das du dieses Wissen zur Verfügung stellst! Echt super hilfreich!

Antworten
bclmnt
Kommentar schreiben

Vom Autor Empfohlen
close