// SPDX-FileCopyrightText: 2003-2026 Sébastien Helleu <flashcode@flashtux.org>
//
// SPDX-License-Identifier: GPL-3.0-or-later

= WeeChat API Relay
:author: Sébastien Helleu
:email: flashcode@flashtux.org
:lang: en
include::includes/attributes-en.adoc[]

[[introduction]]
== Introduction

This document is the specification of _api_ relay protocol: the protocol used
to relay WeeChat data to clients using an HTTP REST API.

[[terminology]]
=== Terminology

The following terms are used in this document:

* _relay_: this is the WeeChat with relay plugin, which acts as "server" and
  allows _clients_ to connect
* _client_: this is a software connected to _relay_ via a network connection
  (WeeChat itself or a remote interface).

[[network_diagram]]
=== Network diagram

The _clients_ are connected to _relay_ like shown in this diagram:

include::includes/relay.en.adoc[tag=diagram]

All clients here are clients using _api_ protocol in _relay_ plugin. +
The _relay_ plugin also allows _irc_ and _weechat_ protocols (not described in this document).

[[protocol_generalities]]
== Protocol generalities

* Connections from _client_ to _relay_ are made using TCP sockets on IP/port
  used by _relay_ plugin to listen to new connections.
* Number of _clients_ is limited by the option _relay.network.max_clients_.
* Each _client_ is independent from other clients.
* The _api_ relay is an HTTP REST API using JSON format for input/output.
* Messages are automatically compressed (deflate, gzip, zstd and permessage-deflate
  for websocket protocol).
* WeeChat can be used as client of this relay.

[[api_versioning]]
=== API versioning

The API is versioned using a "practical" https://semver.org[semantic Versioning ^↗^^],
like WeeChat, on three digits `X.Y.Z`, where:

* `X` is the major version
* `Y` is the minor version
* `Z` is the patch version.

Example: version `2.0.0` brings breaking changes vs version `1.2.3`.

The API version is returned by the <<resource_version,version>> resource.

[[api_schema]]
=== API schema

You can browse and test the API online: https://weechat.org/api/[WeeChat Relay API ^↗^^].

[[response_codes]]
=== Response codes

The following HTTP response codes can be sent back to the client:

* `200 OK`: response OK with a body (JSON)
* `204 No Content`: response OK without body
* `400 Bad Request`: invalid request received
* `401 Unauthorized`: missing or invalid credentials
* `403 Forbidden`: insufficient permissions
* `404 Not Found`: resource not found
* `500 Internal Server Error`: internal server error
* `503 Service Unavailable`: service unavailable

When connected via websocket protocol, an extra response code is sent when WeeChat
pushes data to the client on events:

* `0 Event`: event pushed to the client, if synchronization is enabled with
  <<resource_sync,sync>> resource.

[[date_format]]
=== Date format

The date format is https://en.wikipedia.org/wiki/ISO_8601[ISO 8601 ^↗^^],
using UTC timezone (this is then different from the display by WeeChat which
uses the local timezone). +
The dates are returned with maximum precision: up to microseconds if possible,
or milliseconds, or just seconds.

Examples:

----
2023-12-05T19:46:03.847625Z
2023-12-05T19:46:03.847Z
2023-12-05T19:46:03Z
----

[[authentication]]
== Authentication

The password must be sent in the header `Authorization` with `Basic`
authentication schema or in the header `Sec-WebSocket-Protocol` (see details
below).

The password can be sent as plain text or hashed, with one of these formats
for user and password:

* `plain:<password>`
* `hash:sha256:<timestamp>:<hash>`
* `hash:sha512:<timestamp>:<hash>`
* `hash:pbkdf2+sha256:<timestamp>:<iterations>:<hash>`
* `hash:pbkdf2+sha512:<timestamp>:<iterations>:<hash>`

Where:

* `<password>` is the password as plain text
* `<timestamp>` is the current timestamp as integer (number of seconds since
  the Unix Epoch); it is used to prevent replay attacks
* `<iterations>` is the number of iterations (for PBKDF2 algorithm only)
* `<hash>` is the hashed value of timestamp + password (as hexadecimal)

[NOTE]
The max number of seconds allowed before and after the received time
(when password is sent hashed) can be configured with option _relay.network.time_window_.

Example:

* current timestamp is `1706431066`
* password is `secret_password`
* hash algorithm is `sha256`
* result hash is the SHA256 of string `1706431066secret_password` which is as hexadecimal:
  `dfa1db3f6bb6445d18d9ec7427c10f6421274e3a4751e6c1ffc7dd28c94eadf6`
