Skip to content

kapp.poll & kapp.prediction ​

Sondages et prédictions natifs Kappapps : votes par chat côté serveur ou API, mises en monnaie interne du channel et affichage natif optionnel. Service partagé appli + widget.

Scopes : kapp-poll:manage et kapp-prediction:manage pour toutes les opérations du service correspondant. Les widgets ne déclarent pas de scope. Pour les points de chaîne Twitch, utiliser les services distincts Twitch.

Un seul sondage actif et une seule prédiction en cours par channel. Le Core V2 est requis (legacy_core_unsupported).

Création et paramètres communs ​

  • kapp.poll.start(data): Promise<Poll> ; kapp.prediction.start(data): Promise<Prediction>.
  • participation_mode?: 'native'|'app_only', défaut 'native' :
    • native : page viewers active ; chat selon chat_voting_enabled / chat_betting_enabled.
    • app_only : seuls les appels API de l'installation propriétaire peuvent participer. Chat et page viewers refusés côté serveur, même si le chat a été demandé actif.
  • results_visibility?: 'live'|'after_end', défaut 'live' : dans le second mode, résultats intermédiaires masqués sur la page viewers et l'affichage natif. Le streamer et l'appli propriétaire conservent les résultats. Les réponses proposées et le propre vote du viewer restent visibles.
  • show_display?: boolean, défaut true : contrôle l'encart overlay, indépendamment de la participation et de la visibilité.

Le résultat est révélé à la clôture du sondage, à la résolution ou à l'annulation de la prédiction ; un simple verrouillage ne le révèle pas.

Cycle de vie et reprise ​

  • Les opérations survivent à la fin de l'exécution : collecte native, échéances et contrôles du dashboard streamer continuent.
  • Les événements et le contrôle ciblent l'exécution pilote courante. Une nouvelle exécution de la même installation reprend avec kapp.poll.resume(id): Promise<Poll> ou kapp.prediction.resume(id): Promise<Prediction>.
  • La reprise conserve participations, réglages et dates ; l'ancien UUID perd le contrôle. Une autre installation reçoit un 403.
  • kapp.poll.get(id) / kapp.prediction.get(id) retournent un modèle, y compris terminé, sans transférer le contrôle. getActive() / getRunning() conservent leur retour de données ou null.
  • Les opérations anciennes sans propriétaire durable restent contrôlables par leur UUID original et le dashboard, sans reprise par déduction.
  • Le panneau streamer est accessible depuis la notification de lancement et à /channel/polls.

Sondages ​

Création : {title, choices: string[], duration: number|null, max_choices?, allow_vote_change?, chat_voting_enabled?, chat_command?, ...}.

  • duration : 10 à 1800 secondes, ou null pour une clôture manuelle sans échéance.
  • max_choices : 1 par défaut, entre 1 et le nombre de réponses ; 2 à 10 réponses au total.
  • allow_vote_change : true par défaut.
  • chat_command : '!vote' par défaut ; null permet les numéros seuls. Multichoix : !vote 1 3 remplace le bulletin complet.
  • vote(user, choice: number|number[], method = 'id', options = {}): Promise<void> : options.weight est un entier positif, défaut 1. Chaque réponse sélectionnée reçoit tout le poids. Le tableau remplace atomiquement le bulletin, il n'ajoute pas des réponses au bulletin précédent.
  • Un bulletin identique est sans effet ; une modification est refusée si les changements sont désactivés.
  • terminate() clôture avec résultats ; cancel() annule.
  • getData() recharge les données ; getTallies() lit les décomptes déjà reçus.
  • tallies[].votes et results[].votes sont les voix pondérées par réponse, pas un nombre de viewers uniques.

Prédictions ​

Création : {title, outcomes: string[], prediction_window?, betting_enabled?, min_bet?, max_bet?, fixed_reward?, auto_cancel_after?, chat_betting_enabled?, chat_command?, ...}.

  • Mises activées par défaut : chat !predict <réponse> <montant>, débit immédiat. Monnaie et feature monnaie requises.
  • prediction_window : 10 à 86400 secondes ; omettre pour un verrouillage manuel.
  • auto_cancel_after : secondes après verrouillage, défaut 86400. Annulation avec remboursement si non résolue. La reprise ne repousse pas cette échéance.
  • betting_enabled: false : participation sans mise, fixed_reward éventuel par gagnant.
  • bet(user, outcome_id, amount = 0, method = 'id', options = {}): Promise<void>. Choix et montant définitifs après acceptation.
  • lock() ferme les participations ; resolve(outcome_id) règle la prédiction ; cancel() rembourse.
  • Le pot est partagé au prorata : gain brut = floor(mise / mises_gagnantes * pot). Le gain brut inclut la mise initiale. Le reliquat d'arrondi n'est pas redistribué. Sans mise sur l'issue gagnante, toutes les mises sont remboursées.
  • Un échec de règlement n'est pas annoncé comme un paiement terminé : l'opération échoue et peut être réessayée.

