Skip to content

Lancer une autre appli ou un widget et attendre sa réponse (kapp.apps)

Une appli (ou un widget) peut en lancer une autre cible pour une action ponctuelle, puis récupérer la data que celle-ci renvoie quand elle se termine. Deux types de cible :

  • une appli oneshot installée, désignée par son slugkapp.apps.runApp(slug) ;
  • un widget du channel, désigné par son id numérique → kapp.apps.runWidget(id).

Le tout via le service kapp.apps.

À ne pas confondre avec kapp.services.call() (services.md) : call appelle une fonction d'une appli déjà lancée ; les lanceurs de kapp.apps lancent une cible dédiée, la laissent vivre (elle peut afficher une UI, faire interagir les viewers…) et résolvent avec la data renvoyée à son finish(result).

Le lanceur générique — kapp.apps.run(target)

run prend une cible discriminée et dispatche tout seul vers runApp/runWidget :

ts
const r = await kapp.apps.run(target, args?, options?);
// target = { type: 'app', slug: 'color-picker' }
//        | { type: 'widget', id: 23 }
// r: { resolved: boolean, data?: any, reason?: string }

C'est exactement la forme produite par l'input de préférence runTarget : le streamer choisit la cible (appli ou widget) dans les réglages de ton appli, et tu passes l'objet tel quel à runsans brancher toi-même sur le type.

ts
// la cible vient des préférences de ton appli, pas codée en dur
const cible = kapp.preferences.get('cible');   // { type: 'app', slug } | { type: 'widget', id } | null
if (cible) {
    const r = await kapp.apps.run(cible, { mise: 100 });
    if (r.resolved) console.log(r.data);
}
  • target mal formé (type inconnu, slug/id manquant) → { resolved: false, reason: 'invalid_target' }.
  • Un id de widget arrivant en string (valeurs de form/préférence) est converti automatiquement.
  • Pour plusieurs cibles (préférence de type collection), boucle : for (const t of cibles) await kapp.apps.run(t, args).

L'input de préférence runTarget

Déclare un item de type runTarget dans preferences.config.js : le dashboard streamer affiche un picker mixant les applis oneshot installées sur le channel et les widgets du channel, et la valeur lue côté appli est déjà une cible prête pour run.

js
// preferences.config.js
cible: {
    type: 'runTarget',
    label: { en: 'Target to run', fr: 'Cible à lancer' },
},
cibleApp: {                              // restreint aux applis oneshot
    type: 'runTarget',
    label: { en: 'App to run', fr: 'Appli à lancer' },
    wuOptions: { targets: 'app' },
},
ciblesBonus: {                           // plusieurs cibles → collection
    type: 'collection',
    label: { en: 'Bonus targets', fr: 'Cibles bonus' },
    collectionItems: { type: 'runTarget' },
},
  • Valeur hydratée : { type: 'app', slug: '...' } | { type: 'widget', id: 12 } | null si le streamer n'a rien choisi — gère toujours ce cas.
  • wuOptions.targets: 'app' | 'widget' restreint le picker à un seul type de cible (sinon les deux, séparés en deux groupes).
  • default est interdit (l'id d'un widget est local au channel, non portable) et l'item n'est jamais required : prévois le comportement « pas de cible ».
  • En collection, la valeur est un tableau de cibles.
  • Un widget désactivé reste sélectionnable dans le picker (marqué comme tel) ; à l'exécution run renverra { resolved: false, reason: 'disabled' } tant qu'il est off.
  • kapp.preferences.set() accepte le même objet cible en data pour un chemin runTarget.

Les raccourcis typés — runApp / runWidget

Quand tu sais déjà quel type tu veux (cible codée en dur, découverte via list), utilise directement :

ts
const r = await kapp.apps.runApp(appliSlug, args?, options?);   // appli oneshot, par slug
const r = await kapp.apps.runWidget(widgetId, args?, options?); // widget, par id
ParamètreRôle
appliSlug (string)Slug de l'appli à lancer (identifiant public stable, ex. color-picker — pas un nombre). Elle doit être installée sur le channel et de type oneshot. Découvre-le via kapp.apps.listApps().
widgetId (number)Id numérique du widget, local au channel (pas portable d'un channel à l'autre). Découvre-le via kapp.apps.listWidgets() (ou laisse le streamer le choisir via un input de préférence runTarget).
args (objet)Arguments transmis à la cible. Ils passent par son schéma d'arguments déclaré (typage + validation required), puis sont dispo là-bas via kapp.arguments. Les clés non déclarées par la cible sont ignorées.
options.timeoutSecondsDélai max d'attente avant de résoudre {resolved:false, reason:'timeout'}. Défaut 120.

Le résultat — { resolved, data?, reason? }

Aucun lanceur ne lève d'exception : ils résolvent toujours un objet.

  • Succès : { resolved: true, data }data = ce que la cible a passé à kapp.finish(result).
  • Échec : { resolved: false, reason } avec reason ∈ :
    • communes : aborted (la cible a fait kapp.abort()), no_result (finish() sans data), timeout (pas terminée dans le délai), argument '…' is required / … must be of type … (validation des args), invalid_target (objet target malformé, run seulement).
    • apps (runApp) : busy (une instance tourne déjà — unicité garantie), not_installed / not_found (appli absente), not_oneshot (pas en launch-mode: oneshot), invalid_output (le result ne respecte pas le type de sortie déclaré, voir run-output).
    • widgets (runWidget) : not_found (id inconnu ou pas sur ce channel), disabled (widget désactivé). Pas de busy (concurrence autorisée), pas de invalid_output (sortie non typée).