* the `Authorization` header is the base64 encoded string
  `hash:sha256:1706431066:dfa1db3f6bb6445d18d9ec7427c10f6421274e3a4751e6c1ffc7dd28c94eadf6`:
  `aGFzaDpzaGEyNTY6MTcwNjQzMTA2NjpkZmExZGIzZjZiYjY0NDVkMThkOWVjNzQyN2MxMGY2NDIxMjc0ZTNhNDc1MWU2YzFmZmM3ZGQyOGM5NGVhZGY2`.

The headers `Authorization` and `Sec-WebSocket-Protocol` are allowed in the first
request with the websocket protocol or any HTTP request in the other cases.

Request example with plain text password:

[source,shell]
----
curl -L -u 'plain:secret_password' 'https://localhost:9000/api/version'
----

Request example with hashed password (SHA256):

[source,shell]
----
curl -L -u 'hash:sha256:1706431066:dfa1db3f6bb6445d18d9ec7427c10f6421274e3a4751e6c1ffc7dd28c94eadf6' 'https://localhost:9000/api/version'
----

If TOTP (Time-based One-Time Password) is enabled on WeeChat/relay side
(option `relay.network.totp_secret` is set), you must send the TOTP value
in the `x-weechat-totp` header like this:

[source,shell]
----
curl -L -u 'hash:sha256:1706431066:dfa1db3f6bb6445d18d9ec7427c10f6421274e3a4751e6c1ffc7dd28c94eadf6' -H "x-weechat-totp: 123456" 'https://localhost:9000/api/version'
----

In case of error, a response `401 Unauthorized` is returned with a field `error`
in JSON data that describes the error.

Response: missing password:

[source,http]
----
HTTP/1.1 401 Unauthorized
----

[source,json]
----
{
    "error": "Missing password"
}
----

Response: wrong password:

[source,http]
----
HTTP/1.1 401 Unauthorized
----

[source,json]
----
{
    "error": "Invalid password"
}
----

Response: invalid hash algorithm:

[source,http]
----
HTTP/1.1 401 Unauthorized
----

[source,json]
----
{
    "error": "Invalid hash algorithm (not found or not supported)"
}
----

Response: invalid timestamp:

[source,http]
----
HTTP/1.1 401 Unauthorized
----

[source,json]
----
{
    "error": "Invalid timestamp"
}
----

Response: invalid number of iterations:

[source,http]
----
HTTP/1.1 401 Unauthorized
----

[source,json]
----
{
    "error": "Invalid number of iterations"
}
----

Response: missing TOTP:

[source,http]
----
HTTP/1.1 401 Unauthorized
----

[source,json]
----
{
    "error": "Missing TOTP"
}
----

Response: wrong TOTP:

[source,http]
----
HTTP/1.1 401 Unauthorized
----

[source,json]
----
{
    "error": "Invalid TOTP"
}
----

[[authentication_sec_websocket_protocol]]
=== Sec-WebSocket-Protocol

The JavaScript WebSocket API used in current web browsers does not support
specifying the `Authorization` header. Therefore it's also supported to send
the password in the `Sec-WebSocket-Protocol` header which is the only header
possible to set with this API.

To use this header, you must specify the sub-protocols `api.weechat` and
`base64url.bearer.authorization.weechat.<auth>` where `<auth>` is the base64url
encoded string of the password in the same format as explained above.

Example with password `secret_password` encoded in plain text. This makes the
string to base64url encode `plain:secret_password` which is
`cGxhaW46c2VjcmV0X3Bhc3N3b3Jk`.

----
Sec-WebSocket-Protocol: api.weechat, base64url.bearer.authorization.weechat.cGxhaW46c2VjcmV0X3Bhc3N3b3Jk
----

This can be set with the JavaScript WebSocket API like this:

[source,javascript]
----
const ws = new WebSocket("wss://localhost:9000/api", [
  "api.weechat",
  "base64url.bearer.authorization.weechat.cGxhaW46c2VjcmV0X3Bhc3N3b3Jk",
])
----

[[compression]]
== Compression

Compression of response body is automatic and based on header `Accept-Encoding`
sent by the client.

Supported compression formats are:

* `deflate` (zlib)
* `gzip`
* `zstd`

Request example:

[source,shell]
----
curl -L -u 'plain:secret_password' -H "Accept-Encoding: gzip" 'https://localhost:9000/api/version'
----

Response:

[source,http]
----
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Content-Length: 77
----

