Robots de clavardage
Guide complet pour créer un robot qui lit le clavardage d’une chaîne par WebSocket et y répond par l’API REST.
Vue d’ensemble
Un robot de clavardage Woupi est un programme qui reçoit les messages d’une chaîne et publie des réponses au nom d’un compte Woupi. Il combine trois éléments :
- un jeton d’utilisateur du compte du robot, avec les portées
chat:readetchat:write; - une connexion WebSocket avec un abonnement
channel.chat.messagepar chaîne; - des appels à
POST /v1/chat/messagespour répondre.
Remarque
Utilisez de préférence un compte Woupi distinct pour le robot : ses messages s’affichent sous le nom de ce compte.
1. Créer l’application
Dans le portail, ouvrez Applications, puis « Nouvelle application ». Choisissez la catégorie robot de clavardage (chat_bot) et le type confidentiel, puisque le robot tourne sur un serveur qui peut garder un secret. Ajoutez une URL de redirection, par exemple http://localhost:3000/callback pendant le développement.
Notez l’identifiant client et le secret client (wps_...). Le secret n’est affiché qu’une seule fois.
2. Obtenir le jeton du robot
Deux façons d’obtenir un jeton d’utilisateur avec chat:read chat:write :
- Générateur de jetons : si le robot utilise votre propre compte développeur, ouvrez la page de l’application, cochez
chat:readetchat:write, puis cliquez sur « Générer ». Vous obtenez un jeton d’accès et un jeton d’actualisation. - Flux par code d’autorisation : si le robot a son propre compte, connectez-vous à Woupi avec ce compte, puis ouvrez l’URL d’autorisation ci-dessous et cliquez sur « Autoriser ».
https://woupi.buzz/oauth/authorize?response_type=code&client_id=k3j9x0p2m8q7w1z5r4t6y2u8i0o3a1&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&scope=chat%3Aread%20chat%3Awrite&state=c29tZS1yYW5kb20tc3RhdGUWoupi redirige vers http://localhost:3000/callback?code=wpc_...&scope=...&state=.... Vérifiez state, puis échangez le code dans les 10 minutes :
curl -X POST https://api.woupi.buzz/oauth2/token \
-d grant_type=authorization_code \
-d code=wpc_9sVb2kQ7xN4mR1tY8wZ3pL6cF0hJ5dGa \
-d redirect_uri=http://localhost:3000/callback \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET"{
"access_token": "wpa_Qm7xT2vN9kR4pL1sW8yZ3cF6hJ0dGb5a",
"refresh_token": "wpr_Lp4nX8cV1bM6zQ3wR9tY2kH7jF5dSa0e",
"expires_in": 14400,
"scope": "chat:read chat:write",
"token_type": "bearer"
}Le jeton d’accès expire après 4 heures. Actualisez-le avec le jeton d’actualisation; chaque actualisation remplace les deux jetons, gardez donc toujours le nouveau jeton d’actualisation :
curl -X POST https://api.woupi.buzz/oauth2/token \
-d grant_type=refresh_token \
-d refresh_token="$BOT_REFRESH_TOKEN" \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET"Tous les détails des flux sont dans Authentification.
3. Trouver les identifiants
Le robot a besoin de son propre identifiant d’utilisateur et de celui de chaque chaîne. Pour le robot, /oauth2/validate retourne user_id (un appel à GET /v1/users sans paramètre retourne aussi l’utilisateur du jeton) :
curl https://api.woupi.buzz/oauth2/validate \
-H "Authorization: OAuth $BOT_TOKEN"{
"client_id": "k3j9x0p2m8q7w1z5r4t6y2u8i0o3a1",
"login": "lunabot",
"user_id": "2177",
"scopes": ["chat:read", "chat:write"],
"expires_in": 14312
}Pour une chaîne, cherchez-la par son nom d’utilisateur; l’identifiant du diffuseur est data[0].id. Le paramètre login peut être répété jusqu’à 100 fois.
curl "https://api.woupi.buzz/v1/users?login=lunaplays" \
-H "Authorization: Bearer $BOT_TOKEN" \
-H "Client-Id: $CLIENT_ID"{
"data": [
{
"id": "1042",
"login": "lunaplays",
"display_name": "LunaPlays",
"avatar_url": "https://woupi.buzz/uploads/u1042/avatar.webp",
"bio": "Jeux de rythme tous les soirs",
"level": 12,
"created_at": "2025-03-02T18:22:10.000Z"
}
]
}4. Écouter le clavardage
Ouvrez une connexion vers wss://api.woupi.buzz/eventsub et attendez le message session_welcome. Dans les 10 secondes, créez un abonnement channel.chat.message par chaîne, avec la condition broadcaster_user_id (la chaîne) et user_id (le robot), en utilisant le jeton du robot :
curl -X POST https://api.woupi.buzz/v1/eventsub/subscriptions \
-H "Authorization: Bearer $BOT_TOKEN" \
-H "Client-Id: $CLIENT_ID" \
-H "Content-Type: application/json" \
-d '{
"type": "channel.chat.message",
"version": "1",
"condition": { "broadcaster_user_id": "1042", "user_id": "2177" },
"transport": {
"method": "websocket",
"session_id": "9b4e7c2a-51f3-4d8e-a0c6-3e7f2b1d9a54"
}
}'Chaque message publié dans la chaîne arrive ensuite dans une notification; voici son objet event :
{
"broadcaster_user_id": "1042",
"broadcaster_user_login": "lunaplays",
"broadcaster_user_name": "LunaPlays",
"chatter_user_id": "3318",
"chatter_user_login": "max_quebec",
"chatter_user_name": "Max",
"message_id": "1790266224905-x7k2qa",
"message": { "text": "!ping" },
"chatter_level": 7
}Attention
Le robot reçoit aussi ses propres messages. Ignorez les événements dont chatter_user_id est l’identifiant du robot, sinon il risque de se répondre en boucle.
Une session accepte 100 abonnements. Le détail des messages, de la détection des connexions perdues et des codes de fermeture est dans Transport WebSocket.
5. Répondre
Publiez un message avec POST /v1/chat/messages (portée chat:write). Le corps contient broadcaster_id et message; le message est publié au nom de l’utilisateur du jeton.
curl -X POST https://api.woupi.buzz/v1/chat/messages \
-H "Authorization: Bearer $BOT_TOKEN" \
-H "Client-Id: $CLIENT_ID" \
-H "Content-Type: application/json" \
-d '{ "broadcaster_id": "1042", "message": "pong" }'{
"data": [
{ "message_id": "1790266225318-p9d3mf", "is_sent": true }
]
}Si le clavardage refuse le message, la réponse indique is_sent: false et la raison dans drop_reason :
{
"data": [
{ "message_id": null, "is_sent": false, "drop_reason": "chat.sys.slowMode" }
]
}drop_reason | Cause |
|---|---|
chat.sys.slowMode | Le mode lent est actif et le robot a écrit trop récemment. |
chat.sys.followersOnly | Clavardage réservé aux suiveurs : le compte du robot doit suivre la chaîne. |
chat.sys.subsOnly | Clavardage réservé aux abonnés de la chaîne. |
chat.sys.emotesOnly | Clavardage limité aux émoticônes. |
chat.sys.bannedWords | Le message contient un mot interdit par la chaîne. |
Les messages sont limités à 300 caractères. Un broadcaster_id inconnu donne 404 unknown broadcaster.
Exemple complet : !ping
Ce robot Node.js résout les chaînes indiquées dans WOUPI_CHANNELS, s’abonne à leur clavardage, répond pong à !ping, actualise son jeton quand il expire, respecte la limite de débit et se reconnecte en cas de coupure.
// npm install ws (Node.js 18+ for the global fetch)
// WOUPI_CHANNELS=lunaplays,autrechaine node bot.mjs
import WebSocket from 'ws';
const API = 'https://api.woupi.buzz';
const CLIENT_ID = process.env.WOUPI_CLIENT_ID;
const CLIENT_SECRET = process.env.WOUPI_CLIENT_SECRET;
const CHANNELS = (process.env.WOUPI_CHANNELS || '').split(',').filter(Boolean); // channel logins
let accessToken = process.env.WOUPI_ACCESS_TOKEN; // bot account token: chat:read chat:write
let refreshToken = process.env.WOUPI_REFRESH_TOKEN;
let botUserId = null;
async function refresh() {
const res = await fetch(`${API}/oauth2/token`, {
method: 'POST',
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`refresh failed: ${res.status} ${await res.text()}`);
const data = await res.json();
accessToken = data.access_token;
refreshToken = data.refresh_token; // refresh tokens rotate: always keep the new one
}
async function call(method, path, body, retried = false) {
const res = await fetch(`${API}/v1${path}`, {
method,
headers: {
Authorization: `Bearer ${accessToken}`,
'Client-Id': CLIENT_ID,
...(body ? { 'Content-Type': 'application/json' } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 401 && !retried) {
await refresh(); // the access token expires after 4 hours
return call(method, path, body, true);
}
if (res.status === 429) {
const resetMs = Number(res.headers.get('Ratelimit-Reset')) * 1000;
await new Promise((r) => setTimeout(r, Math.max(resetMs - Date.now(), 1000)));
return call(method, path, body, retried);
}
if (res.status === 204) return null;
const data = await res.json();
if (!res.ok) throw new Error(`${method} ${path}: ${res.status} ${data.message}`);
return data;
}
async function say(broadcasterId, message) {
const { data } = await call('POST', '/chat/messages', { broadcaster_id: broadcasterId, message });
if (!data[0].is_sent) console.warn('message refused:', data[0].drop_reason);
}
async function onChatMessage(event) {
if (event.chatter_user_id === botUserId) return; // never answer our own messages
if (event.message.text.trim() === '!ping') {
await say(event.broadcaster_user_id, 'pong');
}
}
async function subscribeAll(sessionId, broadcasterIds) {
for (const broadcasterId of broadcasterIds) {
await call('POST', '/eventsub/subscriptions', {
type: 'channel.chat.message',
version: '1',
condition: { broadcaster_user_id: broadcasterId, user_id: botUserId },
transport: { method: 'websocket', session_id: sessionId },
});
}
}
function connect(broadcasterIds) {
const ws = new WebSocket('wss://api.woupi.buzz/eventsub');
let timeoutMs = 15000;
let watchdog = null;
const arm = () => {
clearTimeout(watchdog);
watchdog = setTimeout(() => ws.terminate(), timeoutMs);
};
ws.on('open', arm);
ws.on('message', (raw) => {
arm();
const { metadata, payload } = JSON.parse(raw.toString());
if (metadata.message_type === 'session_welcome') {
timeoutMs = (payload.session.keepalive_timeout_seconds + 5) * 1000;
arm();
subscribeAll(payload.session.id, broadcasterIds).catch((err) => {
console.error(err.message);
ws.close();
});
} else if (metadata.message_type === 'notification'
&& metadata.subscription_type === 'channel.chat.message') {
onChatMessage(payload.event).catch((err) => console.error(err.message));
}
});
ws.on('close', (code, reason) => {
clearTimeout(watchdog);
console.log(`socket closed (${code} ${reason}), reconnecting in 5 s`);
setTimeout(() => connect(broadcasterIds), 5000);
});
ws.on('error', (err) => console.error(err.message));
}
async function main() {
const me = await call('GET', '/users'); // no parameters: the token's user
botUserId = me.data[0].id;
const query = CHANNELS.map((login) => `login=${encodeURIComponent(login)}`).join('&');
const channels = await call('GET', `/users?${query}`);
console.log(`${me.data[0].login} joins ${channels.data.map((u) => u.login).join(', ')}`);
connect(channels.data.map((u) => u.id));
}
main().catch((err) => {
console.error(err);
process.exit(1);
});Remarque
En production, enregistrez le nouveau jeton d’actualisation (fichier ou base de données) après chaque actualisation : l’ancien ne fonctionne plus.
Modération
Supprimer un message demande la portée moderator:manage:chat et un jeton du diffuseur lui-même : l’utilisateur du jeton doit être le propriétaire de la chaîne (broadcaster_id). Un robot qui modère doit donc utiliser un jeton que le diffuseur a accordé à votre application pour sa propre chaîne. L’identifiant du message est event.message_id.
curl -X DELETE "https://api.woupi.buzz/v1/moderation/chat?broadcaster_id=1042&message_id=1790266224905-x7k2qa" \
-H "Authorization: Bearer $BROADCASTER_TOKEN" \
-H "Client-Id: $CLIENT_ID"La réponse est 204 si le message a été supprimé, 404 s’il est introuvable. Avec le même jeton, GET et PATCH /v1/chat/settings lisent et modifient les modes du clavardage (mode lent, suiveurs seulement, etc.). La suppression déclenche l’événement channel.chat.message_delete.
Limites de débit et bonnes pratiques
L’API accepte 800 requêtes par période glissante de 60 secondes pour chaque couple application et utilisateur. Tous les appels du robot, y compris les messages envoyés, comptent dans la même limite. Chaque réponse indique où vous en êtes :
HTTP/1.1 200 OK
Ratelimit-Limit: 800
Ratelimit-Remaining: 793
Ratelimit-Reset: 1790266285- En cas de réponse 429, attendez le moment indiqué par
Ratelimit-Reset(secondes depuis l’époque Unix) avant de réessayer. - Le mode lent de la chaîne s’applique aussi au robot : regroupez vos réponses plutôt que d’en envoyer plusieurs d’affilée.
- Ne répondez pas à chaque message; réagissez aux commandes explicites comme
!ping. - Si le jeton est révoqué (l’utilisateur a déconnecté votre application), les appels répondent 401 et les abonnements sont supprimés : il faut une nouvelle autorisation.
Voir aussi Limites et erreurs.
