Exemples complets

Mode code

Ces exemples sont aussi dans l’onglet **Exemples** du Studio Code : un clic les charge dans ton module avec leurs commandes et leurs réglages.

Bot complet

Tout ce qu’un vrai bot sait faire : météo en direct par internet avec autocomplétion des villes, radio en vocal, sondage natif de Discord, compteur de temps en vocal, journal d’audit envoyé par webhook, et deux actions au clic droit.

Ce que tu apprends : ctx.fetch · module.autocomplete · Sous-commandes et choix · ctx.voice · Sondage natif · Embeds · Webhooks · auditLogEntry · userMenu · messageMenu.

Commandes : /meteo, /radio, /sondage, /vocal, clic droit sur un membre « Infos du membre », clic droit sur un message « Envoyer au journal ».

Réglages : logs (Salon du journal), vocal (Salon vocal de la radio par défaut).

bot-complet.js
module.uses('geocoding-api.open-meteo.com', 'api.open-meteo.com', 'icecast.radiofrance.fr');

const RADIOS = {
  fip: { nom: 'FIP', url: 'https://icecast.radiofrance.fr/fip-midfi.mp3' },
  inter: { nom: 'France Inter', url: 'https://icecast.radiofrance.fr/franceinter-midfi.mp3' },
  culture: { nom: 'France Culture', url: 'https://icecast.radiofrance.fr/franceculture-midfi.mp3' },
};

const CIEL = { 0: '☀️ Ciel dégagé', 1: '🌤️ Plutôt beau', 2: '⛅ Nuageux', 3: '☁️ Couvert', 45: '🌫️ Brouillard', 51: '🌦️ Bruine', 61: '🌧️ Pluie', 63: '🌧️ Pluie', 65: '🌧️ Forte pluie', 71: '🌨️ Neige', 80: '🌦️ Averses', 95: '⛈️ Orage' };

const AUDIT = { 20: 'Expulsion', 22: 'Bannissement', 23: 'Débannissement', 25: 'Rôles modifiés', 72: 'Messages supprimés par la modération' };

module.autocomplete('meteo', async (ctx) => {
  const texte = ctx.focused.value.trim();
  if (texte.length < 2) return ['Paris', 'Lyon', 'Marseille', 'Lille', 'Bordeaux'];
  const res = await ctx.fetch('https://geocoding-api.open-meteo.com/v1/search?count=8&language=fr&name=' + encodeURIComponent(texte));
  return (res.json?.results ?? []).map((v) => ({
    name: v.name + (v.admin1 ? ' (' + v.admin1 + ')' : ''),
    value: v.latitude + ';' + v.longitude + ';' + v.name,
  }));
});

module.command('meteo', async (ctx) => {
  await ctx.defer({ public: true });
  let [lat, lon, nom] = String(ctx.args.ville).split(';');
  if (!nom) {
    const geo = await ctx.fetch('https://geocoding-api.open-meteo.com/v1/search?count=1&language=fr&name=' + encodeURIComponent(ctx.args.ville));
    const ville = geo.json?.results?.[0];
    if (!ville) return ctx.editReply('Je ne trouve pas cette ville.');
    [lat, lon, nom] = [ville.latitude, ville.longitude, ville.name];
  }
  const res = await ctx.fetch('https://api.open-meteo.com/v1/forecast?current=temperature_2m,weather_code,wind_speed_10m&timezone=Europe/Paris&latitude=' + lat + '&longitude=' + lon);
  const now = res.json?.current;
  if (!now) return ctx.editReply('La météo n’est pas disponible pour le moment.');
  await ctx.editReply({
    embeds: [{
      title: 'Météo à ' + nom,
      description: CIEL[now.weather_code] ?? 'Temps variable',
      color: 0x86a8cf,
      fields: [
        { name: 'Température', value: Math.round(now.temperature_2m) + ' °C', inline: true },
        { name: 'Vent', value: Math.round(now.wind_speed_10m) + ' km/h', inline: true },
      ],
      footer: { text: 'Données Open-Meteo' },
    }],
  });
});

