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

> Cómo construir y operar un servidor Labeler con Laravel Bluesky. Explica la herencia de AbstractLabeler, la definición de etiquetas, las conexiones WebSocket y el despliegue con Laravel Forge.

<Warning>
  Labeler es una función para usuarios avanzados. Como tiene que ejecutarse continuamente como servidor, está pensado para personas que usen Laravel Forge o puedan montar un servidor por su cuenta. No se recomienda para quienes empiezan con Laravel. No hay soporte.

  **Laravel Cloud no es compatible.** Labeler necesita ejecutarse permanentemente como servidor WebSocket y modificar la configuración de nginx, pero Laravel Cloud no permite editar la configuración de nginx, así que no se puede ejecutar Labeler ahí.
</Warning>

## ¿Qué es Labeler?

Labeler es un servicio de AT Protocol (Bluesky) que aplica etiquetas al contenido. Se puede aprovechar para moderación, clasificación de contenido, filtrado personalizado y más.

Es necesario entender previamente el concepto de Labeler.

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

Los starter kits para otros lenguajes también son buenas referencias.

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

Implementaciones de ejemplo:

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

```mermaid theme={null}
sequenceDiagram
    participant Client as Cliente<br>de Bluesky
    participant Labeler as Servidor Laravel<br>Labeler
    participant DB as Base de datos

    Client->>Labeler: WebSocket: subscribeLabels
    Labeler-->>Client: Stream de etiquetas

    Client->>Labeler: POST /xrpc/tools.ozone.moderation.emitEvent
    Labeler->>DB: Guarda la etiqueta
    Labeler-->>Client: Respuesta
```

## Preparación

Para ejecutar Labeler necesitas lo siguiente.

* **Una cuenta nueva de Bluesky dedicada al Labeler** (no uses tu cuenta habitual)
* **Un proyecto Laravel nuevo dedicado al Labeler** (se recomienda separarlo del resto)
* **Un (sub)dominio**
* **Un servidor Linux de producción**, como VPS o AWS EC2 (no valen Laravel Cloud, Laravel Vapor ni Vercel)

<Info>
  Si solo dispones de hosting compartido, es difícil operar Labeler.
</Info>

## Instalación de paquetes adicionales

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

## Configuración

Primero, genera la clave privada.

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

Añade la clave privada generada y los valores de configuración relacionados en `.env`.

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

BLUESKY_LABELER_PRIVATE_KEY=""
```

## Crear la clase Labeler

Crea una clase Labeler propia que herede de `AbstractLabeler`. Puedes ubicar el archivo donde prefieras.

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

use Revolution\Bluesky\Labeler\AbstractLabeler;

readonly class ArtisanLabeler extends AbstractLabeler
{
    // Implementa los métodos
}
```

Como el paquete se encarga de la mayor parte del procesamiento del Labeler, solo tienes que implementar las partes que quieras personalizar.

<Info>
  Implementación de ejemplo: [ArtisanLabeler.php](https://github.com/invokable/laralabeler/blob/main/app/Labeler/ArtisanLabeler.php)
</Info>

### labels()

Devuelve las definiciones de etiqueta. Las constantes del starter kit de skyware sirven como referencia.

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

Se invoca justo después de la conexión WebSocket. Devuelve un iterador de `SubscribeLabelResponse`.

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

    // Si vas a devolver una respuesta de error, lanza siempre 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()

Se invoca cuando llega una solicitud para añadir o eliminar etiquetas. Devuelve un iterador de `UnsignedLabel`.

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

Guarda la etiqueta firmada en la base de datos. Devuelve 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 debes crearlo tú
    $saved = Label::create($signed->toArray());

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

Para la migración y el modelo Eloquent, consulta los ejemplos incluidos en el paquete.

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

### createReport()

Se invoca cuando el usuario envía una apelación u otro tipo de informe.

```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
{
    // Procesa el informe y devuelve un array
    // Campos obligatorios: 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()

Consulta las etiquetas vía HTTP API en lugar de WebSocket. Bluesky no lo utiliza oficialmente; lo usan terceros. Si no lo necesitas, devuelve un array vacío.

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

## Registro en AppServiceProvider

Registra la clase Labeler creada en `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);
    }
}
```

## Configuración de la cuenta

Inicializa la cuenta como cuenta Labeler.

<Warning>
  En este comando debes introducir la contraseña real de la cuenta, no una app password. Durante el proceso recibirás un correo de confirmación titulado "PLC Update Operation Requested"; sigue las indicaciones del comando y responde.
</Warning>

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

Si la URL del endpoint está bien configurada, también puedes ejecutarlo desde el entorno local.

## Declaración de las definiciones de etiquetas

Registra las definiciones de etiquetas en la cuenta Labeler.

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

Este comando también se puede ejecutar desde el entorno local.

## Otros comandos

Eliminar las definiciones de etiquetas:

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

Revertir la cuenta Labeler a una cuenta normal:

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

## Ejecución con Laravel Forge

Tras activar el SSL, inicia el servidor Labeler con la siguiente configuración.

### Configuración de nginx

Añade tres bloques `location` a la configuración nginx de Forge.

```nginx theme={null}
# WebSocket: suscripción a etiquetas
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: envío de eventos
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 de despliegue

Detén el servidor Labeler al desplegar. Después de detenerlo, Supervisor lo reiniciará automáticamente.

```bash theme={null}
# Pasos habituales de despliegue
$FORGE_PHP artisan bluesky:labeler:server stop

# Si lo anterior no funciona, reinicia directamente el daemon
sudo -S supervisorctl restart daemon-{id}:*
```

### Configuración del proceso en segundo plano (daemon)

En la pantalla de configuración de procesos en segundo plano de Forge, selecciona la pestaña **Custom**, no la de Queue Worker.

**Comando:**

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

Si vas a ejecutarlo junto con Jetstream o Firehose, especifica las opciones.
No puedes ejecutarlo al mismo tiempo que los comandos `bluesky:ws` o `bluesky:firehose`.

```bash theme={null}
# Junto con Jetstream
php artisan bluesky:labeler:server start --jetstream

# Filtrando una colección concreta
php artisan bluesky:labeler:server start --jetstream -C app.bsky.graph.follow -C app.bsky.feed.like

# Junto con Firehose
php artisan bluesky:labeler:server start --firehose
```

## Aplicar etiquetas

Cómo aplicas las etiquetas depende de la implementación de tu aplicación. En el ejemplo se usan los eventos de Laravel para aplicar etiquetas "cuando alguien te sigue".

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

    // Aplica la etiqueta vía la API Ozone de Bluesky
    Bluesky::login(
        identifier: config('bluesky.labeler.identifier'),
        password: config('bluesky.labeler.password'),
    )->createLabels(
        subject: RepoRef::to($followerDid),
        labels: ['artisan'],
    );
}
```

Se recomienda aplicar el etiquetado también con la programación de tareas para cubrir posibles eventos que se pierdan.

<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)](/es/packages/laravel-bluesky/websocket.md)
- [BlueskyManager y HasShortHand](/es/packages/laravel-bluesky/bluesky-manager.md)
- [Crypto — criptografía de AT Protocol](/es/packages/laravel-bluesky/crypto.md)
- [Rutas](/es/packages/laravel-bluesky/route.md)