Participations individuelles ​

  • model.onParticipation(callback) : callback de chaque participation ou changement accepté pendant la connexion ; retourne une fonction de désabonnement. Le callback reçoit la participation enrichie de revision et event_id pour la rapprocher des pages lues en parallèle. Les doublons et changements plus anciens du même viewer sont ignorés ; aucun ordre global entre viewers n'est garanti. Les paquets chat restent transportés par paquets.
  • model.getParticipants({cursor?, limit?}) : état courant/final réservé à l'installation propriétaire, page {items, next_cursor, revision} ; 100 entrées par défaut, maximum 200. Suivre next_cursor jusqu'à null, même si une page est vide après filtrage.
  • Sondage : {viewer: {id, login, display_name}, choice_ids, weight, source, created_at, updated_at}.
  • Prédiction : {viewer, outcome_id, amount, payout, refunded, source, created_at}. payout est le montant brut crédité, refunded signale la restitution de amount.
  • Les viewers ayant interdit l'appli ne sont pas exposés à cette appli.
  • Après relance, reprendre et relire les pages pour récupérer l'état actuel. Les callbacks ne rejouent pas les changements passés ; aucun historique exhaustif des modifications n'est fourni. Pendant une opération active, les pages décrivent l'état au moment de chaque lecture ; après clôture, l'état final est stable.

Refus, timeout et événements ​

  • Les refus métier rejettent la promesse avec {status, reason, code} : notamment closed, invalid_choice, invalid_choice_count, invalid_weight, vote_change_not_allowed, already_bet, insufficient_balance, bet_below_minimum, bet_above_maximum, participation_disabled, currency_unavailable.
  • options.idempotency_key peut être fourni à vote() et bet(). Le SDK en génère un sinon. Pour renvoyer après timeout, réutiliser la même clé et le même contenu, y compris après reprise ou clôture. Cela retrouve l'accusé précédent, sans appliquer à nouveau l'opération. Une autre demande avec cette clé donne 409 idempotency_conflict.
  • Une demande volontairement nouvelle, même après un refus définitif, utilise une nouvelle clé.
  • model.onEnd(callback) reçoit le modèle à la clôture/annulation d'un sondage ou à la résolution/annulation d'une prédiction ; le verrouillage ne le déclenche pas. Retourne une fonction de désabonnement. Les fins passées ne sont pas rejouées : lire aussi model.data après une lecture/reprise.
  • Le callback onEnd() peut être reçu avant le retour de terminate(), resolve() ou cancel(). S'abonner avant l'appel et lire aussi l'état connu du modèle après le retour. Aucun ordre de réception HTTP/socket n'est promis.
  • Les états portent une revision ; les doublons et états plus anciens ne doivent pas remplacer les données plus récentes.

Exemple V2 ​

js
let poll;
try {
    poll = await kapp.poll.start({ title: 'On joue à quoi ?', choices: ['Minecraft', 'Valorant'], duration: 60 });
} catch (err) {
    const reason = err && typeof err === 'object' && 'reason' in err ? err.reason : err;
    console.error('Sondage impossible : ' + String(reason));
    return;
}
await new Promise(resolve => {
    const stop = poll.onEnd(ended => { stop(); resolve(ended); });
    if (poll.data && poll.data.status !== 'ACTIVE') { stop(); resolve(poll); }
});
const winner = [...(poll.data?.results || [])].sort((a, b) => b.votes - a.votes)[0];
if (winner) await kapp.chat.say(`Résultat : « ${winner.title} » avec ${winner.votes} votes !`);

Reprendre, rejouer et récupérer l'état final ​

L'appelant conserve l'identifiant du sondage dès sa création et, avant le premier envoi, la demande {user, choices, method, weight, idempotency_key} dans le stockage adapté à son runtime. pendingVote ci-dessous est exactement cette demande dont l'issue était inconnue après un timeout ; ce n'est pas un nouveau vote. Aucune nouvelle clé n'est générée au rejeu.

Cet exemple reprend un sondage en cours, rejoue la demande puis le clôture. La clôture avant pagination est ici un choix pour lire un état final stable. Pour une opération déjà terminée, utiliser get(id) au lieu de resume(id) ; le même rejeu reste possible, et aucun événement passé ne sera rejoué.

js
/**
 * @param {number} pollId
 * @param {{user: string, choices: number[], method: 'id'|'login', weight: number, idempotency_key: string}} pendingVote
 */
async function resumeAndClose(pollId, pendingVote) {
    const poll = await kapp.poll.resume(pollId);
    const stop = poll.onEnd(ended => {
        console.info('Sondage terminé :', ended.data?.status);
    });
    try {
        await poll.vote(pendingVote.user, pendingVote.choices, pendingVote.method, {
            weight: pendingVote.weight,
            idempotency_key: pendingVote.idempotency_key,
        });
        await poll.terminate();

        // Le retour HTTP met aussi le modèle à jour ; aucun événement n'est attendu ici.
        const participants = [];
        let cursor;
        do {
            const page = await poll.getParticipants({cursor, limit: 100});
            participants.push(...page.items);
            cursor = page.next_cursor ?? undefined;
        } while (cursor !== undefined);
        return {state: poll.data, participants};
    } finally {
        stop();
    }
}

Une page vide ne termine pas la lecture si elle fournit encore un curseur. Après l'accusé de réception du vote, l'appelant peut retirer la demande en attente de son stockage. Un refus métier se traite via code ; il ne justifie pas de changer automatiquement de clé pour réessayer la même action.