module.command('radio', async (ctx) => {
  if (ctx.subcommand === 'stop') {
    await ctx.voice.leave();
    return ctx.reply('Radio coupée.');
  }
  const radio = RADIOS[ctx.args.station] ?? RADIOS.fip;
  const etat = await ctx.members.voiceState(ctx.member).catch(() => null);
  const salon = etat?.channel_id ?? ctx.config.vocal;
  if (!salon) return ctx.reply('Rejoins d’abord un salon vocal.');
  await ctx.voice.play(radio.url, { channel: salon, title: radio.nom });
  await ctx.reply('📻 ' + radio.nom + ' en direct dans <#' + salon + '>. /radio stop pour couper.', { public: true });
});

module.on('audioError', async (ctx) => {
  await ctx.log('Radio interrompue : ' + (ctx.event?.error ?? 'erreur inconnue'));
});

module.command('sondage', async (ctx) => {
  const choix = String(ctx.args.choix).split(',').map((c) => c.trim()).filter(Boolean).slice(0, 10);
  if (choix.length < 2) return ctx.reply('Donne au moins deux choix, séparés par des virgules.');
  await ctx.reply({
    poll: {
      question: { text: ctx.args.question },
      answers: choix.map((text) => ({ poll_media: { text: text.slice(0, 55) } })),
      duration: ctx.args.heures ?? 24,
      allow_multiselect: false,
    },
  }, { public: true });
});

module.on('voiceJoin', async (ctx) => {
  await ctx.memory.set('vocal_debut', Date.now());
});

module.on('voiceLeave', async (ctx) => {
  const debut = await ctx.memory.get('vocal_debut');
  if (!debut) return;
  await ctx.memory.delete('vocal_debut');
  const minutes = Math.round((Date.now() - debut) / 60000);
  if (minutes > 0) await ctx.points('vocal').add(ctx.member, minutes);
});

module.command('vocal', async (ctx) => {
  const top = await ctx.points('vocal').top(10);
  const moi = await ctx.points('vocal').get(ctx.member);
  await ctx.reply({
    title: '🎙️ Temps passé en vocal',
    text: (top.text || 'Personne pour l’instant.') + '\n\nToi : ' + moi.points + ' min',
    color: '#c38eb4',
  });
});

async function journal(ctx, embed) {
  if (!ctx.config.logs) return;
  let hook = await ctx.storage.get('webhook');
  if (!hook || hook.salon !== ctx.config.logs) {
    const cree = await ctx.webhooks.create(ctx.config.logs, { name: 'Journal' });
    hook = { id: cree.id, token: cree.token, salon: ctx.config.logs };
    await ctx.storage.set('webhook', hook);
  }
  await ctx.webhooks.send(hook, { embeds: [{ timestamp: new Date().toISOString(), ...embed }] }, { username: 'Journal du serveur' });
}

module.on('auditLogEntry', async (ctx) => {
  const quoi = AUDIT[ctx.event.action_type];
  if (!quoi) return;
  await journal(ctx, {
    title: quoi,
    color: 0xc38eb4,
    description: 'Par <@' + ctx.event.user_id + '>' + (ctx.event.target_id ? ' sur <@' + ctx.event.target_id + '>' : '') + (ctx.event.reason ? '\nRaison : ' + ctx.event.reason : ''),
  });
});

module.userMenu('Infos du membre', async (ctx) => {
  const m = ctx.target;
  await ctx.reply({
    embeds: [{
      title: m.name,
      color: 0x86a8cf,
      thumbnail: m.avatar ? { url: m.avatar } : undefined,
      fields: [
        { name: 'Compte créé', value: 'il y a ' + m.accountAgeDays + ' jours', inline: true },
        { name: 'Sur le serveur', value: m.joinedDays == null ? '—' : 'depuis ' + m.joinedDays + ' jours', inline: true },
        { name: 'Rôles', value: m.roles.map((r) => '<@&' + r + '>').join(' ') || 'aucun' },
      ],
    }],
  });
});

