Skip to content

kapp.twitch

Piloter des actions Twitch côté chaîne : raid, clip, sondage, prédiction, marqueur, infos de stream — lire les compteurs de la chaîne et suivre le Hype Train.

Service partagé appli + widget : API identique.

Quand l'utiliser (vs alternative)

  • Actions « broadcaster » : lancer un raid, créer un clip, ouvrir un sondage/prédiction natif Twitch.
  • Compteurs de la chaîne (nb de followers/subs/viewers) : followerCount(), subscriberCount(), viewerCount(). Récupérés sur appel, jamais au boot. (Titre/jeu = kapp.twitch.infos ; heure de début du live = kapp.stream.stream_started_at.)
  • Pour parler dans le chat, c'est kapp.chat ; pour les infos d'un viewer, kapp.users.
  • Pour des sondages/prédictions natifs Kappapps (multi-canal, mises en monnaie interne), voir polls.

Méthodes

Actions (broadcaster)

  • raid(channel: string): Promise<void> — démarre un raid vers channel (login). async.
  • unraid(): Promise<void> — annule le raid en cours. async.
  • clip({ title?, duration? }): Promise<ClipCreateResponse> — crée un clip et rend l'accusé de réception (id + edit_url). title = titre du clip (sinon Twitch en génère un), duration = 5 à 60 s (défaut 30). Nécessite que la chaîne soit en live, sinon rejet. async.
  • clip({ wait: true, title?, duration?, timeout? }): Promise<ReadyClip> — même chose, mais attend que Twitch ait publié le clip et rend le clip complet (ClipData + edit_url) : url, embed_url, thumbnail_url, duration… Compte quelques secondes. async.
  • getClip(clip_id: string): Promise<ClipData | null> — données complètes d'un clip, ou null s'il n'existe pas (encore). async.
  • getClips({ channel } | { game_id }, filtres?): Promise<ClipsPage> — page de clips d'une chaîne (login) ou d'une catégorie. Filtres : started_at, ended_at, is_featured, first (1-100), after, before. Rend { clips, cursor }. async.
  • marker(description: string): Promise<void> — pose un marqueur sur le VOD. async.
  • setInfos({ title?, category? }): Promise<void> — change titre / catégorie du stream. async.
  • poll(data): Promise<TwitchPoll> — crée un sondage natif Twitch. async.
  • prediction(data): Promise<TwitchPrediction> — crée une prédiction native Twitch. async.
  • channel(login: string): Promise<TwitchChannelInformations> — infos d'une chaîne. async.
  • game(name, method = 'name'): Promise<TwitchGameData> — recherche un jeu. async.

Compteurs de la chaîne (lecture, à la demande)

