Appearance
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 verschannel(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, ounulls'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.0si 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.0si 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), etkapp.stream.stream_started_at(pour l'uptime). Seul le nombre de viewers manquait vraiment, d'oùviewerCount().Scope (
infos.json→scopes) :followerCount()etsubscriberCount()exigent la permissionchannel-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()(nullsi 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'apptwitch-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, avecended_atetcooldown_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. Scopetwitch-ads:read.ads.snooze(): Promise<AdSnoozeResult>etads.start(30|60|90|120|150|180): Promise<AdStartResult>— actions sensibles, scopetwitch-ads:manage.ads.onBegin(cb)— début d'une coupure manuelle ou automatique, avec durée et requester typés.goals.getActive(): Promise<CreatorGoalsStatus>puisgoals.onBegin/onProgress/onEnd(cb)— Creator Goals. Scopetwitch-goals:read.charity.getCampaign(): Promise<CharityCampaignStatus>puischarity.onStart/onProgress/onStop/onDonate(cb)— campagne active et dons. Scopetwitch-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>etschedule.getNext(): Promise<TwitchScheduleSegment | null>— planning en lecture seule.bits.getLeaderboard(options?): Promise<BitsLeaderboardStatus>,bits.getCheermotes(): Promise<TwitchCheermote[]>,bits.getPowerUps(): Promise<BitsPowerUpsStatus>— scopetwitch-bits:readpour 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 })ouvideos.list({ game_id, ...filtres }):Promise<VideosPage>— exactement une source est requise.videos.get(id)renvoieTwitchVideo | null.getMarkers(options?): Promise<StreamMarkersStatus>— liste les marqueurs du dernier VOD ou d'une vidéo précise. Scopestream-markers:read.
Sécurité avancée (kapp.twitch.moderation)
checkMessage(text): Promise<AutoModCheckResult>— pré-valide un message via AutoMod. Scopemoderation:check; appel limité, ne pas l'utiliser sur chaque frappe.getShieldMode(): Promise<ShieldModeStatus>— scopeshield-mode:read.setShieldMode(enabled): Promise<ShieldModeStatus>— scope sensibleshield-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, lisevent.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'untry/catch.- Après
clip(), Twitch met quelques secondes à rendre le clip disponible :getClip(id)retournenullpendant ce délai (Twitch dit de considérer l'échec passé 60 s). N'écris pas la boucle de polling toi-même →clip({ wait: true })le fait et te rend le clip complet. En cas de dépassement, rejet avec une erreurname === 'ClipNotReadyError'portantclip_idetedit_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 deurl/thumbnail_url. Pour juste déclencher un clip,clip()suffit.titleetdurationse passent à la création — plus besoin de l'edit_urlpour ça. Letitleest 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 →RangeErrorlevé immédiatement par le SDK, sans appel réseau.has_delayest 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_offsetvautnulletvideo_idest 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 declip({ 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 avecstarted_at/ended_at— trier par date n'est pas possible.getClips({ started_at })sansended_at= fenêtre de 7 jours à partir destarted_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êtresstarted_at/ended_atau lieu de continuer à suivre lecursor. - Si tu lances un raid avec décompte, pense à
unraid()dans un hook de sortie (onBeforeAbortwidget /onBeforeFinishappli) pour les annulations. channel/login: pseudo Twitch en minuscules, sans@.onSub≠onResub≠onSubGift: trois events Twitch distincts. Pour réagir à tout abo, enregistre les trois.event.tierest la valeur brute Twitch ("1000"/"2000"/"3000") → utiliseevent.getTier()pour l'échelon1|2|3.onSubGiftdécrit le donneur et le nombre de dons (total), pas chaque bénéficiaire.getGifter()=nullsi 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 eventsonFollow/onSubà un point de départ lu une fois, plutôt que de poller la méthode. followerCount()/subscriberCount()renvoient0(et non une erreur) tant que le streamer n'a pas reconnecté sa chaîne après l'ajout des permissions Twitch correspondantes — traite0comme « indisponible », pas forcément « zéro abonné ».viewerCount()renvoie0quand la chaîne est hors ligne (indistinct d'un live à 0 viewer). Pour savoir si la chaîne est en live, testekapp.stream.stream_id(non-nullsi live) ou écoutekapp.stream(STREAM_ONLINE/STREAM_OFFLINE).- Ne re-sers pas titre/jeu/uptime via un compteur :
kapp.twitch.infos(titre/catégorie) etkapp.stream.stream_started_atsont 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).