module.messageMenu('Envoyer au journal', async (ctx) => {
  const msg = ctx.target;
  await journal(ctx, {
    title: 'Message signalé par ' + ctx.member.name,
    description: (msg.content || '(sans texte)').slice(0, 1500) + '\n\n[Voir le message](' + msg.url + ')',
    color: 0x86a8cf,
    footer: { text: 'Auteur : ' + (msg.authorName ?? 'inconnu') },
  });
  await ctx.reply(ctx.config.logs ? 'Envoyé dans le journal.' : 'Aucun salon de journal n’est choisi dans les réglages.');
});

Sondage à boutons

Une question, deux boutons, des compteurs qui se mettent à jour en direct. Chaque membre vote une fois et peut changer d’avis.

Ce que tu apprends : Commande avec option · Boutons avec arg · setButtonLabels · storage.

Commandes : /sondage.

Réglages : salon (Salon des sondages).

sondage.js
module.command('sondage', async (ctx) => {
  const numero = String(await ctx.storage.increment('sondages'));
  await ctx.storage.set('sondage-' + numero, { oui: [], non: [] });
  await ctx.send(ctx.config.salon, {
    title: ctx.args.question,
    text: 'Sondage lancé par ' + ctx.member.mention,
    color: '#86a8cf',
    buttons: [
      { id: 'oui', label: 'Oui (0)', style: 'success', arg: numero },
      { id: 'non', label: 'Non (0)', style: 'danger', arg: numero },
    ],
  });
  await ctx.reply('Sondage publié.');
});

async function voter(ctx, choix) {
  const cle = 'sondage-' + ctx.button.arg;
  const votes = (await ctx.storage.get(cle)) ?? { oui: [], non: [] };
  votes.oui = votes.oui.filter((id) => id !== ctx.member.id);
  votes.non = votes.non.filter((id) => id !== ctx.member.id);
  votes[choix].push(ctx.member.id);
  await ctx.storage.set(cle, votes);
  await ctx.setButtonLabels({ oui: 'Oui (' + votes.oui.length + ')', non: 'Non (' + votes.non.length + ')' });
}

module.button('oui', (ctx) => voter(ctx, 'oui'));
module.button('non', (ctx) => voter(ctx, 'non'));

Rappels personnels

/rappel 30 Sortir le chien : Opale envoie un message privé 30 minutes plus tard. Jusqu’à 7 jours à l’avance.

Ce que tu apprends : Options de commande · ctx.later · module.task · ctx.dm.

Commandes : /rappel.

rappels.js
module.command('rappel', async (ctx) => {
  const minutes = Number(ctx.args.minutes);
  if (!(minutes >= 1 && minutes <= 10080)) return ctx.reply('Choisis entre 1 minute et 7 jours (10080 minutes).');
  await ctx.later('rappel', minutes, { texte: ctx.args.texte });
  const heure = new Date(Date.now() + minutes * 60000).toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit' });
  await ctx.reply('C’est noté : je te le rappelle vers ' + heure + '.');
});

module.task('rappel', async (ctx) => {
  await ctx.dm(ctx.member, { title: 'Rappel', text: ctx.task.texte, color: '#c38eb4' });
});

Signalements au staff

/signaler ouvre un formulaire. Le staff est prévenu dans son salon et traite chaque signalement depuis une liste du dashboard.

Ce que tu apprends : ctx.openForm · module.form · collection · module.row · Page liste.

Commandes : /signaler.

Réglages : salon (Salon du staff).