ts
const r = await kapp.apps.runApp('color-picker', { question: 'Choisis une couleur' }, { timeoutSeconds: 60 });
if (r.resolved) {
    console.log('réponse du viewer :', r.data); // ex. { color: 'red' }
} else {
    console.log('pas de réponse :', r.reason);  // 'busy' | 'timeout' | 'aborted' | …
}

Côté cible — renvoyer la data avec finish

Une appli oneshot

Une appli ordinaire. Pour recevoir des args, elle doit les déclarer dans ses arguments d'app (config/trigger_arguments.config.json) — même schéma qu'un trigger configuré par le streamer. Les arguments déclarés arrivent typés (PlayerUser, int, bool, string, string_liststring[]) dans kapp.arguments. Elle fait son travail, puis renvoie sa data via finish :

ts
(async function () {
    const question = kapp.arguments.question;     // argument DÉCLARÉ (clé "question")
    kapp.display();
    const color = await askViewer(question);      // ta logique (UI, vote, interaction…)
    await kapp.finish({ color });                 // renvoie la data à l'appelant
})();

Un widget

Le widget doit avoir un script (script_enabled) qui appelle kapp.finish(result) :

js
window.executeWidgetFunction = async function () {
    const choix = await demandeAuViewer(kapp.arguments.question);
    kapp.finish({ choix });   // ← remonte à l'appelant comme { resolved: true, data }
};

Le widget s'affiche dans son propre overlay configuré (indépendant de l'appelant), comme tout déclenchement de ce widget.

Règles communes

  • await kapp.finish(result) : result (n'importe quelle valeur JSON) remonte comme { resolved: true, data: result }.
  • kapp.finish() sans argument{ resolved: false, reason: 'no_result' }.
  • kapp.abort('raison'){ resolved: false, reason: 'raison' }.
  • result n'a d'effet que si la cible a été lancée via kapp.apps (sinon il est ignoré).

Listes d'arguments — type string_list

string_list est un type d'argument de trigger comme les autres (catalogue dans ../../widgets/reference/anatomy.md) — utilisable pour n'importe quel trigger (commande chat, etc.), apps comme widgets. Pour recevoir plusieurs valeurs, déclare un argument string_list (= string[]). Sa coercion accepte deux sources :

  • lancé par une app/widget : passe un vrai tableau → kapp.apps.runApp('random-pick', { options: ['a','b','c'] }) (solide, pas de parsing).
  • chat / reward : la string est splittée par un séparateur (défaut ,, configurable via separator côté appli ; les widgets utilisent toujours ,). Ex. !pick a, b, c['a','b','c'].

Déclaration appli (config/trigger_arguments.config.js) :

js
module.exports = [
  { name: 'Options', key: 'options', type: 'string_list', separator: ',', required: true }
];

Déclarer le format de sortie — run-output (apps)

Dans infos.json, une appli peut déclarer le type de son result ; le backend le valide au finish (mismatch → {resolved:false, reason:'invalid_output'}). Types : string, string_list, int, bool, object (JSON libre, pas de contrainte), void.

json
{ "run-output": { "type": "string", "description": "La valeur choisie" } }

Les widgets n'ont pas de run-output : leur sortie n'est pas typée/validée.

Contrats standards — kind (apps)

Plutôt que déclarer args + output séparément, une appli peut adopter un kind (contrat clé-en-main, in + out prédéfinis) via "kind" dans infos.json. Le kind prime sur les déclarations fichier.

kindEntréeSortie
pick-stringoptions (string_list, requis)string

Appli pick-string (cible) :

jsonc
// infos.json
{ "launch-mode": "oneshot", "kind": "pick-string" }
js
// script.js
(async function () {
    const opts = kapp.arguments.options;            // string[] (fourni par le kind)
    await kapp.finish(opts[Math.floor(Math.random() * opts.length)]);
})();

Appelant :

ts
const r = await kapp.apps.runApp('random-pick', { options: ['rouge', 'vert', 'bleu'] });
// r = { resolved: true, data: 'vert' }

Découvrir les cibles lançables

Trois entrées de découverte :

ts
const all     = await kapp.apps.list();         // apps + widgets, chaque entrée taguée { type }
const apps    = await kapp.apps.listApps();     // applis oneshot seulement
const widgets = await kapp.apps.listWidgets();  // widgets seulement
  • listApps()[{ slug, title, version, kind, arguments: [{ name, key, type, required, separator }], output: { type, description } }, …] (schéma d'entrée et de sortie déclaré).
  • listWidgets()[{ id, name, enabled, arguments: [{ name, key, type, description, required }] }, …].
  • list() → la concaténation des deux, chaque entrée préfixée de type: 'app' | 'widget' (même forme que la cible de run). Filtre par type au besoin.
ts
// trouver une appli par contrat, puis la lancer
const apps = await kapp.apps.listApps();
const picker = apps.find(a => a.kind === 'pick-string');
if (picker) {
    const r = await kapp.apps.runApp(picker.slug, { options: ['a', 'b', 'c'] });
}

// vue unifiée
const all = await kapp.apps.list();
const cibles = all.filter(x => x.type === 'widget');

Pièges

  • L'appelant a besoin du scope apps:run (infos.jsonscopes) — pour tous les lanceurs et listes, apps comme widgets.
  • La cible doit déclarer ses arguments pour les recevoir : sans déclaration, run/runApp/runWidget(args) ne lui transmet rien.
  • Unicité des apps : si l'appli cible tourne déjà, runApp répond busy — gère ce cas. (Les widgets, eux, autorisent la concurrence.)
  • Id de widget non portable : ne le code pas en dur, découvre-le via listWidgets() ou laisse le streamer le choisir via un input de préférence runTarget.
  • Garde la cible bornée : elle doit appeler finish()/abort(), sinon l'appelant attend jusqu'au timeout.