Aller au contenu
Meletia

Récupérer les résultats dans votre base

Enregistrer chaque partie dans votre propre base de données, avec ou sans le nom du joueur.

Sans rien faire, Meletia compte déjà les parties pour vous : onglet Résultats de chaque quiz (parties, score moyen, questions les plus ratées). Ce guide sert si vous voulez aussi garder chaque partie dans votre propre base de données, par exemple pour afficher ses scores à chaque membre de votre site.

Deux façons de faire

CritèreA. Dans la pageB. Par webhook
PrincipeÀ la fin d'une partie, votre page reçoit le score et l'enregistre.À la fin d'une partie, Meletia envoie le résultat à une adresse de votre site.
Qui a jouéConnu : votre site sait quel membre est connecté.Inconnu : les parties arrivent sans nom.
DétailScore total.Score total, et réponse et résultat de chaque question.
FiabilitéCalculé dans le navigateur : un joueur habile peut le modifier.Signé par Meletia : impossible à falsifier.
Mise en placeSimple : un peu de code dans la page.Une fonction côté serveur sur votre site.

Lequel choisir ? Votre site a des comptes et vous voulez montrer à chacun ses scores : A. Vous voulez un historique fiable de toutes les parties, pour vos analyses ou un concours : B. Rien n'empêche d'utiliser les deux.

Les exemples ci-dessous utilisent Supabase, la base de données de la plupart des sites créés avec Lovable ou Bolt. Avec une autre base, les consignes pour l'IA restent valables telles quelles.

A. Enregistrer le score dans la page

  1. Le quiz doit être intégré avec le code complet, script embed.js compris (voir Intégrer un quiz).
  2. Donnez cette consigne à l'IA de votre site :
Consigne à donner à votre IA
Sur la page qui contient le quiz Meletia, enregistre le résultat de chaque partie pour l'utilisateur connecté.

1. Crée la table quiz_results si elle n'existe pas : id, user_id (l'utilisateur connecté), quiz_id (texte), quiz_title (texte), points (nombre décimal), max_points (nombre décimal), percent (entier), reason (texte), created_at (date, par défaut maintenant). Chaque utilisateur ne peut créer et lire que ses propres lignes.
2. Dans la page, écoute l'événement JavaScript « meletia:complete » sur window : window.addEventListener("meletia:complete", (event) => { … }). Il est déclenché par le script embed.js de Meletia à la fin d'une partie.
3. event.detail contient : quizId, quizTitle, points, maxPoints, percent, reason ("done" : quiz terminé, "time" : temps écoulé, "lives" : plus de vie).
4. Si un utilisateur est connecté, insère une ligne dans quiz_results avec son identifiant et ces valeurs. Sinon, n'enregistre rien.
5. Ne modifie pas l'iframe ni le script du quiz.
  1. Jouez une partie en étant connecté, puis vérifiez qu'une ligne apparaît dans la table.

Pour qui écrit le code

À la fin d'une partie, le script embed.js déclenche l'événement meletia:complete sur l'iframe ; il remonte jusqu'à window.

HTML
<script>
  window.addEventListener("meletia:complete", function (event) {
    var result = event.detail;
    // result.quizId, result.quizTitle, result.points, result.maxPoints,
    // result.percent, result.reason ("done", "time" ou "lives")
    console.log("Quiz terminé :", result.percent + " %");
  });
</script>

Avec Supabase, la table et l'enregistrement :

SQL (Supabase)
create table quiz_results (
  id bigint generated always as identity primary key,
  user_id uuid not null default auth.uid() references auth.users on delete cascade,
  quiz_id text not null,
  quiz_title text,
  points real not null,
  max_points real not null,
  percent integer not null,
  reason text,
  created_at timestamptz not null default now()
);

alter table quiz_results enable row level security;

create policy "Chacun enregistre ses résultats" on quiz_results
  for insert to authenticated with check (user_id = auth.uid());

create policy "Chacun lit ses résultats" on quiz_results
  for select to authenticated using (user_id = auth.uid());
JavaScript
import { supabase } from "./supabaseClient";

