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

> Come costruire e gestire un server Labeler con Laravel Bluesky. Illustra l'ereditarietà di AbstractLabeler, la definizione delle label, la connessione WebSocket e il deploy con Laravel Forge.

<Warning>
  Labeler è una funzionalità per utenti avanzati. Poiché deve essere eseguito come server continuativamente, è pensato per chi usa Laravel Forge o è in grado di configurare un server autonomamente. Non è consigliato ai principianti di Laravel. Non c'è supporto.

  **Laravel Cloud non è supportato.** Labeler richiede di avviare un server WebSocket sempre attivo e di modificare la configurazione di nginx; Laravel Cloud non permette di configurare nginx, quindi non è possibile eseguire Labeler.
</Warning>

## Cos'è Labeler

Labeler è un servizio che applica etichette (label) ai contenuti su AT Protocol (Bluesky). Si può usare per moderazione, classificazione dei contenuti, filtri personalizzati e altro.

È necessario comprendere in anticipo il concetto di Labeler.

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

Anche gli starter kit per altri linguaggi sono un utile riferimento.

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

Implementazione di esempio:

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

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

    Client->>Labeler: WebSocket: subscribeLabels
    Labeler-->>Client: Stream di label

    Client->>Labeler: POST /xrpc/tools.ozone.moderation.emitEvent
    Labeler->>DB: Salva la label
    Labeler-->>Client: Risposta
```

## Preparazione

Per far funzionare Labeler ti serve quanto segue.

* **Un nuovo account Bluesky dedicato al Labeler** (non usare l'account personale che usi abitualmente)
* **Un nuovo progetto Laravel dedicato al Labeler** (è consigliato tenere i progetti separati)
* **Un (sotto)dominio**
* **Un server Linux di produzione, come VPS o AWS EC2** (Laravel Cloud, Laravel Vapor e Vercel non sono utilizzabili)

<Info>
  Se puoi usare solo hosting condiviso, gestire un Labeler è difficile.
</Info>

## Installazione di pacchetti aggiuntivi

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

## Configurazione

Per prima cosa genera la chiave privata.

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

Aggiungi la chiave privata generata e i valori di configurazione correlati a `.env`.

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

BLUESKY_LABELER_PRIVATE_KEY=""
```

## Creazione della classe Labeler

Crea una classe Labeler personalizzata che estende `AbstractLabeler`. Il file puoi collocarlo dove preferisci.

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

use Revolution\Bluesky\Labeler\AbstractLabeler;

readonly class ArtisanLabeler extends AbstractLabeler
{
    // Implementa i vari metodi
}
```

Il pacchetto si occupa della maggior parte del lavoro del Labeler, quindi implementi soltanto le parti che vuoi personalizzare.

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

### labels()

Restituisce le definizioni delle label. Le costanti definite nello starter kit skyware sono un buon riferimento.

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

Viene chiamato subito dopo la connessione WebSocket. Restituisci `SubscribeLabelResponse` come 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;
    }

    // Se devi restituire una risposta di errore lancia sempre LabelerException
    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()

Viene chiamato quando arriva una richiesta di aggiunta o rimozione di una label. Restituisci `UnsignedLabel` come 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()

Salva la label firmata nel database. Restituisce un `SavedLabel`.

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

public function saveLabel(SignedLabel $signed, string $sign): ?SavedLabel
{
    // App\Models\Label lo devi creare tu
    $saved = Label::create($signed->toArray());

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

Per migrazione e modello Eloquent, consulta gli esempi nel pacchetto.

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

### createReport()

Viene chiamato quando arriva un ricorso o simile da un utente.

```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
{
    // Elabora la segnalazione e restituisci un array
    // Campi obbligatori: 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 delle label via API HTTP al posto del WebSocket. Non è usato da Bluesky ufficiale ma da terze parti. Se non ti serve, restituisci un array vuoto.

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

## Registrazione in AppServiceProvider

Registra la classe Labeler creata 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);
    }
}
```

## Setup dell'account

Inizializza l'account come Labeler.

<Warning>
  In questo comando inserisci l'effettiva password dell'account e non l'app password. Durante l'operazione ti arriverà un'email di conferma "PLC Update Operation Requested": segui le istruzioni del comando per inserirla.
</Warning>

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

Se l'URL dell'endpoint è configurato correttamente puoi eseguirlo anche dall'ambiente locale.

## Dichiarazione delle definizioni di label

Registra le definizioni di label sull'account Labeler.

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

Anche questo comando può essere eseguito dall'ambiente locale.

## Altri comandi

Eliminazione delle definizioni di label:

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

Riportare l'account Labeler ad account normale:

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

## Esecuzione su Laravel Forge

Dopo aver abilitato SSL, avvia il server Labeler con la seguente configurazione.

### Configurazione nginx

Aggiungi 3 blocchi `location` alla configurazione nginx di Forge.

```nginx theme={null}
# WebSocket: sottoscrizione alle label
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: invio di eventi
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 "";
}

# Health check
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 "";
}
```

### Script di deploy

Al deploy ferma il server Labeler. Dopo lo stop, Supervisor lo riavvierà automaticamente.

```bash theme={null}
# Procedura di deploy standard
$FORGE_PHP artisan bluesky:labeler:server stop

# Se il comando sopra non funziona, riavvia direttamente il daemon
sudo -S supervisorctl restart daemon-{id}:*
```

### Configurazione del processo in background (daemon)

Nella schermata di configurazione dei background process di Forge, seleziona la scheda **Custom** invece di Queue Worker.

**Comando:**

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

Se lo avvii insieme a Jetstream o Firehose, specifica le opzioni.
Non puoi eseguirlo contemporaneamente ai comandi `bluesky:ws` o `bluesky:firehose`.

```bash theme={null}
# Avvio insieme a Jetstream
php artisan bluesky:labeler:server start --jetstream

# Filtrando collezioni specifiche
php artisan bluesky:labeler:server start --jetstream -C app.bsky.graph.follow -C app.bsky.feed.like

# Avvio insieme a Firehose
php artisan bluesky:labeler:server start --firehose
```

## Aggiunta di label

Come applichi le label dipende dall'implementazione della tua applicazione. Nell'esempio, usando il sistema di eventi di Laravel, viene applicata una label "quando ricevi un follow".

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

    // Applica la label tramite l'API Ozone di Bluesky
    Bluesky::login(
        identifier: config('bluesky.labeler.identifier'),
        password: config('bluesky.labeler.password'),
    )->createLabels(
        subject: RepoRef::to($followerDid),
        labels: ['artisan'],
    );
}
```

Per gestire eventuali eventi persi, è consigliato applicare label anche tramite task scheduler.

<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)](/it/packages/laravel-bluesky/websocket.md)
- [BlueskyManager e HasShortHand](/it/packages/laravel-bluesky/bluesky-manager.md)
- [Crypto — crittografia AT Protocol](/it/packages/laravel-bluesky/crypto.md)
- [Rotte](/it/packages/laravel-bluesky/route.md)
