WeeChat specifications

Relay HTTP REST API

Context

WeeChat allows external clients to connect via two protocols in relay plugin:

The weechat protocol uses a custom binary protocol that is complicated to implement in a client and exposes internal structures with pointers, which is unsafe.

Goals

Purpose of this specification is to add a third relay protocol called api, with the following goals:

Out of scope

Existing relay protocols irc and weechat are unchanged.

Protocol weechat will benefit of websocket extension permessage-deflate (compression of messages exchanged with the client, in both directions).
This is transparent for existing clients: if they do not support this extension, WeeChat will still send uncompressed messages to the client.

Changes

In the examples below, the JSON output is formatted for readability, but the WeeChat response is sent without any indentation, for example:

{"weechat_version":"4.2.0-dev","weechat_version_git":"v4.1.0-143-g0b1cda1c4",...}

New relay protocol

A new relay protocol called api is added.

Example:

/relay add tls.api 9000

This protocol allows communication using a HTTP REST API exposed by WeeChat/relay.
All resources start with /api/, followed by the resource name.

The following methods and paths are available:

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

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

API schema

The description of API is available as an OpenAPI document, auto-generated when WeeChat is built (relay plugin must be enabled in the build).

API versioning

The API is versioned using a “practical” Semantic Versioning, like WeeChat, on three digits X.Y.Z, where:

Example: version 2.0.0 brings breaking changes vs version 1.2.3.

The API version is returned by the Version resource.

Date format

The date format is ISO 8601, using UTC timezone (so this can differ from the dates displayed in WeeChat itself, which is using the local timezone).

The dates are returned with maximum precision: up to microseconds if available (if not, it may include milliseconds, or nothing after the seconds).

Examples:

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

Authentication

The password must be sent in the header Authorization with Basic authentication schema.

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

Where:

A new option relay.network.time_window is added in WeeChat to set the max number of seconds allowed before and after the received time (when password is sent hashed). Default value is 5.

Example:

The header Authorization is allowed in the first websocket request (see Handshake) or any HTTP request when websocket is not used and when a JWT token is not sent.

Request example with plain password:

curl -L -u 'plain:secret_password' 'https://localhost:9000/api/version'

Request example with hashed password (SHA256):

curl -L -u 'hash:sha256:1706431066:dfa1db3f6bb6445d18d9ec7427c10f6421274e3a4751e6c1ffc7dd28c94eadf6' \
  'https://localhost:9000/api/version'

If TOTP 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:

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 that describes the error.

Response: missing password:

HTTP/1.1 401 Unauthorized
{
    "error": "Missing password"
}

Response: wrong password:

HTTP/1.1 401 Unauthorized
{
    "error": "Invalid password"
}

Response: invalid hash algorithm:

HTTP/1.1 401 Unauthorized
{
    "error": "Invalid hash algorithm (not found or not supported)"
}

Response: invalid timestamp:

HTTP/1.1 401 Unauthorized
{
    "error": "Invalid timestamp"
}

Response: invalid number of iterations:

HTTP/1.1 401 Unauthorized
{
    "error": "Invalid number of iterations"
}

Response: missing TOTP:

HTTP/1.1 401 Unauthorized
{
    "error": "Missing TOTP"
}

Response: wrong TOTP:

HTTP/1.1 401 Unauthorized
{
    "error": "Invalid TOTP"
}

Compression

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

Supported compression formats:

Request example:

curl -L -u 'plain:secret_password' -H "Accept-Encoding: gzip" 'https://localhost:9000/api/version'

Response:

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

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

Resource: handshake

Perform an handshake between the client and WeeChat.

This resource does not require authentication.

Endpoint:

POST /api/handshake

Body parameters:

The response has the following fields:

Request example:

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

Response:

HTTP/1.1 200 OK
{
    "password_hash_algo": "sha512",
    "password_hash_iterations": 100000,
    "totp": false
}

Resource: version

Return the WeeChat and relay API versions.

Endpoint:

GET /api/version

Request example:

curl -L -u 'plain:secret_password' 'https://localhost:9000/api/version'

Response:

HTTP/1.1 200 OK
{
    "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

Return buffers, lines and nicks.

Endpoints:

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

Path parameters:

Query parameters:

Request example: get all buffers without lines:

curl -L -u 'plain:secret_password' 'https://localhost:9000/api/buffers'

Response:

HTTP/1.1 200 OK
[
    {
        "id": 1709932823238637,
        "name": "core.weechat",
        "short_name": "weechat",
        "number": 1,
        "type": "formatted",
        "title": "WeeChat 4.2.0-dev (C) 2003-2023 - https://weechat.org/",
        "local_variables": {
            "plugin": "core",
            "name": "weechat"
        }
    },
    {
        "id": 1709932823423765,
        "name": "irc.server.libera",
        "short_name": "libera",
        "number": 2,
        "type": "formatted",
        "title": "IRC: irc.libera.chat/6697 (2001:4b7a:a008::6667)",
        "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"
        }
    },
    {
        "id": 1709932823649069,
        "name": "irc.libera.#weechat",
        "short_name": "#weechat",
        "number": 3,
        "type": "formatted",
        "title": "Welcome to the WeeChat official support channel",
        "local_variables": {
            "plugin": "irc",
            "name": "libera.#weechat",
            "type": "channel",
            "server": "libera",
            "channel": "#weechat",
            "nick": "alice",
            "host": "~alice@example.com"
        }
    }
]