Chiffres de la chaîne courante, récupérés sur appel (jamais poussés au boot). Valeurs cachées ~30-60 s côté serveur (pas du temps réel strict).

  • followerCount(): Promise<number> — nombre total de followers. 0 si illisible (streamer pas reconnecté depuis l'ajout du scope). async.
  • subscriberCount(): Promise<{ count: number, points: number }> — nombre d'abonnés (count) + total de sub points (points). { count: 0, points: 0 } si illisible. async.
  • viewerCount(): Promise<number> — nombre de viewers en direct. 0 si hors ligne. async.

Titre / jeu / uptime : pas ici. Déjà dispo sans appel réseau et en temps réel : kapp.twitch.infos.title + kapp.twitch.infos.category_name / category_id (le jeu), et kapp.stream.stream_started_at (pour l'uptime). Seul le nombre de viewers manquait vraiment, d'où viewerCount().

Scope (infos.jsonscopes) : followerCount() et subscriberCount() exigent la permission channel-stats:read. viewerCount() n'exige aucun scope (le nombre de viewers est public).

Events d'alerte temps réel (écoute)

Enregistre un callback appelé à chaque occurrence de l'event, tant que l'app/widget tourne. Chaque callback reçoit un model typé (props brutes + helpers). sync (l'enregistrement ; le callback, lui, peut être async).

  • onFollow(cb: (event: FollowEvent) => void) — nouveau follow. event.user_name, event.getUser().
  • onSub(cb: (event: SubEvent) => void)nouvel abo (hors gift/resub). event.getTier() (1|2|3), event.getTierRaw() ("1000"…).
  • onResub(cb: (event: ResubEvent) => void) — resub avec message partagé. event.getCumulativeMonths(), event.getStreakMonths(), event.getMessage().
  • onSubGift(cb: (event: SubGiftEvent) => void) — don d'abo(s). event.total, event.isAnonymous(), event.getGifter() (null si anonyme).
  • onRaid(cb: (event: RaidEvent) => void) — raid entrant. event.from_broadcaster_user_name, event.viewers, event.getRaider().

Hype Train (état + temps réel)

  • hypeTrain.getStatus(): Promise<HypeTrainStatus> — état initial, train actif (current) et records. Cache serveur ~45 s. Exige le scope d'app twitch-hype-train:read.
  • hypeTrain.onBegin(cb) — début d'un train.
  • hypeTrain.onProgress(cb) — progression (niveau, total, objectif, top contributeurs, Shared Hype Train).
  • hypeTrain.onEnd(cb) — fin, avec ended_at et cooldown_ends_at.

Le résultat vaut { available: false, reason: 'missing_twitch_scope', ... } si le streamer n'a pas encore accordé channel:read:hype_train. Hydrate une fois avec getStatus(), puis utilise les événements : ne poll pas Helix depuis le widget. Twitch peut livrer progress avant begin, donc le handler onProgress doit aussi savoir initialiser son affichage.

Pubs, Goals et Charity (état + temps réel)

  • ads.getSchedule(): Promise<AdScheduleStatus> — prochain/dernier ad, snoozes et temps sans preroll. Scope twitch-ads:read.
  • ads.snooze(): Promise<AdSnoozeResult> et ads.start(30|60|90|120|150|180): Promise<AdStartResult> — actions sensibles, scope twitch-ads:manage.
  • ads.onBegin(cb) — début d'une coupure manuelle ou automatique, avec durée et requester typés.
  • goals.getActive(): Promise<CreatorGoalsStatus> puis goals.onBegin/onProgress/onEnd(cb) — Creator Goals. Scope twitch-goals:read.
  • charity.getCampaign(): Promise<CharityCampaignStatus> puis charity.onStart/onProgress/onStop/onDonate(cb) — campagne active et dons. Scope twitch-charity:read.

Comme pour le Hype Train, hydrate une fois par get*(), puis maintiens l'affichage par EventSub. Les réponses d'état portent available et éventuellement reason: 'missing_twitch_scope'; ne transforme pas une indisponibilité en faux zéro.

Planning, Bits, Shared Chat, vidéos et marqueurs (lecture)

  • schedule.get(options?): Promise<TwitchSchedule> et schedule.getNext(): Promise<TwitchScheduleSegment | null> — planning en lecture seule.
  • bits.getLeaderboard(options?): Promise<BitsLeaderboardStatus>, bits.getCheermotes(): Promise<TwitchCheermote[]>, bits.getPowerUps(): Promise<BitsPowerUpsStatus> — scope twitch-bits:read pour leaderboard/Power-ups; les Cheermotes réutilisent le cache serveur partagé.
  • sharedChat.getSession(): Promise<SharedChatSession | null> — session active et participants. Les messages bot Kappapps restent limités à la chaîne source; les annonces broadcaster se propagent à la session.
  • videos.list({ channel, ...filtres }) ou videos.list({ game_id, ...filtres }): Promise<VideosPage> — exactement une source est requise. videos.get(id) renvoie TwitchVideo | null.
  • getMarkers(options?): Promise<StreamMarkersStatus> — liste les marqueurs du dernier VOD ou d'une vidéo précise. Scope stream-markers:read.

Sécurité avancée (kapp.twitch.moderation)

  • checkMessage(text): Promise<AutoModCheckResult> — pré-valide un message via AutoMod. Scope moderation:check; appel limité, ne pas l'utiliser sur chaque frappe.
  • getShieldMode(): Promise<ShieldModeStatus> — scope shield-mode:read.
  • setShieldMode(enabled): Promise<ShieldModeStatus> — scope sensible shield-mode:manage, avec rate limit et audit backend structurés.

Ces opérations portent sur la chaîne Twitch dans son ensemble et vivent donc sous kapp.twitch.moderation. kapp.users.moderation reste réservé aux actions visant un viewer précis (ban, timeout, VIP, modérateur, blacklist).

Écoute (k.twitch.onX) vs trigger de lancement (ONSUB/ONFOLLOW/ONRAID) — deux mécanismes distincts :

  • Trigger = le widget est lancé par l'event (one-shot), données dans kapp.triggerRequest.setter. Pour « afficher une alerte quand quelqu'un sub ».
  • onX = une app/widget qui tourne déjà réagit à l'event (continu). Pour un jeu/overlay persistant qui doit réagir aux subs/raids sans être relancé.

getUser()/getGifter()/getRaider() sont async (lookup du profil Kappapps : currency, rôle…). Pour juste afficher un pseudo, lis event.user_name (synchrone).

Exemple (V2, exécutable)

js
// Action broadcaster : lancer un raid
const target = 'gamesdone';
try {
    await kapp.twitch.raid(target.trim().toLowerCase().replace(/^@/, ''));
} catch (err) {
    console.error('Raid impossible : ' + (err?.message || err));
}

// Compteurs de la chaîne, à la demande
const followers = await kapp.twitch.followerCount();
const subs = await kapp.twitch.subscriberCount();
const viewers = await kapp.twitch.viewerCount();
kapp.chat.say(`${followers} followers · ${subs.count} abos (${subs.points} pts) · ${viewers} viewers`);

// Titre / jeu / uptime : pas d'appel réseau, déjà en mémoire et en temps réel
console.log(`${kapp.twitch.infos.title} — ${kapp.twitch.infos.category_name}`);
if (kapp.stream.stream_started_at) {
    const uptime = Math.floor(Date.now() / 1000 - kapp.stream.stream_started_at);
    console.log(`En live depuis ${uptime}s`);
}

// Hype Train : hydratation initiale, puis mises à jour EventSub
const hype = await kapp.twitch.hypeTrain.getStatus();
if (hype.available && hype.current) {
    console.log(`Hype Train niveau ${hype.current.level}: ${hype.current.progress}/${hype.current.goal}`);
}
kapp.twitch.hypeTrain.onProgress((train) => {
    console.log(`Hype Train niveau ${train.level}: ${train.progress}/${train.goal}`);
});

// Écoute d'alertes : une app qui tourne réagit aux subs & raids
kapp.twitch.onSub((event) => {
    kapp.chat.say(`Bienvenue ${event.user_name} — abo tier ${event.getTier()} ! 🎉`);
});
kapp.twitch.onResub((event) => {
    kapp.chat.say(`${event.user_name} resub (${event.getCumulativeMonths()} mois) : ${event.getMessage()}`);
});
kapp.twitch.onSubGift((event) => {
    const who = event.isAnonymous() ? 'Un viewer anonyme' : event.user_name;
    kapp.chat.say(`${who} a offert ${event.total} abo(s) ! 🎁`);
});
kapp.twitch.onRaid((event) => {
    kapp.chat.say(`Raid de ${event.from_broadcaster_user_name} (+${event.viewers}) ! 🚀`);
});

Pièges

  • raid() peut throw (chaîne hors-ligne, droits…) → entoure d'un try/catch.
  • Après clip(), Twitch met quelques secondes à rendre le clip disponible : getClip(id) retourne null pendant ce délai (Twitch dit de considérer l'échec passé 60 s). N'écris pas la boucle de polling toi-mêmeclip({ wait: true }) le fait et te rend le clip complet. En cas de dépassement, rejet avec une erreur name === 'ClipNotReadyError' portant clip_id et edit_url (le clip existe, c'est sa publication qui traîne).
  • clip({ wait: true }) coûte quelques appels API de plus : réserve-le aux cas où tu as besoin de url / thumbnail_url. Pour juste déclencher un clip, clip() suffit.
  • title et duration se passent à la création — plus besoin de l'edit_url pour ça. Le title est soumis à AutoMod : un titre refusé fait échouer tout l'appel (400), et c'est imprévisible côté appli. Twitch n'impose aucune limite de longueur sur ce titre (les 140 caractères, c'est le titre du stream, pas du clip).
  • duration = 5 à 60 s inclus, intervalle continu (pas une liste de valeurs comme les pubs), arrondi à 0,1 s. Hors bornes → RangeError levé immédiatement par le SDK, sans appel réseau.
  • has_delay est mort : Twitch a retiré le paramètre (il n'avait aucun effet). Il reste accepté pour ne rien casser, mais n'est plus transmis — ne l'utilise pas dans du nouveau code.
  • vod_offset vaut null et video_id est une chaîne vide sur un clip pris pendant le live en cours (la VOD n'est pas encore découpée) — c'est le cas normal de clip({ wait: true }), ne fais pas de calcul dessus sans tester.
  • clip() capture ~90 s autour de l'appel (~85 s avant, ~5 s après) et publie les 30 dernières secondes par défaut. L'edit_url (re-découper l'extrait, mettre en avant le clip) est valide 24 h ou jusqu'à publication.
  • clip() exige aussi que les clips soient activés dans les réglages du créateur ; les restrictions follower-only / sub-only s'appliquent. Le message d'erreur remonté vient de Twitch.
  • getClips() est trié par nombre de vues décroissant, PAS du plus récent au plus ancien. Pour « les derniers clips », filtre avec started_at / ended_at — trier par date n'est pas possible.
  • getClips({ started_at }) sans ended_at = fenêtre de 7 jours à partir de started_at (défaut Twitch), pas « depuis cette date jusqu'à maintenant ».
  • La pagination de getClips() plafonne à ~1000 clips cumulés : au-delà, découpe en plusieurs fenêtres started_at/ended_at au lieu de continuer à suivre le cursor.
  • Si tu lances un raid avec décompte, pense à unraid() dans un hook de sortie (onBeforeAbort widget / onBeforeFinish appli) pour les annulations.
  • channel/login : pseudo Twitch en minuscules, sans @.
  • onSubonResubonSubGift : trois events Twitch distincts. Pour réagir à tout abo, enregistre les trois.
  • event.tier est la valeur brute Twitch ("1000"/"2000"/"3000") → utilise event.getTier() pour l'échelon 1|2|3.
  • onSubGift décrit le donneur et le nombre de dons (total), pas chaque bénéficiaire. getGifter() = null si le don est anonyme.
  • Le cheer/bits n'a pas d'event dédié ici : il arrive via le chat → chatEvent.isCheer() / getCheer() (voir chat.md).
  • Compteurs (followerCount/subscriberCount/viewerCount) ≠ temps réel : valeurs cachées ~30-60 s. Pour un décompte exact à la seconde (barre de progression d'un goal…), additionne les events onFollow/onSub à un point de départ lu une fois, plutôt que de poller la méthode.
  • followerCount()/subscriberCount() renvoient 0 (et non une erreur) tant que le streamer n'a pas reconnecté sa chaîne après l'ajout des permissions Twitch correspondantes — traite 0 comme « indisponible », pas forcément « zéro abonné ».
  • viewerCount() renvoie 0 quand la chaîne est hors ligne (indistinct d'un live à 0 viewer). Pour savoir si la chaîne est en live, teste kapp.stream.stream_id (non-null si live) ou écoute kapp.stream (STREAM_ONLINE/STREAM_OFFLINE).
  • Ne re-sers pas titre/jeu/uptime via un compteur : kapp.twitch.infos (titre/catégorie) et kapp.stream.stream_started_at sont en temps réel et gratuits (mis à jour par event), là où un appel serait câché ~30 s.
  • Ces callbacks servent une app/widget qui tourne. Pour lancer un widget au moment d'un sub/raid/follow, utilise plutôt un trigger ONSUB/ONRAID/ONFOLLOW (voir triggers.md).