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

> Comment construire et exploiter un serveur Labeler avec Laravel Bluesky. Extension d'AbstractLabeler, définition de labels, connexion WebSocket et déploiement sur Laravel Forge.

<Warning>
  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.
</Warning>

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

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

Les starter kits pour d'autres langages sont également des références utiles.

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

Implémentations d'exemple :

* [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 Serveur<br>Laravel Labeler
    participant DB as Base de données

    Client->>Labeler: WebSocket : subscribeLabels
    Labeler-->>Client: Flux de labels

    Client->>Labeler: POST /xrpc/tools.ozone.moderation.emitEvent
    Labeler->>DB: Enregistrement du label
    Labeler-->>Client: Réponse
```

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

<Info>
  Si vous n'avez accès qu'à un hébergement mutualisé, l'exploitation de Labeler est difficile.
</Info>

## Installation de packages supplémentaires

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

## Configuration

Commencez par générer une clé privée.

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

Ajoutez à `.env` la clé privée générée et les valeurs de configuration associées.

```dotenv theme={null}
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.

```php theme={null}
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.

<Info>
  Implémentation d'exemple : [ArtisanLabeler.php](https://github.com/invokable/laralabeler/blob/main/app/Labeler/ArtisanLabeler.php)
</Info>

### labels()

Renvoie la définition des labels. Les constantes du starter kit skyware sont une bonne référence.

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

Appelée immédiatement après la connexion WebSocket. Renvoie `SubscribeLabelResponse` sous forme d'itérateur.

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

    // 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.

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

Enregistre le label signé en base de données. Renvoie 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 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.

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

### createReport()

Appelée lorsqu'un utilisateur envoie un appel (appeal) ou similaire.

```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
{
    // 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.

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

## Enregistrement dans AppServiceProvider

Enregistrez la classe Labeler créée dans `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);
    }
}
```

## Configuration du compte

Initialisez le compte en tant que Labeler.

<Warning>
  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.
</Warning>

```bash theme={null}
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.

```bash theme={null}
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 :

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

Restaurer le compte Labeler en compte normal :

```bash theme={null}
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.

```nginx theme={null}
# 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.

```bash theme={null}
# É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 :**

```bash theme={null}
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`.

```bash theme={null}
# 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 ».

```php theme={null}
// 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.

<Info>
  Source : [docs/labeler.md](https://github.com/invokable/laravel-bluesky/blob/main/docs/labeler.md)
  Exemple : [invokable/laralabeler](https://github.com/invokable/laralabeler)
</Info>


## Related topics

- [WebSocket (Jetstream / Firehose)](/fr/packages/laravel-bluesky/websocket.md)
- [BlueskyManager et HasShortHand](/fr/packages/laravel-bluesky/bluesky-manager.md)
- [Crypto — chiffrement AT Protocol](/fr/packages/laravel-bluesky/crypto.md)
- [Routes](/fr/packages/laravel-bluesky/route.md)
