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

> Hoe je met Laravel Bluesky een Labeler-server bouwt en beheert. Uitleg over het overerven van AbstractLabeler, labeldefinities, WebSocket-verbindingen en deployen met Laravel Forge.

<Warning>
  De Labeler is een functie voor gevorderden. Omdat deze continu als server moet draaien, is dit bedoeld voor wie Laravel Forge gebruikt of zelf een server kan opzetten. Niet aanbevolen voor Laravel-beginners. Er is geen ondersteuning.

  **Laravel Cloud wordt niet ondersteund.** Een Labeler moet permanent draaien als WebSocket-server en vereist aanpassingen aan de nginx-configuratie, maar op Laravel Cloud kun je nginx niet configureren, dus daar kan een Labeler niet draaien.
</Warning>

## Wat is een Labeler?

Een Labeler is een service die labels toekent aan content op het AT Protocol (Bluesky). Je kunt hem inzetten voor moderatie, contentclassificatie, eigen filters en meer.

Het is nodig dat je vooraf het concept van een Labeler begrijpt.

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

Starter kits voor andere talen zijn ook nuttig als referentie.

* [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)

Voorbeeldimplementaties:

* [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 Database

    Client->>Labeler: WebSocket: subscribeLabels
    Labeler-->>Client: Labelstream

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

## Voorbereiding

Om een Labeler te draaien heb je het volgende nodig.

* **Een nieuw Bluesky-account speciaal voor de Labeler** (gebruik niet je dagelijkse account)
* **Een nieuw Laravel-project speciaal voor de Labeler** (aparte projecten worden aanbevolen)
* **Een (sub)domein**
* **Een productie-Linux-server zoals een VPS of AWS EC2** (Laravel Cloud, Laravel Vapor en Vercel zijn niet mogelijk)

<Info>
  Als je alleen shared hosting kunt gebruiken, is het lastig om een Labeler te draaien.
</Info>

## Extra pakketten installeren

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

## Configuratie

Genereer eerst een privésleutel.

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

Voeg de gegenereerde privésleutel en de bijbehorende configuratiewaarden toe aan `.env`.

```dotenv theme={null}
BLUESKY_LABELER_DID=did:plc:***
BLUESKY_LABELER_IDENTIFIER=***.bsky.social
BLUESKY_LABELER_APP_PASSWORD=

BLUESKY_LABELER_PRIVATE_KEY=""
```

## Een Labeler-klasse maken

Maak een eigen Labeler-klasse die erft van `AbstractLabeler`. Het bestand mag op een willekeurige plek staan.

```php theme={null}
namespace App\Labeler;

use Revolution\Bluesky\Labeler\AbstractLabeler;

readonly class ArtisanLabeler extends AbstractLabeler
{
    // Implementeer de afzonderlijke methods
}
```

Het pakket neemt het grootste deel van de Labeler-verwerking voor zijn rekening, dus je implementeert alleen de onderdelen die je wilt aanpassen.

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

### labels()

Geeft de labeldefinities terug. De constantedefinities in de skyware-starterkit zijn een goede referentie.

```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()

Wordt direct na het opzetten van de WebSocket-verbinding aangeroepen. Geeft `SubscribeLabelResponse`-objecten terug als iterator.

```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;
    }

    // Gooi altijd een LabelerException als je een foutresponse wilt teruggeven
    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()

Wordt aangeroepen wanneer er een request binnenkomt om een label toe te voegen of te verwijderen. Geeft `UnsignedLabel`-objecten terug als iterator.

```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()

Slaat het ondertekende label op in de database. Geeft een `SavedLabel` terug.

```php theme={null}
use Revolution\Bluesky\Labeler\SavedLabel;
use Revolution\Bluesky\Labeler\SignedLabel;

