> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Labeler

> Aufbau und Betrieb eines Labeler-Servers mit Laravel Bluesky. Erläutert das Ableiten von AbstractLabeler, Label-Definitionen, die WebSocket-Verbindung und das Deployment auf Laravel Forge.

<Warning>
  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.
</Warning>

## 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.

* [AT Protocol: Label spec](https://atproto.com/specs/label)
* [Bluesky's Moderation Architecture](https://docs.bsky.app/blog/blueskys-moderation-architecture)

Auch Starter-Kits für andere Sprachen sind hilfreich.

* [skyware.js.org — Labeler guide](https://skyware.js.org/guides/labeler/introduction/getting-started/)
* [aliceisjustplaying/labeler-starter-kit-bsky](https://github.com/aliceisjustplaying/labeler-starter-kit-bsky)

Beispielimplementierung:

* [laralabeler.bsky.social](https://bsky.app/profile/laralabeler.bsky.social)
* [invokable/laralabeler](https://github.com/invokable/laralabeler)

```mermaid theme={null}
sequenceDiagram
    participant Client as Bluesky-<br>Client
    participant Labeler as Laravel<br>Labeler-Server
    participant DB as Datenbank

    Client->>Labeler: WebSocket: subscribeLabels
    Labeler-->>Client: Label-Stream

    Client->>Labeler: POST /xrpc/tools.ozone.moderation.emitEvent
    Labeler->>DB: Label speichern
    Labeler-->>Client: Response
```

## 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).

<Info>
  Wenn Sie nur Shared Hosting einsetzen können, ist der Betrieb eines Labelers schwierig.
</Info>

## Installation zusätzlicher Pakete

```bash theme={null}
composer require workerman/workerman revolt/event-loop
```

## Konfiguration

Erzeugen Sie zunächst einen privaten Schlüssel.

```bash theme={null}
php artisan bluesky:labeler:new-private-key
```

Fügen Sie den erzeugten privaten Schlüssel und die zugehörigen Werte in `.env` hinzu.

```dotenv theme={null}
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.

```php theme={null}
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.

<Info>
  Beispielimplementierung: [ArtisanLabeler.php](https://github.com/invokable/laralabeler/blob/main/app/Labeler/ArtisanLabeler.php)
</Info>

### labels()

Gibt die Label-Definitionen zurück. Die Konstanten-Definitionen des skyware-Starter-Kits sind eine gute Referenz.

```php theme={null}
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.

```php theme={null}
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.

```php theme={null}
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.

```php theme={null}
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.

* [Migration](https://github.com/invokable/laravel-bluesky/blob/main/workbench/database/migrations/2024_12_31_000000_create_labels_table.php)
* [Eloquent-Modell](https://github.com/invokable/laravel-bluesky/blob/main/workbench/app/Models/Label.php)

### createReport()

Wird aufgerufen, wenn Nutzer beispielsweise einen Widerspruch (Appeal) einreichen.

```php theme={null}
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.

```php theme={null}
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()`.

```php theme={null}
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.

<Warning>
  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.
</Warning>

```bash theme={null}
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.

```bash theme={null}
php artisan bluesky:labeler:declare-labels
```

Auch dieses Kommando kann lokal ausgeführt werden.

## Weitere Kommandos

Label-Definitionen löschen:

```bash theme={null}
php artisan bluesky:labeler:delete-labels
```

Labeler-Account in einen normalen Account zurückwandeln:

```bash theme={null}
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.

```nginx theme={null}
# 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.

```bash theme={null}
# Ü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:**

```bash theme={null}
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.

```bash theme={null}
# 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".

```php theme={null}
// 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.

<Info>
  Source: [docs/labeler.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/labeler.md)
  Sample: [invokable/laralabeler](https://github.com/invokable/laralabeler)
</Info>


## Related topics

- [WebSocket (Jetstream / Firehose)](/de/packages/laravel-bluesky/websocket.md)
- [BlueskyManager und HasShortHand](/de/packages/laravel-bluesky/bluesky-manager.md)
- [Crypto – Kryptografie im AT Protocol](/de/packages/laravel-bluesky/crypto.md)
- [Routen](/de/packages/laravel-bluesky/route.md)
