# discordplace-sync

SDK officiel pour synchroniser les statistiques de votre bot Discord avec [DiscordPlace](https://discordplace.com).

> 🇬🇧 **English speakers:** see the [English quick guide](#-english-quick-guide) at the bottom.

```bash
npm install discordplace-sync
```

---

## ⚠️ Le point le plus important : synchronisez en BOUCLE, pas une seule fois

La pastille **« en ligne »** de votre bot sur DiscordPlace est dérivée de la **fraîcheur** de vos
synchronisations. Discord n'expose pas la présence d'un bot en REST : votre SDK est le seul signal
dont nous disposons pour savoir que votre bot tourne.

- Tant que vous synchronisez régulièrement → pastille verte, stats à jour.
- Si vous ne synchronisez **qu'au démarrage** de votre bot → vous apparaîtrez **hors ligne** en
  permanence, même si votre bot tourne parfaitement depuis des semaines.

C'est l'erreur la plus fréquente. Le `SyncClient` du [démarrage rapide](#-démarrage-rapide-recommandé)
s'en occupe pour vous ; si vous utilisez la [classe bas niveau](#usage-manuel-discordplacesdk),
lisez attentivement les deux pièges signalés.

## Soumettre votre bot

La soumission d'un bot sur DiscordPlace **n'exige pas** que le SDK soit installé ni actif — vous
pouvez soumettre d'abord et brancher le SDK ensuite. Ce qui est exigé, c'est la **vérification de
propriété** du bot (via le site, avant la soumission).

Sans SDK, votre fiche affichera « — » à la place des serveurs, utilisateurs, commandes et de la
pastille en ligne, jusqu'à votre première synchronisation.

> Les versions ≤ 1.8.12 de ce README annonçaient une soumission refusée sans synchronisation dans
> les 10 dernières minutes. Cette règle a été retirée côté serveur : elle ne s'applique plus.

---

## 🚀 Démarrage rapide (recommandé)

`SyncClient` s'abonne lui-même à l'événement « prêt » de votre client (`clientReady` depuis
discord.js 14.22, `ready` avant — détecté tout seul), envoie une première
synchronisation, puis **répète toutes les 5 minutes**. Vous n'avez rien d'autre à appeler —
en particulier, **pas** de `.start()`.

```javascript
const { Client, GatewayIntentBits } = require('discord.js');
const createSyncClient = require('discordplace-sync');

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

// Avant OU après client.login() : il démarre tout seul quand le client est prêt.
const sync = createSyncClient(
  process.env.DISCORDPLACE_API_KEY, // 1) votre clé API
  client,                           // 2) votre client Discord.js
  { interval: 5 * 60 * 1000 }       // 3) options (facultatif) — 5 min par défaut
);

sync.on('synced', (bot) => console.log(`✅ ${bot.guildCount} serveurs synchronisés`));
sync.on('error',  (err) => console.error('❌ Sync error:', err.message));

client.login(process.env.BOT_TOKEN);
```

L'import nommé fonctionne aussi, si vous préférez :

```javascript
const { SyncClient } = require('discordplace-sync');
const sync = new SyncClient(process.env.DISCORDPLACE_API_KEY, client);
```

Les alias `AutoPoster`, `DJSPoster` et `DJSSharderPoster` pointent vers la même classe
(rétrocompatibilité).

À chaque cycle, `SyncClient` envoie le nom, l'avatar, les serveurs, les utilisateurs, la latence
**et la liste détaillée de vos commandes**.

### Sharding Discord.js

**Fichier du manager** — passez votre `ShardingManager` à la place du client. Il peut être créé
avant ou après `spawn()` : la synchronisation démarre quand **tous** les shards sont prêts, et un
cycle où un shard redémarre est sauté plutôt que d'envoyer un nombre de serveurs partiel.

```javascript
const { ShardingManager } = require('discord.js');
const createSyncClient = require('discordplace-sync');

const manager = new ShardingManager('./bot.js', { token: process.env.BOT_TOKEN });
const sync = createSyncClient(process.env.DISCORDPLACE_API_KEY, manager);
sync.on('error', (err) => console.error('DiscordPlace :', err.message));

manager.spawn();
```

**Fichier de chaque shard** (`bot.js`) — `trackUsage(client)` pour que le manager relève l'usage
de tous les shards ; et, pour publier la liste des commandes (le manager ne peut pas la lire), un
`syncBotInfo()` depuis le shard 0. Sur un client shardé, `syncBotInfo()` agrège lui-même les
compteurs de tous les shards (ou n'en envoie aucun si l'agrégat est impossible) : il ne fait plus
osciller le nombre de serveurs.

```javascript
const { Client, GatewayIntentBits, Events } = require('discord.js');
const { trackUsage, DiscordPlaceSDK } = require('discordplace-sync');

const client = new Client({ intents: [GatewayIntentBits.Guilds] });
trackUsage(client);

client.once(Events.ClientReady || 'ready', async () => {
  if (!client.shard.ids.includes(0)) return; // un seul shard publie les commandes
  const sdk = new DiscordPlaceSDK({ apiKey: process.env.DISCORDPLACE_API_KEY, botId: client.user.id });
  sdk.start();
  await sdk.syncBotInfo(client).catch((err) => console.error('DiscordPlace :', err.message));
});

client.login(process.env.BOT_TOKEN);
```

> Un `SyncClient` dans **chaque** shard fonctionne aussi depuis 1.9.1 : seul le shard 0
> synchronise (avec les compteurs agrégés), les autres n'envoient que leur usage. Ne combinez pas
> les deux montages.

> **Corrigé en 1.8.13** — jusqu'à la 1.8.12 incluse, le montage `ShardingManager` levait
> « Client Discord.js invalide ou non connecté » à chaque cycle : aucun bot shardé ne se
> synchronisait. Mettez à jour si vous êtes concerné.

---

## Usage manuel (`DiscordPlaceSDK`)

La classe bas niveau vous donne le contrôle total. Elle expose deux mécanismes **distincts**, qui
ne se remplacent pas l'un l'autre :

| Méthode | Endpoint | Contenu | Répétée automatiquement ? |
|---|---|---|---|
| `updateStats()` / `postStats()` / `autoCollectStats()` | `/bot-stats/stats` | Chiffres (serveurs, utilisateurs, latence, uptime…) | Oui, si `autoSync: true` |
| `syncBotInfo(client)` | `/bot-stats/sync-info` | Chiffres **+ liste des commandes** | ❌ **Non — un seul envoi par appel** |

### ⚠️ Piège n° 1 — `start()` seul ne synchronise rien

`start()` n'installe le minuteur **que si** l'option `autoSync: true` a été passée au constructeur.
Sans elle, `start()` se contente de rendre le SDK utilisable (`updateStats()` refuse de s'exécuter
avant), émet `'start'`… et n'envoie plus jamais rien. Aucune erreur n'est levée.

### ⚠️ Piège n° 2 — `autoSync` ne réémet que des statistiques déjà envoyées

Le minuteur d'`autoSync` renvoie le **dernier jeu de statistiques connu**, qui n'est renseigné que
par `updateStats()`, `postStats()` ou `autoCollectStats()`. Si vous n'appelez que `syncBotInfo()`,
le minuteur tourne à vide (visible uniquement avec `debug: true` : « Aucune statistique à envoyer »).

### Exemple correct et complet

```javascript
const { Client, GatewayIntentBits, Events } = require('discord.js');
const { DiscordPlaceSDK } = require('discordplace-sync');

const client = new Client({ intents: [GatewayIntentBits.Guilds] });

const sdk = new DiscordPlaceSDK({
  apiKey: process.env.DISCORDPLACE_API_KEY,
  botId: process.env.BOT_ID,
  autoSync: true,              // ← indispensable pour la répétition automatique
  syncInterval: 5 * 60 * 1000  // 5 min (défaut)
});

client.once(Events.ClientReady || 'ready', async () => {
  sdk.start();                     // 1) arme le SDK et le minuteur
  await sdk.autoCollectStats(client); // 2) premier envoi → alimente le minuteur (piège n° 2)
  await sdk.syncBotInfo(client);      // 3) pousse la liste des commandes (ponctuel)
});

// Rafraîchir les commandes après un déploiement : rappelez syncBotInfo().
client.login(process.env.BOT_TOKEN);
```

Si vous préférez fournir vos propres chiffres plutôt qu'`autoCollectStats` :

```javascript
await sdk.postStats({
  guildCount: client.guilds.cache.size,
  userCount: totalUsers,
  latency: client.ws.ping,
  isOnline: true
});
```

---

## 🎯 Synchronisation des commandes

`syncBotInfo(client)` détecte vos commandes Discord.js et transmet, pour chacune :

- **nom** et **description** ;
- **type** : `slash_command`, `prefix_command`, ou menu contextuel `user_command` /
  `message_command` (clic droit — ce n'est pas une commande à préfixe) ;
- **options/paramètres** : nom, description, type (`STRING`, `INTEGER`, `USER`, `CHANNEL`, `ROLE`…),
  obligatoire ou non, choix prédéfinis, bornes min/max (lues dans `data.toJSON()`).

```javascript
// /ban <user> [reason] [duration]
// - user     : USER    (obligatoire) — Utilisateur à bannir
// - reason   : STRING  (optionnel)   — Raison du bannissement
// - duration : INTEGER (optionnel)   — Durée en minutes

await sdk.syncBotInfo(client);
```

Ces détails apparaissent sur la page publique de votre bot, dans la liste des bots, et dans le
panneau de modération.

Options utiles :

```javascript
await sdk.syncBotInfo(client, {
  treatAllAsHybrid: true,             // toutes les commandes en slash + prefix
  prefixOnlyCommands: ['eval'],       // ces commandes sont uniquement prefix
  slashOnlyCommands: ['config']       // ces commandes sont uniquement slash
});
```

### Commandes privées (réservées au développeur du bot)

Seules les commandes **publiques** doivent figurer sur votre fiche. Le SDK ne publie pas :

- une commande **enregistrée sur un seul serveur** (votre serveur support), quand votre bot a par
  ailleurs des commandes globales — lu dans `client.application.commands.cache` ;
- une commande dont le gestionnaire porte `ownerOnly`, `devOnly`, `developerOnly`, `botOwnerOnly`
  ou `testOnly` à `true`, ou une liste `guildIds` (aussi sous `config` / `conf`) ;
- une commande que **vous** désignez :

```javascript
new SyncClient(process.env.DISCORDPLACE_API_KEY, client, {
  privateCommands: ['eval', 'guild'],                    // par nom
  commandFilter: (cmd) => cmd.category !== 'owner',      // ou par une règle : false = non publiée
});
```

Ces trois options se passent de la même façon à `syncBotInfo(client, { … })`.

⚠️ **Retirer une commande de `client.commands` ne suffit pas toujours.** Le SDK fusionne
`client.commands`, `client.slashCommands`, `client.prefixCommands` et les commandes que Discord
connaît de votre application : une commande absente de l'une peut revenir par une autre. Une
commande privée est écartée de **toutes** les sources, par son nom. Les sous-commandes que votre
bot range comme des commandes à part entière (`/guild backup` rangée sous `backup`) se désignent
une par une, ou par `commandFilter`.

`detectPrivateCommands: false` désactive les deux détections automatiques ; `privateCommands` et
`commandFilter` restent appliqués. Avec `debug: true`, le SDK écrit les commandes qu'il n'a pas
publiées. La fiche suit à la synchronisation suivante (5 minutes par défaut) : il n'y a rien à
« resynchroniser » à la main.

---

## Obtenir votre clé API

1. Connectez-vous sur [DiscordPlace](https://discordplace.com) ;
2. ouvrez votre **Dashboard** puis votre bot ;
3. onglet **SDK** → **Générer une clé API** ;
4. copiez-la dans une variable d'environnement — elle ne s'affiche qu'une fois.

Ce même onglet indique la **date de votre dernière synchronisation reçue** : c'est l'endroit où
vérifier que votre intégration fonctionne réellement.

---

## Options

### `SyncClient` / `createSyncClient(apiKey, client, options)`

| Option | Type | Description | Défaut |
|--------|------|-------------|--------|
| `interval` | number | Intervalle entre deux synchronisations, en ms | `300000` (5 min) |
| `endpoint` | string | URL de l'API DiscordPlace | `https://discordplace.com/api` |
| `trackUsage` | boolean | Compte l'usage du bot (voir « Usage compté ») | `true` |
| `webhook` | object | `{ port, secret, path?, host? }` — reçoit le webhook de vote | — |
| `updateNotice` | boolean | Avertit une fois dans la console quand une version plus récente est publiée | `true` |
| `debug` | boolean | Journal détaillé dans la console | `false` |
| `timeout` | number | Timeout d'une requête, en ms | `10000` |
| `retries` | number | Nombre **total** de tentatives (réseau, 5xx, 429) | `3` |
| `botVersion` | string | Version de **votre** bot, affichée sur sa fiche | — |
| `treatAllAsHybrid`, `prefixOnlyCommands`, `slashOnlyCommands` | | Transmis à `syncBotInfo()` à chaque cycle | — |
| `privateCommands`, `commandFilter`, `detectPrivateCommands` | | Commandes privées, jamais publiées — voir « Commandes privées » | — |

### `new DiscordPlaceSDK(options)`

| Option | Type | Description | Défaut |
|--------|------|-------------|--------|
| `apiKey` | string | Clé API DiscordPlace (**obligatoire**) | — |
| `botId` | string | ID Discord de votre bot (**obligatoire**) | — |
| `autoSync` | boolean | Réémission automatique des statistiques — voir pièges 1 et 2 | `false` |
| `syncInterval` | number | Intervalle d'`autoSync`, en ms | `300000` (5 min) |
| `endpoint` | string | URL de l'API DiscordPlace | `https://discordplace.com/api` |
| `timeout` | number | Timeout des requêtes, en ms | `10000` |
| `retries` | number | Nombre **total** de tentatives (réseau, 5xx, 429) | `3` |
| `debug` | boolean | Logs détaillés dans la console | `false` |
| `botVersion` | string | Version de **votre** bot, envoyée par `autoCollectStats()` et `syncBotInfo()` | — |

---

## Événements

### `SyncClient`

| Événement | Émis quand | Charge utile |
|---|---|---|
| `ready` | le minuteur de synchronisation est armé | — |
| `synced` | une synchronisation a réussi | le bot mis à jour (`guildCount`, `isOnline`, `lastStatsUpdate`…) |
| `error` | un cycle de synchronisation a échoué (une fois par cycle au plus) | une `DiscordPlaceError` |
| `vote` | quelqu'un a voté pour votre bot (option `webhook`) — l'écouteur peut être `async` | `{ id, botId, userId, votedAt, nextVoteAt }` |
| `voteTest` | le bouton « Envoyer un test » du panneau SDK | même forme, `type: 'test'` |
| `usageSynced` | l'usage compté a été envoyé | `{ batches }` |
| `updateAvailable` | une version plus récente du SDK est publiée (une fois par processus) | `{ current, latest }` |

Tant qu'aucun écouteur `error` n'est enregistré, chaque échec donne **une** ligne
`console.warn` — jamais un plantage du bot (avant 1.9.1, un échec sans écouteur faisait tomber le
processus). Avec un écouteur `vote` asynchrone, sa promesse est attendue : un rejet fait répondre
500 et DiscordPlace réessaie ; un vote déjà traité (même `id`) n'est pas transmis deux fois.

### Erreurs

Toute erreur rendue par le SDK est une `DiscordPlaceError` (exportée) : `status` (code HTTP, `null`
si DiscordPlace n'a pas répondu), `code` (`HTTP_401`, `ECONNREFUSED`…), `endpoint`, `retryable`.
Jamais l'erreur brute d'axios, qui contenait l'en-tête `Authorization` — donc votre clé API.

Les méthodes que **vous** appelez (`hasVoted`, `syncBotInfo`, `updateStats`…) **rejettent** leur
promesse et n'émettent rien : chaque échec est signalé une seule fois.

> **Corrigé en 1.8.13** — `synced` n'était jamais émis dans les versions précédentes : le
> client n'écoutait qu'un événement interne que le cycle automatique n'utilisait pas. Si votre
> callback `synced` ne s'est jamais déclenché, c'était ce bug, pas votre code.

### `DiscordPlaceSDK`

| Événement | Émis quand |
|---|---|
| `start` / `stop` | `start()` / `stop()` ont été appelés |
| `success` | `updateStats()` / `postStats()` a réussi |
| `syncSuccess` | `syncBotInfo()` a réussi |
| `error` | la réémission `autoSync` a échoué (un appel direct, lui, rejette sa promesse) |
| `updateAvailable` | une version plus récente du SDK est publiée |

```javascript
sdk.on('success', (data) => console.log('Stats synchronisées :', data));
sdk.on('error',   (err)  => console.error('Erreur SDK DiscordPlace :', err.message));
```

---

## Méthodes de `DiscordPlaceSDK`

| Méthode | Effet |
|---|---|
| `start()` | Arme le SDK ; installe le minuteur **si `autoSync: true`** |
| `stop()` | Désarme le SDK et son minuteur |
| `updateStats(stats)` | Envoie des statistiques. Échoue si `start()` n'a pas été appelé, ou si `guildCount` est absent (seul champ obligatoire) |
| `postStats(stats)` | Alias d'`updateStats` |
| `autoCollectStats(client)` | Collecte serveurs / utilisateurs / salons / latence / uptime / mémoire depuis le client, puis les envoie |
| `syncBotInfo(client, options)` | Envoie les informations complètes **avec les commandes** (ponctuel) |
| `hasVoted(userId)` | `{ voted, votedAt, expiresAt }` : a-t-il voté dans les 12 h ? (403 si le bot n'est pas encore approuvé) |
| `getStats()` | Les statistiques **actuelles** enregistrées par DiscordPlace. Il n'y a pas d'historique : l'argument `period` est accepté pour compatibilité et ignoré |
| `checkSyncStatus()` | La dernière synchronisation reçue, et si elle est encore « fraîche » (même fenêtre que la pastille en ligne) |

### Méthodes de `SyncClient`

| Méthode | Effet |
|---|---|
| `hasVoted(userId)` | Comme ci-dessus, utilisable dès que le client est connecté |
| `trackCommand(nom, userId?)` | Compte une commande à préfixe |
| `postNow()` | Lance un cycle tout de suite (les échecs vont à `error`) |
| `stop()` | Arrête tout, y compris une attente du « prêt » pas encore aboutie et le serveur du webhook |

---

## Statistiques collectées

Les deux mécanismes ne remontent pas les mêmes champs — c'est voulu, ils visent deux endpoints
différents :

| Champ | `syncBotInfo()` | `autoCollectStats()` / `postStats()` |
|---|:--:|:--:|
| Nom et avatar (`name`, `avatarUrl`) | ✅ | — |
| Serveurs (`guildCount`) | ✅ | ✅ |
| Utilisateurs (`userCount`) | ✅ | ✅ |
| Latence (`latency`) | ✅ | ✅ |
| Statut en ligne (`isOnline`) | ✅ | ✅ |
| Commandes détaillées (`slashCommands`, `prefixCommands`) | ✅ | — |
| Salons (`channelCount`) | ✅ | ✅ |
| Uptime et mémoire (`uptime`, `memoryUsage`) | ✅ | ✅ |
| Shards (`shardCount`) | ✅ | ✅ |
| Version (`version`) | ✅ (option `version` / `botVersion`) | ✅ (si vous la fournissez) |

`SyncClient` utilise `syncBotInfo()` avec un client Discord.js, et la seconde colonne avec un
`ShardingManager` (les commandes n'étant pas lisibles depuis le manager).

Les champs absents d'un envoi ne sont pas écrasés côté serveur : la dernière valeur connue est
conservée.

---

## 🗳️ Récompenser les votes : le webhook

À chaque vote pour votre bot, DiscordPlace appelle **votre** adresse, à la seconde. Vous créditez
la personne tout de suite, sans attendre qu'elle relance une commande.

1. Page de votre bot → onglet **SDK** → **Webhook de vote** : collez l'adresse publique de votre
   bot (par exemple `https://monbot.fr/discordplace/vote`) et gardez le **secret** affiché.
2. Côté bot, le plus simple :

```javascript
const sync = createSyncClient(process.env.DP_API_KEY, client, {
  webhook: { port: 8080, secret: process.env.DP_WEBHOOK_SECRET }, // écoute /discordplace/vote
});

sync.on('vote', async (vote) => {
  // vote = { id, botId, userId, votedAt, nextVoteAt }
  await donnerRecompense(vote.userId);
});
```

Avec Express, montez le gestionnaire **avant** `express.json()` — la signature porte sur le corps brut :

```javascript
const { createWebhookHandler } = require('discordplace-sync');
app.post('/discordplace/vote', createWebhookHandler({
  secret: process.env.DP_WEBHOOK_SECRET,
  onVote: (vote) => donnerRecompense(vote.userId),
}));
```

- Chaque envoi est signé : `X-DiscordPlace-Signature: t=<horodatage>,v1=<HMAC-SHA256(secret, "t.corps")>`.
  `verifySignature(secret, corpsBrut, entete)` le vérifie, et refuse un envoi de plus de 5 minutes.
- Répondez **2xx en moins de 5 s**. Sinon DiscordPlace réessaie : 1 min, 5 min, 30 min, 2 h, 6 h.
- `id` est unique par vote : ignorez un `id` déjà traité.
- Le panneau SDK montre les 15 derniers envois et leur code HTTP, et envoie un test à la demande.
- Sans webhook, `sdk.hasVoted(userId)` reste disponible : il dit si la personne a voté dans les 12 h.

---

## 📊 Usage compté (1.9)

`SyncClient` compte désormais l'usage de votre bot et l'envoie avec les statistiques. Ces chiffres
alimentent la carte et la fiche de votre bot (« commandes par jour », « répond à 99 % »…) et les
tris **Le plus utilisé** et **Fiable** de discordplace.com/bots.

| Mesure | Comment |
|---|---|
| Commandes exécutées, au total et par commande | écoute de `interactionCreate` |
| Boutons, menus, modales, autocomplétions | idem |
| Interactions **sans réponse** après 3 s | `replied` / `deferred` vérifiés 3 s après l'arrivée |
| Serveurs ajoutés et retirés | `guildCreate` / `guildDelete` après le démarrage |
| Utilisateurs actifs par jour | dédoublonnés **dans votre processus**, seul le nombre part |
| Langues et tailles des serveurs | `preferredLocale` et `memberCount`, en répartition |
| Intents, version de discord.js et de Node | ce que votre bot peut voir, affiché sur sa fiche |
| Installations sur compte utilisateur | `application.approximateUserInstallCount`, une fois par heure |

**Ce qui ne part jamais** : un identifiant d'utilisateur, un identifiant ou un nom de serveur, le
contenu d'un message ou d'une option de commande.

- Commandes à préfixe : `sync.trackCommand('nom', message.author.id)`.
- Sharding : appelez `trackUsage(client)` dans le fichier de chaque shard ; le SyncClient du
  manager relève les compteurs de tous les shards.
- Pour tout couper : `createSyncClient(key, client, { trackUsage: false })`.

---

## 🏷️ Affichez votre badge DiscordPlace

Un badge live, qui se met à jour tout seul et renvoie vers votre page :

```markdown
[![My Bot Discord Bot](https://discordplace.com/api/widget/bot/YOUR_BOT_ID.svg)](https://discordplace.com/bots/YOUR_BOT_ID)
```

Remplacez `YOUR_BOT_ID` par l'ID Discord de votre bot — ou copiez un extrait prêt à l'emploi
(HTML, Markdown, iframe, BBCode) depuis la page de votre bot ou votre dashboard.

---

## Journal des versions

### v1.9.1
- **Correctif critique** : sans écouteur `error`, un échec de synchronisation faisait **planter le
  bot** (`emit('error')` sans écouteur, depuis un minuteur). Désormais : une ligne `console.warn`.
- **Correctif critique (sharding)** : le démarrage appelait `broadcastEval` pendant le `spawn()`
  (ShardingInProcess) et un `SyncClient` créé après `spawn()` ne démarrait jamais. Il attend
  maintenant que tous les shards soient prêts, qu'il soit créé avant ou après.
- **Sécurité** : l'erreur brute d'axios, qui contenait votre clé API, n'est plus jamais transmise :
  `DiscordPlaceError` (`status`, `code`, `endpoint`, `retryable`).
- Un échec = **un** signalement (il en produisait 3 à 6, plus un `console.error` inconditionnel).
- `clientReady` (discord.js ≥ 14.22) avec repli sur `ready`.
- Écouteurs `vote` asynchrones attendus (un échec → 500 → DiscordPlace réessaie), votes rejoués
  ignorés.
- `SyncClient.hasVoted()`, `debug: true` qui journalise vraiment, `timeout` / `retries` transmis,
  `syncBotInfo()` envoie salons, uptime, mémoire, shards et version ; menus contextuels reconnus ;
  bornes min/max lues dans `toJSON()` ; avatar en PNG avec discord.js v14.
- Minuteurs `unref()`, `stop()` avant le « prêt » annule tout, `ready` émis après le constructeur.
- Plus de lot d'usage vide quand les shards n'appellent pas `trackUsage(client)`.
- **Commandes privées** : une commande réservée au développeur du bot n'est plus publiée ni
  comptée — détection automatique (enregistrée sur un seul serveur, `ownerOnly`, `devOnly`…) et
  options `privateCommands`, `commandFilter`, `detectPrivateCommands`. Voir « Commandes privées ».

### v1.9.0
- **Webhook de vote** : option `webhook`, événements `vote` / `voteTest`, et `createWebhookHandler`,
  `verifySignature`, `startWebhookServer` pour l'héberger vous-même.
- **Usage compté** (`trackUsage`, activé par défaut) : commandes, interactions sans réponse,
  serveurs ajoutés et retirés, utilisateurs actifs, langues et tailles des serveurs, intents.
  Uniquement des nombres. `trackCommand()` pour les commandes à préfixe, `trackUsage(client)` pour
  les shards.
- `sdk.hasVoted(userId)` : la personne a-t-elle voté dans les 12 dernières heures ?
- **Avis de mise à jour** : quand une version plus récente est publiée, le SDK le dit une fois
  dans la console (événement `updateAvailable`, désactivable par `updateNotice: false`).

### v1.8.13
- **Correctif** : l'événement `synced` de `SyncClient` n'était jamais émis. Il l'est désormais à
  chaque synchronisation réussie, et transporte directement le bot mis à jour.
- **Correctif** : passer un `ShardingManager` à `createSyncClient` échouait à chaque cycle
  (« Client Discord.js invalide ou non connecté »). Les statistiques des shards sont maintenant
  agrégées et envoyées correctement.
- **Correctif** : `shardCount` était supprimé silencieusement avant l'envoi — il est transmis.
- **Retrait** : `getApiKeyInfo()`, qui répondait 401 en toutes circonstances (route réservée à
  une session du dashboard). Utilisez `checkSyncStatus()`, ou l'onglet SDK du dashboard.
- **Documentation** : README entièrement revérifié contre le code. Plusieurs affirmations
  étaient fausses, dont « `start()` lance la synchronisation automatique » (il ne le fait que
  si `autoSync: true`) et l'intervalle par défaut annoncé à 30 min au lieu de 5.

### v1.8.12
- Badge « Featured on DiscordPlace » à intégrer dans votre README.
- Correction du nom de package dans les exemples : `discordplace-sync` (et non `@discordplace/sync`).

### v1.8.4
- **Mode silencieux par défaut** : les logs du SDK sont désactivés. Activez-les avec `debug: true`.

### v1.2.2
- Synchronisation des **options** de commandes : types, paramètres obligatoires, choix prédéfinis,
  bornes min/max.
- Affichage détaillé des commandes sur les pages publiques et dans le panneau de modération.

### v1.2.0
- Synchronisation des descriptions de commandes et comptage automatique (`commandCount`).

---

## 🇬🇧 English quick guide

```javascript
const { Client, GatewayIntentBits } = require('discord.js');
const createSyncClient = require('discordplace-sync');

const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const sync = createSyncClient(process.env.DISCORDPLACE_API_KEY, client);

sync.on('synced', (bot) => console.log(`Synced: ${bot.guildCount} servers`));
sync.on('error', (err) => console.error('DiscordPlace:', err.message)); // one per failed cycle

client.login(process.env.BOT_TOKEN);
```

- **Keep syncing**: the "online" badge on DiscordPlace comes from sync freshness. `SyncClient`
  syncs every 5 minutes on its own; syncing only at startup makes your bot look offline.
- **API key**: your bot page → **SDK** tab → *Generate an API key*. Keep it in an environment
  variable.
- **Sharding**: pass your `ShardingManager` instead of the client (before or after `spawn()`), and
  call `trackUsage(client)` in each shard file. To publish your command list, call
  `syncBotInfo(client)` once from shard 0 — counts are aggregated across shards, never partial.
- **Votes**: `await sync.hasVoted(userId)` → `{ voted, votedAt, expiresAt }` (last 12 h). For
  instant rewards, set a webhook URL in the SDK tab and pass `webhook: { port, secret }`; `vote`
  listeners may be `async` — a rejection answers 500 and DiscordPlace retries.
- **Usage tracking** (default on, numbers only, never IDs): commands, unanswered interactions,
  guilds added/removed, daily active users. `trackCommand(name, userId)` for prefix commands;
  `trackUsage: false` to opt out.
- **Private commands**: owner-only commands are not published nor counted. Detected on their
  own: a command registered on a single guild (when your bot also has global commands), or a
  handler flagged `ownerOnly`, `devOnly`, `developerOnly`, `botOwnerOnly`, `testOnly` or carrying
  `guildIds`. Name the others with `privateCommands: ['eval']`, or `commandFilter: (cmd, name) =>
  false`. Removing a command from `client.commands` is not always enough: the SDK merges it with
  the commands Discord knows for your application. `detectPrivateCommands: false` turns the
  detection off.
- **Errors**: every error is a `DiscordPlaceError` (`status`, `code`, `endpoint`, `retryable`) and
  never contains your API key. Methods you call reject; background failures go to `error`, or to
  a single `console.warn` line if you have no listener — the SDK never crashes your bot.
- **Options**: `interval`, `endpoint`, `trackUsage`, `webhook`, `updateNotice`, `debug`,
  `timeout`, `retries`, `botVersion`, `privateCommands`, `commandFilter`,
  `detectPrivateCommands`.

## Support

- Serveur Discord : [discord.gg/KTzmX4gUyx](https://discord.gg/KTzmX4gUyx)
- Onglet **SDK** de votre dashboard : clé API, date de la dernière synchronisation reçue, diagnostic.

## Licence

MIT
