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.
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.
Die Mastodon API ist eine 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/
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.
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:
Schritte, die in der Web-Komponente passieren, um einen Feed darzustellen:
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);
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.
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;
}
Hinterlasse mir gerne einen Kommentar zum Artikel und wie er dir weitergeholfen hat beziehungsweise, was dir helfen würde das Thema besser zu verstehen. Oder hast du einen Fehler entdeckt, den ich korrigieren sollte? Schreibe mir auch dazu gerne ein Feedback!
Vielen Dank Felix das du dieses Wissen zur Verfügung stellst! Echt super hilfreich!