window.addEventListener("meletia:complete", async (event) => {
  const { data } = await supabase.auth.getUser();
  if (!data.user) return; // visiteur non connecté : rien à enregistrer

  const result = event.detail;
  await supabase.from("quiz_results").insert({
    user_id: data.user.id,
    quiz_id: result.quizId,
    quiz_title: result.quizTitle,
    points: result.points,
    max_points: result.maxPoints,
    percent: result.percent,
    reason: result.reason,
  });
});

Le score vient du navigateur du joueur : ne vous en servez pas pour attribuer un lot ou une note d'examen. Pour cela, utilisez le webhook.

B. Recevoir chaque partie par webhook

Le webhook est une adresse de votre site que Meletia appelle à la fin de chaque partie. Comptez quinze minutes ; l'ordre des étapes compte.

  1. Créer l'adresse de réception. Donnez cette consigne à l'IA de votre site. Elle crée la table et la fonction, puis vous donne l'adresse publique de la fonction (avec Supabase, elle ressemble à https://xxxx.supabase.co/functions/v1/meletia-webhook).
Consigne à donner à votre IA
Crée une adresse qui reçoit les résultats des quiz Meletia (un « webhook ») et les enregistre dans la base.

1. Crée la table quiz_games : id, delivery_id (texte, unique), quiz_id (texte), quiz_title (texte), completed (booléen), points (nombre décimal), max_points (nombre décimal), answers (JSON), received_at (date, par défaut maintenant). Seul le serveur écrit dans cette table.
2. Crée une fonction côté serveur (par exemple une Edge Function Supabase nommée meletia-webhook, ou une route API /api/meletia-webhook), accessible publiquement en POST, sans connexion utilisateur. Pour une Edge Function Supabase, désactive la vérification JWT (verify_jwt = false).
3. Elle lit le corps brut de la requête (texte, avant tout JSON.parse) et vérifie la signature :
   - l'en-tête « Meletia-Signature » a la forme t=<horodatage>,v1=<signature> ;
   - calcule un HMAC SHA-256 en hexadécimal de « t.corps » (l'horodatage, un point, puis le corps brut) avec le secret de la variable d'environnement MELETIA_WEBHOOK_SECRET ;
   - compare-le à v1 en temps constant ; réponds 401 s'il est différent, ou si t date de plus de 5 minutes.
4. Le corps est un JSON : { id, type: "game.finished", createdAt, test, data: { quiz: { id, title }, completed, points, maxPoints, answers } }.
5. Si test vaut true (envoi d'essai), réponds 200 sans rien enregistrer.
6. Sinon, insère une ligne dans quiz_games avec delivery_id = id ; si cet id existe déjà, ne fais rien (Meletia peut renvoyer le même envoi).
7. Réponds 200 en moins de 5 secondes.
8. Prépare la variable d'environnement secrète MELETIA_WEBHOOK_SECRET : je te donnerai sa valeur. Dis-moi l'adresse publique complète de la fonction.
  1. Déclarer l'adresse dans Meletia. Menu API et webhooks, rubrique Webhook : collez l'adresse donnée par l'IA, puis Enregistrer l'adresse.
  2. Donner le secret à votre site. Toujours dans API et webhooks, cliquez sur Afficher le secret puis Copier le secret. Dans votre outil, ajoutez une variable secrète nommée MELETIA_WEBHOOK_SECRET avec cette valeur (Supabase : Edge Functions, Secrets ; Lovable et Bolt : le panneau des secrets ou des variables d'environnement). Ne collez pas le secret dans la discussion avec l'IA.
  3. Tester. Dans API et webhooks, cliquez sur Envoyer un essai. Le message « Essai reçu par votre serveur (réponse 200) » confirme que tout est branché. Un échec 401 indique un secret différent de chaque côté ; un échec « adresse injoignable », une adresse erronée.
  4. Activer le webhook sur le quiz. Dans le quiz, onglet Réglages, choisissez « Envoyés à mon serveur (webhook) » comme stockage des résultats, puis enregistrez.
  5. Vérifier. Jouez une partie : une ligne apparaît dans la table quiz_games quelques secondes après la fin.

Pour qui écrit le code

La table, puis la fonction Supabase complète :

SQL (Supabase)
create table quiz_games (
  id bigint generated always as identity primary key,
  delivery_id text not null unique,
  quiz_id text not null,
  quiz_title text,
  completed boolean not null,
  points real not null,
  max_points real not null,
  answers jsonb not null,
  received_at timestamptz not null default now()
);

-- Seule la fonction serveur (clé de service) écrit dans cette table.
alter table quiz_games enable row level security;
TypeScript (Supabase Edge Function)
// supabase/functions/meletia-webhook/index.ts
// À déployer sans vérification JWT : supabase functions deploy meletia-webhook --no-verify-jwt
import { createClient } from "npm:@supabase/supabase-js@2";

const secret = Deno.env.get("MELETIA_WEBHOOK_SECRET")!;
const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);

async function hmac(payload: string) {
  const key = await crypto.subtle.importKey(
    "raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
  );
  const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload));
  return [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
}

function sameText(a: string, b: string) {
  if (a.length !== b.length) return false;
  let diff = 0;
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  return diff === 0;
}

Deno.serve(async (req) => {
  if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });

  const body = await req.text();
  const header = req.headers.get("meletia-signature") ?? "";
  const t = header.match(/t=(\d+)/)?.[1];
  const v1 = header.match(/v1=([0-9a-f]{64})/)?.[1];
  const fresh = t !== undefined && Math.abs(Date.now() / 1000 - Number(t)) <= 300;
  if (!t || !v1 || !fresh || !sameText(await hmac(`${t}.${body}`), v1)) {
    return new Response("Invalid signature", { status: 401 });
  }

  const event = JSON.parse(body);
  if (event.test) return new Response("ok"); // envoi d'essai depuis Meletia

  const game = event.data;
  const { error } = await supabase.from("quiz_games").upsert(
    {
      delivery_id: event.id,
      quiz_id: game.quiz.id,
      quiz_title: game.quiz.title,
      completed: game.completed,
      points: game.points,
      max_points: game.maxPoints,
      answers: game.answers,
    },
    { onConflict: "delivery_id", ignoreDuplicates: true },
  );
  if (error) return new Response("Database error", { status: 500 });
  return new Response("ok");
});