Request example: get WeeChat core buffer with only last line:

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

Response:

HTTP/1.1 200 OK
{
    "id": 1709932823238637,
    "name": "core.weechat",
    "short_name": "weechat",
    "number": 1,
    "type": "formatted",
    "title": "WeeChat 4.2.0-dev (C) 2003-2023 - https://weechat.org/",
    "local_variables": {
        "plugin": "core",
        "name": "weechat"
    },
    "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": []
        }
    ]
}

Request example: get a channel buffers with nicks:

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

Response:

HTTP/1.1 200 OK
{
    "id": 1709932823649069,
    "name": "irc.libera.#weechat",
    "short_name": "#weechat",
    "number": 3,
    "type": "formatted",
    "title": "Welcome to the WeeChat official support channel",
    "local_variables": {
        "plugin": "irc",
        "name": "libera.#weechat",
        "type": "channel",
        "server": "libera",
        "channel": "#weechat",
        "nick": "alice",
        "host": "~alice@example.com"
    },
    "nicklist": {
        "id": 0,
        "parent_group_id": -1,
        "name": "root",
        "color": "",
        "visible": false,
        "groups": [
            {
                "id": 1709932823649181,
                "parent_group_id": 0,
                "name": "000|o",
                "color": "\u001b[32m",
                "visible": true,
                "groups": [],
                "nicks": [
                    {
                        "id": 1709932823649184,
                        "parent_group_id": 1709932823649181,
                        "prefix": "@",
                        "prefix_color": "\u001b[92m",
                        "name": "alice",
                        "color": "",
                        "visible": true
                    }
                ]
            },
            {
                "id": 1709932823649189,
                "parent_group_id": 0,
                "name": "001|h",
                "color": "\u001b[32m",
                "visible": true,
                "groups": [],
                "nicks": []
            },
            {
                "id": 1709932823649203,
                "parent_group_id": 0,
                "name": "002|v",
                "color": "\u001b[32m",
                "visible": true,
                "groups": [],
                "nicks": []
            },
            {
                "id": 1709932823649210,
                "parent_group_id": 0,
                "name": "999|...",
                "color": "\u001b[32m",
                "visible": true,
                "groups": [],
                "nicks": []
            }
        ],
        "nicks": []
    }
}

Sub-resource: buffers / 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:

Query parameters:

Request example: get last 1000 lines of a buffer:

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

Response:

HTTP/1.1 200 OK
[
    {
        "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"
        ]
    }
]

Sub-resource: buffers / nicks

Return nicks in a buffer.

Endpoint:

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

Path parameters:

Request example: get nicks of a buffer:

curl -L -u 'plain:secret_password' \
  'https://localhost:9000/api/buffers/irc.libera.%23weechat/nicks'

Response:

HTTP/1.1 200 OK
{
    "id": 0,
    "parent_group_id": -1,
    "name": "root",
    "color": "",
    "visible": false,
    "groups": [
        {
            "id": 1709932823649181,
            "parent_group_id": 0,
            "name": "000|o",
            "color": "\u001b[32m",
            "visible": true,
            "groups": [],
            "nicks": [
                {
                    "id": 1709932823649184,
                    "parent_group_id": 1709932823649181,
                    "prefix": "@",
                    "prefix_color": "\u001b[92m",
                    "name": "alice",
                    "color": "",
                    "visible": true
                }
            ]
        },
        {
            "id": 1709932823649189,
            "parent_group_id": 0,
            "name": "001|h",
            "color": "\u001b[32m",
            "visible": true,
            "groups": [],
            "nicks": []
        },
        {
            "id": 1709932823649203,
            "parent_group_id": 0,
            "name": "002|v",
            "color": "\u001b[32m",
            "visible": true,
            "groups": [],
            "nicks": []
        },
        {
            "id": 1709932823649210,
            "parent_group_id": 0,
            "name": "999|...",
            "color": "\u001b[32m",
            "visible": true,
            "groups": [],
            "nicks": []
        }
    ],
    "nicks": []
}

Resource: hotlist

Return hotlist.

Endpoints:

GET /api/hotlist

Request example:

curl -L -u 'plain:secret_password' 'https://localhost:9000/api/hotlist'

Response:

HTTP/1.1 200 OK
[
    {
        "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

Send command or text to a buffer.

Endpoint:

POST /api/input

Body parameters:

Request example: say “hello!” on channel #weechat:

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

Response:

HTTP/1.1 204 No content

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

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

Response:

HTTP/1.1 204 No content

Resource: ping

Send a “ping” request.

Endpoint:

POST /api/ping

Body parameters:

Request example: no body:

curl -L -u 'plain:secret_password' -X POST 'https://localhost:9000/api/ping'

Response:

HTTP/1.1 204 No content

Request example: with data:

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

Response:

HTTP/1.1 200 OK
{
    "data": "1702835741"
}

Resource: sync

Start/stop synchronization of data with WeeChat.

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

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

Endpoint:

POST /api/sync

Body parameters:

Request example with websocket:

{
    "request": "POST /api/sync",
    "body": {
        "nicks": false
    }
}

Response:

{
    "code": 204,
    "message": "No Content"
}

Request example without websocket (error):

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

Response:

HTTP/1.1 403 Forbidden
{
    "error": "Sync resource is available only with a websocket connection"
}

Websocket

Websocket is used to make a persistent connection between the client and WeeChat and receive events in real-time if synchronization is enabled with Sync resource.

Authentication must be done only one time when a websocket is used (see Authentication).

Handshake

To establish the connection, a handshake is performed. The client’s handshake must be done on endpoint /api and looks like:

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

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).

The support of permessage-deflate is added, that’s why WeeChat includes it in the handshake reply.

Frames

When the client is connected via a websocket:

Requests to WeeChat are made with an object containing these fields:

Responses to client are made with an object containing these fields:

When the response has a body, these two extra fields are returned:

Body types that can be returned:

Request example: get version:

{
    "request": "GET /api/version"
}

Response:

{
    "code": 200,
    "message": "OK",
    "request": "GET /api/version",
    "request_body": null,
    "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:

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

Response:

{
    "code": 204,
    "message": "No Content",
    "request": "POST /api/input",
    "request_body": {
        "buffer_name": "irc.libera.#weechat",
        "command": "hello!"
    }
}

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 JSON sent has code set to 0, message set to Event and an extra object event with the following data:

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

Event Buffer id Description of data sent
buffer_opened -1 buffer with all lines
buffer_type_changed -1 buffer
buffer_moved -1 buffer
buffer_merged -1 buffer
buffer_unmerged -1 buffer
buffer_hidden -1 buffer
buffer_unhidden -1 buffer
buffer_renamed -1 buffer
buffer_title_changed -1 buffer
buffer_localvar_added -1 buffer
buffer_localvar_changed -1 buffer
buffer_localvar_removed -1 buffer
buffer_cleared -1 buffer
buffer_closing -1 buffer
buffer_line_added buffer id buffer line
upgrade -1 (no body)
upgrade_ended -1 (no body)
nicklist_group_changed buffer id nick group
nicklist_group_added buffer id nick group
nicklist_group_removing buffer id nick group
nicklist_nick_added buffer id nick
nicklist_nick_removing buffer id nick
nicklist_nick_changed buffer id nick

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

{
    "code": 0,
    "message": "Event",
    "event": {
        "name": "buffer_opened",
        "buffer_id": -1
    },
    "body_type": "buffer",
    "body": {
        "id": 1709932823649069,
        "name": "irc.libera.#test",
        "short_name": "",
        "number": 4,
        "type": "formatted",
        "title": "",
        "local_variables": {
            "plugin": "irc",
            "name": "libera.#test",
            "type": "channel",
            "nick": "alice",
            "host": "~alice@example.com",
            "server": "libera",
            "channel": "#test"
        },
        "lines": []
    }
}

Example: new line displayed on channel #weechat:

{
    "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 as operator in channel #weechat:

{
    "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": "\u001b[92m",
        "name": "bob",
        "color": "",
        "visible": true
    }
}

Example: WeeChat is upgrading:

{
    "code": 0,
    "message": "Event",
    "event": {
        "name": "upgrade",
        "buffer_id": -1
    }
}

Example: upgrade has been done:

{
    "code": 0,
    "message": "Event",
    "event": {
        "name": "upgrade_ended",
        "buffer_id": -1
    }
}

Remote

A new /remote command is added to manage and connect to remotes. A remote is another WeeChat running with a relay “api”.

Each remote is defined by:

Command /remote

[relay]  /remote  list|listfull [<name>]
                  add <name> <url> [-<option>[=<value>]]
                  connect <name>
                  send <name> <json>
                  disconnect <name>
                  rename <name> <new_name>
                  del <name>

control of remote relay servers

      list: list remote relay servers (without argument, this list is displayed)
  listfull: list remote relay servers (verbose)
       add: add a remote relay server
      name: name of remote relay server, for internal and display use; this name is used to connect to the remote relay and to set remote relay options: relay.remote.name.xxx
       url: URL of the remote relay, format is https://example.com:9000 or http://example.com:9000 (plain-text connection, not recommended)
    option: set option for remote relay: proxy, password or totp_secret
   connect: connect to a remote relay server
      send: send JSON data to a remote relay server
disconnect: disconnect from a remote relay server
    rename: rename a remote relay server
       del: delete a remote relay server

Examples:
  /remote add example https://localhost:9000 -password=my_secret_password -totp_secret=secrettotp
  /remote connect example
  /remote del example

Planning

The changes must be implemented in this order:

  1. Add “api” protocol with JSON support and plain-text password authentication
  2. Add support of websocket extension “permessage-deflate”
  3. Add support of hashed passwords (SHA and PBKDF2)
  4. Add “handshake” resource
  5. Add command /remote to manage and connect to remote WeeChat relay/api

References