----
[77 bytes data]
----

Note: with websocket protocol, the extension "permessage-deflate" allows to
compress messages with zlib.

[[resources]]
== Resources

[[resource_preflight]]
=== Preflight request

The preflight request with HTTP method `OPTIONS` is used by web browsers to check
that the server (WeeChat) will permit the actual request.

Request example: check that request `GET /api/version` is authorized:

[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
----

Response:

[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]]
=== Handshake

Perform an handshake between the client and WeeChat.

This resource is accessible without authentication.

Endpoint:

----
POST /api/handshake
----

Body parameters:

* `password_hash_algo` (array of strings, optional): list of hash algorithms
  supported by the client, each string can be:
** `plain`: plain-text password (no hash)
** `sha256`: hash SHA256
** `sha512`: hash SHA512
** `pbkdf2+sha256`: hash PBKDF2 with SHA256
** `pbkdf2+sha512`: hash PBKDF2 with SHA512

The response has the following fields:

* `password_hash_algo` (string): the hash algorithm to use
  (`null` if no algorithm is compatible)
* `password_hash_iterations` (integer): the number of iterations to use if
  hash PBKDF2 is used
* `totp` (boolean): `true` if TOTP is enabled in WeeChat (then the client must
  send TOTP in specific header), `false` otherwise

Request example:

[source,shell]
----
curl -L -X POST -d '{"password_hash_algo": ["plain", "sha256", "sha512"]}' 'https://localhost:9000/api/handshake'
----

Response:

[source,http]
----
HTTP/1.1 200 OK
----

[source,json]
----
{
    "password_hash_algo": "sha512",
    "password_hash_iterations": 100000,
    "totp": false
}
----

[[resource_version]]
=== Version

Return the WeeChat and relay API versions.

Endpoint:

----
GET /api/version
----

Request example:

[source,shell]
----
curl -L -u 'plain:secret_password' 'https://localhost:9000/api/version'
----

Response:

[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]]
=== Buffers

Return buffers, lines and nicks.

Endpoints:

----
GET /api/buffers
GET /api/buffers/{buffer_id}
GET /api/buffers/{buffer_name}
----

Path parameters:

* `buffer_id` (integer, optional): buffer unique identifier (not to be confused
  with the buffer number, which is different)
* `buffer_name` (string, optional): buffer name

Query parameters:

* `lines` (integer, optional, default: `0`): number of lines to return in buffers
  with formatted content:
** negative number: return N lines from the end of buffer (newest lines)
** `0`: do not return any line
** positive number: return N lines from the beginning of buffer (oldest lines)
* `lines_free` (integer, optional, default: `0` if `lines` is `0`, otherwise all lines):
  number of lines to return in buffers with free content:
** negative number: return N lines from the end of buffer
** `0`: do not return any line
** positive number: return N lines from the beginning of buffer
* `nicks` (boolean, optional, default: `false`): return nicks in buffer
* `colors` (string, optional, default: `ansi`): how to return strings with color codes:
** `ansi`: return ANSI color codes
** `weechat`: return WeeChat internal color codes
** `strip`: strip colors

Request example: get all buffers without lines:

[source,shell]
----
curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers'
----

Response:

[source,http]
----
HTTP/1.1 200 OK
----

[source,json]
----
[
    {
        "id": 1709932823238637,
        "name": "core.weechat",
        "short_name": "weechat",
        "number": 1,
        "type": "formatted",
        "hidden": false,
        "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",
        "hidden": false,
        "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",
        "hidden": false,
        "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
    }
]
----

Request example: get WeeChat core buffer with only last line and no color codes:

[source,shell]
----
curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers/core.weechat?lines=-1&colors=strip'
----

Response:

[source,http]
----
HTTP/1.1 200 OK
----

[source,json]
----
{
    "id": 1709932823238637,
    "name": "core.weechat",
    "short_name": "weechat",
    "number": 1,
    "type": "formatted",
    "hidden": false,
    "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
}
----

Request example: get an IRC channel buffers with nicks:

[source,shell]
----
curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers/irc.libera.%23weechat?nicks=true'
----

Response:

[source,http]
----
HTTP/1.1 200 OK
----

[source,json]
----
{
    "id": 1709932823649069,
    "name": "irc.libera.#weechat",
    "short_name": "#weechat",
    "number": 3,
    "type": "formatted",
    "hidden": false,
    "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": []
    }
}
----

Request example: get fset buffer:

[source,shell]
----
curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers/fset.fset'
----

Response:

[source,http]
----
HTTP/1.1 200 OK
----

[source,json]
----
{
    "id": 1709932823897200,
    "name": "fset.fset",
    "short_name": "",
    "number": 4,
    "type": "free",
    "hidden": false,
    "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]]
==== Lines

Return lines in a buffer.

Endpoints:

----
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}
----

Path parameters:

* `buffer_id` (integer, **required**): buffer unique identifier (not to be
  confused with the buffer number, which is different)
* `buffer_name` (string, **required**): buffer name
* `line_id` (integer, optional): return a single line with this identifier

Query parameters:

* `lines` (integer, optional, default: all lines): number of lines to return:
** negative number: return N lines from the end of buffer (newest lines)
** `0`: do not return any line (allowed but doesn't make sense with this resource)
** positive number: return N lines from the beginning of buffer (oldest lines)
* `colors` (string, optional, default: `ansi`): how to return strings with color codes:
** `ansi`: return ANSI color codes
** `weechat`: return WeeChat internal color codes
** `strip`: strip colors

Request example: get last 1000 lines of a buffer, without color codes:

[source,shell]
----
curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers/irc.libera.%23weechat/lines?lines=-1000&colors=strip'
----

Response:

[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]]
==== Nicks

Return nicks in a buffer.

Endpoints:

----
GET /api/buffers/{buffer_id}/nicks
GET /api/buffers/{buffer_name}/nicks
----

Path parameters:

* `buffer_id` (integer, **required**): buffer unique identifier (not to be
  confused with the buffer number, which is different)
* `buffer_name` (string, **required**): buffer name

Request example: get nicks of a buffer:

[source,shell]
----
curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers/irc.libera.%23weechat/nicks'
----

Response:

[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

Return hotlist.

Endpoint:

----
GET /api/hotlist
----

Request example:

[source,shell]
----
curl -L -u 'plain:secret_password' 'https://localhost:9000/api/hotlist'
----

Response:

[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]]
=== Input

Send command or text to a buffer.

Endpoint:

----
POST /api/input
----

Body parameters:

* `buffer_id` (integer, optional): buffer unique identifier (not to be confused
  with the buffer number, which is different)
* `buffer_name` (string, optional, default: `core.weechat`): buffer name
* `command` (string, **required**): command or text to send to the buffer

Request example: say "hello!" on channel #weechat:

[source,shell]
----
curl -L -u 'plain:secret_password' -X POST \
  -d '{"buffer_name": "irc.libera.#weechat", "command": "hello!"}' \
  'https://localhost:9000/api/input'
----

Response:

[source,http]
----
HTTP/1.1 204 No content
----

Request example: part and close channel #weechat (command executed on WeeChat
core buffer):

[source,shell]
----
curl -L -u 'plain:secret_password' -X POST \
  -d '{"command": "/buffer close irc.libera.#weechat"}' \
  'https://localhost:9000/api/input'
----

Response:

[source,http]
----
HTTP/1.1 204 No content
----

[[resource_completion]]
=== Completion

Complete user command or text in a buffer.

Endpoint:

----
POST /api/completion
----

Body parameters:

* `buffer_id` (integer, optional): buffer unique identifier (not to be confused
  with the buffer number, which is different)
* `buffer_name` (string, optional, default: `core.weechat`): buffer name
* `command` (string, **required**): command or text to complete
* `position` (integer, optional, default: end of string): position in command
  (first position is 0)

Request example: complete command `/qu` on channel #weechat:

[source,shell]
----
curl -L -u 'plain:secret_password' -X POST \
  -d '{"buffer_name": "irc.libera.#weechat", "command": "/qu"}' \
  'https://localhost:9000/api/completion'
----

Response:

