Plugins entwickeln
Du möchtest FediSuite um eigene Funktionen erweitern? Diese Seite erklärt, wie ein Plugin aufgebaut ist, wie Du mit dem Scaffold-Generator startest und welche Möglichkeiten die Plugin-API (Version 1) bietet. Für den Einstieg brauchst Du kein tiefes Vorwissen.
Auf dieser Seite
- Für wen ist diese Seite?
- Voraussetzungen
- Schnelleinstieg mit dem Scaffold
- Die Plugin-Struktur
- Das Plugin-Manifest (plugin.json)
- Capabilities und Berechtigungen
- Die Einstiegsdatei: server/index.js
- Plugin-Typen und Beispiele
- Hooks
- Plugin-Einstellungen
- Eigene Web-Seiten (web/)
- Mehrsprachigkeit (i18n)
- Plugin lokal testen
- Typische Fehler & Debugging
Für wen ist diese Seite?
Diese Seite richtet sich an alle, die eigene Plugins für FediSuite bauen oder verstehen möchten, wie das Plugin-System von innen funktioniert. Der Scaffold-Generator übernimmt die Grundarbeit, und die Beispiele auf dieser Seite zeigen, was wohin gehört. Die eigentliche Logik Deines Plugins schreibst Du selbst.
Diese Seite ist für Dich, wenn …
- … Du ein eigenes Plugin entwickeln möchtest.
- … Du verstehen möchtest, wie Plugins aufgebaut sind.
- … Du ein bestehendes Plugin anpassen möchtest.
Diese Seite ist nicht für Dich, wenn …
- … Du nur Plugins installieren möchtest Plugins installieren
- … Du Plugins ein- oder ausschalten möchtest Plugin-Verwaltung
Voraussetzungen
Für ein einfaches Plugin reichen grundlegende JavaScript-Kenntnisse. Der Server-Teil eines Plugins ist ein ES-Modul, das eine Funktion register(context) exportiert.
Plugins werden in JavaScript geschrieben. Für einfache Admin-Seiten oder Widgets genügt es, Funktionen und Objekte zu kennen.
Zum Ausprobieren brauchst Du eine FediSuite-Instanz, bei der ./plugins nach /app/plugins gemountet ist (Standard im Self-Hosting-Repository). Teste neue Plugins besser nicht auf einer Instanz mit echten Konten, denn ein Plugin läuft mit denselben Rechten wie die Anwendung selbst.
Der Scaffold-Generator steckt im App-Container, dort ist Node.js bereits vorhanden. Alternativ führst Du ihn aus dem Quellcode-Repository mit einer lokalen Node.js-Installation aus. Prüfen mit: node --version
Zum Klonen des Plugin-Repositories und für Versionierung und Updates.
Schnelleinstieg mit dem Scaffold
Der empfohlene Weg zum eigenen Plugin ist der Scaffold-Generator. Er ist Teil der FediSuite-Anwendung (scripts/create-plugin-scaffold.mjs, aufgerufen über npm run plugins:create) und legt mit einem Befehl eine lauffähige Plugin-Struktur an: plugin.json, server/index.js, Sprachdateien, bei Bedarf Web-Seiten und eine README. Du fängst also nicht bei null an.
Variante 1: im laufenden App-Container
Öffne eine Shell im App-Container und führe den Befehl im Verzeichnis /app aus. Dort öffnet docker compose exec die Shell standardmäßig. Ohne --target entsteht das Plugin unter plugins/<id>, im Container also in /app/plugins und damit im gemounteten Ordner ./plugins auf dem Host:
# Shell im App-Container öffnen
docker compose exec app sh
# Scaffold im Hauptverzeichnis der Anwendung ausführen
npm run plugins:create -- \
--id mein-plugin \
--name "Mein Plugin" \
--author "Dein Name" \
--presets admin-page
Plugin scaffold created successfully.
Path: /app/plugins/mein-plugin
Plugin ID: mein-plugin
Presets: admin-page
root. Übernimm sie bei Bedarf mit sudo chown -R "$USER": plugins/mein-plugin. Im Entwicklungs-Compose des Quellcode-Repositories (docker-compose.build.yml) ist ./plugins nur lesbar eingebunden (:ro). Lege das Plugin dort mit Variante 2 an.
Variante 2: aus dem Quellcode-Repository
Mit Node.js auf dem Rechner klonst Du das Hauptrepository FediSuite-Docker-Image (öffnet in neuem Tab) und gibst mit --target den Zielordner an. Das Ziel darf noch nicht existieren:
git clone https://forge.chrislo.de/FediSuite/FediSuite-Docker-Image.git
cd FediSuite-Docker-Image
npm run plugins:create -- \
--id mein-plugin \
--name "Mein Plugin" \
--author "Dein Name" \
--presets admin-page \
--target /pfad/zu/fedisuite/plugins/mein-plugin
Die Optionen
--id
Pflicht. Die technische ID des Plugins. Der Generator macht daraus Kleinbuchstaben, Ziffern und Bindestriche (aus „Mein Plugin“ wird mein-plugin). Die ID dient als eindeutiger Schlüssel in FediSuite, im API-Pfad /api/plugins/<id>/ und im Sprach-Namespace. Einmal vergeben, nicht mehr ändern.
--name
Pflicht. Der Anzeigename, der in der Plugin-Verwaltung erscheint. Er darf Leerzeichen und Sonderzeichen enthalten.
--author
Pflicht. Dein Name oder der Deiner Organisation. Er erscheint in der Plugin-Verwaltung.
--presets
Optional. Welche Bausteine angelegt werden. Mehrere Presets trennst Du mit Kommas. Ohne Angabe entsteht ein Plugin ohne Bausteine.
--description
Optional. Die Beschreibung für die plugin.json. Standard: „A FediSuite plugin scaffold for <Name>.“
--version
Optional. Die Startversion. Standard: 1.0.0.
--target
Optional. Der Zielordner. Standard: plugins/<id> im aktuellen Verzeichnis. Existiert er schon, bricht der Generator ab.
--help
Zeigt die Hilfe mit allen Optionen und Presets.
Verfügbare Presets
Jedes Preset legt den passenden Baustein samt Beispieltexten an und trägt Capability und Berechtigungen in die plugin.json ein. Presets lassen sich kombinieren: --presets app-page,dashboard-widget
admin-page
Capability: admin.section
Berechtigungen: admin.sections, web.runtime, api.routes
Seite im Admin-Bereich mit Markdown-Inhalt und einer Beispiel-Webseite, dazu eine Status-Route.
app-page
Capability: app.section
Berechtigungen: app.sections, web.runtime, api.routes
Eigener Eintrag in der Seitenleiste für alle Nutzer*innen, ebenfalls mit Markdown-Inhalt, Beispiel-Webseite und Status-Route.
dashboard-widget
Capability: dashboard.widget
Berechtigungen: dashboard.widgets
Statistik-Widget auf einem Tab der Seite „Analysen“.
composer-extension
Capability: composer.extension
Berechtigungen: composer.extensions
Zusätzliche Felder im Composer und eine Textumwandlung vor dem Veröffentlichen.
provider
Capability: provider
Berechtigungen: providers
Eine zusätzliche Plattform, deren Konten sich verbinden lassen (Beispiel-Verbindungsfluss zum Anpassen).
auth-provider
Capability: auth.provider
Berechtigungen: auth.providers
Ein zusätzlicher Login für FediSuite selbst (Beispiel für Start und Rückkehr).
insights-provider
Capability: insights.provider
Berechtigungen: insight.providers
Ein Anbieter für zusätzliche Tipps zu einem Konto, zunächst mit einem festen Beispiel-Tipp.
settings
Capability: settings.section
Berechtigungen: settings.global.read/write, settings.user.read/write
Ein settingsSchema im Manifest für plugin-eigene Einstellungen (Beispielfelder für global und pro Nutzer*in).
--presets admin-page. Eine einfache Admin-Seite zeigt das komplette Grundgerüst, ohne dass Du Provider-Logik brauchst. Weitere Presets kannst Du später ergänzen. Das Repository FediSuite-Plugins-Repository (öffnet in neuem Tab) enthält mit my-plugin ein solches Scaffold-Ergebnis zum Anschauen.
Die Plugin-Struktur
Jedes Plugin ist ein Ordner direkt im plugins/-Verzeichnis. FediSuite erkennt ein Plugin daran, dass in diesem Ordner unmittelbar eine plugin.json liegt, tiefer verschachtelte Ordner werden nicht durchsucht. Der Ordnername entspricht üblicherweise der Plugin-ID, maßgeblich ist aber die id in der plugin.json.
Mindestens nötig
mein-plugin/
├── plugin.json ← Pflicht
└── server/
└── index.js ← Pflicht
Vom Scaffold erzeugt (admin-page)
mein-plugin/
├── plugin.json
├── README.md
├── server/
│ └── index.js
├── i18n/
│ ├── de.json
│ ├── en.json
│ ├── es.json
│ ├── fr.json
│ └── it.json
└── web/ ← nur bei admin-page / app-page
├── manifest.json
├── admin-main.html
└── main.js
plugin.json
Das Manifest: beschreibt das Plugin, seine Berechtigungen und Einstiegspunkte. FediSuite liest diese Datei als erstes.
server/index.js
Die Einstiegsdatei auf der Serverseite. Hier registrierst Du alles, was das Plugin in FediSuite einbindet.
i18n/<sprache>.json
Sprachdateien (optional, aber empfohlen). Der Dateiname ist der Sprachcode, das Scaffold legt de, en, it, fr und es an. Die Dateien enthalten die Texte des Plugins als verschachtelte Schlüssel mit Zeichenketten.
web/manifest.json
Optional. Wird nur benötigt, wenn das Plugin eigene HTML/JS-Seiten in FediSuite einbettet.
README.md
Vom Scaffold erzeugte Notizen zum Plugin. FediSuite liest die Datei nicht.
Das Plugin-Manifest (plugin.json)
Die plugin.json ist die wichtigste Datei eines Plugins. Sie liegt im Stammverzeichnis des Plugin-Ordners und sagt FediSuite, wer das Plugin ist, was es mitbringt und welche Berechtigungen es braucht. Fehlt ein Pflichtfeld oder ist die Datei ungültig, wird das Plugin nicht geladen und mit dem Status „Fehler“ angezeigt.
So sieht das Manifest aus, das das Scaffold mit --presets admin-page erzeugt:
{
"id": "mein-plugin",
"name": "Mein Plugin",
"version": "1.0.0",
"pluginApiVersion": 1,
"description": "A FediSuite plugin scaffold for Mein Plugin.",
"i18nDir": "./i18n",
"displayNameKey": "meta.name",
"descriptionKey": "meta.description",
"author": "Dein Name",
"license": "GPL-3.0-or-later",
"capabilities": [
"admin.section"
],
"requiredPermissions": [
"admin.sections",
"web.runtime",
"api.routes"
],
"webEntry": "./web/manifest.json",
"serverEntry": "./server/index.js"
}
Pflichtfelder
id
string
Die stabile, technische ID. Erlaubt sind Buchstaben, Ziffern, Punkte, Bindestriche und Unterstriche. Sie muss mindestens 2 Zeichen lang sein und mit einem Buchstaben oder einer Ziffer beginnen. Die ID wird im API-Pfad (/api/plugins/<id>/...) und im Sprach-Namespace (plugin.<id>) verwendet. Einmal vergeben, nicht mehr ändern.
name
string
Der Anzeigename, wenn keine Übersetzung vorhanden ist. Mit displayNameKey wird er durch den übersetzten Text ersetzt.
version
string
Die Versionsnummer des Plugins, üblicherweise nach dem Schema Major.Minor.Patch, zum Beispiel "1.0.0".
pluginApiVersion
number
Die Plugin-API-Version, für die das Plugin geschrieben wurde. Sie muss genau der Version entsprechen, die FediSuite erwartet. Aktuell ist das 1 (Umgebungsvariable PLUGIN_API_VERSION). Bei einer anderen Version wird das Plugin abgelehnt.
description
string
Eine kurze Beschreibung, die ohne Übersetzung angezeigt wird. Mit descriptionKey wird sie durch den übersetzten Text ersetzt.
author
string
Name der Autor*in oder der Organisation.
license
string
Die Lizenz des Plugins, zum Beispiel "GPL-3.0-or-later" oder "MIT".
capabilities
array
Welche Arten von Erweiterung das Plugin liefert, zum Beispiel ["admin.section"]. Das Feld muss vorhanden sein (ein leeres Array genügt). FediSuite zeigt die Werte in der Plugin-Verwaltung an. Welche Funktionen das Plugin registrieren darf, bestimmt requiredPermissions.
serverEntry
string
Relativer Pfad zur JavaScript-Einstiegsdatei auf der Serverseite, üblicherweise "./server/index.js". Die Datei muss existieren und eine Funktion register exportieren.
Berechtigungen, Texte und Web-Seiten
requiredPermissions
array
Die Berechtigungen, die das Plugin braucht, zum Beispiel ["admin.sections"]. Formal ist das Feld optional. Sobald das Plugin aber etwas registriert, ohne die passende Berechtigung zu nennen, scheitert der Start mit einer klaren Fehlermeldung. Details im nächsten Abschnitt.
i18nDir
string
Pfad zum Verzeichnis mit den Sprachdateien, zum Beispiel "./i18n". Ist das Feld gesetzt, muss das Verzeichnis existieren.
displayNameKey
string
Sprachschlüssel für den Anzeigenamen, zum Beispiel "meta.name". Erlaubt sind Buchstaben, Ziffern, Punkte, Bindestriche und Unterstriche.
descriptionKey
string
Sprachschlüssel für die Beschreibung, zum Beispiel "meta.description".
webEntry
string
Pfad zum Web-Manifest, zum Beispiel "./web/manifest.json". Nur setzen, wenn das Plugin eigene HTML/JS-Seiten einbettet. Dafür ist die Berechtigung web.runtime Pflicht.
Weitere optionale Felder
settingsSchema
object
Beschreibt plugin-eigene Einstellungen. Siehe Abschnitt „Plugin-Einstellungen“.
homepage, repository
string
Links zur Projektseite und zum Quellcode. Die Plugin-Verwaltung zeigt sie in den Details an.
minAppVersion, maxAppVersion
string
Kleinste und größte FediSuite-Version, mit der das Plugin zusammenarbeitet, zum Beispiel "2.0.0". Liegt die laufende Version außerhalb, wird das Plugin abgelehnt.
requiredEnv
array
Namen von Umgebungsvariablen, die das Plugin erwartet. Die Plugin-Verwaltung listet sie auf, FediSuite prüft sie nicht.
serverEntry, webEntry und i18nDir sind relativ zum Plugin-Ordner. FediSuite lehnt Pfade ab, die aus dem Ordner herausführen, auch über Symlinks.
Capabilities und Berechtigungen
In der plugin.json stehen zwei Felder, die leicht zu verwechseln sind: capabilities und requiredPermissions.
capabilities
Beschreiben, welche Art von Erweiterung das Plugin liefert. Die Plugin-Verwaltung zeigt sie an. FediSuite prüft nicht, ob sie zu dem passen, was das Plugin tatsächlich registriert.
requiredPermissions
Bestimmen, was das Plugin registrieren und nutzen darf. Jede Methode des Kontexts prüft die passende Berechtigung. Fehlt sie, bricht der Start des Plugins mit einer Fehlermeldung ab.
Welche Capability gehört zu welcher Berechtigung?
admin.section
admin.sections
Eine Seite im Admin-Bereich. Eigene Web-Seiten brauchen web.runtime, eigene API-Routen api.routes.
Optional: web.runtime, api.routes
app.section
app.sections
Ein Eintrag in der Seitenleiste, sichtbar für alle angemeldeten Nutzer*innen.
Optional: web.runtime, api.routes
dashboard.widget
dashboard.widgets
Ein Widget auf einem Tab der Seite „Analysen“.
composer.extension
composer.extensions
Zusätzliche Felder und Transformationen im Composer.
provider
providers
Verbindet eine zusätzliche Plattform mit FediSuite (wie das Bluesky-Plugin).
Optional: web.runtime
auth.provider
auth.providers
Ein zusätzlicher Login für FediSuite selbst.
insights.provider
insight.providers
Liefert zusätzliche Tipps zu einem Konto. Beachte den Singular „insight“ in der Berechtigung.
settings.section
settings.<bereich>.read/write
Lesen und Schreiben der Einstellungen. Der Bereich ist global, user oder account, und es gibt je eine Berechtigung für Lesen und Schreiben.
Weitere Berechtigungen, die unabhängig vom Plugin-Typ gelten:
api.routes
Eigene API-Routen unter /api/plugins/<id>/ registrieren (auth: user oder public).
api.routes.admin
Zusätzlich nötig für Routen mit auth: admin.
web.runtime
Eigene Web-Seiten einbetten. Pflicht, sobald die plugin.json ein webEntry enthält.
hooks
Hooks registrieren. Zusätzlich braucht jedes Ereignis die Berechtigung seiner Gruppe, siehe Abschnitt „Hooks“.
hooks.server, hooks.posts, hooks.accounts, hooks.insights, hooks.settings
Die Gruppen-Berechtigungen für Hooks.
settings.global.read, settings.global.write
Globale Plugin-Einstellungen lesen und schreiben. Pflicht für die Karte in der Plugin-Verwaltung.
settings.user.read, settings.user.write
Einstellungen pro Nutzer*in lesen und schreiben. Pflicht für den Abschnitt in den Einstellungen.
settings.account.read, settings.account.write
Einstellungen pro verbundenem Konto lesen und schreiben.
providers deklarieren, sonst scheitert der Start mit der Meldung Plugin "…" must declare permission "providers" to register provider integrations. Das Scaffold trägt die passenden Berechtigungen für die gewählten Presets automatisch ein.
Die Einstiegsdatei: server/index.js
Die Datei server/index.js ist das Herzstück jedes Plugins. FediSuite importiert sie beim Start des Plugins und ruft die exportierte Funktion register(context) auf. Sie darf async sein. Gibt sie eine Funktion zurück, ruft FediSuite diese beim Deaktivieren auf, damit das Plugin aufräumen kann. Dasselbe leistet context.onDispose(callback), auch mehrfach.
Das minimale Gerüst:
export function register(context) {
// Hier registrierst Du alles, was das Plugin in FediSuite einbindet.
// Der context-Parameter ist Deine Schnittstelle zu FediSuite.
}
Was ist context?
context ist die offizielle Schnittstelle des Plugins zu FediSuite. Über ihn registrierst Du die Funktionen Deines Plugins. Jede Registrierung prüft die passende Berechtigung aus der plugin.json. Die Methoden mit dem Präfix registerSimple… und registerMarkdown… sind Komfort-Helfer für den Normalfall, die Methoden ohne diese Präfixe geben Dir volle Kontrolle über die Definition.
Informationen über das Plugin
context.plugin
Enthält die Metadaten des Plugins, zum Beispiel context.plugin.plugin_id für die Plugin-ID.
context.namespace
Der Sprach-Namespace des Plugins, zum Beispiel plugin.mein-plugin.
context.settings
Die beim Start geladenen globalen Einstellungen. Für aktuelle Werte oder andere Bereiche nutze getSettings().
context.hasPermission(name), context.requirePermission(name)
Prüft, ob das Plugin eine Berechtigung deklariert hat. requirePermission wirft bei einer fehlenden Berechtigung einen Fehler.
Seiten & UI
registerMarkdownAdminPage()
Einfachster Weg zu einer Admin-Seite: Der Inhalt kommt als Markdown aus der Sprachdatei.
registerMarkdownAppPage()
Wie oben, aber als Eintrag der Seitenleiste für alle Nutzer*innen (zusätzlich mit navLabelKey für die Beschriftung des Menüeintrags).
registerAdminSection(), registerAppSection()
Die Basis-Methoden hinter den Markdown-Helfern. Eine Definition besteht aus id, title, description, badge und content ({ markdown } oder { text }). App-Seiten haben zusätzlich nav_label. Für eigene HTML/JS-Seiten gehört die id zu einem Eintrag im web/manifest.json.
Widgets & Erweiterungen
registerSimpleDashboardWidget()
Statistik-Widget mit Titel, Werten und optionaler Liste.
registerDashboardWidget()
Dashboard-Widget mit vollständiger Definition (title, description, badge, content mit stats, items, text oder markdown).
registerSimpleComposerExtension()
Felder und Transformation im Composer.
registerComposerExtension()
Composer-Erweiterung mit vollständiger Definition (fields, sections, instanceTypes).
Provider
registerSimpleProvider(), registerProvider()
Verbindet eine zusätzliche Plattform. registerSimpleProvider ist der Einstieg, registerProvider gibt die volle Kontrolle (siehe Bluesky-Plugin).
registerSimpleAuthProvider(), registerAuthProvider()
Fügt einen Login-Provider hinzu.
registerOidcAuthProvider()
Fertiger Login über OpenID Connect. Er braucht die Berechtigung auth.providers, und Aussteller und Client-Angaben können aus den Plugin-Einstellungen kommen.
registerSimpleInsightProvider(), registerInsightProvider()
Liefert zusätzliche Tipps zu einem Konto.
API, Hooks & Einstellungen
registerApiRoute()
Registriert einen HTTP-Endpunkt unter /api/plugins/<pluginId>/... Die Plugin-Web-Seiten rufen ihn über das SDK auf.
registerStatusRoute()
Registriert unter /status eine fertige Route, die die Plugin-ID, den Status und die Einstellungen liefert. Das Scaffold nutzt sie für die Beispiel-Webseite.
registerHook()
Reagiert auf ein Ereignis, zum Beispiel nach dem Veröffentlichen eines Beitrags. Siehe Abschnitt „Hooks“.
getSettings(bereich), getSetting(key, fallback, bereich)
Liest die Einstellungen eines Bereichs (global, user oder account) oder einen einzelnen Wert. Ohne Bereichsangabe gilt global. Details im Abschnitt „Plugin-Einstellungen“.
getSettingsState(bereich), saveSettings(werte, bereich)
Liest die Einstellungen samt Schema und Zeitstempel oder speichert Werte. Beides setzt die passende settings-Berechtigung voraus.
Mehrsprachigkeit & Lebenszyklus
context.ref(key, fallback), context.createI18nRef(key, fallback)
Erstellt eine Referenz auf einen Sprachschlüssel des Plugins. ref ist die Kurzform von createI18nRef.
context.onDispose(callback)
Registriert eine Aufräumfunktion, die beim Deaktivieren des Plugins läuft, zum Beispiel für Timer.
Plugin-Typen und Beispiele
Die folgenden Beispiele zeigen, wie die wichtigsten Plugin-Typen in server/index.js registriert werden. Sie entsprechen dem, was das Scaffold erzeugt.
Texte, deren Name auf Key endet (titleKey, descriptionKey und so weiter), nehmen einen Schlüssel aus den Sprachdateien des Plugins entgegen, zum Beispiel 'adminSections.main.title'. Alternativ geht dasselbe mit context.ref('adminSections.main.title', 'Fallback-Text'). Mehr dazu im Abschnitt „Mehrsprachigkeit“.
Der einfachste Plugin-Typ. Der Inhalt der Seite kommt als Markdown aus der Sprachdatei, es braucht weder HTML noch JavaScript. Die Seite erscheint in der Seitenleiste unter Admin-Bereich in der Gruppe „Plugin-Seiten“. Gibt es zur id einen Eintrag im web/manifest.json, zeigt FediSuite stattdessen diese Web-Seite.
export function register(context) {
context.registerMarkdownAdminPage({
id: 'main',
titleKey: 'adminSections.main.title',
descriptionKey: 'adminSections.main.description',
badgeKey: 'adminSections.main.badge',
markdownKey: 'adminSections.main.markdown',
});
}
Berechtigung: admin.sections
Wie die Admin-Seite, aber für alle angemeldeten Nutzer*innen. Der Eintrag steht in der Seitenleiste in der Gruppe „Plugin-Bereiche“, beschriftet mit navLabelKey und dem Plugin-Namen darunter.
export function register(context) {
context.registerMarkdownAppPage({
id: 'main',
titleKey: 'appSections.main.title',
navLabelKey: 'appSections.main.navLabel',
descriptionKey: 'appSections.main.description',
markdownKey: 'appSections.main.markdown',
});
}
Berechtigung: app.sections
Fügt eine Karte mit Kennzahlen hinzu. Mit tab wählst Du den Tab der Seite „Analysen“ (Standard: overview), mit width die Breite (half oder full, Standard: half). Statt statusLabelKey und statusValueKey kannst Du eine Liste stats mit Einträgen aus label, value und tone übergeben (default, info, success, warning oder danger). Eine Liste items fügt Textzeilen unter den Kennzahlen hinzu.
export function register(context) {
context.registerSimpleDashboardWidget({
id: 'main',
tab: 'interactions', // optional, Standard: 'overview'
titleKey: 'dashboardWidgets.main.title',
descriptionKey: 'dashboardWidgets.main.description',
badgeKey: 'dashboardWidgets.main.badge',
statusLabelKey: 'dashboardWidgets.main.stats.statusLabel',
statusValueKey: 'dashboardWidgets.main.stats.statusValue',
});
}
Berechtigung: dashboard.widgets
Mögliche Werte für tab: overview (Übersicht), growth (Wachstum), interactions (Interaktionen), engagement (Engagement), hashtags (Hashtags), content (Inhalte), optimize (Optimieren) und tips (Tipps). Ein unbekannter Wert führt dazu, dass das Widget im Tab „Übersicht“ erscheint.
Erweitert den Composer um eigene Felder in der Karte „Plugin-Erweiterungen“. Feldtypen sind text, textarea, boolean, number und select (Passwortfelder sind im Composer nicht erlaubt). transformPost läuft, bevor der Beitrag veröffentlicht wird, und erhält { account, post, data, extension, settings }. Die Funktion gibt die zu ändernden Felder des Beitrags zurück (zum Beispiel content, title, spoiler_text, visibility, language) oder ein leeres Objekt, wenn nichts zu ändern ist. Mit instanceTypes beschränkst Du die Erweiterung auf bestimmte Plattformen.
export function register(context) {
context.registerSimpleComposerExtension({
id: 'main',
displayNameKey: 'composer.main.displayName',
descriptionKey: 'composer.main.description',
fields: [
{
key: 'enabled',
type: 'boolean',
labelKey: 'composer.main.fields.enabled.label',
descriptionKey: 'composer.main.fields.enabled.description',
defaultValue: false,
},
{
key: 'suffix',
type: 'text',
labelKey: 'composer.main.fields.suffix.label',
descriptionKey: 'composer.main.fields.suffix.description',
defaultValue: ' - sent with my plugin',
},
],
transformPost: async ({ post, data }) => {
if (!data?.enabled || !String(data?.suffix || '').trim()) return {};
return {
content: `${String(post.content || '').trim()}${String(data.suffix)}`.trim(),
};
},
});
}
Berechtigung: composer.extensions
Verbindet eine zusätzliche Plattform mit FediSuite. beginConnection startet den Verbindungsfluss und liefert eine redirect_url. handleCallback verarbeitet die Rückkehr und liefert die Kontodaten. Der Standard-Rückkehrpfad lautet /api/providers/<providerId>/callback. Die verbundenen Konten erscheinen unter Einstellungen → Verbundene Konten im Bereich „Plugin-Konnektoren“. Dieses Beispiel entspricht dem Scaffold und ist ein Platzhalter, den Du durch Deinen echten Ablauf ersetzt:
export function register(context) {
context.registerSimpleProvider({
id: 'mein-plugin-provider',
displayNameKey: 'providers.main.displayName',
descriptionKey: 'providers.main.description',
beginConnection: async ({ req }) => ({
provider_id: 'mein-plugin-provider',
user_id: req.user?.id || null,
redirect_url: `${req.protocol}://${req.get('host')}/api/providers/mein-plugin-provider/callback?code=demo-code`,
state: 'demo-state',
}),
handleCallback: async ({ req }) => ({
provider_id: 'mein-plugin-provider',
user_id: Number(req.query.user_id || 1) || 1,
account: {
instance_url: 'https://example.com',
username: 'demo',
display_name: 'Demo-Konto',
stats_followers: 0,
stats_following: 0,
stats_statuses: 0,
max_characters: 500,
max_media_attachments: 4,
},
start_import: false,
}),
disconnect: async ({ account }) => ({
provider_id: 'mein-plugin-provider',
disconnected: true,
account_id: account?.id || null,
}),
});
}
Berechtigung: providers
Veröffentlichen, Import und Statistiken
Ein vollständiger Provider wie das Bluesky-Plugin nutzt registerProvider und deklariert, was er kann. Das Plugin fedisuite-plugin-bluesky im Plugin-Repository ist die beste Vorlage dafür. Die Handler:
beginConnection, handleCallback
Verbindungsfluss. Bluesky nutzt eine eigene Plugin-Webseite als Formular (Handle, App-Passwort, optional PDS-Host) und nimmt die Eingaben per POST im Callback entgegen.
disconnect
Wird beim Trennen eines Kontos aufgerufen.
publishPost
Veröffentlicht einen Beitrag auf der Plattform.
importHistoricalData
Importiert den bisherigen Verlauf eines Kontos.
refreshStats, refreshPosts
Aktualisieren Kontostatistiken und Beiträge.
Neben auth (Art der Anmeldung, zum Beispiel { type: 'credentials' }) und capabilities (connect, disconnect, publish, import_historical, refresh_stats, refresh_posts und composer_profile für das Aussehen des Composers) gehören diese Handler in die Definition. Die Handler erhalten unter anderem req, pool (Datenbank), account und APP_URL.
Ergänzt die Anmeldeseite um „Oder mit Plugin anmelden“. beginLogin liefert die redirect_url zum externen Dienst, handleCallback liefert die Identität (external_subject, email, profile). Start und Rückkehr laufen standardmäßig über /api/auth/providers/<id>/start und /api/auth/providers/<id>/callback.
export function register(context) {
context.registerSimpleAuthProvider({
id: 'mein-plugin-login',
displayNameKey: 'authProviders.main.displayName',
descriptionKey: 'authProviders.main.description',
beginLogin: async ({ req }) => {
const baseUrl = `${req.protocol}://${req.get('host')}`;
return {
auth_provider_id: 'mein-plugin-login',
redirect_url: `${baseUrl}/api/auth/providers/mein-plugin-login/callback?demo_email=plugin-login@example.com&demo_subject=demo-user`,
};
},
handleCallback: async ({ req }) => ({
identity: {
external_subject: String(req.query.demo_subject || 'demo-user'),
email: String(req.query.demo_email || 'plugin-login@example.com'),
profile: { provider: 'mein-plugin-login', display_name: 'Demo-Login' },
},
tab: 'dashboard',
}),
});
}
Berechtigung: auth.providers
identity.email daher nur Adressen zurück, die der externe Dienst tatsächlich bestätigt hat. Die Demo-Werte des Scaffolds sind nur für Tests gedacht.
Liefert zusätzliche Tipps zu einem Konto, die FediSuite mit den eigenen Tipps zusammenführt und sortiert. Ein Tipp braucht eine id. Dazu kommen title, text, optional reason, priority, confidence (low, medium oder high) und evidence. Statt einer festen Liste kannst Du eine Funktion generateInsights(context) angeben. Sie erhält unter anderem accountId, userId, days, timezone, settings und stats und gibt eine Liste von Tipps zurück.
export function register(context) {
context.registerSimpleInsightProvider({
id: 'mein-plugin-insights',
displayNameKey: 'insights.main.displayName',
tips: [
{
id: 'erster-tipp',
title: 'Beispiel-Tipp',
text: 'Dieser Tipp kommt direkt aus dem Plugin.',
confidence: 'low',
priority: 1,
},
],
});
}
Berechtigung: insight.providers
Macht einen eigenen HTTP-Endpunkt unter /api/plugins/<pluginId>/... erreichbar, typischerweise für Plugin-Web-Seiten, die ihn über das SDK aufrufen. Erlaubt sind get, post, put, patch und delete. Der Pfad muss mit / beginnen und wird exakt verglichen, Platzhalter wie :id gibt es nicht (nimm Query-Parameter). Mit auth legst Du fest, wer die Route aufrufen darf: 'user' (Standard) verlangt eine angemeldete Person, 'admin' verlangt Admin-Rechte und die zusätzliche Berechtigung api.routes.admin, 'public' verlangt keine Anmeldung. Der Handler erhält unter anderem req, res, pool und plugin_id.
export function register(context) {
context.registerApiRoute({
method: 'get',
path: '/status', // erreichbar unter /api/plugins/<id>/status
auth: 'user', // 'user' (Standard), 'admin' oder 'public'
summary: 'Gibt den Plugin-Status zurück.',
handler: async ({ req, res }) => {
res.json({ plugin_id: context.plugin.plugin_id, user_id: req.user?.id || null, status: 'ok' });
},
});
}
Berechtigung: api.routes
Hooks
Mit context.registerHook({ event, handler }) reagiert ein Plugin auf Ereignisse in FediSuite. Dafür braucht das Plugin die Berechtigung hooks und zusätzlich die Berechtigung der Gruppe, zu der das Ereignis gehört. Ein Hook darf async sein. Wirft er einen Fehler, schreibt FediSuite eine Warnung ins Log und macht weiter, ein Hook kann eine Aktion also nicht abbrechen. Hooks laufen im Prozess, in dem das Ereignis eintritt: Beiträge veröffentlicht zum Beispiel der Worker mit dem Scheduler.
export function register(context) {
context.registerHook({
event: 'post.after_publish',
handler: async ({ post, account, publishedId }) => {
console.log(`Beitrag ${post.id} wurde als ${publishedId} veröffentlicht.`);
},
});
}
server.start
Berechtigung: hooks.server
Nachdem der Server zu lauschen begonnen hat.
Kontext: port, appUrl
post.before_saved
Berechtigung: hooks.posts
Bevor neue Beiträge gespeichert werden.
Kontext: postPayloads, userId, accountId, scheduledAt
post.after_saved
Berechtigung: hooks.posts
Nachdem Beiträge gespeichert wurden.
Kontext: posts, userId, accountId
post.before_publish
Berechtigung: hooks.posts
Bevor ein Beitrag an die Plattform gesendet wird.
Kontext: post, account, mediaIds
post.after_publish
Berechtigung: hooks.posts
Nachdem ein Beitrag veröffentlicht wurde.
Kontext: post, account, publishedId, parentFediverseId
post.publish_failed
Berechtigung: hooks.posts
Wenn das Veröffentlichen eines geplanten Beitrags fehlgeschlagen ist.
Kontext: post, account, errorMessage
account.connected
Berechtigung: hooks.accounts
Nachdem ein Konto über einen Plugin-Provider verbunden wurde.
Kontext: accountId, userId, providerId, instanceType
account.before_disconnect
Berechtigung: hooks.accounts
Bevor ein Konto getrennt wird.
Kontext: accountId, userId, account
account.after_disconnect
Berechtigung: hooks.accounts
Nachdem ein Konto getrennt wurde.
Kontext: accountId, userId, account
insights.generated
Berechtigung: hooks.insights
Nachdem die Tipps zu einem Konto erzeugt wurden.
Kontext: accountId, userId, days, timezone, account, tips, meta
settings.updated
Berechtigung: hooks.settings
Nachdem Plugin-Einstellungen gespeichert wurden.
Kontext: pluginId, scope, scopeRefId, userId, accountId, settings
Plugin-Einstellungen
Ein Plugin beschreibt seine Einstellungen im Feld settingsSchema der plugin.json. FediSuite erzeugt daraus die Eingabemasken: die Karte „Plugin-Einstellungen“ in der Plugin-Verwaltung für die globalen Werte (nur Admins) und den Abschnitt „Plugin-Einstellungen“ unter Einstellungen → Einstellungen für die Werte pro Nutzer*in. Das Preset settings legt ein Beispiel an.
"requiredPermissions": [
"settings.global.read",
"settings.global.write",
"settings.user.read",
"settings.user.write"
],
"settingsSchema": {
"title": { "key": "settings.title", "fallback": "Plugin settings" },
"sections": [
{
"id": "general",
"title": { "key": "settings.sections.general.title", "fallback": "General" },
"fields": [
{
"key": "enabled",
"type": "boolean",
"label": { "key": "settings.fields.enabled.label", "fallback": "Enabled" },
"defaultValue": true
},
{
"key": "welcomeMessage",
"type": "text",
"label": { "key": "settings.fields.welcomeMessage.label", "fallback": "Welcome message" },
"defaultValue": "Hello from plugin settings"
}
]
}
]
}
Ein settingsSchema enthält entweder eine Liste fields oder Abschnitte (sections) mit je einer eigenen Liste fields. Jeder Schlüssel (key) darf nur einmal vorkommen.
text, textarea, password, boolean, number und select. Passwortfelder gelten als geheim: Gespeicherte Werte gibt FediSuite nicht im Klartext zurück. Text darf bis zu 4096 Zeichen lang sein, textarea bis zu 65536.
label, description, placeholder, required, defaultValue, bei number zusätzlich min, max und step, bei select eine Liste options mit value und label. Texte dürfen Zeichenketten oder Sprachreferenzen der Form { "key": "…", "fallback": "…" } sein.
global (für die ganze Instanz, nur von Admins änderbar), user (pro Nutzer*in) und account (pro verbundenem Konto). Der wirksame Wert entsteht aus dem Standardwert, den globalen Werten und darüber den Werten von Nutzer*in und Konto.
Im Code liest Du die Werte mit getSettings() und getSetting(). Ohne Bereichsangabe gilt global. Für user und account musst Du die ID mitgeben, zum Beispiel innerhalb einer Route mit req.user.id:
export function register(context) {
context.registerApiRoute({
method: 'get',
path: '/greeting',
auth: 'user',
handler: async ({ req, res }) => {
const global = await context.getSettings(); // globale Werte
const message = await context.getSetting(
'welcomeMessage',
'Hallo',
{ scope: 'user', userId: req.user.id }, // Wert dieser Person
);
res.json({ enabled: global.enabled, message });
},
});
}
Mit saveSettings(werte, bereich) speicherst Du Werte aus dem Plugin heraus, dafür ist die passende write-Berechtigung nötig. Speichern Admins oder Nutzer*innen Werte in der Oberfläche, löst das den Hook settings.updated aus.
Eigene Web-Seiten (web/)
Wenn Dein Plugin eine interaktive Seite mit eigenem HTML und JavaScript braucht, stellst Du Web-Dateien bereit. FediSuite lädt die HTML-Datei und bettet sie direkt in die App ein, ohne iframe. Du brauchst keinen eigenen Server. Stylesheets werden auf den Plugin-Bereich begrenzt (Selektoren wie body oder :root gelten nur dort), relative Pfade wie ./main.js oder ./style.css werden relativ zur Seite aufgelöst, und Skripte (auch type="module") werden ausgeführt. Die CSS-Begrenzung ist keine Sicherheitsgrenze.
Das web/-Verzeichnis ist optional. Für einfache Text- oder Markdown-Inhalte brauchst Du es nicht. Ein Plugin mit webEntry braucht die Berechtigung web.runtime. FediSuite liefert nur diese Dateitypen aus: html, js, mjs, css, json, svg, txt, png, jpg, jpeg, gif, webp, ico, woff, woff2 und ttf.
web/manifest.json
Diese Datei beschreibt, welche HTML-Datei zu welcher Seite gehört. frontendApiVersion muss 1 sein. Die sectionId muss exakt mit der id übereinstimmen, die Du in server/index.js vergeben hast. entry ist ein Pfad relativ zu web/, ohne führenden Schrägstrich und ohne ...
Für Admin-Seiten
{
"frontendApiVersion": 1,
"adminPages": [
{
"sectionId": "main",
"entry": "admin-main.html"
}
]
}
Für App-Seiten
{
"frontendApiVersion": 1,
"appPages": [
{
"sectionId": "main",
"entry": "app-main.html"
}
]
}
sectionId im web/manifest.json muss identisch mit der id der registrierten Seite sein (registerMarkdownAdminPage, registerAdminSection und die App-Varianten). Schon ein Unterschied in der Groß- und Kleinschreibung führt dazu, dass FediSuite die Web-Seite nicht findet und stattdessen den Markdown-Inhalt zeigt. Eine Admin-Seite gehört in adminPages, eine App-Seite in appPages.
Das Plugin-Web-SDK
Für die Kommunikation zwischen der HTML-Seite und FediSuite gibt es ein fertiges SDK. Es kümmert sich um Authentifizierung und API-Aufrufe, Du musst weder Tokens noch HTTP-Header selbst verwalten. Alle Aufrufe gehen an die Routen, die Dein Plugin mit registerApiRoute registriert hat.
Binde das SDK in Deiner HTML-Datei ein:
<script src="/plugin-sdk/fedisuite-plugin-web.js"></script>
Danach steht window.FediSuitePluginWeb zur Verfügung. Das Grundgerüst, das auch das Scaffold erzeugt (web/main.js):
const output = document.getElementById('output');
async function boot() {
const plugin = window.FediSuitePluginWeb;
// Auf die Initialisierung durch FediSuite warten, vor allen anderen Aufrufen
const context = await plugin.ready();
// context enthält: pluginId, sectionId, sectionKind ('admin' oder 'app'), language
try {
// Plugin-API aufrufen (Route aus registerApiRoute oder registerStatusRoute)
const status = await plugin.get('/status');
output.textContent = JSON.stringify({ context, status }, null, 2);
} catch (error) {
output.textContent = error instanceof Error ? error.message : String(error);
}
}
boot();
Verfügbare SDK-Methoden
ready()
Liefert die Kontextdaten (pluginId, sectionId, sectionKind, language). Ruf die Methode zuerst auf, vor allen API-Aufrufen.
getContext()
Liefert denselben Kontext sofort, ohne zu warten.
get(path)
Sendet einen GET-Request an /api/plugins/<pluginId>/<path>. Der Pfad beginnt mit /.
post(path, body)
Sendet einen POST-Request. body ist ein JavaScript-Objekt und wird als JSON übertragen.
put(path, body), patch(path, body), delete(path, body)
Entsprechende Requests mit JSON-Body.
request(path, { method, body })
Die allgemeine Form hinter den Methoden oben. Ein Fehlerstatus der Route führt zu einem abgelehnten Promise mit der Fehlermeldung.
resize(), autoResize()
Nur aus Kompatibilitätsgründen vorhanden. Da die Seite direkt in die App eingebettet ist, brauchst Du beide nicht.
Mehrsprachigkeit (i18n)
FediSuite unterstützt mehrere Sprachen. Damit Dein Plugin dazu passt, lagerst Du Titel, Beschreibungen und Inhalte in Sprachdateien im Verzeichnis i18n/ aus (in der plugin.json über i18nDir eingetragen). Der Dateiname ist der Sprachcode, zum Beispiel de.json. Jede Datei ist ein JSON-Objekt, dessen Werte ausschließlich Zeichenketten (oder Listen von Zeichenketten) sind. Zahlen und Wahrheitswerte sind nicht erlaubt.
So sieht die Sprachdatei des Scaffolds für eine Admin-Seite aus:
{
"meta": {
"name": "Mein Plugin",
"description": "Kurze Beschreibung meines Plugins."
},
"adminSections": {
"main": {
"title": "Mein Plugin Admin",
"description": "Was dieses Plugin macht.",
"badge": "Plugin",
"markdown": "## Willkommen\n\nHier steht der Inhalt."
}
}
}
Im Code übergibst Du den Schlüssel, nicht den Text. Eine Referenz mit Fallback erzeugst Du bei Bedarf mit context.ref:
// Empfohlen: Schlüssel aus der Sprachdatei
titleKey: 'adminSections.main.title'
// Gleichwertig, mit Fallback-Text für den Fall, dass der Schlüssel fehlt
titleKey: context.ref('adminSections.main.title', 'Mein Plugin')
// Möglich, aber nicht übersetzt: ein fester Text
title: 'Mein Plugin'
- FediSuite wählt die Sprachdatei in dieser Reihenfolge: Sprache der Oberfläche, deren Basissprache (de statt de-AT), dann Englisch. Eine en.json ist deshalb als Rückfallebene sinnvoll.
- Fehlt der Schlüssel in der gewählten Sprachdatei, zeigt FediSuite den Fallback-Text von context.ref. Gibt es keinen, erscheint der Schlüssel selbst.
- Der Anzeigename und die Beschreibung in der Plugin-Verwaltung kommen aus den Schlüsseln displayNameKey und descriptionKey der plugin.json.
- Ein Text, der wie ein Schlüssel aussieht (Segmente aus Buchstaben, Ziffern, Bindestrichen und Unterstrichen, durch Punkte getrennt, ohne Leerzeichen), wird als Schlüssel behandelt. Jeder andere Text bleibt ein fester Text.
Plugin lokal testen
Liegt Dein Plugin in plugins/<id>/ und ist der Ordner eingebunden, läuft der Test immer gleich ab:
Plugin anlegen oder ändern, zum Beispiel mit dem Scaffold.
Die Container neu starten, denn FediSuite liest Plugins und ihre Manifeste nur beim Start ein: docker compose restart app worker1 worker2
In der Plugin-Verwaltung nachsehen, ob das Plugin erscheint. Bei einem Eintrag mit dem Status „Fehler“ nennen die aufgeklappten Details die Ursache.
Das Plugin aktivieren und danach die Worker neu starten (docker compose restart worker1 worker2), wenn das Plugin dort mitlaufen soll.
Die Seite, das Widget oder den Composer prüfen (siehe „Was ein aktives Plugin ergänzt“ in der Plugin-Verwaltung) und dabei die Logs beobachten: docker compose logs -f app
Typische Fehler & Debugging
Ob ein Plugin gestartet ist, siehst Du am Status in der Plugin-Verwaltung. Bei „Fehler“ steht die Ursache in den aufgeklappten Details. Die Container-Logs nennen sie nicht. Die folgenden Meldungen kommen dort am häufigsten vor:
Plugin manifest is missing required field "…"
Ein Pflichtfeld fehlt in der plugin.json.
Plugin API version X is not compatible with supported version 1.
pluginApiVersion passt nicht zur Version, die FediSuite erwartet.
Plugin requires at least app version …
minAppVersion oder maxAppVersion schließt die laufende FediSuite-Version aus.
Plugin entry file not found: …
serverEntry, webEntry oder i18nDir zeigt auf eine Datei, die es nicht gibt.
Plugin "…" does not export a register(context) function.
server/index.js exportiert keine Funktion register.
Plugin "…" must declare permission "…" to …
Das Plugin registriert etwas, ohne die Berechtigung in requiredPermissions zu nennen.
Plugin web manifest … / frontendApiVersion …
Das web/manifest.json ist ungültig, hat eine andere frontendApiVersion als 1, einen unsicheren Pfad oder doppelte sectionId-Werte.
Plugin wird nicht erkannt
- Liegt plugin.json direkt im Plugin-Ordner (nicht eine Ebene tiefer)?
- Ist der plugins/-Ordner als Volume nach /app/plugins eingebunden?
- Wurden die Container nach der Änderung neu gestartet?
- Stehen PLUGINS_ENABLED und PLUGIN_SCAN_ON_START nicht auf false?
ls -la plugins/mein-plugin/
docker compose config | grep plugins
docker compose exec app ls /app/plugins
docker compose restart app worker1 worker2
Plugin startet nicht (Status „Fehler“)
- Stimmt pluginApiVersion in plugin.json? Derzeit muss der Wert 1 sein.
- Existiert die Datei, auf die serverEntry zeigt, und exportiert sie eine Funktion register?
- Sind alle Berechtigungen in requiredPermissions gesetzt, die Deine register-Funktion braucht?
- Ist das JSON in plugin.json und in den Sprachdateien gültig, und enthalten die Sprachdateien nur Zeichenketten?
docker compose exec app node --check /app/plugins/mein-plugin/server/index.js
python3 -m json.tool plugins/mein-plugin/plugin.json
Seite oder Widget erscheint nicht in der App
- Wurde die Seite mit der passenden Methode registriert (Admin-Seite oder App-Seite)?
- Ist die passende Berechtigung in requiredPermissions eingetragen?
- Ist das Plugin in der Plugin-Verwaltung aktiviert und hat den Status „Gestartet“?
- Hast Du die Seite nach dem Aktivieren neu geladen?
docker compose logs app
Plugin-Web-Seite bleibt leer oder lädt nicht
- Ist webEntry in plugin.json gesetzt und die Berechtigung web.runtime vorhanden?
- Ist web/manifest.json gültiges JSON mit frontendApiVersion 1?
- Stimmt die sectionId in web/manifest.json exakt mit der id der registrierten Seite überein?
- Steht die Seite in der richtigen Liste (adminPages oder appPages)?
- Ist das SDK eingebunden: <script src="/plugin-sdk/fedisuite-plugin-web.js">?
- Wird await window.FediSuitePluginWeb.ready() vor den API-Aufrufen aufgerufen?
- Sind Asset-Pfade relativ zum web/-Verzeichnis (./main.js, ./style.css)?
- Existiert die Route, die die Seite aufruft, und erlaubt ihr auth-Wert den Zugriff?
docker compose logs app
server/plugins (öffnet in neuem Tab).