// SPDX-FileCopyrightText: 2003-2026 Sébastien Helleu // // SPDX-License-Identifier: GPL-3.0-or-later = Relai API WeeChat :author: Sébastien Helleu :email: flashcode@flashtux.org :lang: fr include::includes/attributes-fr.adoc[] [[introduction]] == Introduction Ce document est une spécification du protocole relay _api_ : le protocole utilisé pour relayer les données de WeeChat aux clients avec une API REST HTTP. [[terminology]] === Terminologie Les termes suivants sont utilisés dans ce document : * _relay_ : il s'agit de l'extension "relay" de WeeChat, qui agit comme un "serveur" et autorise les _clients_ à se connecter * _client_ : il s'agit d'un logiciel connecté au _relay_ via une connexion réseau (WeeChat lui-même ou une interface distante). [[network_diagram]] === Diagramme réseau Les _clients_ sont connectés au _relay_ comme dans le diagramme ci-dessous : include::includes/relay.fr.adoc[tag=diagram] [NOTE] Tous les clients ici utilisent le protocole _api_ dans l'extension _relay_. + L'extension _relay_ autorise aussi les protocoles _irc_ et _weechat_ (non décrits dans ce document). [[protocol_generalities]] == Généralités sur le protocole * Les connexions du _client_ vers _relay_ sont faites avec des sockets TCP sur l'IP/port utilisé par _relay_ pour écouter les nouvelles connexions. * Le nombre de clients est limité par l'option _relay.network.max_clients_. * Chaque _client_ est indépendant des autres clients. * Le relai _api_ est une API REST HTTP utilisant le format JSON pour les entrées/sorties. * Les messages sont automatiquement compressés (deflate, gzip, zstd et permessage-deflate pour le protocole websocket). * WeeChat peut être utilisé comme client de ce relai. [[api_versioning]] === Version de l'API La version de l'API suit une https://semver.org/lang/fr/[version sémantique ^↗^^] « pragmatique », comme WeeChat, sur 3 chiffres `X.Y.Z`, où : * `X` est la version majeure * `Y` est la version mineure * `Z` est la version de patch. Exemple : la version `2.0.0` apporte des changements incompatibles avec la version `1.2.3`. La version de l'API est retournée par la ressource <>. [[api_schema]] === Schéma d'API Vous pouvez parcourir et tester l'API en ligne : https://weechat.org/api/[API relay WeeChat ^↗^^]. [[response_codes]] === Codes réponse Les codes réponse HTTP suivants peuvent être retournés au client : * `200 OK` : réponse OK avec un corps (JSON) * `204 No Content` : réponse OK sans corps * `400 Bad Request` : requête invalide reçue * `401 Unauthorized` : informations d'identification manquantes ou invalides * `403 Forbidden` : permissions insuffisantes * `404 Not Found` : ressource non trouvée * `500 Internal Server Error` : erreur interne du serveur * `503 Service Unavailable` : service non disponible Lors d'une connexion via le protocole websocket, un code de réponse supplémentaire est envoyé quand WeeChat pousse des données vers le client sur des évènements : * `0 Event` : évènement poussé au client, si la synchronisation est activée par la ressource <>. [[date_format]] === Format de date Le format de date est https://en.wikipedia.org/wiki/ISO_8601[ISO 8601 ^↗^^], en utilisant le fuseau horaire UTC (cela est donc différent de l'affichage par WeeChat qui utilise le fuseau horaire local). + Les dates sont retournées avec le maximum de précision : jusqu'aux microsecondes si cela est possible ou bien les millisecondes, ou juste secondes. Exemples : ---- 2023-12-05T19:46:03.847625Z 2023-12-05T19:46:03.847Z 2023-12-05T19:46:03Z ---- [[authentication]] == Authentification Le mot de passe doit être envoyé dans l'en-tête `Authorization` avec le schéma d'authentification `Basic` ou l'en-tête `Sec-WebSocket-Protocol` (voir les détails ci-dessous). Le mot de passe peut être envoyé en clair ou haché, avec l'un des formats suivants pour l'utilisateur et le mot de passe : * `plain:` * `hash:sha256::` * `hash:sha512::` * `hash:pbkdf2+sha256:::` * `hash:pbkdf2+sha512:::` Où : * `` est le mot de passe en clair * `` est l'horodatage courant sous forme d'entier (nombre de secondes depuis l'époque Unix) ; il est utilisé pour prévenir les attaques par rejeu * `` est le nombre d'itérations (pour l'algorithme PBKDF2 seulement) * `` est la valeur hachée de l'horodatage + mot de passe (en hexadécimal) [NOTE] Le nombre maximal de secondes autorisé avant et après l'heure reçue (lorsque le mot de passe est haché) est configurable avec l'option _relay.network.time_window_. Exemple : * l'horodatage courant est `1706431066` * le mot de passe est `secret_password` * l'algorithme de hachage est `sha256` * le résultat du "hash" est le SHA256 de la chaîne `1706431066secret_password` qui est en hexadécimal : `dfa1db3f6bb6445d18d9ec7427c10f6421274e3a4751e6c1ffc7dd28c94eadf6` * l'en-tête `Authorization` est la valeur encodée en base64 de la chaîne `hash:sha256:1706431066:dfa1db3f6bb6445d18d9ec7427c10f6421274e3a4751e6c1ffc7dd28c94eadf6` : `aGFzaDpzaGEyNTY6MTcwNjQzMTA2NjpkZmExZGIzZjZiYjY0NDVkMThkOWVjNzQyN2MxMGY2NDIxMjc0ZTNhNDc1MWU2YzFmZmM3ZGQyOGM5NGVhZGY2`. Les en-têtes `Authorization` et `Sec-WebSocket-Protocol` sont autorisés dans la première requête avec le protocole websocket ou toute requête HTTP dans les autres cas. Exemple de requête avec un mot de passe en clair : [source,shell] ---- curl -L -u 'plain:secret_password' 'https://localhost:9000/api/version' ---- Exemple de requête avec un mot de passe haché (SHA256) : [source,shell] ---- curl -L -u 'hash:sha256:1706431066:dfa1db3f6bb6445d18d9ec7427c10f6421274e3a4751e6c1ffc7dd28c94eadf6' 'https://localhost:9000/api/version' ---- Si TOTP (« Time-based One-Time Password » : mot de passe à usage unique basé sur le temps) est activé dans WeeChat/relay (option `relay.network.totp_secret` définie), vous devez envoyer la valeur TOTP dans l'en-tête `x-weechat-totp` comme ceci : [source,shell] ---- curl -L -u 'hash:sha256:1706431066:dfa1db3f6bb6445d18d9ec7427c10f6421274e3a4751e6c1ffc7dd28c94eadf6' -H "x-weechat-totp: 123456" 'https://localhost:9000/api/version' ---- En cas d'erreur, une réponse `401 Unauthorized` est retournée avec un champ `error` dans les données JSON qui décrit l'erreur. Réponse : mot de passe manquant : [source,http] ---- HTTP/1.1 401 Unauthorized ---- [source,json] ---- { "error": "Missing password" } ---- Réponse : mot de passe invalide : [source,http] ---- HTTP/1.1 401 Unauthorized ---- [source,json] ---- { "error": "Invalid password" } ---- Réponse : algorithme de hachage invalide : [source,http] ---- HTTP/1.1 401 Unauthorized ---- [source,json] ---- { "error": "Invalid hash algorithm (not found or not supported)" } ---- Réponse : horodatage invalide : [source,http] ---- HTTP/1.1 401 Unauthorized ---- [source,json] ---- { "error": "Invalid timestamp" } ---- Réponse : nombre d'itérations invalide : [source,http] ---- HTTP/1.1 401 Unauthorized ---- [source,json] ---- { "error": "Invalid number of iterations" } ---- Réponse : TOTP manquant : [source,http] ---- HTTP/1.1 401 Unauthorized ---- [source,json] ---- { "error": "Missing TOTP" } ---- Réponse : TOTP invalide : [source,http] ---- HTTP/1.1 401 Unauthorized ---- [source,json] ---- { "error": "Invalid TOTP" } ---- [[authentication_sec_websocket_protocol]] === Sec-WebSocket-Protocol L'API WebSocket de JavaScript utilisée dans les navigateurs actuels ne supporte pas l'utilisation de l'en-tête `Authorization`. Il est donc aussi possible d'envoyer le mot de passe dans l'en-tête `Sec-WebSocket-Protocol`, qui est le seul en-tête utilisable avec cette API. Pour utiliser cet en-tête, vous devez spécifier les sous-protocoles `api.weechat` et `base64url.bearer.authorization.weechat.` où `` est la chaîne encodée en base64url avec le mot de passe, dans le même format que ci-dessus. Exemple avec un mot de passe `secret_password` en clair. Cela produit une chaîne encodée en base64url avec le contenu `plain:secret_password` qui est `cGxhaW46c2VjcmV0X3Bhc3N3b3Jk`. ---- Sec-WebSocket-Protocol: api.weechat, base64url.bearer.authorization.weechat.cGxhaW46c2VjcmV0X3Bhc3N3b3Jk ---- Cela peut être défini avec l'API WebSocket de JavaScript comme ceci : [source,javascript] ---- const ws = new WebSocket("wss://localhost:9000/api", [ "api.weechat", "base64url.bearer.authorization.weechat.cGxhaW46c2VjcmV0X3Bhc3N3b3Jk", ]) ---- [[compression]] == Compression La compression du corps de la réponse est automatique et basée sur l'en-tête `Accept-Encoding` envoyé par le client. Les formats de compression supportés sont : * `deflate` (zlib) * `gzip` * `zstd` Exemple de requête : [source,shell] ---- curl -L -u 'plain:secret_password' -H "Accept-Encoding: gzip" 'https://localhost:9000/api/version' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Content-Encoding: gzip Content-Length: 77 ---- ---- [77 octets de données] ---- [NOTE] Avec le protocole websocket, l'extension "permessage-deflate" autorise la compression des messages avec zlib. [[resources]] == Ressources [[resource_preflight]] === Requête « preflight » La requête « preflight » avec la méthode HTTP `OPTIONS` est utilisée par les navigateurs web pour vérifier que le serveur (WeeChat) autorisera la requête proprement dite. Exemple de requête : vérifier que la requête `GET /api/version` est autorisée : [source,http] ---- OPTIONS /api/version HTTP/1.1 Host: localhost:9000 Connection: keep-alive Accept: */* Access-Control-Request-Method: GET Access-Control-Request-Headers: authorization Origin: https://localhost User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Sec-Fetch-Mode: cors Sec-Fetch-Site: same-site Sec-Fetch-Dest: empty Referer: https://localhost/ Accept-Encoding: gzip, deflate, br, zstd Accept-Language: en-US,en;q=0.9,fr;q=0.8 ---- Réponse : [source,http] ---- HTTP/1.1 204 No Content Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: origin, content-type, accept, authorization Access-Control-Allow-Origin: * Content-Type: application/json; charset=utf-8 Content-Length: 0 ---- [[resource_handshake]] === Poignée de main Effectuer une poignée de main entre le client et WeeChat. Cette ressource est accessible sans authentification. Point de terminaison : ---- POST /api/handshake ---- Paramètres du corps : * `password_hash_algo` (tableau de chaînes, facultatif) : liste des algorithmes de hachage supportés par le client, chaque chaîne peut être : ** `plain` : mot de passe en clair (pas de hachage) ** `sha256` : hash SHA256 ** `sha512` : hash SHA512 ** `pbkdf2+sha256` : hash PBKDF2 avec SHA256 ** `pbkdf2+sha512` : hash PBKDF2 avec SHA512 La réponse a les champs suivants : * `password_hash_algo` (chaîne) : l'algorithme de hachage à utiliser (`null` si aucun algorithme n'est compatible) * `password_hash_iterations` (entier) : le nombre d'itérations à utiliser si le hachage PBKDF2 est utilisé * `totp` (booléen) : `true` si le TOTP est activé dans WeeChat (le client doit alors envoyer le TOTP dans un en-tête spécifique), `false` sinon Exemple de requête : [source,shell] ---- curl -L -X POST -d '{"password_hash_algo": ["plain", "sha256", "sha512"]}' 'https://localhost:9000/api/handshake' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK ---- [source,json] ---- { "password_hash_algo": "sha512", "password_hash_iterations": 100000, "totp": false } ---- [[resource_version]] === Version Retourner la version de WeeChat et du relai API. Point de terminaison : ---- GET /api/version ---- Exemple de requête : [source,shell] ---- curl -L -u 'plain:secret_password' 'https://localhost:9000/api/version' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK ---- [source,json] ---- { "weechat_version": "4.2.0-dev", "weechat_version_git": "v4.1.0-143-g0b1cda1c4", "weechat_version_number": 67239936, "relay_api_version": "0.0.1", "relay_api_version_number": 1 } ---- [[resource_buffers]] === Tampons Retourner les tampons, lignes et pseudos. Points de terminaison : ---- GET /api/buffers GET /api/buffers/{buffer_id} GET /api/buffers/{buffer_name} ---- Paramètres du chemin : * `buffer_id` (entier, facultatif) : identifiant unique du tampon (à ne pas confondre avec le numéro du tampon, qui est différent) * `buffer_name` (chaîne, facultatif) : nom de tampon Paramètres de la requête : * `lines` (entier, facultatif, par défaut : `0`) : nombre de lignes à retourner dans les tampons avec contenu formaté : ** nombre négatif : retourner N lignes depuis la fin du tampon (lignes les plus récentes) ** `0` : ne retourner aucune ligne ** nombre positif : retourner N lignes depuis le début du tampon (lignes les plus anciennes) * `lines_free` (entier, facultatif, par défaut : `0` si `lines` est `0`, sinon toutes les lignes) : nombre de lignes à retourner avec un contenu libre : ** nombre négatif : retourner N lignes depuis la fin du tampon ** `0` : ne retourner aucune ligne ** nombre positif : retourner N lignes depuis le début du tampon * `nicks` (booléen, facultatif, par défaut : `false`) : retourner les pseudos du tampon * `colors` (chaîne, facultatif, par défaut : `ansi`) : comment les chaînes avec des couleurs sont retournées : ** `ansi` : retourner les codes couleur ANSI ** `weechat` : retourner les codes couleur internes WeeChat ** `strip` : supprimer les couleurs Exemple de requête : obtenir tous les tampons sans les lignes : [source,shell] ---- curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK ---- [source,json] ---- [ { "id": 1709932823238637, "name": "core.weechat", "short_name": "weechat", "number": 1, "type": "formatted", "title": "WeeChat 4.2.0-dev (C) 2003-2023 - https://weechat.org/", "modes": "", "input_prompt": "", "input": "", "input_position": 0, "input_multiline": false, "nicklist": false, "nicklist_case_sensitive": false, "nicklist_display_groups": true, "time_displayed": true, "local_variables": { "plugin": "core", "name": "weechat" }, "keys": [], "last_read_line_id": -1 }, { "id": 1709932823423765, "name": "irc.server.libera", "short_name": "libera", "number": 2, "type": "formatted", "title": "IRC: irc.libera.chat/6697 (2001:4b7a:a008::6667)", "modes": "", "input_prompt": "", "input": "", "input_position": 0, "input_multiline": false, "nicklist": false, "nicklist_case_sensitive": false, "nicklist_display_groups": true, "time_displayed": true, "local_variables": { "plugin": "irc", "name": "server.libera", "type": "server", "server": "libera", "channel": "libera", "charset_modifier": "irc.libera", "nick": "alice", "tls_version": "TLS1.3", "host": "~alice@example.com" }, "keys": [], "last_read_line_id": -1 }, { "id": 1709932823649069, "name": "irc.libera.#weechat", "short_name": "#weechat", "number": 3, "type": "formatted", "title": "Welcome to the WeeChat official support channel", "modes": "+nt", "input_prompt": "\u001b[92m@\u001b[96malice\u001b[48;5;22m(\u001b[39mi\u001b[48;5;22m)", "input": "", "input_position": 0, "input_multiline": false, "nicklist": true, "nicklist_case_sensitive": false, "nicklist_display_groups": false, "time_displayed": true, "local_variables": { "plugin": "irc", "name": "libera.#weechat", "type": "channel", "server": "libera", "channel": "#weechat", "nick": "alice", "host": "~alice@example.com" }, "keys": [], "last_read_line_id": -1 } ] ---- Exemple de requête : obtenir le tampon WeeChat "core" avec la dernière ligne seulement et aucun code couleur : [source,shell] ---- curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers/core.weechat?lines=-1&colors=strip' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK ---- [source,json] ---- { "id": 1709932823238637, "name": "core.weechat", "short_name": "weechat", "number": 1, "type": "formatted", "title": "WeeChat 4.2.0-dev (C) 2003-2023 - https://weechat.org/", "modes": "", "input_prompt": "", "input": "", "input_position": 0, "input_multiline": false, "nicklist": false, "nicklist_case_sensitive": false, "nicklist_display_groups": true, "time_displayed": true, "local_variables": { "plugin": "core", "name": "weechat" }, "keys": [], "lines": [ { "id": 10, "y": -1, "date": "2023-12-24T08:17:20.786538Z", "date_printed": "2023-12-24T08:17:20.786538Z", "displayed": true, "highlight": false, "notify_level": 0, "prefix": "", "message": "Plugins loaded: alias, buflist, charset, exec, fifo, fset, guile, irc, javascript, logger, lua, perl, php, python, relay, ruby, script, spell, tcl, trigger, typing, xfer", "tags": [] } ], "last_read_line_id": -1 } ---- Exemple de requête : obtenir un tampon de canal IRC avec les pseudos : [source,shell] ---- curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers/irc.libera.%23weechat?nicks=true' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK ---- [source,json] ---- { "id": 1709932823649069, "name": "irc.libera.#weechat", "short_name": "#weechat", "number": 3, "type": "formatted", "title": "Welcome to the WeeChat official support channel", "modes": "+nt", "input_prompt": "\u001b[92m@\u001b[96malice\u001b[48;5;22m(\u001b[39mi\u001b[48;5;22m)", "input": "", "input_position": 0, "input_multiline": false, "nicklist": true, "nicklist_case_sensitive": false, "nicklist_display_groups": false, "time_displayed": true, "local_variables": { "plugin": "irc", "name": "libera.#weechat", "type": "channel", "server": "libera", "channel": "#weechat", "nick": "alice", "host": "~alice@example.com" }, "keys": [], "last_read_line_id": -1, "nicklist_root": { "id": 0, "parent_group_id": -1, "name": "root", "color_name": "", "color": "", "visible": false, "groups": [ { "id": 1709932823649181, "parent_group_id": 0, "name": "000|o", "color_name": "weechat.color.nicklist_group", "color": "\u001b[32m", "visible": true, "groups": [], "nicks": [ { "id": 1709932823649184, "parent_group_id": 1709932823649181, "prefix": "@", "prefix_color_name": "lightgreen", "prefix_color": "\u001b[92m", "name": "alice", "color_name": "bar_fg", "color": "", "visible": true } ] }, { "id": 1709932823649189, "parent_group_id": 0, "name": "001|h", "color_name": "weechat.color.nicklist_group", "color": "\u001b[32m", "visible": true, "groups": [], "nicks": [] }, { "id": 1709932823649203, "parent_group_id": 0, "name": "002|v", "color_name": "weechat.color.nicklist_group", "color": "\u001b[32m", "visible": true, "groups": [], "nicks": [] }, { "id": 1709932823649210, "parent_group_id": 0, "name": "999|...", "color_name": "weechat.color.nicklist_group", "color": "\u001b[32m", "visible": true, "groups": [], "nicks": [] } ], "nicks": [] } } ---- Exemple de requête : obtenir le tampon fset : [source,shell] ---- curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers/fset.fset' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK ---- [source,json] ---- { "id": 1709932823897200, "name": "fset.fset", "short_name": "", "number": 4, "type": "free", "title": "\u001b[96m1/\u001b[36m3565 | Filter: \u001b[93m* | Sort: \u001b[97m~name | Key(input): alt+space=toggle boolean, alt+'-'(-)=subtract 1 or set, alt+'+'(+)=add 1 or append, alt+f,alt+r(r)=reset, alt+f,alt+u(u)=unset, alt+enter(s)=set, alt+f,alt+n(n)=set new value, alt+f,alt+a(a)=append, alt+','=mark/unmark, shift+down=mark and move down, shift+up=move up and mark, ($)=refresh, ($$)=unmark/refresh, (m)=mark matching options, (u)=unmark matching options, alt+p(p)=toggle plugins desc, alt+v(v)=toggle help bar, ctrl+x(x)=switch format, (q)=close buffer", "modes": "", "input_prompt": "", "input": "", "input_position": 0, "input_multiline": false, "nicklist": false, "nicklist_case_sensitive": false, "nicklist_display_groups": true, "time_displayed": true, "local_variables": { "plugin": "fset", "name": "fset", "type": "option", "filter": "*" }, "keys": [ { "key": "ctrl-l", "command": "/fset -refresh" }, { "key": "ctrl-n", "command": "/eval ${if:${weechat.bar.buflist.hidden}?/fset -down:/buffer +1}" }, { "key": "ctrl-x", "command": "/fset -format" }, { "key": "down", "command": "/fset -down" }, { "key": "f11", "command": "/fset -left" }, { "key": "f12", "command": "/fset -right" }, { "key": "meta-+", "command": "/fset -add 1" }, { "key": "meta--", "command": "/fset -add -1" }, { "key": "meta-comma", "command": "/fset -mark" }, { "key": "meta-end", "command": "/fset -go end" }, { "key": "meta-f,meta-a", "command": "/fset -append" }, { "key": "meta-f,meta-n", "command": "/fset -setnew" }, { "key": "meta-f,meta-r", "command": "/fset -reset" }, { "key": "meta-f,meta-u", "command": "/fset -unset" }, { "key": "meta-home", "command": "/fset -go 0" }, { "key": "meta-p", "command": "/mute /set fset.look.show_plugins_desc toggle" }, { "key": "meta-q", "command": "/input insert meta-q fset ${property}" }, { "key": "meta-return", "command": "/fset -set" }, { "key": "meta-space", "command": "/fset -toggle" }, { "key": "meta-v", "command": "/bar toggle fset" }, { "key": "shift-down", "command": "/fset -mark; /fset -down" }, { "key": "shift-up", "command": "/fset -up; /fset -mark" }, { "key": "up", "command": "/fset -up" } ], "last_read_line_id": -1 } ---- [[resource_buffers_lines]] ==== Lignes Retourner les lignes d'un tampon. Points de terminaison : ---- GET /api/buffers/{buffer_id}/lines GET /api/buffers/{buffer_id}/lines/{line_id} GET /api/buffers/{buffer_name}/lines GET /api/buffers/{buffer_name}/lines/{line_id} ---- Paramètres de chemin : * `buffer_id` (entier, **obligatoire**) : identifiant unique de tampon (à ne pas confondre avec le numéro du tampon, qui est différent) * `buffer_name` (chaîne, **obligatoire**) : nom du tampon * `line_id` (entier, facultatif) : retourner une seule ligne avec cet identifiant Paramètres de requête : * `lines` (entier, facultatif, par défaut : toutes les lignes) : nombre de lignes à retourner : ** nombre négatif : retourner N lignes depuis la fin du tampon (lignes les plus récentes) ** `0` : ne retourner aucune ligne (autorisé mais ne fait pas vraiment de sens avec cette ressource) ** nombre positif : retourner N lignes depuis le début du tampon (lignes les plus anciennes) * `colors` (chaîne, facultatif, par défaut : `ansi`) : comment les chaînes avec des couleurs sont retournées : ** `ansi` : retourner les codes couleur ANSI ** `weechat` : retourner les codes couleur internes WeeChat ** `strip` : supprimer les couleurs Exemple de requête : obtenir les 1000 dernières lignes d'un tampon, sans codes couleur : [source,shell] ---- curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers/irc.libera.%23weechat/lines?lines=-1000&colors=strip' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK ---- [source,json] ---- [ { "id": 0, "y": -1, "date": "2023-12-05T19:46:03.847625Z", "date_printed": "2023-12-05T19:46:03.847625Z", "displayed": true, "highlight": false, "notify_level": 0, "prefix": "-->", "message": "alice (~alice@example.com) has joined #test", "tags": [ "irc_join", "irc_tag_account=alice", "irc_tag_time=2023-12-05T19:46:03.847Z", "nick_alice", "host_~alice@example.com", "log4" ] }, { "id": 1, "y": -1, "date": "2023-12-05T19:46:03.986543Z", "date_printed": "2023-12-05T19:46:03.986543Z", "displayed": true, "highlight": false, "notify_level": 0, "prefix": "--", "message": "Mode #test [+Cnst] by zirconium.libera.chat", "tags": [ "irc_mode", "irc_tag_time=2023-12-05T19:46:03.986Z", "nick_zirconium.libera.chat", "log3" ] }, { "id": 2, "y": -1, "date": "2023-12-05T19:46:04.287546Z", "date_printed": "2023-12-05T19:46:04.287546Z", "displayed": true, "highlight": false, "notify_level": 0, "prefix": "--", "message": "Channel #test: 1 nick (1 op, 0 voiced, 0 regular)", "tags": [ "irc_366", "irc_numeric", "irc_tag_time=2023-12-05T19:46:04.287Z", "nick_zirconium.libera.chat", "log3" ] } ] ---- [[resources_buffers_nicks]] ==== Pseudos Retourner les pseudos d'un tampon. Points de terminaison : ---- GET /api/buffers/{buffer_id}/nicks GET /api/buffers/{buffer_name}/nicks ---- Paramètres de chemin : * `buffer_id` (entier, **obligatoire**) : identifiant unique de tampon (à ne pas confondre avec le numéro du tampon, qui est différent) * `buffer_name` (chaîne, **obligatoire**) : nom du tampon Exemple de requête : obtenir les pseudos d'un tampon : [source,shell] ---- curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers/irc.libera.%23weechat/nicks' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK ---- [source,json] ---- { "id": 0, "parent_group_id": -1, "name": "root", "color_name": "", "color": "", "visible": false, "groups": [ { "id": 1709932823649181, "parent_group_id": 0, "name": "000|o", "color_name": "weechat.color.nicklist_group", "color": "\u001b[32m", "visible": true, "groups": [], "nicks": [ { "id": 1709932823649184, "parent_group_id": 1709932823649181, "prefix": "@", "prefix_color_name": "lightgreen", "prefix_color": "\u001b[92m", "name": "alice", "color_name": "bar_fg", "color": "", "visible": true } ] }, { "id": 1709932823649189, "parent_group_id": 0, "name": "001|h", "color_name": "weechat.color.nicklist_group", "color": "\u001b[32m", "visible": true, "groups": [], "nicks": [] }, { "id": 1709932823649203, "parent_group_id": 0, "name": "002|v", "color_name": "weechat.color.nicklist_group", "color": "\u001b[32m", "visible": true, "groups": [], "nicks": [] }, { "id": 1709932823649210, "parent_group_id": 0, "name": "999|...", "color_name": "weechat.color.nicklist_group", "color": "\u001b[32m", "visible": true, "groups": [], "nicks": [] } ], "nicks": [] } ---- [[resource_hotlist]] === Hotlist Retourner la "hotlist" (notification d'activité sur les tampons). Point de terminaison : ---- GET /api/hotlist ---- Exemple de requête : [source,shell] ---- curl -L -u 'plain:secret_password' 'https://localhost:9000/api/hotlist' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK ---- [source,json] ---- [ { "priority": 0, "date": "2024-03-17T16:38:51.572834Z", "buffer_id": 1710693531508204, "count": [ 44, 0, 0, 0 ] }, { "priority": 0, "date": "2024-03-17T16:38:51.573028Z", "buffer_id": 1710693530395959, "count": [ 14, 0, 0, 0 ] }, { "priority": 0, "date": "2024-03-17T16:38:51.611617Z", "buffer_id": 1710693531529248, "count": [ 4, 0, 0, 0 ] } ] ---- [[resource_input]] === Entrée Envoyer une commande ou du texte à un tampon. Point de terminaison : ---- POST /api/input ---- Paramètres du corps : * `buffer_id` (entier, facultatif) : identifiant unique du tampon (à ne pas confondre avec le numéro du tampon, qui est différent) * `buffer_name` (chaîne, facultatif, par défaut : `core.weechat`) : nom de tampon * `command` (chaîne, **obligatoire**) : commande ou texte à envoyer au tampon Exemple de requête : dire "hello!" sur le canal #weechat : [source,shell] ---- curl -L -u 'plain:secret_password' -X POST \ -d '{"buffer_name": "irc.libera.#weechat", "command": "hello!"}' \ 'https://localhost:9000/api/input' ---- Réponse : [source,http] ---- HTTP/1.1 204 No content ---- Exemple de requête : quitter et fermer le canal #weechat (commande exécutée sur le tampon "core" WeeChat) : [source,shell] ---- curl -L -u 'plain:secret_password' -X POST \ -d '{"command": "/buffer close irc.libera.#weechat"}' \ 'https://localhost:9000/api/input' ---- Réponse : [source,http] ---- HTTP/1.1 204 No content ---- [[resource_completion]] === Complétion Compléter une commande ou du texte de l'utilisateur sur un tampon. Point de terminaison : ---- POST /api/completion ---- Paramètres du corps : * `buffer_id` (entier, facultatif) : identifiant unique du tampon (à ne pas confondre avec le numéro du tampon, qui est différent) * `buffer_name` (chaîne, facultatif, par défaut : `core.weechat`) : nom de tampon * `command` (chaîne, **obligatoire**) : commande ou texte à compléter * `position` (entier, facultatif, par défaut : fin de la chaîne) : position dans la commande (la première position est 0) Exemple de requête : compléter la commande `/qu` sur le canal #weechat : [source,shell] ---- curl -L -u 'plain:secret_password' -X POST \ -d '{"buffer_name": "irc.libera.#weechat", "command": "/qu"}' \ 'https://localhost:9000/api/completion' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK ---- [source,json] ---- { "context": "command", "base_word": "qu", "position_replace": 1, "add_space": true, "list": [ "query", "quiet", "quit", "quote" ] } ---- [[resource_ping]] === Ping Envoyer une requête "ping". Point de terminaison : ---- POST /api/ping ---- Paramètres du corps : * `data` (chaîne, facultatif) : chaîne qui est retournée dans la réponse Exemple de requête : pas de corps : [source,shell] ---- curl -L -u 'plain:secret_password' -X POST 'https://localhost:9000/api/ping' ---- Réponse : [source,http] ---- HTTP/1.1 204 No content ---- Exemple de requête : avec des données : [source,shell] ---- curl -L -u 'plain:secret_password' -X POST \ -d '{"data": "1702835741"}' \ 'https://localhost:9000/api/ping' ---- Réponse : [source,http] ---- HTTP/1.1 200 OK ---- [source,json] ---- { "data": "1702835741" } ---- [[resource_sync]] === Sync Démarrer ou arrêter la synchronisation des données avec WeeChat. Cette ressource ne peut être utilisée que si le client est connecté avec le protocole websocket, pour que WeeChat puisse pousser des messages au client à tout moment. Si cette ressource est utilisée sans connexion websocket, une erreur 403 (Forbidden) est retournée. Point de terminaison : ---- POST /api/sync ---- Paramètres du corps : * `sync` (booléen, facultatif, par défaut : `true`) : `true` pour activer la synchronisation avec WeeChat * `nicks` (booléen, facultatif, par défaut : `true`) : `true` pour recevoir les mises à jour de pseudos dans les tampons (utilisé seulement si `sync` vaut `true`) * `input` (booléen, facultatif, par défaut : `true`) : `true` pour synchroniser l'entrée de tampon depuis le relai distant vers le client local (utilisé seulement si `sync` vaut `true`) * `colors` (chaîne, facultatif, par défaut : `ansi`) : comment les chaînes avec des couleurs sont retournées : ** `ansi` : retourner les codes couleur ANSI ** `weechat` : retourner les codes couleur internes WeeChat ** `strip` : supprimer les couleurs Exemple de requête avec le protocole websocket : [source,json] ---- { "request": "POST /api/sync", "body": { "nicks": false } } ---- Réponse : [source,json] ---- { "code": 204, "message": "No Content", "request": "POST /api/sync", "request_body": { "nicks": false }, "body_type": null, "body": null } ---- Exemple de requête dans le protocole websocket (non autorisé) : [source,shell] ---- curl -L -u 'plain:secret_password' -X POST \ -d '{"nicks": false}' \ 'https://localhost:9000/api/sync' ---- Réponse : [source,http] ---- HTTP/1.1 403 Forbidden ---- [source,json] ---- { "error": "Sync resource is available only with a websocket connection" } ---- [[websocket]] == Websocket Le protocole websocket est utilisé pour effectuer une connexion persistante entre le client et WeeChat et recevoir les évènements en temps réel si la synchronisation est activée avec la ressource <>. L'authentification doit être effectuée seulement une fois lorsque le protocole websocket est utilisé (voir <>). [[websocket_handshake]] === Poignée de main websocket Pour établir la connexion, une poignée de main est effectuée sur le point de terminaison `/api` et ressemble à ceci : [source,http] ---- GET /api HTTP/1.1 Host: localhost:9000 Connection: Upgrade Pragma: no-cache Cache-Control: no-cache User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Upgrade: websocket Origin: https://example.com Sec-WebSocket-Version: 13 Accept-Encoding: gzip, deflate, br Accept-Language: en-US,en;q=0.9,fr;q=0.8 Sec-WebSocket-Key: 2XE8VAJktqi3Tpw5QnfxVQ== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits ---- WeeChat retourne sa réponse à la poignée de main pour confirmer que le protocole websocket est bien supporté (et que l'authentification est réussie) : [source,http] ---- HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: PaY9vRflWeOKuD0/F7e5gD9At9U= Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits ---- [NOTE] La valeur `Sec-WebSocket-Accept` retournée est le hachage SHA-1 de la valeur reçue, concaténée au GUID `258EAFA5-E914-47DA-95CA-C5AB0DC85B11` (le hachage SHA-1 est encodé en base64). + Dans l'exemple ci-dessus, le hachage SHA-1 de `2XE8VAJktqi3Tpw5QnfxVQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11` est `PaY9vRflWeOKuD0/F7e5gD9At9U=` (en base64). [[websocket_frames]] === Frames Lorsque le client est connecté via le protocole websocket : * les requêtes et réponses sont en JSON, à l'intérieur des "frames" websocket * si la synchronisation est activée avec la ressource <>, WeeChat peut envoyer des "frames" au client à tout moment. Les requêtes envoyées vers WeeChat sont faites avec un objet JSON qui contient les champs suivants : * `request` (chaîne) : la méthode HTTP et le chemin (exemple : `GET /api/buffers?lines=-100`) * `body` (objet ou tableau) : le corps (facultatif, pour les méthodes `POST` et `PUT`) * `request_id` (chaîne) : identifiant renvoyé dans la réponse Plusieurs requêtes peuvent être envoyées simultanément avec un tableau d'objets, chaque objet étant une requête séparée. + Les requêtes sont exécutées dans l'ordre reçu (voir l'exemple ci-dessous). Les réponses vers le client sont faites avec un objet JSON qui contient les champs suivants : * `code` (entier) : code réponse HTTP (exemple : `200`) * `message` (chaîne) : message pour le code (exemple : `OK`) * `request` (chaîne) : la requête (exemple : `GET /api/buffers?lines=-100`) * `request_body` (objet) : le corps de la requête, ou `null` si la requête n'avait pas de corps * `request_id` (chaîne) : l'identifiant de la requête, ou `null` si la requête n'avait pas d'identifiant * `body_type` (chaîne) : type des objets retournés dans le corps (voir ci-dessous), ou `null` si la réponse n'a pas de corps * `body` (objet ou tableau) : le corps retourné, ou `null` si la réponse n'a pas de corps Les types de corps qui peuvent être retournés : * `handshake` (objet) * `version` (objet) * `buffers` (tableau) * `buffer` (objet) * `lines` (tableau) * `line` (objet) * `nick_group` (objet) * `nick` (objet) * `hotlist` (objet) * `ping` (objet) [TIP] Vous pouvez parcourir les schémas en ligne : https://weechat.org/api/[API relay WeeChat ^↗^^]. Exemple de requête : obtenir la version : [source,json] ---- { "request": "GET /api/version", "request_id": "get_version" } ---- Réponse : [source,json] ---- { "code": 200, "message": "OK", "request": "GET /api/version", "request_body": null, "request_id": "get_version", "body_type": "version", "body": { "weechat_version": "4.2.0-dev", "weechat_version_git": "v4.1.0-143-g0b1cda1c4", "weechat_version_number": 67239936, "relay_api_version": "0.0.1", "relay_api_version_number": 1 } } ---- Exemple de requête : dire "hello!" sur le canal #weechat : [source,json] ---- { "request": "POST /api/input", "body": { "buffer_name": "irc.libera.#weechat", "command": "hello!" } } ---- Réponse : [source,json] ---- { "code": 204, "message": "No Content", "request": "POST /api/input", "request_body": { "buffer_name": "irc.libera.#weechat", "command": "hello!" }, "request_id": null, "body_type": null, "body": null } ---- Exemple de requêtes : envoyer deux requêtes en même temps : obtenir la liste des tampons avec les lignes et les pseudos, puis se synchroniser avec le relay distant : [source,json] ---- [ { "request": "GET /api/buffers?lines=-1000&nicks=true&colors=weechat", "request_id": "initial_sync" }, { "request": "POST /api/sync", "body": { "colors": "weechat" } } ] ---- [NOTE] Il est recommandé d'envoyer la requête de synchronisation en même temps que la première requête qui récupère les données, afin qu'aucun évènement ne soit manqué. Première réponse (le "body" avec les tampons est tronqué pour la lisibilité) : [source,json] ---- { "code": 200, "message": "OK", "request": "GET /api/buffers?lines=-1000&nicks=true&colors=weechat", "request_body": null, "request_id": "initial_sync", "body_type": "buffers", "body": [ { "id": 1709932823238637, "name": "core.weechat", "short_name": "weechat", "number": 1, "type": "formatted" } ] } ---- Seconde réponse : [source,json] ---- { "code": 204, "message": "No Content", "request": "POST /api/sync", "request_body": { "colors": "weechat" }, "request_id": null, "body_type": null, "body": null } ---- WeeChat pousse des données au client à tout moment sur des évènements : lorsque des lignes sont affichées, des tampons ajoutés/supprimés/changés, des pseudos ajoutés/supprimés/changés, etc. Les messages envoyés au client ont les champs suivants : * `code` : `0` * `message` : `Event` * `event_name` (chaîne) : le nom de l'évènement (nom du signal ou hsignal) * `buffer_id` (entier) : l'identifiant unique du tampon, défini seulement pour les sous-objets, -1 dans les autres cas Les évènements suivants sont envoyés au client, selon les options de synchronisation : [width="100%",cols="5,3,3,5",options="header"] |=== | Nom d'évènement | Id tampon | Type de corps | Corps | `buffer_opened` | id tampon | `buffer` | tampon avec les lignes et pseudos | `buffer_type_changed` | id tampon | `buffer` | tampon | `buffer_moved` | id tampon | `buffer` | tampon | `buffer_merged` | id tampon | `buffer` | tampon | `buffer_unmerged` | id tampon | `buffer` | tampon | `buffer_hidden` | id tampon | `buffer` | tampon | `buffer_unhidden` | id tampon | `buffer` | tampon | `buffer_renamed` | id tampon | `buffer` | tampon | `buffer_title_changed` | id tampon | `buffer` | tampon | `buffer_time_for_each_line_changed` | id tampon | `buffer` | tampon | `buffer_localvar_added` | id tampon | `buffer` | tampon | `buffer_localvar_changed` | id tampon | `buffer` | tampon | `buffer_localvar_removed` | id tampon | `buffer` | tampon | `buffer_cleared` | id tampon | `buffer` | tampon | `buffer_closing` | id tampon | `buffer` | tampon | `buffer_closed` | id tampon | null | null | `buffer_line_added` | id tampon | `line` | ligne de tampon | `buffer_line_data_changed` | id tampon | `line` | ligne de tampon | `input_text_changed` | id tampon | `buffer` | tampon | `input_text_cursor_moved` | id tampon | `buffer` | tampon | `nicklist_group_changed` | id tampon | `nick_group` | groupe de pseudos | `nicklist_group_added` | id tampon | `nick_group` | groupe de pseudos | `nicklist_group_removing` | id tampon | `nick_group` | groupe de pseudos | `nicklist_nick_added` | id tampon | `nick` | pseudo | `nicklist_nick_removing` | id tampon | `nick` | pseudo | `nicklist_nick_changed` | id tampon | `nick` | pseudo | `upgrade` ^(1)^ | -1 | null | null | `upgrade_ended` ^(1)^ | -1 | null | null | `quit` | -1 | null | null |=== [NOTE] ^(1)^ Les évènements `upgrade` et `upgrade_ended` sont envoyés seulement si le client est connecté sans chiffrement (pas de TLS), car avec TLS le client est déconnecté avant que la mise à jour soit faire (la mise à jour des connexions TLS n'est pas supportée). Exemple : nouveau tampon : le canal `#weechat` a été rejoint : [source,json] ---- { "code": 0, "message": "Event", "event_name": "buffer_opened", "buffer_id": 1709932823649069, "body_type": "buffer", "body": { "id": 1709932823649069, "name": "irc.libera.#test", "short_name": "", "number": 4, "type": "formatted", "title": "", "modes": "+nt", "input_prompt": "\u001b[92m@\u001b[96malice\u001b[48;5;22m(\u001b[39mi\u001b[48;5;22m)", "input": "", "input_position": 0, "input_multiline": false, "nicklist": true, "nicklist_case_sensitive": false, "nicklist_display_groups": false, "time_displayed": true, "local_variables": { "plugin": "irc", "name": "libera.#test", "type": "channel", "nick": "alice", "host": "~alice@example.com", "server": "libera", "channel": "#test" }, "keys": [], "lines": [] } } ---- Exemple : nouvelle ligne affichée sur le canal `#weechat` : [source,json] ---- { "code": 0, "message": "Event", "event_name": "buffer_line_added", "buffer_id": 1709932823649069, "body_type": "line", "body": { "id": 5, "index": -1, "date": "2024-01-07T08:54:00.179483Z", "date_printed": "2024-01-07T08:54:00.179483Z", "displayed": true, "highlight": false, "notify_level": 0, "prefix": "alice", "message": "hello!", "tags": [ "irc_privmsg", "self_msg", "notify_none", "no_highlight", "prefix_nick_white", "nick_alice", "log1" ] } } ---- Exemple : le pseudo `bob` est ajouté avec le statut opérateur sur le canal `#weechat` : [source,json] ---- { "code": 0, "message": "Event", "event_name": "nicklist_nick_added", "buffer_id": 1709932823649069, "body_type": "nick", "body": { "id": 1709932823649902, "parent_group_id": 1709932823649181, "prefix": "@", "prefix_color_name": "lightgreen", "prefix_color": "\u001b[92m", "name": "bob", "color_name": "bar_fg", "color": "", "visible": true } } ---- Exemple : le tampon du canal `#weechat` a été fermé : [source,json] ---- { "code": 0, "message": "Event", "event_name": "buffer_closed", "buffer_id": 1709932823649069, "body_type": null, "body": null } ---- Exemple : WeeChat est en cours de mise à jour : [source,json] ---- { "code": 0, "message": "Event", "event_name": "upgrade", "buffer_id": -1, "body_type": null, "body": null } ---- Exemple : la mise à jour de WeeChat est terminée : [source,json] ---- { "code": 0, "message": "Event", "event_name": "upgrade_ended", "buffer_id": -1, "body_type": null, "body": null } ----