[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

Send a "ping" request.

Endpoint:

----
POST /api/ping
----

Body parameters:

* `data` (string, optional): string that is sent back in the response

Request example: no body:

[source,shell]
----
curl -L -u 'plain:secret_password' -X POST 'https://localhost:9000/api/ping'
----

Response:

[source,http]
----
HTTP/1.1 204 No content
----

Request example: with data:

[source,shell]
----
curl -L -u 'plain:secret_password' -X POST \
  -d '{"data": "1702835741"}' \
  'https://localhost:9000/api/ping'
----

Response:

[source,http]
----
HTTP/1.1 200 OK
----

[source,json]
----
{
    "data": "1702835741"
}
----

[[resource_sync]]
=== Sync

Start or stop synchronization of data with WeeChat.

This resource can be used only when the client is connected with websocket
protocol, as WeeChat will push messages to the client at any time.

If this resource is used without a websocket connection, an error 403 (Forbidden)
is returned.

Endpoint:

----
POST /api/sync
----

Body parameters:

* `sync` (boolean, optional, default: `true`): `true` to enable synchronization
  with WeeChat
* `nicks` (boolean, optional, default: `true`): `true` to receive nick updates
  in buffers (used only if `sync` is `true`)
* `input` (boolean, optional, default: `true`): `true` to synchronize buffer input
  from remote relay to local client (used only if `sync` is `true`)
* `colors` (string, optional, default: `ansi`): how to return strings with
  color codes (used only if `sync` is `true`):
** `ansi`: return ANSI color codes
** `weechat`: return WeeChat internal color codes
** `strip`: strip colors

Request example with websocket protocol:

[source,json]
----
{
    "request": "POST /api/sync",
    "body": {
        "nicks": false
    }
}
----

Response:

[source,json]
----
{
    "code": 204,
    "message": "No Content",
    "request": "POST /api/sync",
    "request_body": {
        "nicks":false
    },
    "body_type": null,
    "body": null
}
----

Request example without websocket protocol (not authorized):

[source,shell]
----
curl -L -u 'plain:secret_password' -X POST \
  -d '{"nicks": false}' \
  'https://localhost:9000/api/sync'
----

Response:

[source,http]
----
HTTP/1.1 403 Forbidden
----

[source,json]
----
{
    "error": "Sync resource is available only with a websocket connection"
}
----

[[websocket]]
== Websocket

Websocket protocol is used to make a persistent connection between the client
and WeeChat and receive events in real-time if synchronization is enabled with
<<resource_sync,sync>> resource.

Authentication must be done only one time when the websocket protocol is used
(see <<authentication,authentication>>).

[[websocket_handshake]]
=== Websocket handshake

To establish the connection, a handshake is performed on the `/api` endpoint
and looks like:

[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 returns its handshake response to confirm that websocket protocol is
properly supported (and authentication was successful):

[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]
The `Sec-WebSocket-Accept` value returned is the SHA-1 hash of the value received,
concatenated to the GUID `258EAFA5-E914-47DA-95CA-C5AB0DC85B11` (the SHA-1 is
encoded in base64). +
In the example above, the SHA-1 of
`2XE8VAJktqi3Tpw5QnfxVQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11` is
`PaY9vRflWeOKuD0/F7e5gD9At9U=` (in base64).

[[websocket_frames]]
=== Frames

When the client is connected via the websocket protocol:

* requests and responses are in JSON, inside websocket frames
* if synchronization is enabled with <<resource_sync,sync>> resource, WeeChat
  can send JSON frames at any time to the client.

Requests to WeeChat are made with a JSON object containing these fields:

* `request` (string): the HTTP method and path (example: `GET /api/buffers?lines=-100`)
* `body` (object or array): the body (optional, for `POST` and `PUT` methods)
* `request_id` (string): identifier sent back in the response

Multiple requests can be sent at once using an array of objects, each object
being a separate request. +
Requests are executed in the order received (see example below).

Responses to client are made with a JSON object containing these fields:

* `code` (integer): HTTP response code (example: `200`)
* `message` (string): message for the code (example: `OK`)
* `request` (string): the request (example: `GET /api/buffers?lines=-100`)
* `request_body` (object): the request body, or `null` if the request had no body
* `request_id` (string): the request id, or `null` if the request had no id
* `body_type` (string): type of objects returned in body (see below), or `null`
  if the response has no body
* `body` (object or array): the body returned, or `null` if the response has no body

Body types that can be returned:

* `handshake` (object)
* `version` (object)
* `buffers` (array)
* `buffer` (object)
* `lines` (array)
* `line` (object)
* `nick_group` (object)
* `nick` (object)
* `hotlist` (object)
* `ping` (object)

[TIP]
You can browse these schemas online: https://weechat.org/api/[WeeChat Relay API ^↗^^].

Request example: get version:

[source,json]
----
{
    "request": "GET /api/version",
    "request_id": "get_version"
}
----

Response:

[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
    }
}
----

Request example: say "hello!" on channel #weechat:

[source,json]
----
{
    "request": "POST /api/input",
    "body": {
        "buffer_name": "irc.libera.#weechat",
        "command": "hello!"
    }
}
----

Response:

[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
}
----

Requests example: send two requests at once: get list of all buffers with lines
and nicks, then synchronize with the remote:

[source,json]
----
[
    {
        "request": "GET /api/buffers?lines=-1000&nicks=true&colors=weechat",
        "request_id": "initial_sync"
    },
    {
        "request": "POST /api/sync",
        "body": {
            "colors": "weechat"
        }
    }
]
----

[NOTE]
It is recommended to send the synchronization request together with the first
request that is fetching data, so that no events are missed.

First response (body with buffers is truncated for readability):

[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"
        }
    ]
}
----

