Passer au contenu principal
Labeler est une fonctionnalité avancée. Comme il nécessite un serveur qui tourne en continu, elle vise les personnes qui utilisent Laravel Forge ou qui savent gérer leur propre serveur. Elle n’est pas recommandée pour les débutants Laravel. Aucun support n’est fourni.Laravel Cloud n’est pas pris en charge. Labeler doit fonctionner en permanence en tant que serveur WebSocket et nécessite une modification de la configuration nginx. Or Laravel Cloud ne permet pas de configurer nginx, il est donc impossible d’y exécuter Labeler.

Qu’est-ce que Labeler ?

Labeler est un service qui applique des labels à du contenu sur AT Protocol (Bluesky). Il peut servir à la modération, à la classification de contenu, au filtrage personnalisé, etc. Il est indispensable de comprendre au préalable le concept de Labeler. Les starter kits pour d’autres langages sont également des références utiles. Implémentations d’exemple :

Préparation

Pour faire fonctionner Labeler, il vous faut :
  • Un nouveau compte Bluesky dédié à Labeler (n’utilisez pas votre compte habituel)
  • Un nouveau projet Laravel dédié à Labeler (il est recommandé de séparer les projets)
  • Un (sous-)domaine
  • Un serveur Linux de production tel qu’un VPS ou une instance AWS EC2 (Laravel Cloud, Laravel Vapor et Vercel ne conviennent pas)
Si vous n’avez accès qu’à un hébergement mutualisé, l’exploitation de Labeler est difficile.

Installation de packages supplémentaires

composer require workerman/workerman revolt/event-loop

Configuration

Commencez par générer une clé privée.
php artisan bluesky:labeler:new-private-key
Ajoutez à .env la clé privée générée et les valeurs de configuration associées.
BLUESKY_LABELER_DID=did:plc:***
BLUESKY_LABELER_IDENTIFIER=***.bsky.social
BLUESKY_LABELER_APP_PASSWORD=

BLUESKY_LABELER_PRIVATE_KEY=""

Création de la classe Labeler

Créez votre classe Labeler en étendant AbstractLabeler. Vous pouvez placer le fichier où vous le souhaitez.
namespace App\Labeler;

use Revolution\Bluesky\Labeler\AbstractLabeler;

readonly class ArtisanLabeler extends AbstractLabeler
{
    // Implémentez chaque méthode
}
Le package prend en charge la majeure partie du traitement du Labeler ; vous n’avez à implémenter que les parties à personnaliser.
Implémentation d’exemple : ArtisanLabeler.php

labels()

Renvoie la définition des labels. Les constantes du starter kit skyware sont une bonne référence.
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()

Appelée immédiatement après la connexion WebSocket. Renvoie SubscribeLabelResponse sous forme d’itérateur.
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;
    }

    // Pour renvoyer une réponse d'erreur, lancez impérativement 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()

Appelée lorsqu’une requête d’ajout ou de retrait de label est reçue. Renvoie UnsignedLabel sous forme d’itérateur.
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()

Enregistre le label signé en base de données. Renvoie un SavedLabel.
use Revolution\Bluesky\Labeler\SavedLabel;
use Revolution\Bluesky\Labeler\SignedLabel;

public function saveLabel(SignedLabel $signed, string $sign): ?SavedLabel
{
    // App\Models\Label est à créer par vos soins
    $saved = Label::create($signed->toArray());

    return new SavedLabel(
        $saved->id,
        $signed,
    );
}
Pour la migration et le modèle Eloquent, référez-vous aux exemples fournis dans le package.

createReport()

Appelée lorsqu’un utilisateur envoie un appel (appeal) ou similaire.
use Illuminate\Http\Request;

/**
 * @link https://docs.bsky.app/docs/api/com-atproto-moderation-create-report
 */
public function createReport(Request $request): array
{
    // Traiter le report et renvoyer un tableau
    // Champs requis : 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()

Interroge les labels via l’API HTTP plutôt que via WebSocket. Non utilisée officiellement par Bluesky ; elle sert à des tiers. Renvoyez un tableau vide si vous n’en avez pas besoin.
use Illuminate\Http\Request;

/**
 * @link https://docs.bsky.app/docs/api/com-atproto-label-query-labels
 */
public function queryLabels(Request $request): array
{
    return [];
}

Enregistrement dans AppServiceProvider

Enregistrez la classe Labeler créée dans AppServiceProvider::boot().
use Revolution\Bluesky\Labeler\Labeler;
use App\Labeler\ArtisanLabeler;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Labeler::register(ArtisanLabeler::class);
    }
}

Configuration du compte

Initialisez le compte en tant que Labeler.
Cette commande demande le mot de passe réel du compte, et non un App Password. Pendant le traitement, un e-mail de confirmation « PLC Update Operation Requested » vous est envoyé : suivez les instructions de la commande.
php artisan bluesky:labeler:setup
La commande peut également être exécutée depuis un environnement local si l’URL de l’endpoint est correctement configurée.

Déclaration des définitions de labels

Enregistrez les définitions de labels sur le compte Labeler.
php artisan bluesky:labeler:declare-labels
Cette commande peut aussi être exécutée depuis l’environnement local.

Autres commandes

Suppression des définitions de labels :
php artisan bluesky:labeler:delete-labels
Restaurer le compte Labeler en compte normal :
php artisan bluesky:labeler:restore

Exécution sur Laravel Forge

Après avoir activé SSL, démarrez le serveur Labeler avec la configuration suivante.

Configuration nginx

Ajoutez 3 blocs location à la configuration nginx de Forge.
# WebSocket : abonnement aux labels
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 : émission d'événements
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 déploiement

Arrêtez le serveur Labeler lors du déploiement. Supervisor le redémarrera automatiquement.
# Étapes de déploiement classiques
$FORGE_PHP artisan bluesky:labeler:server stop

# Si la commande ci-dessus ne fonctionne pas, redémarrez directement le daemon
sudo -S supervisorctl restart daemon-{id}:*

Configuration du processus d’arrière-plan (daemon)

Sur l’écran de configuration des processus d’arrière-plan de Forge, choisissez l’onglet Custom et non Queue Worker. Commande :
php artisan bluesky:labeler:server start
Si vous lancez également Jetstream ou Firehose, ajoutez les options correspondantes. La commande ne peut pas être exécutée simultanément avec bluesky:ws ou bluesky:firehose.
# Démarrer conjointement avec Jetstream
php artisan bluesky:labeler:server start --jetstream

# Filtrer sur certaines collections
php artisan bluesky:labeler:server start --jetstream -C app.bsky.graph.follow -C app.bsky.feed.like

# Démarrer conjointement avec Firehose
php artisan bluesky:labeler:server start --firehose

Ajout de labels

La manière d’appliquer un label dépend de l’implémentation de votre application. L’exemple utilise le système d’événements Laravel pour appliquer un label « lorsqu’un follow est reçu ».
// Idée pour 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');

    // Appliquer le label via l'API Ozone de Bluesky
    Bluesky::login(
        identifier: config('bluesky.labeler.identifier'),
        password: config('bluesky.labeler.password'),
    )->createLabels(
        subject: RepoRef::to($followerDid),
        labels: ['artisan'],
    );
}
Il est recommandé d’appliquer également des labels via la planification de tâches, en prévision d’événements manqués.
Dernière modification le 13 juillet 2026