Saltar al contenido principal
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í.

¿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. Los starter kits para otros lenguajes también son buenas referencias. Implementaciones de ejemplo:

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)
Si solo dispones de hosting compartido, es difícil operar Labeler.

Instalación de paquetes adicionales

composer require workerman/workerman revolt/event-loop

Configuración

Primero, genera la clave privada.
php artisan bluesky:labeler:new-private-key
Añade la clave privada generada y los valores de configuración relacionados en .env.
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.
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.
Implementación de ejemplo: ArtisanLabeler.php

labels()

Devuelve las definiciones de etiqueta. Las constantes del starter kit de skyware sirven como referencia.
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.
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.
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.
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.

createReport()

Se invoca cuando el usuario envía una apelación u otro tipo de informe.
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.
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().
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.
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.
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.
php artisan bluesky:labeler:declare-labels
Este comando también se puede ejecutar desde el entorno local.

Otros comandos

Eliminar las definiciones de etiquetas:
php artisan bluesky:labeler:delete-labels
Revertir la cuenta Labeler a una cuenta normal:
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.
# 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.
# 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:
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.
# 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”.
// 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.
Última modificación el 13 de julio de 2026