public function saveLabel(SignedLabel $signed, string $sign): ?SavedLabel
{
    // App\Models\Label maak je zelf aan
    $saved = Label::create($signed->toArray());

    return new SavedLabel(
        $saved->id,
        $signed,
    );
}
```

Raadpleeg voor de migratie en het Eloquent-model de voorbeelden in het pakket.

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

### createReport()

Wordt aangeroepen wanneer een gebruiker bijvoorbeeld een appeal instuurt.

```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
{
    // Verwerk de report en geef een array terug
    // Verplichte velden: 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()

Query't labels via de HTTP-API in plaats van WebSocket. Wordt niet gebruikt door het officiële Bluesky, maar door derde partijen. Geef een lege array terug als je dit niet nodig hebt.

```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 [];
}
```

## Registreren in de AppServiceProvider

Registreer de gemaakte 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);
    }
}
```

## Het account instellen

Initialiseer het account als Labeler-account.

<Warning>
  Bij dit command voer je niet het app-wachtwoord in maar het echte accountwachtwoord. Tijdens het proces ontvang je een bevestigingsmail met "PLC Update Operation Requested"; volg de instructies van het command om de code in te voeren.
</Warning>

```bash theme={null}
php artisan bluesky:labeler:setup
```

Als de endpoint-URL correct is ingesteld, kun je dit ook vanuit je lokale omgeving uitvoeren.

## Labeldefinities declareren

Registreer de labeldefinities bij het Labeler-account.

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

Ook dit command kun je vanuit je lokale omgeving uitvoeren.

## Overige commands

Labeldefinities verwijderen:

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

Het Labeler-account terugzetten naar een gewoon account:

```bash theme={null}
php artisan bluesky:labeler:restore
```

## Draaien op Laravel Forge

Start na het inschakelen van SSL de Labeler-server met de volgende configuratie.

### nginx-configuratie

Voeg drie `location`-blokken toe aan de nginx-configuratie van Forge.

```nginx theme={null}
# WebSocket: labelabonnement
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: events versturen
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 "";
}
```

### Deployscript

Stop de Labeler-server tijdens het deployen. Na het stoppen herstart Supervisor deze automatisch.

```bash theme={null}
# De gebruikelijke deploystappen
$FORGE_PHP artisan bluesky:labeler:server stop

# Als het bovenstaande niet werkt, herstart de daemon dan rechtstreeks
sudo -S supervisorctl restart daemon-{id}:*
```

### Achtergrondproces (daemon) instellen

Kies in het scherm voor achtergrondprocessen van Forge het tabblad **Custom** in plaats van het tabblad Queue Worker.

**Command:**

```bash theme={null}
php artisan bluesky:labeler:server start
```

Geef opties op als je tegelijk met Jetstream of de Firehose wilt starten.
Gelijktijdig draaien met de commands `bluesky:ws` of `bluesky:firehose` is niet mogelijk.

```bash theme={null}
# Samen met Jetstream starten
php artisan bluesky:labeler:server start --jetstream

# Specifieke collecties filteren
php artisan bluesky:labeler:server start --jetstream -C app.bsky.graph.follow -C app.bsky.feed.like

# Samen met de Firehose starten
php artisan bluesky:labeler:server start --firehose
```

## Labels toevoegen

Hoe je labels toekent, hangt af van de implementatie van je applicatie. In het voorbeeld wordt de eventfunctionaliteit van Laravel gebruikt om een label toe te kennen "wanneer iemand je volgt".

```php theme={null}
// Zoals in 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');

    // Een label toekennen via de Ozone-API van Bluesky
    Bluesky::login(
        identifier: config('bluesky.labeler.identifier'),
        password: config('bluesky.labeler.password'),
    )->createLabels(
        subject: RepoRef::to($followerDid),
        labels: ['artisan'],
    );
}
```

Het wordt aanbevolen om ook via de task scheduler labels toe te kennen, voor het geval er events gemist worden.

<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)](/nl/packages/laravel-bluesky/websocket.md)
- [BlueskyManager en HasShortHand](/nl/packages/laravel-bluesky/bluesky-manager.md)
- [Crypto — AT Protocol-cryptografie](/nl/packages/laravel-bluesky/crypto.md)
- [Routes](/nl/packages/laravel-bluesky/route.md)
- [我的包](/zh-CN/packages/index.md)
