From c6c420c698737cef188ea6b85def9c5e2ac575b3 Mon Sep 17 00:00:00 2001 From: Nils Date: Wed, 30 Oct 2024 15:58:29 +0100 Subject: [PATCH] relay: add completion resource --- src/plugins/relay/api/relay-api-msg.c | 66 ++++++++ src/plugins/relay/api/relay-api-msg.h | 1 + src/plugins/relay/api/relay-api-protocol.c | 159 ++++++++++++++++-- src/plugins/relay/api/relay-api-protocol.h | 1 + src/plugins/relay/api/relay-api.h | 2 +- src/plugins/relay/api/weechat-relay-api.yaml | 100 ++++++++++- .../plugins/relay/api/test-relay-api-msg.cpp | 72 ++++++++ .../relay/api/test-relay-api-protocol.cpp | 82 ++++++++- 8 files changed, 466 insertions(+), 17 deletions(-) diff --git a/src/plugins/relay/api/relay-api-msg.c b/src/plugins/relay/api/relay-api-msg.c index 3167551b1..2c0cd1ac5 100644 --- a/src/plugins/relay/api/relay-api-msg.c +++ b/src/plugins/relay/api/relay-api-msg.c @@ -721,6 +721,72 @@ relay_api_msg_nick_group_to_json (struct t_gui_nick_group *nick_group, return json; } +/* + * Creates a JSON object with a completion entry. + */ + +cJSON * +relay_api_msg_completion_to_json (struct t_gui_completion *completion) +{ + struct t_hdata *hdata; + struct t_gui_completion *pointer; + struct t_gui_completion_word *word; + const char *ptr_string; + struct t_arraylist *ptr_list; + cJSON *json, *json_array; + int context, i, size; + + hdata = relay_hdata_completion; + pointer = completion; + + json = cJSON_CreateObject (); + if (!json) + return NULL; + + if (!completion) + return json; + + ptr_list = weechat_hdata_pointer (relay_hdata_completion, completion, "list"); + if (!ptr_list) + return json; + + /* context */ + context = weechat_hdata_integer (relay_hdata_completion, completion, "context"); + switch (context) + { + case 0: + MSG_ADD_STR_PTR("context", "null"); + break; + case 1: + MSG_ADD_STR_PTR("context", "command"); + break; + case 2: + MSG_ADD_STR_PTR("context", "command_arg"); + break; + default: + MSG_ADD_STR_PTR("context", "auto"); + break; + } + + MSG_ADD_HDATA_STR("base_word", "base_word"); + MSG_ADD_HDATA_VAR(Number, "position_replace", integer, "position_replace"); + MSG_ADD_HDATA_VAR(Bool, "add_space", integer, "add_space"); + + json_array = cJSON_CreateArray (); + size = weechat_arraylist_size (ptr_list); + for (i = 0; i < size; i++) + { + word = (struct t_gui_completion_word *)weechat_arraylist_get (ptr_list, i); + cJSON_AddItemToArray ( + json_array, + cJSON_CreateString ( + weechat_hdata_string (relay_hdata_completion_word, word, "word"))); + } + cJSON_AddItemToObject (json, "list", json_array); + + return json; +} + /* * Creates a JSON object with a hotlist entry. */ diff --git a/src/plugins/relay/api/relay-api-msg.h b/src/plugins/relay/api/relay-api-msg.h index 8d171ef78..527a2f0b4 100644 --- a/src/plugins/relay/api/relay-api-msg.h +++ b/src/plugins/relay/api/relay-api-msg.h @@ -54,6 +54,7 @@ extern cJSON *relay_api_msg_nick_to_json (struct t_gui_nick *nick, enum t_relay_api_colors colors); extern cJSON *relay_api_msg_nick_group_to_json (struct t_gui_nick_group *nick_group, enum t_relay_api_colors colors); +extern cJSON *relay_api_msg_completion_to_json (struct t_gui_completion *completion); extern cJSON *relay_api_msg_hotlist_to_json (struct t_gui_hotlist *hotlist); #endif /* WEECHAT_PLUGIN_RELAY_API_MSG_H */ diff --git a/src/plugins/relay/api/relay-api-protocol.c b/src/plugins/relay/api/relay-api-protocol.c index 10fe0d501..e4390fa6c 100644 --- a/src/plugins/relay/api/relay-api-protocol.c +++ b/src/plugins/relay/api/relay-api-protocol.c @@ -713,6 +713,7 @@ RELAY_API_PROTOCOL_CALLBACK(input) if (!json_body) return RELAY_API_PROTOCOL_RC_BAD_REQUEST; + /* get buffer either by id or by name */ ptr_buffer = NULL; json_buffer_id = cJSON_GetObjectItem (json_body, "buffer_id"); if (json_buffer_id) @@ -726,7 +727,7 @@ RELAY_API_PROTOCOL_CALLBACK(input) { relay_api_msg_send_error_json ( client, - RELAY_HTTP_404_NOT_FOUND, NULL, + RELAY_HTTP_400_BAD_REQUEST, NULL, "Buffer \"%lld\" not found", (long long)cJSON_GetNumberValue (json_buffer_id)); cJSON_Delete (json_body); @@ -747,7 +748,7 @@ RELAY_API_PROTOCOL_CALLBACK(input) { relay_api_msg_send_error_json ( client, - RELAY_HTTP_404_NOT_FOUND, NULL, + RELAY_HTTP_400_BAD_REQUEST, NULL, "Buffer \"%s\" not found", ptr_buffer_name); cJSON_Delete (json_body); @@ -819,6 +820,138 @@ RELAY_API_PROTOCOL_CALLBACK(input) return RELAY_API_PROTOCOL_RC_OK; } +/* + * Callback for resource "completion". + * + * Routes: + * POST /api/completion + */ + +RELAY_API_PROTOCOL_CALLBACK(completion) +{ + cJSON *json_response, *json_body; + cJSON *json_buffer_id, *json_buffer_name; + cJSON *json_command, *json_position; + const char *ptr_buffer_name, *ptr_command; + int position; + char str_id[64]; + struct t_gui_completion *ptr_completion; + struct t_gui_buffer *ptr_buffer; + + json_body = cJSON_Parse (client->http_req->body); + if (!json_body) + return RELAY_API_PROTOCOL_RC_BAD_REQUEST; + + /* get buffer either by id or by name */ + ptr_buffer = NULL; + json_buffer_id = cJSON_GetObjectItem (json_body, "buffer_id"); + if (json_buffer_id) + { + if (cJSON_IsNumber (json_buffer_id)) + { + snprintf (str_id, sizeof(str_id), + "%lld", (long long)cJSON_GetNumberValue (json_buffer_id)); + ptr_buffer = weechat_buffer_search ("==id", str_id); + if (!ptr_buffer) + { + relay_api_msg_send_error_json ( + client, + RELAY_HTTP_400_BAD_REQUEST, NULL, + "Buffer \"%lld\" not found", + (long long)cJSON_GetNumberValue (json_buffer_id)); + cJSON_Delete (json_body); + return RELAY_API_PROTOCOL_RC_BAD_REQUEST; + } + } + } + else + { + json_buffer_name = cJSON_GetObjectItem (json_body, "buffer_name"); + if (json_buffer_name) + { + if (cJSON_IsString (json_buffer_name)) + { + ptr_buffer_name = cJSON_GetStringValue (json_buffer_name); + ptr_buffer = weechat_buffer_search ("==", ptr_buffer_name); + if (!ptr_buffer) + { + relay_api_msg_send_error_json ( + client, + RELAY_HTTP_400_BAD_REQUEST, NULL, + "Buffer \"%s\" not found", + ptr_buffer_name); + cJSON_Delete (json_body); + return RELAY_API_PROTOCOL_RC_BAD_REQUEST; + } + } + } + else + { + ptr_buffer = weechat_buffer_search_main (); + } + } + if (!ptr_buffer) + { + cJSON_Delete (json_body); + return RELAY_API_PROTOCOL_RC_BAD_REQUEST; + } + + /* get command and position (optional) from input json object */ + json_command = cJSON_GetObjectItem (json_body, "command"); + if (json_command && cJSON_IsString (json_command)) + { + ptr_command = cJSON_GetStringValue (json_command); + } + else + { + cJSON_Delete (json_body); + return RELAY_API_PROTOCOL_RC_BAD_REQUEST; + } + json_position = cJSON_GetObjectItem (json_body, "position"); + if (json_position) + { + if (cJSON_IsNumber (json_position)) + { + position = cJSON_GetNumberValue (json_position); + } + else + { + cJSON_Delete (json_body); + return RELAY_API_PROTOCOL_RC_BAD_REQUEST; + } + } + else + { + position = strlen (ptr_command); + } + + /* perform completion */ + ptr_completion = weechat_completion_new (ptr_buffer); + if (!ptr_completion) + { + cJSON_Delete (json_body); + return RELAY_API_PROTOCOL_RC_MEMORY; + } + + if (!weechat_completion_search (ptr_completion, ptr_command, position, 1)) + { + weechat_completion_free (ptr_completion); + cJSON_Delete (json_body); + return RELAY_API_PROTOCOL_RC_BAD_REQUEST; + } + + /* create response */ + json_response = relay_api_msg_completion_to_json (ptr_completion); + relay_api_msg_send_json (client, RELAY_HTTP_200_OK, NULL, "completion", + json_response); + + cJSON_Delete (json_response); + cJSON_Delete (json_body); + weechat_completion_free (ptr_completion); + + return RELAY_API_PROTOCOL_RC_OK; +} + /* * Callback for resource "ping". * @@ -1062,16 +1195,18 @@ relay_api_protocol_recv_http (struct t_relay_client *client) int i, num_args; enum t_relay_api_protocol_rc return_code; struct t_relay_api_protocol_cb protocol_cb[] = { - /* method, resource, auth, min args, max args, callback */ - { "OPTIONS", "*", 0, 0, -1, &relay_api_protocol_cb_options }, - { "POST", "handshake", 0, 0, 0, &relay_api_protocol_cb_handshake }, - { "GET", "version", 1, 0, 0, &relay_api_protocol_cb_version }, - { "GET", "buffers", 1, 0, 3, &relay_api_protocol_cb_buffers }, - { "GET", "hotlist", 1, 0, 3, &relay_api_protocol_cb_hotlist }, - { "POST", "input", 1, 0, 0, &relay_api_protocol_cb_input }, - { "POST", "ping", 1, 0, 0, &relay_api_protocol_cb_ping }, - { "POST", "sync", 1, 0, 0, &relay_api_protocol_cb_sync }, - { NULL, NULL, 0, 0, 0, NULL }, + /* method, resource, auth, args, callback */ + /* min,max */ + { "OPTIONS", "*", 0, 0, -1, RELAY_API_CB(options) }, + { "POST", "handshake", 0, 0, 0, RELAY_API_CB(handshake) }, + { "GET", "version", 1, 0, 0, RELAY_API_CB(version) }, + { "GET", "buffers", 1, 0, 3, RELAY_API_CB(buffers) }, + { "GET", "hotlist", 1, 0, 3, RELAY_API_CB(hotlist) }, + { "POST", "input", 1, 0, 0, RELAY_API_CB(input) }, + { "POST", "completion", 1, 0, 0, RELAY_API_CB(completion) }, + { "POST", "ping", 1, 0, 0, RELAY_API_CB(ping) }, + { "POST", "sync", 1, 0, 0, RELAY_API_CB(sync) }, + { NULL, NULL, 0, 0, 0, NULL }, }; if (!client->http_req || RELAY_STATUS_HAS_ENDED(client->status)) diff --git a/src/plugins/relay/api/relay-api-protocol.h b/src/plugins/relay/api/relay-api-protocol.h index 7da6932f7..96987c3b4 100644 --- a/src/plugins/relay/api/relay-api-protocol.h +++ b/src/plugins/relay/api/relay-api-protocol.h @@ -20,6 +20,7 @@ #ifndef WEECHAT_PLUGIN_RELAY_API_PROTOCOL_H #define WEECHAT_PLUGIN_RELAY_API_PROTOCOL_H +#define RELAY_API_CB(__command) &relay_api_protocol_cb_##__command #define RELAY_API_PROTOCOL_CALLBACK(__command) \ enum t_relay_api_protocol_rc \ relay_api_protocol_cb_##__command (struct t_relay_client *client) diff --git a/src/plugins/relay/api/relay-api.h b/src/plugins/relay/api/relay-api.h index 4476bd82b..c0adb6109 100644 --- a/src/plugins/relay/api/relay-api.h +++ b/src/plugins/relay/api/relay-api.h @@ -24,7 +24,7 @@ struct t_relay_client; enum t_relay_status; #define RELAY_API_VERSION_MAJOR 0 -#define RELAY_API_VERSION_MINOR 3 +#define RELAY_API_VERSION_MINOR 4 #define RELAY_API_VERSION_PATCH 0 #define RELAY_API_VERSION_NUMBER \ ((RELAY_API_VERSION_MAJOR << 16) \ diff --git a/src/plugins/relay/api/weechat-relay-api.yaml b/src/plugins/relay/api/weechat-relay-api.yaml index 3ccb53aea..ad1f86b03 100644 --- a/src/plugins/relay/api/weechat-relay-api.yaml +++ b/src/plugins/relay/api/weechat-relay-api.yaml @@ -13,7 +13,7 @@ info: license: name: CC BY-NC-SA 4.0 url: https://creativecommons.org/licenses/by-nc-sa/4.0/ - version: 0.3.0 + version: 0.4.0 externalDocs: url: https://weechat.org/doc/ @@ -29,6 +29,7 @@ tags: - name: buffers - name: hotlist - name: input + - name: completion - name: ping - name: sync @@ -367,8 +368,30 @@ paths: description: Bad request '401': description: Unauthorized - '404': - description: Buffer not found + security: + - password: [] + /completion: + post: + tags: + - completion + description: | + Complete user command or text. + operationId: input + parameters: + - $ref: '#/components/parameters/totp' + requestBody: + $ref: '#/components/requestBodies/CompletionBody' + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Completion' + '400': + description: Bad request + '401': + description: Unauthorized security: - password: [] /ping: @@ -944,6 +967,42 @@ components: - date - buffer_id - count + Completion: + type: object + properties: + context: + type: string + enum: + - 'null' + - 'command' + - 'command_arg' + - 'auto' + example: 'command_arg' + base_word: + type: string + example: 'qu' + position_replace: + type: integer + format: int32 + example: 1 + add_space: + type: boolean + example: true + list: + type: array + items: + type: string + example: + - 'query' + - 'quiet' + - 'quit' + - 'quote' + required: + - context + - base_word + - position_replace + - add_space + - list Ping: type: object properties: @@ -1002,6 +1061,41 @@ components: example: 'hello, world!' required: - command + example: + buffer_id: 1709932823238637 + command: 'hello, world!' + CompletionBody: + description: Buffer and user text/command to complete + required: true + content: + application/json: + schema: + type: object + properties: + buffer_id: + type: integer + format: int64 + description: Buffer identifier (≥ 0) + example: 1709932823238637 + buffer_name: + type: string + description: >- + Buffer full name + example: 'irc.libera.#weechat' + command: + type: string + description: user command or text to complete + example: '/qu' + position: + type: integer + format: int32 + description: Position in data (≥ 0) + example: 3 + required: + - data + example: + buffer_id: 1709932823238637 + command: '/qu' PingBody: description: Custom data that will be returned in the response required: false diff --git a/tests/unit/plugins/relay/api/test-relay-api-msg.cpp b/tests/unit/plugins/relay/api/test-relay-api-msg.cpp index b7880cb9d..005f5b962 100644 --- a/tests/unit/plugins/relay/api/test-relay-api-msg.cpp +++ b/tests/unit/plugins/relay/api/test-relay-api-msg.cpp @@ -32,6 +32,7 @@ extern "C" #include "src/gui/gui-chat.h" #include "src/gui/gui-color.h" #include "src/gui/gui-hotlist.h" +#include "src/gui/gui-input.h" #include "src/gui/gui-line.h" #include "src/gui/gui-nicklist.h" #include "src/plugins/relay/relay.h" @@ -514,6 +515,77 @@ TEST(RelayApiMsg, LinesToJson) cJSON_Delete (json); } +/* + * Tests functions: + * relay_api_msg_completion_to_json + */ + +TEST(RelayApiMsg, CompletionToJson) +{ + cJSON *json, *json_obj, *json_item; + + // check empty json result + json = relay_api_msg_completion_to_json (NULL); + CHECK(json); + CHECK(cJSON_IsObject (json)); + POINTERS_EQUAL(NULL, cJSON_GetObjectItem (json, "priority")); + cJSON_Delete (json); + + // set example input + gui_buffer_set (gui_buffers, "input", "/co"); + gui_buffer_set (gui_buffers, "input_pos", "3"); + + // perform completion + gui_input_complete_next (gui_buffers); + STRCMP_EQUAL("/color ", gui_buffers->input_buffer); + + // convert to json + json = relay_api_msg_completion_to_json (gui_buffers->completion); + CHECK(json); + CHECK(cJSON_IsObject (json)); + + json_obj = cJSON_GetObjectItem (json, "context"); + CHECK(json_obj); + CHECK(cJSON_IsString (json_obj)); + STRCMP_EQUAL("command", cJSON_GetStringValue (json_obj)); + + json_obj = cJSON_GetObjectItem (json, "base_word"); + CHECK(json_obj); + CHECK(cJSON_IsString (json_obj)); + STRCMP_EQUAL("co", cJSON_GetStringValue (json_obj)); + + json_obj = cJSON_GetObjectItem (json, "position_replace"); + CHECK(json_obj); + CHECK(cJSON_IsNumber (json_obj)); + CHECK_EQUAL(1, cJSON_GetNumberValue (json_obj)); + + json_obj = cJSON_GetObjectItem (json, "add_space"); + CHECK(json_obj); + CHECK(cJSON_IsBool (json_obj)); + CHECK(cJSON_IsTrue (json_obj)); + + json_obj = cJSON_GetObjectItem (json, "list"); + CHECK(json_obj); + CHECK(cJSON_IsArray (json_obj)); + CHECK_EQUAL(3, cJSON_GetArraySize (json_obj)); + json_item = cJSON_GetArrayItem (json_obj, 0); + CHECK(json_item); + CHECK(cJSON_IsString (json_item)); + STRCMP_EQUAL("color", cJSON_GetStringValue (json_item)); + json_item = cJSON_GetArrayItem (json_obj, 1); + CHECK(json_item); + CHECK(cJSON_IsString (json_item)); + STRCMP_EQUAL("command", cJSON_GetStringValue (json_item)); + json_item = cJSON_GetArrayItem (json_obj, 2); + CHECK(json_item); + CHECK(cJSON_IsString (json_item)); + STRCMP_EQUAL("connect", cJSON_GetStringValue (json_item)); + + cJSON_Delete (json); + + gui_buffer_set (gui_buffers, "input", ""); +} + /* * Tests functions: * relay_api_msg_hotlist_to_json diff --git a/tests/unit/plugins/relay/api/test-relay-api-protocol.cpp b/tests/unit/plugins/relay/api/test-relay-api-protocol.cpp index fe48dd35c..e6c63ac22 100644 --- a/tests/unit/plugins/relay/api/test-relay-api-protocol.cpp +++ b/tests/unit/plugins/relay/api/test-relay-api-protocol.cpp @@ -598,6 +598,86 @@ TEST(RelayApiProtocolWithClient, CbHotlist) gui_hotlist_remove_buffer (gui_buffers, 1); } +/* + * Tests functions: + * relay_api_protocol_cb_completion + */ + +TEST(RelayApiProtocolWithClient, CbCompletion) +{ + cJSON *json, *json_obj, *json_array; + + /* error: no body */ + test_client_recv_http ("POST /api/completion", NULL, NULL); + WEE_CHECK_HTTP_CODE(400, "Bad Request"); + STRCMP_EQUAL("HTTP/1.1 400 Bad Request\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Content-Type: application/json; charset=utf-8\r\n" + "Content-Length: 0\r\n" + "\r\n", + data_sent[0]); + + /* error: invalid buffer name */ + test_client_recv_http ("POST /api/completion", + NULL, + "{\"buffer_name\": \"invalid\", " + "\"command\": \"test\"}"); + WEE_CHECK_HTTP_CODE(400, "Bad Request"); + STRCMP_EQUAL("HTTP/1.1 400 Bad Request\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Content-Type: application/json; charset=utf-8\r\n" + "Content-Length: 41\r\n" + "\r\n" + "{\"error\": \"Buffer \\\"invalid\\\" not found\"}", + data_sent[0]); + + /* on core buffer, with buffer name. examples from relay protocol examples: + * https://weechat.org/files/doc/weechat/stable/weechat_relay_weechat.en.html#command_completion + */ + + /* completion core.weechat -1 /help fi */ + test_client_recv_http ("POST /api/completion", + NULL, + "{\"buffer_name\": \"core.weechat\", " + "\"command\": \"/help fi\"}"); + WEE_CHECK_HTTP_CODE(200, "OK"); + json = json_body_sent[0]; + CHECK(json); + CHECK(cJSON_IsObject (json)); + WEE_CHECK_OBJ_STR("command_arg", json, "context"); + WEE_CHECK_OBJ_STR("fi", json, "base_word"); + WEE_CHECK_OBJ_NUM(6, json, "position_replace"); + WEE_CHECK_OBJ_BOOL(0, json, "add_space"); + json_array = cJSON_GetObjectItem (json, "list"); + CHECK(json_array); + CHECK(cJSON_IsArray (json_array)); + CHECK(cJSON_GetArraySize (json_array) == 4); + STRCMP_EQUAL("fifo", cJSON_GetStringValue (cJSON_GetArrayItem (json_array, 0))); + STRCMP_EQUAL("fifo.file.enabled", cJSON_GetStringValue (cJSON_GetArrayItem (json_array, 1))); + STRCMP_EQUAL("fifo.file.path", cJSON_GetStringValue (cJSON_GetArrayItem (json_array, 2))); + STRCMP_EQUAL("filter", cJSON_GetStringValue (cJSON_GetArrayItem (json_array, 3))); + + /* completion core.weechat 5 /quernick */ + test_client_recv_http ("POST /api/completion", + NULL, + "{\"buffer_name\": \"core.weechat\", " + "\"command\": \"/quernick\", " + "\"position\": 5}"); + WEE_CHECK_HTTP_CODE(200, "OK"); + json = json_body_sent[0]; + CHECK(json); + CHECK(cJSON_IsObject (json)); + WEE_CHECK_OBJ_STR("command", json, "context"); + WEE_CHECK_OBJ_STR("quer", json, "base_word"); + WEE_CHECK_OBJ_NUM(1, json, "position_replace"); + WEE_CHECK_OBJ_BOOL(1, json, "add_space"); + json_array = cJSON_GetObjectItem (json, "list"); + CHECK(json_array); + CHECK(cJSON_IsArray (json_array)); + CHECK(cJSON_GetArraySize (json_array) == 1); + STRCMP_EQUAL("query", cJSON_GetStringValue (cJSON_GetArrayItem (json_array, 0))); +} + /* * Tests functions: * relay_api_protocol_cb_input @@ -622,7 +702,7 @@ TEST(RelayApiProtocolWithClient, CbInput) NULL, "{\"buffer_name\": \"invalid\", " "\"command\": \"/print test\"}"); - STRCMP_EQUAL("HTTP/1.1 404 Not Found\r\n" + STRCMP_EQUAL("HTTP/1.1 400 Bad Request\r\n" "Access-Control-Allow-Origin: *\r\n" "Content-Type: application/json; charset=utf-8\r\n" "Content-Length: 41\r\n"