Second response:

[source,json]
----
{
    "code": 204,
    "message": "No Content",
    "request": "POST /api/sync",
    "request_body": {
        "colors": "weechat"
    },
    "request_id": null,
    "body_type": null,
    "body": null
}
----

WeeChat pushes data to the client at any time on some events: when lines are
displayed, buffers added/removed/changed, nicks added/removed/changed, etc.

The messages sent to client have the following fields:

* `code`: `0`
* `message`: `Event`
* `event_name` (string): the event name (name of signal or hsignal)
* `buffer_id` (integer): the buffer unique identifier, set only for sub-objects,
  -1 in other cases

The following events are sent to the client, according to synchronization options:

[width="100%",cols="5,3,3,5",options="header"]
|===
| Event name                          | Buffer id | Body type     | Body
| `buffer_opened`                     | buffer id | `buffer`      | buffer with all lines and nicks
| `buffer_type_changed`               | buffer id | `buffer`      | buffer
| `buffer_moved`                      | buffer id | `buffer`      | buffer
| `buffer_merged`                     | buffer id | `buffer`      | buffer
| `buffer_unmerged`                   | buffer id | `buffer`      | buffer
| `buffer_hidden`                     | buffer id | `buffer`      | buffer
| `buffer_unhidden`                   | buffer id | `buffer`      | buffer
| `buffer_renamed`                    | buffer id | `buffer`      | buffer
| `buffer_title_changed`              | buffer id | `buffer`      | buffer
| `buffer_time_for_each_line_changed` | buffer id | `buffer`      | buffer
| `buffer_localvar_added`             | buffer id | `buffer`      | buffer
| `buffer_localvar_changed`           | buffer id | `buffer`      | buffer
| `buffer_localvar_removed`           | buffer id | `buffer`      | buffer
| `buffer_cleared`                    | buffer id | `buffer`      | buffer
| `buffer_closing`                    | buffer id | `buffer`      | buffer
| `buffer_closed`                     | buffer id | null          | null
| `buffer_line_added`                 | buffer id | `line`        | buffer line
| `buffer_line_data_changed`          | buffer id | `line`        | buffer line
| `input_text_changed`                | buffer id | `buffer`      | buffer
| `input_text_cursor_moved`           | buffer id | `buffer`      | buffer
| `nicklist_group_changed`            | buffer id | `nick_group`  | nick group
| `nicklist_group_added`              | buffer id | `nick_group`  | nick group
| `nicklist_group_removing`           | buffer id | `nick_group`  | nick group
| `nicklist_nick_added`               | buffer id | `nick`        | nick
| `nicklist_nick_removing`            | buffer id | `nick`        | nick
| `nicklist_nick_changed`             | buffer id | `nick`        | nick
| `upgrade` ^(1)^                     | -1        | null          | null
| `upgrade_ended` ^(1)^               | -1        | null          | null
| `quit`                              | -1        | null          | null
|===

[NOTE]
^(1)^ The events `upgrade` and `upgrade_ended` are sent only if the client is
connected with plain text (no TLS), because with TLS the client is disconnected
before the upgrade is done (upgrade of TLS connections is not supported).

Example: new buffer: channel `#weechat` has been joined:

[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",
        "hidden": false,
        "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": []
    }
}
----

Example: new line displayed on channel `#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"
        ]
    }
}
----

Example: nick `bob` added with operator status in channel `#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
    }
}
----

Example: channel buffer `#weechat` has been closed:

[source,json]
----
{
    "code": 0,
    "message": "Event",
    "event_name": "buffer_closed",
    "buffer_id": 1709932823649069,
    "body_type": null,
    "body": null
}
----

Example: WeeChat is upgrading:

[source,json]
----
{
    "code": 0,
    "message": "Event",
    "event_name": "upgrade",
    "buffer_id": -1,
    "body_type": null,
    "body": null
}
----

Example: upgrade of WeeChat is done:

[source,json]
----
{
    "code": 0,
    "message": "Event",
    "event_name": "upgrade_ended",
    "buffer_id": -1,
    "body_type": null,
    "body": null
}
----