Pour un site Next.js, la même vérification dans une route (la fonction saveGame est la vôtre) :

TypeScript (Next.js)
// app/api/meletia-webhook/route.ts
import crypto from "node:crypto";

export async function POST(request: Request) {
  const body = await request.text();
  const header = request.headers.get("meletia-signature") ?? "";
  const t = header.match(/t=(\d+)/)?.[1];
  const v1 = header.match(/v1=([0-9a-f]{64})/)?.[1];
  if (!t || !v1 || Math.abs(Date.now() / 1000 - Number(t)) > 300) {
    return new Response("Invalid signature", { status: 401 });
  }
  const expected = crypto
    .createHmac("sha256", process.env.MELETIA_WEBHOOK_SECRET!)
    .update(`${t}.${body}`)
    .digest("hex");
  if (!crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))) {
    return new Response("Invalid signature", { status: 401 });
  }

  const event = JSON.parse(body);
  if (!event.test) {
    // Enregistrez ici event.data dans votre base (en ignorant un event.id déjà reçu).
    await saveGame(event.id, event.data);
  }
  return new Response("ok");
}

Le format complet des envois, les nouvelles tentatives et les en-têtes sont décrits dans la référence des webhooks.

Questions fréquentes

Les parties arrivent-elles avec le nom du joueur ?

Pas par webhook : Meletia ne sait pas qui joue, et n'enregistre aucune donnée personnelle des joueurs. Pour relier une partie à un membre de votre site, utilisez la méthode A. La transmission d'un identifiant de votre site jusqu'au webhook est prévue.

Que se passe-t-il si mon site ne répond pas ?

Meletia réessaie deux fois (après 2 puis 8 secondes). Au-delà, la partie n'est pas renvoyée, mais elle reste comptée dans l'onglet Résultats. Le dernier envoi et son résultat sont affichés dans API et webhooks.

Les parties au-delà de mon quota sont-elles envoyées ?

Non : au-delà du quota mensuel de votre plan, les nouvelles parties restent jouables mais ne sont ni comptées ni envoyées.