signalements.js
module.command('signaler', async (ctx) => {
  await ctx.openForm({
    id: 'signalement',
    title: 'Signaler un problème',
    fields: [
      { key: 'sujet', label: 'En quelques mots', style: 'short', max: 80 },
      { key: 'details', label: 'Que s’est-il passé ?', max: 1000 },
    ],
  });
});

module.form('signalement', async (ctx) => {
  const { sujet, details } = ctx.form.values;
  const fiche = await ctx.collection('signalements').add({ auteur: ctx.member.name, membre: ctx.member.id, sujet, details, statut: 'nouveau' });
  await ctx.send(ctx.config.salon, {
    title: 'Signalement n°' + fiche.id,
    text: details,
    color: '#e07a7a',
    infos: [{ label: 'Sujet', value: sujet }, { label: 'Par', value: ctx.member.mention }],
    footer: 'À traiter depuis le dashboard',
  });
  await ctx.reply('Merci, le staff a bien reçu ton signalement.');
});

async function conclure(ctx, statut, message) {
  await ctx.collection('signalements').update(ctx.item.id, { statut });
  await ctx.dm(ctx.item.membre, message + ' (« ' + ctx.item.sujet + ' »)');
  await ctx.reply('Le membre a été prévenu.');
}

module.row('traiter', (ctx) => conclure(ctx, 'traité', 'Ton signalement a été traité par le staff, merci'));
module.row('classer', (ctx) => conclure(ctx, 'sans suite', 'Ton signalement a été examiné et classé sans suite'));

Inscriptions à un événement

/evenement crée une annonce avec places limitées. Les membres s’inscrivent d’un clic, reçoivent un rôle, et le nombre de places se met à jour.

Ce que tu apprends : Plusieurs options · Boutons avec arg · collection.find · Rôles · setButtonLabels.

Commandes : /evenement.

Réglages : salon (Salon des événements), role (Rôle des inscrits).

evenement.js
module.command('evenement', async (ctx) => {
  const places = Math.max(1, Math.min(500, Number(ctx.args.places) || 10));
  const evt = await ctx.collection('evenements').add({ titre: ctx.args.titre, places, inscrits: 0, membres: [] });
  await ctx.send(ctx.config.salon, {
    title: ctx.args.titre,
    text: 'Inscriptions ouvertes : ' + places + ' places.',
    color: '#9a88e6',
    buttons: [
      { id: 'inscrire', label: 'S’inscrire (0/' + places + ')', style: 'primary', arg: String(evt.id) },
      { id: 'desinscrire', label: 'Se désinscrire', arg: String(evt.id) },
    ],
  });
  await ctx.reply('Événement publié.');
});

async function maj(ctx, evt) {
  await ctx.collection('evenements').update(evt.id, { membres: evt.membres, inscrits: evt.membres.length });
  await ctx.setButtonLabels({ inscrire: 'S’inscrire (' + evt.membres.length + '/' + evt.places + ')' });
}

module.button('inscrire', async (ctx) => {
  const evt = await ctx.collection('evenements').get(Number(ctx.button.arg));
  if (!evt) return ctx.reply('Cet événement n’existe plus.');
  if (evt.membres.includes(ctx.member.id)) return ctx.reply('Tu es déjà inscrit.');
  if (evt.membres.length >= evt.places) return ctx.reply('Désolé, c’est complet.');
  evt.membres.push(ctx.member.id);
  if (ctx.config.role) await ctx.addRole(ctx.config.role);
  await maj(ctx, evt);
});

module.button('desinscrire', async (ctx) => {
  const evt = await ctx.collection('evenements').get(Number(ctx.button.arg));
  if (!evt || !evt.membres.includes(ctx.member.id)) return ctx.reply('Tu n’étais pas inscrit.');
  evt.membres = evt.membres.filter((id) => id !== ctx.member.id);
  if (ctx.config.role) await ctx.removeRole(ctx.config.role);
  await maj(ctx, evt);
});
Exemples complets · Documentation Opale