{"_id":"@aphrody/wonderbot","_rev":"4-a37f6030ccc857acbf8ef80eea2b037b","name":"@aphrody/wonderbot","dist-tags":{"latest":"0.2.2"},"versions":{"0.1.0":{"name":"@aphrody/wonderbot","version":"0.1.0","keywords":["inazuma-eleven","discord","bot","ietv","streaming"],"license":"Apache-2.0","_id":"@aphrody/wonderbot@0.1.0","maintainers":[{"name":"yohan971","email":"yohanpierre@live.fr"}],"dist":{"shasum":"ddff398085bc73ccf79903f9b199f14879bb3e37","tarball":"https://registry.npmjs.org/@aphrody/wonderbot/-/wonderbot-0.1.0.tgz","fileCount":17,"integrity":"sha512-DIClRcBymEFfRLlJi60UkjnwovqDz0VEDqdLui8Y5j8apVYgHVzLPhn/KNxfdy2yf7BqEBPgFG0r4pYnw4l3lA==","signatures":[{"sig":"MEQCIFK7xAKbAXIWyhgPJCvgYWpB3FSzpra+U417tM340iruAiApb7Slo6tbaNtg79FEIROcYZO/3wNApbzWDoTXB+bNdw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":163333},"main":"./src/index.ts","type":"module","types":"./src/index.ts","shasum":"ddff398085bc73ccf79903f9b199f14879bb3e37","exports":{".":"./src/index.ts","./ui":"./src/ui/index.ts","./config":"./src/config.ts","./annonces":"./src/annonces.ts","./catalogue":"./src/catalogue.ts","./planificateur":"./src/planificateur.ts"},"scripts":{"test":"bun test","typecheck":"tsc --noEmit"},"_npmUser":{"name":"yohan971","email":"yohanpierre@live.fr"},"_integrity":"sha512-DIClRcBymEFfRLlJi60UkjnwovqDz0VEDqdLui8Y5j8apVYgHVzLPhn/KNxfdy2yf7BqEBPgFG0r4pYnw4l3lA==","_npmVersion":"10.8.3","description":"Wonderbot — bot Discord d'un catalogue d'épisodes","directories":{},"_nodeVersion":"24.3.0","dependencies":{"discord.js":"^14.26.4","@aphrody/ietv":"0.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/wonderbot_0.1.0_1788311210055_0.015665357461569274","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@aphrody/wonderbot","version":"0.2.0","keywords":["inazuma-eleven","discord","bot","ietv","streaming"],"license":"Apache-2.0","_id":"@aphrody/wonderbot@0.2.0","maintainers":[{"name":"yohan971","email":"yohanpierre@live.fr"}],"dist":{"shasum":"70ea47d7ad89f866475c354ff94138c0d6e9c999","tarball":"https://registry.npmjs.org/@aphrody/wonderbot/-/wonderbot-0.2.0.tgz","fileCount":15,"integrity":"sha512-KMAmGrIBgatq969FctQzrE14dq+CKs7rfOEs0EBEqMIseiGsdTT6LB8RMjuNkwzT0y54lBdveg59kXY7nzIVwQ==","signatures":[{"sig":"MEYCIQD5K4pVyBLSQb3wnl8HRcJWvVL+nyMTO3W6KTe5oi2k/wIhAIe4lI8eN817jE5kSh4IXKdsy16slZklyA1qaX75txJS","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":115190},"main":"./src/index.ts","type":"module","types":"./src/index.ts","shasum":"70ea47d7ad89f866475c354ff94138c0d6e9c999","engines":{"bun":">=1.3.14"},"exports":{".":"./src/index.ts","./ui":"./src/ui/index.ts","./config":"./src/config.ts","./annonces":"./src/annonces.ts","./catalogue":"./src/catalogue.ts","./planificateur":"./src/planificateur.ts"},"scripts":{"test":"bun test","typecheck":"tsc --noEmit"},"_npmUser":{"name":"yohan971","email":"yohanpierre@live.fr"},"_integrity":"sha512-KMAmGrIBgatq969FctQzrE14dq+CKs7rfOEs0EBEqMIseiGsdTT6LB8RMjuNkwzT0y54lBdveg59kXY7nzIVwQ==","_npmVersion":"10.8.3","description":"Wonderbot — bot Discord d'un catalogue d'épisodes","directories":{},"_nodeVersion":"24.3.0","dependencies":{"discord.js":"^14.26.4","@aphrody/ietv":"0.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/wonderbot_0.2.0_1788431548087_0.964370765075133","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@aphrody/wonderbot","version":"0.2.1","keywords":["inazuma-eleven","discord","bot","ietv","streaming"],"license":"Apache-2.0","_id":"@aphrody/wonderbot@0.2.1","maintainers":[{"name":"yohan971","email":"yohanpierre@live.fr"}],"dist":{"shasum":"beb5cc55c3b5b20fa214b96729949089110e8066","tarball":"https://registry.npmjs.org/@aphrody/wonderbot/-/wonderbot-0.2.1.tgz","fileCount":15,"integrity":"sha512-Bzm5NDDIimajdkONkRDDVPZSV8NsyR3KuxLrb3HRTTDxMgZNVVNz+Ez9W8EOea7eKwVQiwuzWo6Gjn9JMn6xxw==","signatures":[{"sig":"MEUCIQDs1SbgF5Xr/zjxmbAhinWht3QGWcPkQjJamRhxuxMz+wIgUcQKu6lejo+FBFDmJkcXMaZG91wB6AQvw+F1PCLSapY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":115190},"main":"./src/index.ts","type":"module","types":"./src/index.ts","shasum":"beb5cc55c3b5b20fa214b96729949089110e8066","engines":{"bun":">=1.3.14"},"exports":{".":"./src/index.ts","./ui":"./src/ui/index.ts","./config":"./src/config.ts","./annonces":"./src/annonces.ts","./catalogue":"./src/catalogue.ts","./planificateur":"./src/planificateur.ts"},"scripts":{"test":"bun test","typecheck":"tsc --noEmit"},"_npmUser":{"name":"yohan971","email":"yohanpierre@live.fr"},"_integrity":"sha512-Bzm5NDDIimajdkONkRDDVPZSV8NsyR3KuxLrb3HRTTDxMgZNVVNz+Ez9W8EOea7eKwVQiwuzWo6Gjn9JMn6xxw==","_npmVersion":"10.8.3","description":"Wonderbot — bot Discord d'un catalogue d'épisodes","directories":{},"_nodeVersion":"24.3.0","dependencies":{"discord.js":"^14.26.4","@aphrody/ietv":"0.2.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/wonderbot_0.2.1_1788681504651_0.9065838225362497","host":"s3://npm-registry-packages-npm-production"}},"0.2.2":{"name":"@aphrody/wonderbot","version":"0.2.2","publishConfig":{"access":"public"},"description":"Wonderbot — bot Discord d'un catalogue d'épisodes","type":"module","main":"./src/index.ts","types":"./src/index.ts","exports":{".":"./src/index.ts","./config":"./src/config.ts","./catalogue":"./src/catalogue.ts","./annonces":"./src/annonces.ts","./episode-navigation":"./src/episode-navigation.ts","./planificateur":"./src/planificateur.ts","./ui":"./src/ui/index.ts"},"scripts":{"typecheck":"bunx tsc --noEmit","test":"bun test"},"keywords":["inazuma-eleven","discord","bot","ietv","streaming"],"dependencies":{"@aphrody/ietv-client":"workspace:*","@aphrody/ietv":"workspace:*","discord.js":"catalog:"},"peerDependencies":{"@napi-rs/canvas":"catalog:","@discordjs/voice":"catalog:","libsodium-wrappers":"catalog:"},"peerDependenciesMeta":{"@napi-rs/canvas":{"optional":true},"@discordjs/voice":{"optional":true},"libsodium-wrappers":{"optional":true}},"devDependencies":{"@napi-rs/canvas":"catalog:","@discordjs/voice":"catalog:","libsodium-wrappers":"catalog:","typescript":"catalog:","@types/bun":"catalog:"},"license":"Apache-2.0","gitHead":"27f4b6e928b4b99143784f90d3bd1313a6fc6403","_id":"@aphrody/wonderbot@0.2.2","_nodeVersion":"24.3.0","_npmVersion":"9.2.0","dist":{"integrity":"sha512-hiYs6xPn2b5EfzfWGcRBYjL2SiaAprF51AVY3+MBTP8wkv9e/7anILwNadAzLUmOY+4nXdLu+PqekVk21W8Chg==","shasum":"aa80c075a5f2454661dddb3a246a036ff4cd34d4","tarball":"https://registry.npmjs.org/@aphrody/wonderbot/-/wonderbot-0.2.2.tgz","fileCount":30,"unpackedSize":379729,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQC13+/49NocZZcUgrjLY0bk/UloBVMheh11edCG6gs5EgIgd0ZnsThe5ZWI8Pc5b0hu2T+AEKbYAkV6lwVUGDV8ChU="}]},"_npmUser":{"name":"yohan971","email":"yohanpierre@live.fr"},"directories":{},"maintainers":[{"name":"yohan971","email":"yohanpierre@live.fr"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/wonderbot_0.2.2_1789022238682_0.9787995445167148"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-02T01:06:49.784Z","modified":"2026-09-10T06:37:19.033Z","0.1.0":"2026-09-02T01:06:50.190Z","0.2.0":"2026-09-03T10:32:28.227Z","0.2.1":"2026-09-06T07:58:24.782Z","0.2.2":"2026-09-10T06:37:18.871Z"},"license":"Apache-2.0","keywords":["inazuma-eleven","discord","bot","ietv","streaming"],"description":"Wonderbot — bot Discord d'un catalogue d'épisodes","maintainers":[{"name":"yohan971","email":"yohanpierre@live.fr"}],"readme":"# @aphrody/wonderbot\n\n**Wonderbot** — le bot Discord d'un catalogue d'épisodes.\n\nIl sert une seule racine de commandes, `/episodes`, adossée au cache SQLite de\n[`@aphrody/ietv`](../ietv) qu'il rafraîchit lui-même. Aucun serveur HTTP\nintermédiaire, aucun démon cron séparé : un processus, une base.\n\n> **Surfaces publiques.** Le nom de la commande, ses descriptions, le pied de\n> page des embeds et les noms de salons sont vus par chaque membre : aucun n'y\n> emploie de nom déposé. La racine s'appelait `/ietv`, elle a été renommée pour\n> cette raison, et `MARQUE_PAR_DEFAUT.piedDePage` ne nomme plus de produit. Les\n> identifiants internes (paquet, fichiers, variables) ne sont pas concernés.\n\n```bash\nbxc wonderbot doctor     # vérifie la configuration et le catalogue, sans Discord\nbxc wonderbot refresh    # amorce/rafraîchit le catalogue, sans Discord\nbxc wonderbot register   # publie les slash commands puis sort\nbxc wonderbot start      # passerelle + rafraîchissement périodique + annonces\n```\n\n## Commandes\n\n| Commande | Ce qu'elle fait |\n| --- | --- |\n| `/episodes recherche texte:<mot> [langue] [limite]` | Cherche dans les titres, VF et VOSTFR confondues |\n| `/episodes episode saison:<n> numero:<n> [langue]` | Toutes les versions d'un épisode — un champ par source et par langue |\n| `/episodes saison numero:<n> [langue]` | Les épisodes d'une saison, dans l'ordre |\n| `/episodes catalogue` | Volumes, sources, répartition VF/VOSTFR, fraîcheur |\n| `/episodes rafraichir` | Rescrape les sept sources. Réservé aux **administrateurs du serveur** et aux rôles de `WONDERBOT_STAFF_ROLE_IDS`, réponse éphémère |\n\nLes quatre premières répondent en millisecondes : elles lisent le cache, jamais\nYouTube. C'est aussi ce qui évite qu'un serveur de deux mille membres déclenche\ndeux mille scrapings.\n\n## Le forum comme catalogue\n\nQuand `WONDERBOT_FORUM_CHANNEL_ID` désigne un salon **forum**, le bot y tient\n**un fil par saison** : le message d'ouverture liste les épisodes, un lien par\nlangue, et le fil reste ouvert aux discussions des membres.\n\n- **Un fil par saison, pas par épisode** : douze cents fils seraient illisibles,\n  et Discord archive les plus anciens.\n- **Le bot modifie, il ne republie pas.** Le message d'ouverture porte\n  l'identifiant du fil et se modifie ; republier à chaque rafraîchissement\n  noierait les réponses des membres sous des listes identiques.\n- **Le fil est retrouvé par identifiant**, mémorisé dans le cache : un\n  renommage ne casse rien, et un fil supprimé à la main est recréé.\n- **Rien n'est jamais supprimé** : une saison qui disparaît du catalogue est\n  plus souvent un scraping raté qu'une saison retirée.\n- Les étiquettes `VF` / `VOSTFR` sont posées d'après les langues réellement\n  présentes ; une étiquette absente du forum est ignorée plutôt que fatale.\n\nUne saison complète (51 épisodes × 2 langues) **ne tient pas** dans une seule\ndescription d'embed. D'où un format volontairement compact — pas de titre\nd'épisode, liens raccourcis en `youtu.be` — et un découpage en plusieurs embeds\nsous le plafond de 6 000 caractères que Discord applique à l'ensemble d'un\nmessage. Au-delà, le fil renvoie vers `/episodes saison`.\n\nCôté salon : refuser `CREATE_PUBLIC_THREADS` à `@everyone` — la liste des fils\n**est** la liste des saisons, laisser ouvrir un fil quelconque la transformerait\nen salon de discussion.\n\n## Annonces des nouveautés\n\nAprès chaque rafraîchissement, les épisodes absents du passage précédent sont\npubliés dans `WONDERBOT_ANNOUNCE_CHANNEL_ID`.\n\n**Le premier passage n'annonce rien.** Un bot fraîchement installé voit tout le\ncatalogue comme « nouveau » ; il amorce donc son journal en silence, et la\npremière annonce portera sur un épisode paru *après* l'installation.\n\nLe journal mémorise des **identifiants**, pas une date : une source qui remet en\nligne un épisode ancien (rattrapage de saison) serait manquée par un curseur\ntemporel. Il est élagué à chaque passage sur ce que le catalogue contient\nencore, il ne grossit donc pas indéfiniment.\n\n## Rafraîchissement automatique et réparation\n\n**Au démarrage**, le bot rafraîchit **si et seulement si** le catalogue est vide\nou plus vieux que l'intervalle. Le « si » est tout l'intérêt : rescraper à\nchaque `systemctl restart` coûterait plusieurs minutes de navigateur pour rien,\net un service qui redémarre en boucle martèlerait les sources. Ensuite, la\nboucle périodique prend le relais (6 h par défaut).\n\n**Quand un épisode manque au milieu d'une saison**, le bot retente.\n\n- Un **trou** est un numéro absent *entre* le premier et le dernier épisode\n  connus. Une saison qui s'arrête à E12 n'a pas de trou — elle est en cours de\n  diffusion. On ne cherche jamais au-delà du dernier épisode connu.\n- Chaque trou a un nombre **fixe** de tentatives (2 par défaut), espacées de\n  15 min. Sans cette borne, un catalogue durablement incomplet relancerait un\n  scraping toutes les quinze minutes, pour toujours.\n- Passé ce compte, le trou est **confirmé** : le bot cesse d'y revenir et\n  l'affiche dans le fil de la saison, sous « ⚠️ Introuvables ». Un épisode qu'on\n  sait manquant est une information ; la taire laisse croire la liste complète.\n- Un trou qui disparaît **récupère ses tentatives** : une source qui republie\n  son catalogue mérite qu'on retente.\n- Une seule réparation en vol à la fois.\n\n`WONDERBOT_AUTOFIX_ATTEMPTS=0` désactive la réparation,\n`WONDERBOT_REFRESH_ON_START=0` le rafraîchissement de démarrage.\n\n## Configuration\n\nLe premier nom trouvé gagne — les variantes historiques évitent de dupliquer un\n`.env` existant.\n\n| Variable | Rôle |\n| --- | --- |\n| `WONDERBOT_DISCORD_TOKEN` · `DISCORD_BOT_TOKEN` · `DISCORD_TOKEN` | Jeton du bot |\n| `WONDERBOT_APPLICATION_ID` · `DISCORD_APPLICATION_ID` · `DISCORD_CLIENT_ID` | Application ID |\n| `WONDERBOT_GUILD_ID` · `DISCORD_GUILD_ID` | Guilde(s) ; **vide ⇒ commandes globales** |\n| `WONDERBOT_COMMAND_SCOPE` | `guildes` (propagation immédiate) ou `globale` (quelques minutes, tout serveur qui invite) |\n| `WONDERBOT_ANNOUNCE_CHANNEL_ID` | Salon des nouveautés ; absent ⇒ aucune annonce |\n| `WONDERBOT_FORUM_CHANNEL_ID` | Salon forum tenant le catalogue (un fil par saison) ; absent ⇒ pas de forum |\n| `WONDERBOT_ANNOUNCE_ROLE_ID` | Rôle mentionné dans l'annonce |\n| `WONDERBOT_STAFF_ROLE_IDS` | Rôles autorisés à `/episodes rafraichir` **en plus** des administrateurs ; vide ⇒ administrateurs seuls |\n| `WONDERBOT_REFRESH_INTERVAL_MS` | Période, défaut 6 h, plancher 60 s |\n| `WONDERBOT_REFRESH_ON_START` | `0` pour ne pas rafraîchir au démarrage même si le catalogue est périmé |\n| `WONDERBOT_AUTOFIX_ATTEMPTS` | Tentatives par épisode manquant, défaut 2 ; `0` désactive |\n| `WONDERBOT_AUTOFIX_DELAY_MS` | Délai avant une tentative, défaut 15 min, plancher 60 s |\n| `WONDERBOT_ANNOUNCE_LIMIT` | Épisodes annoncés d'un coup, défaut 5 |\n| `IETV_CACHE_PATH` | Base SQLite ; défaut `~/.cache/ietv/episodes.db` |\n\nUne configuration incomplète fait **refuser le démarrage** avec le nom de la\nvariable à poser — plutôt qu'un « An invalid token was provided » qui ne dit ni\nquelle variable, ni où, ni pour quelle application. Deux valeurs présentes mais\ninutilisables sont refusées explicitement : un secret scellé (`eyJ2Ijo…`) et une\nréférence shell non substituée (`$AUTRE`), Bun ne faisant pas l'expansion dans un\n`.env`.\n\n## Permissions Discord\n\nL'URL d'invitation, avec `<APPLICATION_ID>` remplacé :\n\n```\nhttps://discord.com/oauth2/authorize\n  ?client_id=<APPLICATION_ID>\n  &scope=bot%20applications.commands\n  &permissions=19456\n```\n\n`scope=applications.commands` n'est pas optionnel : sans lui, le bot apparaît en\nligne et reste **strictement muet**.\n\n`permissions=19456` est la somme de trois bits, et de trois seulement :\n`ViewChannel` (1024) pour résoudre le salon d'annonces, `SendMessages` (2048)\npour y publier, `EmbedLinks` (16384) parce que l'annonce est un embed. Ni\nadministrateur, ni gestion de messages, ni mention de `@everyone` : le bot ne\nmodère rien et ne modifie personne.\n\n**Aucun intent privilégié.** Le client ne demande que `Guilds` : les rôles de\nl'appelant arrivent dans la charge utile de l'interaction, il n'y a rien à lire\ndans le cache des membres. `GuildMembers` demandé sans être coché dans le\nportail ferme la passerelle (code 4014) et fait boucler le service ;\n`MessageContent` serait un accès au contenu des messages dont le bot n'a aucun\nusage.\n\n## Architecture\n\n```\nsrc/\n├── config.ts          env → configuration validée (PUR)\n├── catalogue.ts       lecture + rafraîchissement du cache IETV (cache et scraper injectables)\n├── annonces.ts        journal des épisodes déjà annoncés (PUR + persistance)\n├── forum.ts           un fil par saison, tenu à jour (passerelle Discord injectable)\n├── lacunes.ts         détection des épisodes manquants + réparation bornée (PUR + persistance)\n├── planificateur.ts   boucle périodique (minuteurs injectables)\n├── commands/ietv.ts   les cinq sous-commandes → embeds (ne connaît PAS discord.js)\n├── ui/                charte : couleurs, icônes, budget des embeds, mise en forme\n└── bot.ts             SEUL module qui parle à discord.js\n```\n\nUn seul module touche la passerelle. Tout le reste — configuration, catalogue,\nannonces, planification, commandes, rendu — se teste avec des objets littéraux :\n`src/wonderbot.test.ts` couvre 63 cas sans jeton, sans réseau, sans SQLite et\nsans navigateur, minuteurs compris (horloge factice, aucune attente réelle).\n\n## Déploiement\n\n```bash\nsudo cp scripts/deploy/bxc-wonderbot.service /etc/systemd/system/\nsudo systemctl daemon-reload && sudo systemctl enable --now bxc-wonderbot\njournalctl -u bxc-wonderbot -f\n```\n\nL'unité durcit le service (`ProtectSystem=strict`, `ProtectHome=read-only`) et\nouvre en écriture le **seul** chemin nécessaire, `~/.cache/ietv` : SQLite en mode\nWAL écrit des fichiers voisins (`-wal`, `-shm`), une base lue seulement ne\nsuffit pas. `MemoryDenyWriteExecute` est proscrit — il casse le JIT de Bun.\n\nDeux codes de sortie sont traités à part : **130** (arrêt propre) est un succès,\nsinon `systemctl stop` laisserait l'unité en `failed` ; **77** (configuration\nrefusée) empêche le redémarrage, parce que relancer ne répare pas un jeton\nabsent.\n\n## Licence\n\nApache-2.0\n","readmeFilename":"README.md"}