Server-Dokumentation · Deutsch
Die Status-API: Wer ist gerade auf dem Server?
Jeder Squorli Server kann Struktur und Anwesenheit als JSON nach außen geben: den Servernamen mit Icon, alle Kategorien und Kanäle in der Reihenfolge, in der der Client sie zeigt, und die Mitglieder, die gerade in einem Sprachkanal sitzen, mit ihrem Kanal, ob Mikrofon oder Ton aus sind und ob Kamera oder Bildschirmfreigabe laufen. Wer in keinem Sprachkanal ist, taucht nicht auf; die Mitgliederliste bleibt im Server. Damit baust du ein Widget für eure Website, ein Stream-Overlay oder einen Bot. Die Schnittstelle ist standardmäßig aus und liefert nie Nachrichten, Rollen, Rechte oder Schlüssel.
Einstellungen
Öffne die Verwaltung über das Zahnrad am Servernamen und wähle unter Server > Status-API einen der drei Modi. Dafür brauchst du das Recht zur Serververwaltung.
| Modus | Wirkung |
|---|---|
| Aus (Standard) | GET /api/status antwortet mit 404 und {"error":"status_api_off"}. Nichts verlässt den Server. |
| Nur mit Schlüssel | Beim ersten Umschalten erzeugt der Server einen Schlüssel und zeigt ihn in der Verwaltung, mit Kopieren-Knopf. Abrufe brauchen ihn als Header Authorization: Bearer <Schlüssel> oder als Parameter ?key=<Schlüssel>; ohne oder mit falschem Schlüssel antwortet der Server mit 401. Neu erzeugen ersetzt den Schlüssel sofort; der alte gilt dann nicht mehr. |
| Öffentlich | Jeder darf die Struktur und die Belegung der Sprachkanäle abrufen, ohne Anmeldung und ohne Schlüssel. |
Im Modus „Öffentlich“ sind Namen, Handles und Zustand aller Mitglieder in Sprachkanälen für jeden sichtbar, der die Adresse kennt. Ein Schlüssel in einem Website-Widget ist ebenfalls für jeden lesbar, der den Quelltext der Seite öffnet; er schützt vor zufälligen Abrufen, nicht vor Besuchern der Seite. Profilbilder sind die Bilder des Directory-Kontos, die dort ohnehin öffentlich sind.
Abruf
Die Adresse ist https://<deine-domain>/api/status. Der Server erlaubt Abrufe von jeder Herkunft (CORS), sodass ein Widget auf einer anderen Website direkt darauf zugreifen kann. Antworten werden für eine Sekunde zwischengespeichert; ein Widget muss also nicht öfter als alle paar Sekunden fragen.
curl -s https://chat.example.org/api/statuscurl -s -H 'Authorization: Bearer <key>' https://chat.example.org/api/status
# or, for a widget that cannot set headers:
curl -s 'https://chat.example.org/api/status?key=<key>'Die Antwort
Die Antwort ist ein JSON-Objekt. categories und channels sind so sortiert, wie der Client sie zeigt: Kanäle ohne Kategorie zuerst, dann die Kategorien nach position, innerhalb einer Kategorie die Kanäle nach position. Jeder Kanal nennt seine Kategorie in categoryId (null = ohne Kategorie). members enthält nur die Mitglieder, die gerade in einem Sprachkanal sitzen; voice ist ihr Sprachkanal mit dem gemeldeten Zustand von Mikrofon (micMuted), Ton (deafened), Kamera (cameraOn) und Bildschirmfreigabe (screenOn); afk heißt seit zehn Minuten ohne Eingabe. iconUrl und avatarUrl sind vollständige Adressen oder null.
{
"name": "Example Community",
"iconUrl": "https://chat.example.org/api/server-icon?v=1758648000000",
"time": "2026-09-23T18:00:00.000Z",
"categories": [
{ "id": "3b39ef51-…", "name": "Allgemein", "position": 0 }
],
"channels": [
{ "id": "8c1e…", "kind": "text", "name": "allgemein", "topic": "Willkommen", "categoryId": "3b39ef51-…", "position": 0 },
{ "id": "f04a…", "kind": "voice", "name": "Lobby", "topic": null, "categoryId": "3b39ef51-…", "position": 1 }
],
"members": [
{ "userId": "6f1c…", "displayName": "Lea", "handle": "lea", "avatarUrl": "https://directory.squorli.com/api/avatars/…",
"afk": false, "isOwner": true,
"voice": { "channelId": "f04a…", "micMuted": false, "deafened": false, "cameraOn": true, "screenOn": false } },
{ "userId": "a2d4…", "displayName": "Jules", "handle": null, "avatarUrl": null,
"afk": true, "isOwner": false,
"voice": { "channelId": "f04a…", "micMuted": true, "deafened": true, "cameraOn": false, "screenOn": false } }
]
}Beispiel: ein kleines Website-Widget
Das folgende Beispiel zeigt Servername, Icon und die Zahl der Personen in Sprachkanälen und darunter jeden Sprachkanal mit den Personen, die gerade darin sitzen, samt Markierungen für Mikrofon aus, Ton aus, Kamera und Bildschirmfreigabe. Es kommt ohne Bibliothek aus und aktualisiert sich alle 15 Sekunden. Trage deine Domain ein und, nur im Modus „Nur mit Schlüssel“, den Schlüssel. Namen werden als Text eingefügt, nie als HTML; behalte das bei, wenn du das Widget erweiterst.
<div id="squorli-status">Loading…</div>
<script src="squorli-widget.js"></script>// squorli-widget.js: who is on the server right now. Vanilla JavaScript, no library.
(function () {
var SERVER = "https://chat.example.org"; // your Squorli server
var KEY = ""; // the key from Administration > Server, only in mode "with key"
var EVERY_MS = 15000; // refresh interval; the server answers from a one-second cache
var box = document.getElementById("squorli-status");
// Names come from the server's members: always insert them as text, never as HTML.
function el(tag, className, text) {
var node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function render(status) {
box.replaceChildren();
var head = el("div", "sq-head");
if (status.iconUrl) { var icon = el("img", "sq-icon"); icon.src = status.iconUrl; icon.alt = ""; head.appendChild(icon); }
// The server lists only the members sitting in a voice channel, so this is the number of people in voice.
head.appendChild(el("strong", null, status.name));
head.appendChild(el("span", "sq-count", status.members.length + " in voice"));
box.appendChild(head);
// Voice channels in the order the client shows them: uncategorized first, then the categories by position.
var order = {};
status.categories.forEach(function (c, i) { order[c.id] = i + 1; });
var voice = status.channels.filter(function (c) { return c.kind === "voice"; }).sort(function (a, b) {
return (order[a.categoryId] || 0) - (order[b.categoryId] || 0) || a.position - b.position;
});
voice.forEach(function (channel) {
var seated = status.members.filter(function (m) { return m.voice.channelId === channel.id; });
var block = el("div", "sq-channel");
block.appendChild(el("div", "sq-channel-name", channel.name + " (" + seated.length + ")"));
seated.forEach(function (m) {
var row = el("div", "sq-member" + (m.afk ? " sq-afk" : ""));
if (m.avatarUrl) { var avatar = el("img", "sq-avatar"); avatar.src = m.avatarUrl; avatar.alt = ""; row.appendChild(avatar); }
row.appendChild(el("span", null, m.displayName));
if (m.voice.micMuted) row.appendChild(el("span", "sq-badge", "mic off"));
if (m.voice.deafened) row.appendChild(el("span", "sq-badge", "sound off"));
if (m.voice.cameraOn) row.appendChild(el("span", "sq-badge", "camera"));
if (m.voice.screenOn) row.appendChild(el("span", "sq-badge", "screen"));
block.appendChild(row);
});
box.appendChild(block);
});
}
function refresh() {
var options = KEY ? { headers: { Authorization: "Bearer " + KEY } } : {};
fetch(SERVER + "/api/status", options)
.then(function (res) {
if (res.status === 404) throw new Error("The status API of this server is off.");
if (res.status === 401) throw new Error("The key is missing or wrong.");
if (!res.ok) throw new Error("Error " + res.status);
return res.json();
})
.then(render)
.catch(function (err) { box.replaceChildren(el("div", "sq-error", err.message)); });
}
refresh();
setInterval(refresh, EVERY_MS);
})();Ein wenig CSS reicht für eine aufgeräumte Darstellung; passe die Farben an deine Seite an.
#squorli-status { font: 14px/1.5 system-ui, sans-serif; border: 1px solid #354762; border-radius: 12px; padding: 12px 16px; max-width: 320px; }
#squorli-status .sq-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
#squorli-status .sq-icon { width: 24px; height: 24px; border-radius: 6px; }
#squorli-status .sq-count { margin-left: auto; opacity: .7; }
#squorli-status .sq-channel-name { font-weight: 600; margin-top: 8px; }
#squorli-status .sq-member { display: flex; align-items: center; gap: 6px; padding-left: 12px; }
#squorli-status .sq-avatar { width: 18px; height: 18px; border-radius: 50%; }
#squorli-status .sq-afk { opacity: .55; }
#squorli-status .sq-badge { font-size: 11px; border: 1px solid currentColor; border-radius: 4px; padding: 0 4px; opacity: .7; }
#squorli-status .sq-error { opacity: .7; }Erweiterungen liegen nahe: Textkanäle mit ihren Themen auflisten, abwesende Mitglieder (afk) blasser zeigen oder ein Overlay bauen, das nur den eigenen Sprachkanal zeigt. Die Felder findest du in der Antwort oben; die Quelle der Schnittstelle liegt im Server-Repository.