Labeler ist ein Feature für fortgeschrittene Anwender. Da er dauerhaft als Server laufen muss, richtet es sich an Nutzer, die Laravel Forge einsetzen oder eigenständig einen Server aufsetzen können. Für Laravel-Einsteiger nicht empfohlen. Kein Support.Laravel Cloud wird nicht unterstützt. Labeler muss durchgehend als WebSocket-Server laufen und benötigt Anpassungen der nginx-Konfiguration. Da Laravel Cloud keine Änderungen an der nginx-Konfiguration erlaubt, kann Labeler dort nicht betrieben werden.
Was ist Labeler?
Labeler ist ein Dienst, der Inhalten auf dem AT Protocol (Bluesky) Labels zuweist. Er lässt sich für Moderation, Content-Klassifikation, benutzerdefinierte Filterung usw. verwenden.
Ein Vorabverständnis des Labeler-Konzepts ist erforderlich.
Auch Starter-Kits für andere Sprachen sind hilfreich.
Beispielimplementierung:
Vorbereitung
Zum Betrieb eines Labelers benötigen Sie:
- Einen neuen, ausschließlich für den Labeler bestimmten Bluesky-Account (nicht Ihren persönlichen Account verwenden).
- Ein neues, ausschließlich für den Labeler bestimmtes Laravel-Projekt (getrennte Projekte werden empfohlen).
- Eine (Sub-)Domain
- Einen produktiven Linux-Server (VPS, AWS EC2 o. Ä.) (Laravel Cloud, Laravel Vapor und Vercel sind nicht möglich).
Wenn Sie nur Shared Hosting einsetzen können, ist der Betrieb eines Labelers schwierig.
Installation zusätzlicher Pakete
composer require workerman/workerman revolt/event-loop
Konfiguration
Erzeugen Sie zunächst einen privaten Schlüssel.
php artisan bluesky:labeler:new-private-key
Fügen Sie den erzeugten privaten Schlüssel und die zugehörigen Werte in .env hinzu.
BLUESKY_LABELER_DID=did:plc:***
BLUESKY_LABELER_IDENTIFIER=***.bsky.social
BLUESKY_LABELER_APP_PASSWORD=
BLUESKY_LABELER_PRIVATE_KEY=""
Labeler-Klasse erstellen
Erstellen Sie eine eigene Labeler-Klasse, die von AbstractLabeler erbt. Die Datei kann beliebig abgelegt werden.
namespace App\Labeler;
use Revolution\Bluesky\Labeler\AbstractLabeler;
readonly class ArtisanLabeler extends AbstractLabeler
{
// Einzelne Methoden implementieren
}
Da das Paket den Großteil der Labeler-Verarbeitung übernimmt, implementieren Sie nur die Teile, die Sie anpassen möchten.
labels()
Gibt die Label-Definitionen zurück. Die Konstanten-Definitionen des skyware-Starter-Kits sind eine gute Referenz.
use Revolution\Bluesky\Labeler\LabelDefinition;
use Revolution\Bluesky\Labeler\LabelLocale;
public function labels(): array
{
return [
new LabelDefinition(
identifier: 'artisan',
locales: [
new LabelLocale(
lang: 'en',
name: 'artisan',
description: 'Web artisan',
),
],
severity: 'inform',
blurs: 'none',
defaultSetting: 'warn',
adultOnly: false,
),
];
}
subscribeLabels()
Wird unmittelbar nach der WebSocket-Verbindung aufgerufen. Gibt SubscribeLabelResponse als Iterator zurück.
use Revolution\Bluesky\Labeler\Labeler;
use Revolution\Bluesky\Labeler\LabelerException;
use Revolution\Bluesky\Labeler\Response\SubscribeLabelResponse;
/**
* @return iterable<SubscribeLabelResponse>
*/
public function subscribeLabels(?int $cursor): iterable
{
if (is_null($cursor)) {
return null;
}
// Für Error-Responses muss unbedingt eine LabelerException geworfen werden
if ($cursor > Label::max('id')) {
throw new LabelerException('FutureCursor', 'Cursor is in the future');
}
foreach (Label::oldest()->where('id', '>', $cursor)->lazy() as $label) {
$arr = $label->toArray();
$arr = Labeler::formatLabel($arr);
yield new SubscribeLabelResponse(
seq: $label->id,
labels: [$arr],
);
}
}
emitEvent()
Wird aufgerufen, wenn ein Request zum Hinzufügen oder Entfernen eines Labels eingeht. Gibt UnsignedLabel als Iterator zurück.
use Illuminate\Http\Request;
use Revolution\Bluesky\Labeler\LabelerException;
use Revolution\Bluesky\Labeler\UnsignedLabel;
/**
* @return iterable<UnsignedLabel>
*
* @link https://docs.bsky.app/docs/api/tools-ozone-moderation-emit-event
*/
public function emitEvent(Request $request, ?string $did, ?string $token): iterable
{
$type = data_get($request->input('event'), '$type');
if ($type !== 'tools.ozone.moderation.defs#modEventLabel') {
throw new LabelerException('InvalidRequest', 'Unsupported event type');
}
$subject = $request->input('subject');
$uri = data_get($subject, 'uri', data_get($subject, 'did'));
$cid = data_get($subject, 'cid');
$createLabelVals = (array) data_get($request->input('event'), 'createLabelVals');
$negateLabelVals = (array) data_get($request->input('event'), 'negateLabelVals');
foreach ($createLabelVals as $val) {
yield new UnsignedLabel(
uri: $uri,
cid: $cid,
val: $val,
src: config('bluesky.labeler.did'),
cts: now()->micro(0)->toISOString(),
);
}
foreach ($negateLabelVals as $val) {
yield new UnsignedLabel(
uri: $uri,
cid: $cid,
val: $val,
src: config('bluesky.labeler.did'),
cts: now()->micro(0)->toISOString(),
neg: true,
);
}
}
saveLabel()
Speichert signierte Labels in der Datenbank. Gibt ein SavedLabel zurück.
use Revolution\Bluesky\Labeler\SavedLabel;
use Revolution\Bluesky\Labeler\SignedLabel;
public function saveLabel(SignedLabel $signed, string $sign): ?SavedLabel
{
// App\Models\Label ist ein eigenes Modell
$saved = Label::create($signed->toArray());
return new SavedLabel(
$saved->id,
$signed,
);
}
Für Migration und Eloquent-Modell finden Sie Beispiele im Paket.
createReport()
Wird aufgerufen, wenn Nutzer beispielsweise einen Widerspruch (Appeal) einreichen.
use Illuminate\Http\Request;
/**
* @link https://docs.bsky.app/docs/api/com-atproto-moderation-create-report
*/
public function createReport(Request $request): array
{
// Report verarbeiten und Array zurückgeben
// Erforderliche Felder: id, reasonType, reason, subject, reportedBy, createdAt
return [
'id' => 1,
'reasonType' => $request->input('reasonType'),
'reason' => $request->input('reason', ''),
'subject' => $request->input('subject'),
'reportedBy' => '',
'createdAt' => now()->toISOString(),
];
}
queryLabels()
Fragt Labels statt über WebSocket über die HTTP-API ab. Wird vom offiziellen Bluesky nicht verwendet, aber von Drittanbietern genutzt. Wenn nicht erforderlich, geben Sie ein leeres Array zurück.
use Illuminate\Http\Request;
/**
* @link https://docs.bsky.app/docs/api/com-atproto-label-query-labels
*/
public function queryLabels(Request $request): array
{
return [];
}
Registrierung im AppServiceProvider
Registrieren Sie die erstellte Labeler-Klasse in AppServiceProvider::boot().
use Revolution\Bluesky\Labeler\Labeler;
use App\Labeler\ArtisanLabeler;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Labeler::register(ArtisanLabeler::class);
}
}
Account-Setup
Initialisieren Sie den Account als Labeler.
Bei diesem Kommando geben Sie nicht das App Password ein, sondern das tatsächliche Account-Passwort. Während der Ausführung erhalten Sie eine Bestätigungs-E-Mail „PLC Update Operation Requested” – folgen Sie den Anweisungen des Kommandos.
php artisan bluesky:labeler:setup
Wenn die Endpoint-URL korrekt gesetzt ist, kann dieses Kommando auch aus der lokalen Umgebung ausgeführt werden.
Label-Definitionen deklarieren
Registrieren Sie die Label-Definitionen am Labeler-Account.
php artisan bluesky:labeler:declare-labels
Auch dieses Kommando kann lokal ausgeführt werden.
Weitere Kommandos
Label-Definitionen löschen:
php artisan bluesky:labeler:delete-labels
Labeler-Account in einen normalen Account zurückwandeln:
php artisan bluesky:labeler:restore
Ausführung auf Laravel Forge
Aktivieren Sie SSL und starten Sie den Labeler-Server mit folgender Konfiguration.
nginx-Konfiguration
Fügen Sie der nginx-Konfiguration von Forge drei location-Blöcke hinzu.
# WebSocket: Label-Subscription
location /xrpc/com.atproto.label.subscribeLabels
{
proxy_pass http://127.0.0.1:7000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Real-IP $remote_addr;
}
# HTTP: Event-Emission
location /xrpc/tools.ozone.moderation.emitEvent
{
proxy_pass http://127.0.0.1:7001;
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Connection "";
}
# Healthcheck
location /xrpc/_health
{
proxy_pass http://127.0.0.1:7001;
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Connection "";
}
Deploy-Skript
Stoppen Sie den Labeler-Server beim Deployment. Danach startet Supervisor ihn automatisch neu.
# Übliche Deploy-Schritte
$FORGE_PHP artisan bluesky:labeler:server stop
# Wenn das nicht funktioniert, starten Sie den Daemon direkt neu
sudo -S supervisorctl restart daemon-{id}:*
Hintergrundprozess (Daemon) konfigurieren
Wählen Sie in Forges Konfigurationsmaske für Hintergrundprozesse nicht den Reiter „Queue Worker”, sondern Custom.
Kommando:
php artisan bluesky:labeler:server start
Wenn Sie ihn gemeinsam mit Jetstream oder Firehose starten möchten, geben Sie Optionen an.
Ein gleichzeitiges Ausführen mit den Kommandos bluesky:ws oder bluesky:firehose ist nicht möglich.
# Zusammen mit Jetstream starten
php artisan bluesky:labeler:server start --jetstream
# Bestimmte Collections filtern
php artisan bluesky:labeler:server start --jetstream -C app.bsky.graph.follow -C app.bsky.feed.like
# Zusammen mit Firehose starten
php artisan bluesky:labeler:server start --firehose
Labels vergeben
Wie Labels vergeben werden, hängt von der Anwendung ab. Das Beispiel nutzt die Event-Funktion von Laravel und vergibt ein Label bei „einer neuen Follower-Beziehung”.
// Skizze von app/Listeners/FollowListener.php
use Revolution\Bluesky\Facades\Bluesky;
use Revolution\Bluesky\Events\Jetstream\JetstreamCommitMessage;
use Revolution\Bluesky\Types\RepoRef;
public function handle(JetstreamCommitMessage $event): void
{
$message = $event->message;
$followerDid = data_get($message, 'did');
// Label über die Bluesky Ozone API vergeben
Bluesky::login(
identifier: config('bluesky.labeler.identifier'),
password: config('bluesky.labeler.password'),
)->createLabels(
subject: RepoRef::to($followerDid),
labels: ['artisan'],
);
}
Zur Absicherung gegen verlorene Events wird empfohlen, das Labeling zusätzlich per Task-Scheduling auszuführen.
Zuletzt geändert am 13. Juli 2026