feat: new JSON-RPC 2.0 RPC API (#7269)

* feat: add enum for JSON-RPC error codes

* feat: new `tr_rpc_request_exec()` overload that accepts string

* feat: add JSON-RPC parse error handling

* feat: add logic for branching to JSON-RPC or legacy API

* feat: error codes for existing errors strings

* refactor: async handlers now take the done cb as parameter

* feat: support non-batch JSON-RPC requests

* feat: support batch JSON-RPC requests

* refactor: move JSON-RPC error codes to header

* test: new tests for JSON-RPC

* refactor(webui): use jsonrpc api

* docs: update docs for jsonrpc

* fix: clang-tidy warning

* perf: avoid copying callback in batch mode

* code review: don't commit to dropping old RPC

* chore: fix shadowed variable warnings
This commit is contained in:
Yat Ho
2025-12-01 00:04:40 +08:00
committed by GitHub
parent d64a1a5699
commit 1cb24a701b
16 changed files with 1410 additions and 445 deletions

View File

@@ -1,8 +1,16 @@
> [!IMPORTANT]
> Transmisson 4.1.0 (`rpc-version` 18) added support for the JSON-RPC 2.0 protocol.
>
> The old bespoke RPC protocol is still supported in Transmission 4 but is deprecated and will be removed in the future. People using the old protocol should update their code!
>
> For documentation of the old RPC protocol, please consult documentation from previous versions.
> https://github.com/transmission/transmission/blob/4.0.6/docs/rpc-spec.md
# Transmission's RPC specification
This document describes a protocol for interacting with Transmission sessions remotely.
### 1.1 Terminology
The [JSON](https://www.json.org/) terminology in [RFC 4627](https://datatracker.ietf.org/doc/html/rfc4627) is used.
The [JSON](https://www.json.org/) terminology in [RFC 8259](https://datatracker.ietf.org/doc/html/rfc8259) is used.
RPC requests and responses are formatted in JSON.
### 1.2 Tools
@@ -24,48 +32,61 @@ Some people outside of the Transmission project have written libraries that wrap
## 2 Message format
Messages are formatted as objects. There are two types: requests (described in [section 2.1](#21-requests)) and responses (described in [section 2.2](#22-responses)).
Transmission follows the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) specification and supports the entirety of it,
except that parameters by-position is not supported, meaning the request parameters must be an Object.
All text **must** be UTF-8 encoded.
### 2.1 Requests
Requests support three keys:
1. A required `method` string telling the name of the method to invoke
2. An optional `arguments` object of key/value pairs. The keys allowed are defined by the `method`.
3. An optional `tag` number used by clients to track responses. If provided by a request, the response MUST include the same tag.
Response parameters are returned in the `result` Object.
#### Example request
```json
{
"arguments": {
"fields": [
"version"
]
"jsonrpc": "2.0",
"params": {
"fields": [ "version" ]
},
"method": "session-get",
"tag": 912313
"id": 912313
}
```
#### Example response
```json
{
"jsonrpc": "2.0",
"result": {
"version": "4.1.0-dev (ae226418eb)"
},
"id": 912313
}
```
### 2.2 Responses
Responses to a request will include:
### 2.1 Error data
1. A required `result` string whose value MUST be `success` on success, or an error string on failure.
2. An optional `arguments` object of key/value pairs. Its keys contents are defined by the `method` and `arguments` of the original request.
3. An optional `tag` number as described in 2.1.
JSON-RPC 2.0 allows for additional information about an error be included in the `data` key of the Error object in an implementation-defined format.
In Transmission, this key is an Object that includes:
1. An optional `errorString` string that provides additional information that is not included in the `message` key of the Error object.
2. An optional `result` Object that contains additional keys defined by the method.
```json
{
"arguments": {
"version": "2.93 (3c5870d4f5)"
"jsonrpc": "2.0",
"error": {
"code": 7,
"message": "HTTP error from backend service",
"data": {
"errorString": "Couldn't test port: No Response (0)",
"result": {
"ipProtocol": "ipv6"
}
}
},
"result": "success",
"tag": 912313
"id": 912313
}
```
### 2.3 Transport mechanism
### 2.2 Transport mechanism
HTTP POSTing a JSON-encoded request is the preferred way of communicating
with a Transmission RPC server. The current Transmission implementation
has the default URL as `http://host:9091/transmission/rpc`. Clients
@@ -73,7 +94,10 @@ may use this as a default, but should allow the URL to be reconfigured,
since the port and path may be changed to allow mapping and/or multiple
daemons to run on a single server.
#### 2.3.1 CSRF protection
The RPC server will normally return HTTP 200 regardless of whether the
request succeeded. For JSON-RPC 2.0 notifications, HTTP 204 will be returned.
#### 2.2.1 CSRF protection
Most Transmission RPC servers require a `X-Transmission-Session-Id`
header to be sent with requests, to prevent CSRF attacks.
@@ -85,7 +109,7 @@ right `X-Transmission-Session-Id` in its own headers.
So, the correct way to handle a 409 response is to update your
`X-Transmission-Session-Id` and to resend the previous request.
#### 2.3.2 DNS rebinding protection
#### 2.2.2 DNS rebinding protection
Additional check is being made on each RPC request to make sure that the
client sending the request does so using one of the allowed hostnames by
which RPC server is meant to be available.
@@ -99,7 +123,7 @@ addresses are always implicitly allowed.
For more information on configuration, see settings.json documentation for
`rpc-host-whitelist-enabled` and `rpc-host-whitelist` keys.
#### 2.3.3 Authentication
#### 2.2.3 Authentication
Enabling authentication is an optional security feature that can be enabled
on Transmission RPC servers. Authentication occurs by method of HTTP Basic
Access Authentication.
@@ -120,8 +144,8 @@ username and password (respectively), separated by a colon.
| `torrent-verify` | tr_torrentVerify | verify torrent
| `torrent-reannounce` | tr_torrentManualUpdate | re-announce to trackers now
Request arguments: `ids`, which specifies which torrents to use.
All torrents are used if the `ids` argument is omitted.
Request parameters: `ids`, which specifies which torrents to use.
All torrents are used if the `ids` parameter is omitted.
`ids` should be one of the following:
@@ -132,12 +156,12 @@ All torrents are used if the `ids` argument is omitted.
Note that integer torrent ids are not stable across Transmission daemon
restarts. Use torrent hashes if you need stable ids.
Response arguments: none
Response parameters: none
### 3.2 Torrent mutator: `torrent-set`
Method name: `torrent-set`
Request arguments:
Request parameters:
| Key | Value Type | Value Description
|:--|:--|:--
@@ -173,20 +197,20 @@ Just as an empty `ids` value is shorthand for "all ids", using an empty array
for `files-wanted`, `files-unwanted`, `priority-high`, `priority-low`, or
`priority-normal` is shorthand for saying "all files".
Response arguments: none
Response parameters: none
### 3.3 Torrent accessor: `torrent-get`
Method name: `torrent-get`.
Request arguments:
Request parameters:
1. An optional `ids` array as described in 3.1.
2. A required `fields` array of keys. (see list below)
3. An optional `format` string specifying how to format the
`torrents` response field. Allowed values are `objects`
(default) and `table`. (see "Response arguments" below)
(default) and `table`. (see "Response parameters" below)
Response arguments:
Response parameters:
1. A `torrents` array.
@@ -426,12 +450,13 @@ Request:
```json
{
"arguments": {
"jsonrpc": "2.0",
"params": {
"fields": [ "id", "name", "totalSize" ],
"ids": [ 7, 10 ]
},
"method": "torrent-get",
"tag": 39693
"id": 39693
}
```
@@ -439,7 +464,8 @@ Response:
```json
{
"arguments": {
"jsonrpc": "2.0",
"result": {
"torrents": [
{
"id": 10,
@@ -453,15 +479,14 @@ Response:
}
]
},
"result": "success",
"tag": 39693
"id": 39693
}
```
### 3.4 Adding a torrent
Method name: `torrent-add`
Request arguments:
Request parameters:
| Key | Value Type | Description
|:--|:--|:--
@@ -481,11 +506,11 @@ Request arguments:
| `sequential_download` | boolean | download torrent pieces sequentially
| `sequential_download_from_piece` | number | download from a specific piece when sequential download is enabled
Either `filename` **or** `metainfo` **must** be included. All other arguments are optional.
Either `filename` **or** `metainfo` **must** be included. All other parameters are optional.
The format of the `cookies` should be `NAME=CONTENTS`, where `NAME` is the cookie name and `CONTENTS` is what the cookie should contain. Set multiple cookies like this: `name1=content1; name2=content2;` etc. See [libcurl documentation](http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTCOOKIE) for more information.
Response arguments:
Response parameters:
* On success, a `torrent-added` object in the form of one of 3.3's torrent objects with the fields for `id`, `name`, and `hashString`.
@@ -499,12 +524,12 @@ Method name: `torrent-remove`
| `ids` | array | torrent list, as described in 3.1
| `delete-local-data` | boolean | delete local data. (default: false)
Response arguments: none
Response parameters: none
### 3.6 Moving a torrent
Method name: `torrent-set-location`
Request arguments:
Request parameters:
| Key | Value Type | Description
|:--|:--|:--
@@ -512,7 +537,7 @@ Request arguments:
| `location` | string | the new torrent location
| `move` | boolean | if true, move from previous location. otherwise, search "location" for files (default: false)
Response arguments: none
Response parameters: none
### 3.7 Renaming a torrent's path
Method name: `torrent-rename-path`
@@ -522,7 +547,7 @@ documentation of `tr_torrentRenamePath()`. In particular, note that if this
call succeeds you'll want to update the torrent's `files` and `name` field
with `torrent-get`.
Request arguments:
Request parameters:
| Key | Value Type | Description
|:--|:--|:--
@@ -530,10 +555,10 @@ Request arguments:
| `path` | string | the path to the file or folder that will be renamed
| `name` | string | the file or folder's new name
Response arguments: `path`, `name`, and `id`, holding the torrent ID integer
Response parameters: `path`, `name`, and `id`, holding the torrent ID integer
## 4 Session requests
### 4.1 Session arguments
### 4.1 Session parameters
| Key | Value Type | Description
|:--|:--|:--
| `alt-speed-down` | number | max global download speed (kB/s)
@@ -619,7 +644,7 @@ to be common behavior.
#### 4.1.1 Mutators
Method name: `session-set`
Request arguments: the mutable properties from 4.1's arguments, i.e. all of them
Request parameters: the mutable properties from 4.1's parameters, i.e. all of them
except:
* `blocklist-size`
@@ -631,22 +656,22 @@ except:
* `units`
* `version`
Response arguments: none
Response parameters: none
#### 4.1.2 Accessors
Method name: `session-get`
Request arguments: an optional `fields` array of keys (see 4.1)
Request parameters: an optional `fields` array of keys (see 4.1)
Response arguments: key/value pairs matching the request's `fields`
argument if present, or all supported fields (see 4.1) otherwise.
Response parameters: key/value pairs matching the request's `fields`
parameter if present, or all supported fields (see 4.1) otherwise.
### 4.2 Session statistics
Method name: `session-stats`
Request arguments: none
Request parameters: none
Response arguments:
Response parameters:
| Key | Value Type | Description
|:--|:--|:--
@@ -671,9 +696,9 @@ A stats object contains:
### 4.3 Blocklist
Method name: `blocklist-update`
Request arguments: none
Request parameters: none
Response arguments: a number `blocklist-size`
Response parameters: a number `blocklist-size`
### 4.4 Port checking
This method tests to see if your incoming peer port is accessible
@@ -681,14 +706,14 @@ from the outside world.
Method name: `port-test`
Request arguments: an optional argument `ip_protocol`.
Request parameters: an optional parameter `ip_protocol`.
`ip_protocol` is a string specifying the IP protocol version to be used for the port test.
Set to `ipv4` to check IPv4, or set to `ipv6` to check IPv6.
For backwards compatibility, it is allowed to omit this argument to get the behaviour before Transmission `4.1.0`,
For backwards compatibility, it is allowed to omit this parameter to get the behaviour before Transmission `4.1.0`,
which is to check whichever IP protocol the OS happened to use to connect to our port test service,
frankly not very useful.
Response arguments:
Response parameters:
| Key | Value Type | Description
| :-- | :-- | :--
@@ -696,13 +721,13 @@ Response arguments:
| `ip_protocol` | string | `ipv4` if the test was carried out on IPv4, `ipv6` if the test was carried out on IPv6, unset if it cannot be determined
### 4.5 Session shutdown
This method tells the transmission session to shut down.
This method tells the Transmission session to shut down.
Method name: `session-close`
Request arguments: none
Request parameters: none
Response arguments: none
Response parameters: none
### 4.6 Queue movement requests
| Method name | transmission.h source
@@ -712,13 +737,13 @@ Response arguments: none
| `queue-move-down` | tr_torrentQueueMoveDown()
| `queue-move-bottom` | tr_torrentQueueMoveBottom()
Request arguments:
Request parameters:
| Key | Value Type | Description
|:--|:--|:--
| `ids` | array | torrent list, as described in 3.1.
Response arguments: none
Response parameters: none
### 4.7 Free space
This method tests how much free space is available in a
@@ -726,17 +751,17 @@ client-specified folder.
Method name: `free-space`
Request arguments:
Request parameters:
| Key | Value type | Description
|:--|:--|:--
| `path` | string | the directory to query
Response arguments:
Response parameters:
| Key | Value type | Description
|:--|:--|:--
| `path` | string | same as the Request argument
| `path` | string | same as the Request parameter
| `size-bytes` | number | the size, in bytes, of the free space in that directory
| `total_size` | number | the total capacity, in bytes, of that directory
@@ -755,17 +780,17 @@ Request parameters:
| `speed-limit-up-enabled` | boolean | true means enabled
| `speed-limit-up` | number | max global upload speed (kB/s)
Response arguments: none
Response parameters: none
#### 4.8.2 Bandwidth group accessor: `group-get`
Method name: `group-get`
Request arguments: An optional argument `group`.
Request parameters: An optional parameter `group`.
`group` is either a string naming the bandwidth group,
or a list of such strings.
If `group` is omitted, all bandwidth groups are used.
Response arguments:
Response parameters:
| Key | Value type | Description
|:--|:--|:--
@@ -1037,7 +1062,10 @@ Transmission 4.0.0 (`rpc-version-semver` 5.3.0, `rpc-version`: 17)
| `group-get` | new method
| `torrent-get` | :warning: old arg `wanted` was implemented as an array of `0` or `1` in Transmission 3.00 and older, despite being documented as an array of booleans. Transmission 4.0.0 and 4.0.1 "fixed" this by returning an array of booleans; but in practical terms, this change caused an unannounced breaking change for any 3rd party code that expected `0` or `1`. For this reason, 4.0.2 restored the 3.00 behavior and updated this spec to match the code.
Transmission 4.1.0 (`rpc-version-semver` 5.4.0, `rpc-version`: 18)
Transmission 4.1.0 (`rpc-version-semver` 6.0.0, `rpc-version`: 18)
:bomb: switch to the JSON-RPC 2.0 protocol
| Method | Description
|:---|:---
| `session-get` | new arg `sequential_download`

View File

@@ -65,6 +65,7 @@ auto constexpr MyStatic = std::array<std::string_view, TR_N_KEYS>{
"clientIsChoked"sv,
"clientIsInterested"sv,
"clientName"sv,
"code"sv,
"comment"sv,
"compact-view"sv,
"complete"sv,
@@ -77,6 +78,7 @@ auto constexpr MyStatic = std::array<std::string_view, TR_N_KEYS>{
"creator"sv,
"cumulative-stats"sv,
"current-stats"sv,
"data"sv,
"date"sv,
"dateCreated"sv,
"default-trackers"sv,
@@ -170,6 +172,7 @@ auto constexpr MyStatic = std::array<std::string_view, TR_N_KEYS>{
"isStalled"sv,
"isUTP"sv,
"isUploadingTo"sv,
"jsonrpc"sv,
"labels"sv,
"lastAnnouncePeerCount"sv,
"lastAnnounceResult"sv,
@@ -200,6 +203,7 @@ auto constexpr MyStatic = std::array<std::string_view, TR_N_KEYS>{
"maxConnectedPeers"sv,
"memory-bytes"sv,
"memory-units"sv,
"message"sv,
"message-level"sv,
"metadataPercentComplete"sv,
"metadata_size"sv,
@@ -215,6 +219,7 @@ auto constexpr MyStatic = std::array<std::string_view, TR_N_KEYS>{
"nodes6"sv,
"open-dialog-dir"sv,
"p"sv,
"params"sv,
"path"sv,
"paused"sv,
"pausedTorrentCount"sv,

View File

@@ -67,6 +67,7 @@ enum // NOLINT(performance-enum-size)
TR_KEY_clientIsChoked,
TR_KEY_clientIsInterested,
TR_KEY_clientName,
TR_KEY_code,
TR_KEY_comment,
TR_KEY_compact_view,
TR_KEY_complete,
@@ -79,6 +80,7 @@ enum // NOLINT(performance-enum-size)
TR_KEY_creator,
TR_KEY_cumulative_stats,
TR_KEY_current_stats,
TR_KEY_data,
TR_KEY_date,
TR_KEY_dateCreated,
TR_KEY_default_trackers,
@@ -172,6 +174,7 @@ enum // NOLINT(performance-enum-size)
TR_KEY_isStalled,
TR_KEY_isUTP,
TR_KEY_isUploadingTo,
TR_KEY_jsonrpc,
TR_KEY_labels,
TR_KEY_lastAnnouncePeerCount,
TR_KEY_lastAnnounceResult,
@@ -202,6 +205,7 @@ enum // NOLINT(performance-enum-size)
TR_KEY_maxConnectedPeers,
TR_KEY_memory_bytes,
TR_KEY_memory_units,
TR_KEY_message,
TR_KEY_message_level,
TR_KEY_metadataPercentComplete,
TR_KEY_metadata_size,
@@ -217,6 +221,7 @@ enum // NOLINT(performance-enum-size)
TR_KEY_nodes6,
TR_KEY_open_dialog_dir,
TR_KEY_p,
TR_KEY_params,
TR_KEY_path,
TR_KEY_paused,
TR_KEY_pausedTorrentCount,

View File

@@ -356,20 +356,23 @@ void handle_web_client(struct evhttp_request* req, tr_rpc_server const* server)
void handle_rpc_from_json(struct evhttp_request* req, tr_rpc_server* server, std::string_view json)
{
if (auto otop = tr_variant_serde::json().inplace().parse(json); otop)
{
tr_rpc_request_exec(
server->session,
*otop,
[req, server](tr_session* /*session*/, tr_variant&& content)
tr_rpc_request_exec(
server->session,
json,
[req, server](tr_session* /*session*/, tr_variant&& content)
{
if (!content.has_value())
{
auto* const output_headers = evhttp_request_get_output_headers(req);
auto* const response = make_response(req, server, tr_variant_serde::json().compact().to_string(content));
evhttp_add_header(output_headers, "Content-Type", "application/json; charset=UTF-8");
evhttp_send_reply(req, HTTP_OK, "OK", response);
evbuffer_free(response);
});
}
evhttp_send_reply(req, HTTP_NOCONTENT, "OK", nullptr);
return;
}
auto* const output_headers = evhttp_request_get_output_headers(req);
auto* const response = make_response(req, server, tr_variant_serde::json().compact().to_string(content));
evhttp_add_header(output_headers, "Content-Type", "application/json; charset=UTF-8");
evhttp_send_reply(req, HTTP_OK, "OK", response);
evbuffer_free(response);
});
}
void handle_rpc(struct evhttp_request* req, tr_rpc_server* server)

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,35 @@
struct tr_session;
struct tr_variant;
namespace JsonRpc
{
auto constexpr Version = std::string_view{ "2.0" };
namespace Error
{
enum Code : int16_t
{
PARSE_ERROR = -32700,
INVALID_REQUEST = -32600,
METHOD_NOT_FOUND = -32601,
INVALID_PARAMS = -32602,
INTERNAL_ERROR = -32603,
SUCCESS = 0,
SET_ANNOUNCE_LIST,
INVALID_TRACKER_LIST,
PATH_NOT_ABSOLUTE,
UNRECOGNIZED_INFO,
SYSTEM_ERROR,
FILE_IDX_OOR,
PIECE_IDX_OOR,
HTTP_ERROR,
CORRUPT_TORRENT
};
}
} // namespace JsonRpc
using tr_rpc_response_func = std::function<void(tr_session* session, tr_variant&& response)>;
void tr_rpc_request_exec(tr_session* session, tr_variant const& request, tr_rpc_response_func&& callback = {});
void tr_rpc_request_exec(tr_session* session, std::string_view request, tr_rpc_response_func&& callback = {});

View File

@@ -2438,7 +2438,7 @@ void renameTorrentFileString(tr_torrent* tor, std::string_view oldpath, std::str
void tr_torrent::rename_path_in_session_thread(
std::string_view const oldpath,
std::string_view const newname,
tr_torrent_rename_done_func const callback,
tr_torrent_rename_done_func const& callback,
void* const callback_user_data)
{
using namespace rename_helpers;
@@ -2482,19 +2482,19 @@ void tr_torrent::rename_path_in_session_thread(
{
auto const szold = tr_pathbuf{ oldpath };
auto const sznew = tr_pathbuf{ newname };
(*callback)(this, szold.c_str(), sznew.c_str(), error, callback_user_data);
callback(this, szold.c_str(), sznew.c_str(), error, callback_user_data);
}
}
void tr_torrent::rename_path(
std::string_view oldpath,
std::string_view newname,
tr_torrent_rename_done_func callback,
tr_torrent_rename_done_func&& callback,
void* callback_user_data)
{
this->session->run_in_session_thread(
[this, oldpath = std::string(oldpath), newname = std::string(newname), callback, callback_user_data]()
{ rename_path_in_session_thread(oldpath, newname, callback, callback_user_data); });
[this, oldpath = std::string(oldpath), newname = std::string(newname), cb = std::move(callback), callback_user_data]()
{ rename_path_in_session_thread(oldpath, newname, std::move(cb), callback_user_data); });
}
void tr_torrentRenamePath(
@@ -2507,7 +2507,7 @@ void tr_torrentRenamePath(
oldpath = oldpath != nullptr ? oldpath : "";
newname = newname != nullptr ? newname : "";
tor->rename_path(oldpath, newname, callback, callback_user_data);
tor->rename_path(oldpath, newname, std::move(callback), callback_user_data);
}
// ---

View File

@@ -183,7 +183,7 @@ struct tr_torrent
void rename_path(
std::string_view oldpath,
std::string_view newname,
tr_torrent_rename_done_func callback,
tr_torrent_rename_done_func&& callback,
void* callback_user_data);
// these functions should become private when possible,
@@ -1323,7 +1323,7 @@ private:
void rename_path_in_session_thread(
std::string_view oldpath,
std::string_view newname,
tr_torrent_rename_done_func callback,
tr_torrent_rename_done_func const& callback,
void* callback_user_data);
void start_in_session_thread();

View File

@@ -15,6 +15,7 @@
#include <time.h> // time_t
#ifdef __cplusplus
#include <functional>
#include <string>
#include <string_view>
#else
@@ -853,12 +854,8 @@ void tr_torrentStart(tr_torrent* torrent);
/** @brief Stop (pause) a torrent */
void tr_torrentStop(tr_torrent* torrent);
using tr_torrent_rename_done_func = void (*)( //
tr_torrent* torrent,
char const* oldpath,
char const* newname,
int error,
void* user_data);
using tr_torrent_rename_done_func = std::function<
void(tr_torrent* torrent, char const* oldpath, char const* newname, int error, void* user_data)>;
/**
* @brief Rename a file or directory in a torrent.

View File

@@ -30,7 +30,195 @@ namespace libtransmission::test
using RpcTest = SessionTest;
TEST_F(RpcTest, tagSync)
TEST_F(RpcTest, EmptyRequest)
{
static auto constexpr Request = ""sv;
auto response = tr_variant{};
tr_rpc_request_exec(
session_,
Request,
[&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); });
auto const* const response_map = response.get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
auto const* const result = response_map->find_if<tr_variant::Map>(TR_KEY_result);
EXPECT_EQ(result, nullptr);
auto const* const error = response_map->find_if<tr_variant::Map>(TR_KEY_error);
ASSERT_NE(error, nullptr);
auto const error_code = error->value_if<int64_t>(TR_KEY_code);
ASSERT_TRUE(error_code);
EXPECT_EQ(*error_code, -32700); // don't use constants here in case they are wrong
auto const error_message = error->value_if<std::string_view>(TR_KEY_message);
ASSERT_TRUE(error_message);
EXPECT_EQ(*error_message, "Parse error"sv);
auto const id = response_map->value_if<std::nullptr_t>(TR_KEY_id);
EXPECT_TRUE(id);
}
TEST_F(RpcTest, NotArrayOrObject)
{
auto requests = std::vector<tr_variant>{};
requests.emplace_back(12345);
requests.emplace_back(0.5);
requests.emplace_back("12345"sv);
requests.emplace_back(nullptr);
requests.emplace_back(true);
for (auto const& req : requests)
{
auto response = tr_variant{};
tr_rpc_request_exec(
session_,
req,
[&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); });
auto const* const response_map = response.get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
auto const result = response_map->find(TR_KEY_result);
EXPECT_EQ(result, std::end(*response_map));
auto const* const error = response_map->find_if<tr_variant::Map>(TR_KEY_error);
ASSERT_NE(error, nullptr);
auto const error_code = error->value_if<int64_t>(TR_KEY_code);
ASSERT_TRUE(error_code);
EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong
auto const error_message = error->value_if<std::string_view>(TR_KEY_message);
ASSERT_TRUE(error_message);
EXPECT_EQ(*error_message, "Invalid Request"sv);
auto const error_data = error->find_if<tr_variant::Map>(TR_KEY_data);
ASSERT_NE(error_data, nullptr);
auto const error_string = error_data->value_if<std::string_view>(TR_KEY_errorString);
ASSERT_TRUE(error_string);
EXPECT_EQ(*error_string, "request must be an Array or Object"sv);
auto const id = response_map->value_if<std::nullptr_t>(TR_KEY_id);
EXPECT_TRUE(id);
}
}
TEST_F(RpcTest, JsonRpcWrongVersion)
{
auto request_map = tr_variant::Map{ 3U };
request_map.try_emplace(TR_KEY_jsonrpc, "1.0");
request_map.try_emplace(TR_KEY_method, "session_stats");
request_map.try_emplace(TR_KEY_id, 12345);
auto response = tr_variant{};
tr_rpc_request_exec(
session_,
std::move(request_map),
[&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); });
auto const* const response_map = response.get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
auto const result = response_map->find(TR_KEY_result);
EXPECT_EQ(result, std::end(*response_map));
auto const* const error = response_map->find_if<tr_variant::Map>(TR_KEY_error);
ASSERT_NE(error, nullptr);
auto const error_code = error->value_if<int64_t>(TR_KEY_code);
ASSERT_TRUE(error_code);
EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong
auto const error_message = error->value_if<std::string_view>(TR_KEY_message);
ASSERT_TRUE(error_message);
EXPECT_EQ(*error_message, "Invalid Request"sv);
auto const error_data = error->find_if<tr_variant::Map>(TR_KEY_data);
ASSERT_NE(error_data, nullptr);
auto const error_string = error_data->value_if<std::string_view>(TR_KEY_errorString);
ASSERT_TRUE(error_string);
EXPECT_EQ(*error_string, "JSON-RPC version is not 2.0"sv);
auto const id = response_map->value_if<std::nullptr_t>(TR_KEY_id);
EXPECT_TRUE(id);
}
TEST_F(RpcTest, idSync)
{
auto ids = std::vector<tr_variant>{};
ids.emplace_back(12345);
ids.emplace_back(0.5);
ids.emplace_back("12345"sv);
ids.emplace_back(nullptr);
for (auto const& request_id : ids)
{
auto request_map = tr_variant::Map{ 3U };
request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request_map.try_emplace(TR_KEY_method, "session-stats");
request_map[TR_KEY_id].merge(request_id); // copy
auto response = tr_variant{};
tr_rpc_request_exec(
session_,
std::move(request_map),
[&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); });
auto const* const response_map = response.get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
auto const* const result = response_map->find_if<tr_variant::Map>(TR_KEY_result);
EXPECT_NE(result, nullptr);
auto const error = response_map->find(TR_KEY_error);
EXPECT_EQ(error, std::end(*response_map));
switch (request_id.index())
{
case tr_variant::IntIndex:
EXPECT_EQ(request_id.value_if<int64_t>(), response_map->value_if<int64_t>(TR_KEY_id));
break;
case tr_variant::DoubleIndex:
EXPECT_EQ(request_id.value_if<double>(), response_map->value_if<double>(TR_KEY_id));
break;
case tr_variant::StringIndex:
EXPECT_EQ(request_id.value_if<std::string_view>(), response_map->value_if<std::string_view>(TR_KEY_id));
break;
case tr_variant::NullIndex:
EXPECT_EQ(request_id.value_if<std::nullptr_t>(), response_map->value_if<std::nullptr_t>(TR_KEY_id));
break;
default:
break;
}
}
}
TEST_F(RpcTest, idWrongType)
{
auto ids = std::vector<tr_variant>{};
ids.emplace_back(tr_variant::Map{});
ids.emplace_back(tr_variant::Vector{});
ids.emplace_back(true);
for (auto const& request_id : ids)
{
auto request_map = tr_variant::Map{ 3U };
request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request_map.try_emplace(TR_KEY_method, "session_stats");
request_map[TR_KEY_id].merge(request_id); // copy
auto response = tr_variant{};
tr_rpc_request_exec(
session_,
std::move(request_map),
[&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); });
auto const* const response_map = response.get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
auto const result = response_map->find(TR_KEY_result);
EXPECT_EQ(result, std::end(*response_map));
auto const error = response_map->find_if<tr_variant::Map>(TR_KEY_error);
ASSERT_NE(error, nullptr);
auto const error_code = error->value_if<int64_t>(TR_KEY_code);
ASSERT_TRUE(error_code);
EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong
auto const error_message = error->value_if<std::string_view>(TR_KEY_message);
ASSERT_TRUE(error_message);
EXPECT_EQ(*error_message, "Invalid Request"sv);
auto const error_data = error->find_if<tr_variant::Map>(TR_KEY_data);
ASSERT_NE(error_data, nullptr);
auto const error_string = error_data->value_if<std::string_view>(TR_KEY_errorString);
ASSERT_TRUE(error_string);
EXPECT_EQ(*error_string, "id type must be String, Number, or Null"sv);
auto const id = response_map->value_if<std::nullptr_t>(TR_KEY_id);
EXPECT_TRUE(id);
}
}
TEST_F(RpcTest, tagSyncLegacy)
{
auto request_map = tr_variant::Map{ 2U };
request_map.try_emplace(TR_KEY_method, "session-stats");
@@ -39,7 +227,7 @@ TEST_F(RpcTest, tagSync)
auto response = tr_variant{};
tr_rpc_request_exec(
session_,
tr_variant{ std::move(request_map) },
std::move(request_map),
[&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); });
auto const* const response_map = response.get_if<tr_variant::Map>();
@@ -52,7 +240,67 @@ TEST_F(RpcTest, tagSync)
EXPECT_EQ(*tag, 12345);
}
TEST_F(RpcTest, tagAsync)
TEST_F(RpcTest, idAsync)
{
auto ids = std::vector<tr_variant>{};
ids.emplace_back(12345);
ids.emplace_back(0.5);
ids.emplace_back("12345"sv);
ids.emplace_back(nullptr);
for (auto const& request_id : ids)
{
auto* tor = zeroTorrentInit(ZeroTorrentState::Complete);
EXPECT_NE(nullptr, tor);
auto request_map = tr_variant::Map{ 3U };
request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request_map.try_emplace(TR_KEY_method, "torrent-rename-path");
request_map[TR_KEY_id].merge(request_id); // copy
auto params_map = tr_variant::Map{ 2U };
params_map.try_emplace(TR_KEY_path, "files-filled-with-zeroes/512");
params_map.try_emplace(TR_KEY_name, "512_test");
request_map.try_emplace(TR_KEY_params, std::move(params_map));
auto promise = std::promise<tr_variant>{};
auto future = promise.get_future();
tr_rpc_request_exec(
session_,
std::move(request_map),
[&promise](tr_session* /*session*/, tr_variant&& resp) { promise.set_value(std::move(resp)); });
auto const response = future.get();
auto const* const response_map = response.get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
auto const result = response_map->find_if<tr_variant::Map>(TR_KEY_result);
EXPECT_NE(result, nullptr);
auto const error = response_map->find(TR_KEY_error);
EXPECT_EQ(error, std::end(*response_map));
switch (request_id.index())
{
case tr_variant::IntIndex:
EXPECT_EQ(request_id.value_if<int64_t>(), response_map->value_if<int64_t>(TR_KEY_id));
break;
case tr_variant::DoubleIndex:
EXPECT_EQ(request_id.value_if<double>(), response_map->value_if<double>(TR_KEY_id));
break;
case tr_variant::StringIndex:
EXPECT_EQ(request_id.value_if<std::string_view>(), response_map->value_if<std::string_view>(TR_KEY_id));
break;
case tr_variant::NullIndex:
EXPECT_EQ(request_id.value_if<std::nullptr_t>(), response_map->value_if<std::nullptr_t>(TR_KEY_id));
break;
default:
break;
}
// cleanup
tr_torrentRemove(tor, false, nullptr, nullptr, nullptr, nullptr);
}
}
TEST_F(RpcTest, tagAsyncLegacy)
{
auto* tor = zeroTorrentInit(ZeroTorrentState::Complete);
EXPECT_NE(nullptr, tor);
@@ -70,7 +318,7 @@ TEST_F(RpcTest, tagAsync)
auto future = promise.get_future();
tr_rpc_request_exec(
session_,
tr_variant{ std::move(request_map) },
std::move(request_map),
[&promise](tr_session* /*session*/, tr_variant&& resp) { promise.set_value(std::move(resp)); });
auto const response = future.get();
@@ -87,7 +335,83 @@ TEST_F(RpcTest, tagAsync)
tr_torrentRemove(tor, false, nullptr, nullptr, nullptr, nullptr);
}
TEST_F(RpcTest, NotificationSync)
{
auto request_map = tr_variant::Map{ 2U };
request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request_map.try_emplace(TR_KEY_method, "session_stats");
auto response = tr_variant{};
tr_rpc_request_exec(
session_,
std::move(request_map),
[&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); });
EXPECT_FALSE(response.has_value());
}
TEST_F(RpcTest, NotificationAsync)
{
auto* tor = zeroTorrentInit(ZeroTorrentState::Complete);
EXPECT_NE(nullptr, tor);
auto request_map = tr_variant::Map{ 2U };
request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request_map.try_emplace(TR_KEY_method, "torrent_rename_path");
auto params_map = tr_variant::Map{ 2U };
params_map.try_emplace(TR_KEY_path, "files-filled-with-zeroes/512");
params_map.try_emplace(TR_KEY_name, "512_test");
request_map.try_emplace(TR_KEY_params, std::move(params_map));
auto promise = std::promise<tr_variant>{};
auto future = promise.get_future();
tr_rpc_request_exec(
session_,
std::move(request_map),
[&promise](tr_session* /*session*/, tr_variant&& resp) { promise.set_value(std::move(resp)); });
auto const response = future.get();
EXPECT_FALSE(response.has_value());
// cleanup
tr_torrentRemove(tor, false, nullptr, nullptr, nullptr, nullptr);
}
TEST_F(RpcTest, tagNoHandler)
{
auto request_map = tr_variant::Map{ 3U };
request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request_map.try_emplace(TR_KEY_method, "sdgdhsgg");
request_map.try_emplace(TR_KEY_id, 12345);
auto response = tr_variant{};
tr_rpc_request_exec(
session_,
std::move(request_map),
[&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); });
auto const* const response_map = response.get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
auto const jsonrpc = response_map->value_if<std::string_view>(TR_KEY_jsonrpc);
ASSERT_TRUE(jsonrpc);
EXPECT_EQ(*jsonrpc, JsonRpc::Version);
auto const result = response_map->find_if<tr_variant::Map>(TR_KEY_result);
EXPECT_EQ(result, nullptr);
auto const error = response_map->find_if<tr_variant::Map>(TR_KEY_error);
ASSERT_NE(error, nullptr);
auto const error_code = error->value_if<int64_t>(TR_KEY_code);
ASSERT_TRUE(error_code);
EXPECT_EQ(*error_code, JsonRpc::Error::METHOD_NOT_FOUND);
auto const error_message = error->value_if<std::string_view>(TR_KEY_message);
ASSERT_TRUE(error_message);
EXPECT_EQ(*error_message, "Method not found"sv);
auto const id = response_map->value_if<int64_t>(TR_KEY_id);
ASSERT_TRUE(id);
EXPECT_EQ(*id, 12345);
}
TEST_F(RpcTest, tagNoHandlerLegacy)
{
auto request_map = tr_variant::Map{ 2U };
request_map.try_emplace(TR_KEY_method, "sdgdhsgg");
@@ -96,7 +420,7 @@ TEST_F(RpcTest, tagNoHandler)
auto response = tr_variant{};
tr_rpc_request_exec(
session_,
tr_variant{ std::move(request_map) },
std::move(request_map),
[&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); });
auto const* const response_map = response.get_if<tr_variant::Map>();
@@ -109,6 +433,154 @@ TEST_F(RpcTest, tagNoHandler)
EXPECT_EQ(*tag, 12345);
}
TEST_F(RpcTest, batch)
{
auto request_vec = tr_variant::Vector{};
request_vec.reserve(8U);
auto request = tr_variant::Map{ 3U };
request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request.try_emplace(TR_KEY_method, "session-stats");
request.try_emplace(TR_KEY_id, 12345);
request_vec.emplace_back(std::move(request));
request = tr_variant::Map{ 2U };
request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request.try_emplace(TR_KEY_method, "session-set");
request_vec.emplace_back(std::move(request));
request = tr_variant::Map{ 3U };
request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request.try_emplace(TR_KEY_method, "session-stats");
request.try_emplace(TR_KEY_id, "12345"sv);
request_vec.emplace_back(std::move(request));
request = tr_variant::Map{ 1U };
request.try_emplace(tr_quark_new("foo"sv), "boo"sv);
request_vec.emplace_back(std::move(request));
request_vec.emplace_back(1);
request = tr_variant::Map{ 3U };
request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request.try_emplace(TR_KEY_method, "dnfsojnsdkjf");
request.try_emplace(TR_KEY_id, 12345);
request_vec.emplace_back(std::move(request));
request = tr_variant::Map{ 1U };
request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request.try_emplace(TR_KEY_method, "dnfsojnsdkjf");
request_vec.emplace_back(std::move(request));
request = tr_variant::Map{ 2U };
request.try_emplace(TR_KEY_method, "session-stats");
request.try_emplace(TR_KEY_tag, 12345);
request_vec.emplace_back(std::move(request));
auto response = tr_variant{};
tr_rpc_request_exec(
session_,
std::move(request_vec),
[&response](tr_session* /*session*/, tr_variant&& resp) { response = std::move(resp); });
auto* const response_vec_ptr = response.get_if<tr_variant::Vector>();
ASSERT_NE(response_vec_ptr, nullptr);
auto const& response_vec = *response_vec_ptr;
ASSERT_EQ(std::size(response_vec), 6U);
auto const* response_map = response_vec[0].get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
auto const* result = response_map->find_if<tr_variant::Map>(TR_KEY_result);
EXPECT_NE(result, nullptr);
auto error_it = response_map->find(TR_KEY_error);
EXPECT_EQ(error_it, std::end(*response_map));
auto id_int = response_map->value_if<int64_t>(TR_KEY_id);
ASSERT_TRUE(id_int);
EXPECT_EQ(*id_int, 12345);
response_map = response_vec[1].get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
result = response_map->find_if<tr_variant::Map>(TR_KEY_result);
EXPECT_NE(result, nullptr);
error_it = response_map->find(TR_KEY_error);
EXPECT_EQ(error_it, std::end(*response_map));
auto id_str = response_map->value_if<std::string_view>(TR_KEY_id);
ASSERT_TRUE(id_str);
EXPECT_EQ(*id_str, "12345"sv);
response_map = response_vec[2].get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
auto result_it = response_map->find(TR_KEY_result);
EXPECT_EQ(result_it, std::end(*response_map));
auto error = response_map->find_if<tr_variant::Map>(TR_KEY_error);
ASSERT_NE(error, nullptr);
auto error_code = error->value_if<int64_t>(TR_KEY_code);
ASSERT_TRUE(error_code);
EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong
auto error_message = error->value_if<std::string_view>(TR_KEY_message);
ASSERT_TRUE(error_message);
EXPECT_EQ(*error_message, "Invalid Request"sv);
auto id_null = response_map->value_if<std::nullptr_t>(TR_KEY_id);
EXPECT_TRUE(id_null);
response_map = response_vec[3].get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
result_it = response_map->find(TR_KEY_result);
EXPECT_EQ(result_it, std::end(*response_map));
error = response_map->find_if<tr_variant::Map>(TR_KEY_error);
ASSERT_NE(error, nullptr);
error_code = error->value_if<int64_t>(TR_KEY_code);
ASSERT_TRUE(error_code);
EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong
error_message = error->value_if<std::string_view>(TR_KEY_message);
ASSERT_TRUE(error_message);
EXPECT_EQ(*error_message, "Invalid Request"sv);
auto error_data = error->find_if<tr_variant::Map>(TR_KEY_data);
ASSERT_NE(error_data, nullptr);
auto error_string = error_data->value_if<std::string_view>(TR_KEY_errorString);
ASSERT_TRUE(error_string);
EXPECT_EQ(*error_string, "request must be an Object"sv);
id_null = response_map->value_if<std::nullptr_t>(TR_KEY_id);
EXPECT_TRUE(id_null);
response_map = response_vec[4].get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
result_it = response_map->find(TR_KEY_result);
EXPECT_EQ(result_it, std::end(*response_map));
error = response_map->find_if<tr_variant::Map>(TR_KEY_error);
ASSERT_NE(error, nullptr);
error_code = error->value_if<int64_t>(TR_KEY_code);
ASSERT_TRUE(error_code);
EXPECT_EQ(*error_code, -32601); // don't use constants here in case they are wrong
error_message = error->value_if<std::string_view>(TR_KEY_message);
ASSERT_TRUE(error_message);
EXPECT_EQ(*error_message, "Method not found"sv);
id_int = response_map->value_if<int64_t>(TR_KEY_id);
ASSERT_TRUE(id_int);
EXPECT_EQ(*id_int, 12345);
response_map = response_vec[5].get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
result_it = response_map->find(TR_KEY_result);
EXPECT_EQ(result_it, std::end(*response_map));
error = response_map->find_if<tr_variant::Map>(TR_KEY_error);
ASSERT_NE(error, nullptr);
error_code = error->value_if<int64_t>(TR_KEY_code);
ASSERT_TRUE(error_code);
EXPECT_EQ(*error_code, -32600); // don't use constants here in case they are wrong
error_message = error->value_if<std::string_view>(TR_KEY_message);
ASSERT_TRUE(error_message);
EXPECT_EQ(*error_message, "Invalid Request"sv);
error_data = error->find_if<tr_variant::Map>(TR_KEY_data);
ASSERT_NE(error_data, nullptr);
error_string = error_data->value_if<std::string_view>(TR_KEY_errorString);
ASSERT_TRUE(error_string);
EXPECT_EQ(*error_string, "JSON-RPC version is not 2.0"sv);
id_null = response_map->value_if<std::nullptr_t>(TR_KEY_id);
EXPECT_TRUE(id_null);
}
/***
****
***/
@@ -118,8 +590,10 @@ TEST_F(RpcTest, sessionGet)
auto* tor = zeroTorrentInit(ZeroTorrentState::NoFiles);
EXPECT_NE(nullptr, tor);
auto request_map = tr_variant::Map{ 1U };
auto request_map = tr_variant::Map{ 3U };
request_map.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request_map.try_emplace(TR_KEY_method, "session-get"sv);
request_map.try_emplace(TR_KEY_id, 12345);
auto response = tr_variant{};
tr_rpc_request_exec(
session_,
@@ -128,7 +602,7 @@ TEST_F(RpcTest, sessionGet)
auto* response_map = response.get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
auto* args_map = response_map->find_if<tr_variant::Map>(TR_KEY_arguments);
auto* args_map = response_map->find_if<tr_variant::Map>(TR_KEY_result);
ASSERT_NE(args_map, nullptr);
// what we expected
@@ -231,15 +705,17 @@ TEST_F(RpcTest, torrentGet)
auto* tor = zeroTorrentInit(ZeroTorrentState::NoFiles);
EXPECT_NE(nullptr, tor);
auto request = tr_variant::Map{ 1U };
auto request = tr_variant::Map{ 3U };
request.try_emplace(TR_KEY_method, "torrent-get");
request.try_emplace(TR_KEY_jsonrpc, JsonRpc::Version);
request.try_emplace(TR_KEY_method, "torrent-get"sv);
request.try_emplace(TR_KEY_id, 12345);
auto args_in = tr_variant::Map{ 1U };
auto params = tr_variant::Map{ 1U };
auto fields = tr_variant::Vector{};
fields.emplace_back(tr_quark_get_string_view(TR_KEY_id));
args_in.try_emplace(TR_KEY_fields, std::move(fields));
request.try_emplace(TR_KEY_arguments, std::move(args_in));
params.try_emplace(TR_KEY_fields, std::move(fields));
request.try_emplace(TR_KEY_params, std::move(params));
auto response = tr_variant{};
tr_rpc_request_exec(
@@ -249,10 +725,10 @@ TEST_F(RpcTest, torrentGet)
auto* response_map = response.get_if<tr_variant::Map>();
ASSERT_NE(response_map, nullptr);
auto* args_out = response_map->find_if<tr_variant::Map>(TR_KEY_arguments);
ASSERT_NE(args_out, nullptr);
auto* result = response_map->find_if<tr_variant::Map>(TR_KEY_result);
ASSERT_NE(result, nullptr);
auto* torrents = args_out->find_if<tr_variant::Vector>(TR_KEY_torrents);
auto* torrents = result->find_if<tr_variant::Vector>(TR_KEY_torrents);
ASSERT_NE(torrents, nullptr);
EXPECT_EQ(1UL, std::size(*torrents));

View File

@@ -5,6 +5,7 @@
import { AlertDialog } from './alert-dialog.js';
import { Formatter } from './formatter.js';
import { RPC } from './remote.js';
import { createDialogContainer, makeUUID } from './utils.js';
const is_ios =
@@ -76,20 +77,24 @@ export class OpenDialog extends EventTarget {
return;
}
const o = {
arguments: {
id: 'webui',
jsonrpc: RPC._JsonRpcVersion,
method: 'torrent-add',
params: {
'download-dir': destination,
metainfo: contents.slice(Math.max(0, index + key.length)),
paused,
},
method: 'torrent-add',
};
remote.sendRequest(o, (response) => {
if (response.result !== 'success') {
alert(`Error adding "${file.name}": ${response.result}`);
if ('error' in response) {
const message =
response.error.data?.errorString ?? response.error.message;
alert(`Error adding "${file.name}": ${message}`);
controller.setCurrentPopup(
new AlertDialog({
heading: `Error adding "${file.name}"`,
message: response.result,
message,
}),
);
}
@@ -104,19 +109,21 @@ export class OpenDialog extends EventTarget {
url = `magnet:?xt=urn:btih:${url}`;
}
const o = {
arguments: {
id: 'webui',
jsonrpc: RPC._JsonRpcVersion,
method: 'torrent-add',
params: {
'download-dir': destination,
filename: url,
paused,
},
method: 'torrent-add',
};
remote.sendRequest(o, (payload) => {
if (payload.result !== 'success') {
if ('error' in payload) {
controller.setCurrentPopup(
new AlertDialog({
heading: `Error adding "${url}"`,
message: payload.result,
message: payload.error.data?.errorString ?? payload.error.message,
}),
);
}

View File

@@ -61,10 +61,11 @@ export class PrefsDialog extends EventTarget {
return;
}
const args = response.result ?? response.error?.data ?? {};
const element = this.elements.network.port_status_label[ip_protocol];
const is_open = response.arguments['port-is-open'] || false;
const is_open = args['port-is-open'] ?? false;
element.dataset.open = is_open;
if ('port-is-open' in response.arguments) {
if ('port-is-open' in args) {
setTextContent(element, is_open ? 'Open' : 'Closed');
} else {
setTextContent(element, 'Error');

View File

@@ -8,6 +8,7 @@ export const RPC = {
_DaemonVersion: 'version',
_DownSpeedLimit: 'speed-limit-down',
_DownSpeedLimited: 'speed-limit-down-enabled',
_JsonRpcVersion: '2.0',
_QueueMoveBottom: 'queue-move-bottom',
_QueueMoveDown: 'queue-move-down',
_QueueMoveTop: 'queue-move-top',
@@ -46,12 +47,17 @@ export class Remote {
})
.then((response) => {
response_argument = response;
if (response.status === 409) {
const error = new Error(Remote._SessionHeader);
error.header = response.headers.get(Remote._SessionHeader);
throw error;
switch (response.status) {
case 409: {
const error = new Error(Remote._SessionHeader);
error.header = response.headers.get(Remote._SessionHeader);
throw error;
}
case 204:
return null;
default:
return response.json();
}
return response.json();
})
.then((payload) => {
if (callback) {
@@ -85,6 +91,8 @@ export class Remote {
// TODO: return a Promise
loadDaemonPrefs(callback, context) {
const o = {
id: 'webui',
jsonrpc: RPC._JsonRpcVersion,
method: 'session-get',
};
this.sendRequest(o, callback, context);
@@ -92,36 +100,50 @@ export class Remote {
checkPort(ip_protocol, callback, context) {
const o = {
arguments: {
id: 'webui',
jsonrpc: RPC._JsonRpcVersion,
method: 'port-test',
params: {
ip_protocol,
},
method: 'port-test',
};
this.sendRequest(o, callback, context);
}
renameTorrent(torrentIds, oldpath, newname, callback, context) {
const o = {
arguments: {
id: 'webui',
jsonrpc: RPC._JsonRpcVersion,
method: 'torrent-rename-path',
params: {
ids: torrentIds,
name: newname,
path: oldpath,
},
method: 'torrent-rename-path',
};
this.sendRequest(o, callback, context);
}
setLabels(torrentIds, labels, callback) {
const args = {
const params = {
ids: torrentIds,
labels,
};
this.sendRequest({ arguments: args, method: 'torrent-set' }, callback);
this.sendRequest(
{
id: 'webui',
jsonrpc: RPC._JsonRpcVersion,
method: 'torrent-set',
params,
},
callback,
);
}
loadDaemonStats(callback, context) {
const o = {
id: 'webui',
jsonrpc: RPC._JsonRpcVersion,
method: 'session-stats',
};
this.sendRequest(o, callback, context);
@@ -129,43 +151,46 @@ export class Remote {
updateTorrents(torrentIds, fields, callback, context) {
const o = {
arguments: {
id: 'webui',
jsonrpc: RPC._JsonRpcVersion,
method: 'torrent-get',
params: {
fields,
format: 'table',
},
method: 'torrent-get',
};
if (torrentIds) {
o.arguments.ids = torrentIds;
o.params.ids = torrentIds;
}
this.sendRequest(o, (response) => {
const arguments_ = response['arguments'];
callback.call(context, arguments_.torrents, arguments_.removed);
const { torrents, removed } = response.result;
callback.call(context, torrents, removed);
});
}
getFreeSpace(dir, callback, context) {
const o = {
arguments: {
path: dir,
},
id: 'webui',
jsonrpc: RPC._JsonRpcVersion,
method: 'free-space',
params: { path: dir },
};
this.sendRequest(o, (response) => {
const arguments_ = response['arguments'];
callback.call(context, arguments_.path, arguments_['size-bytes']);
const { path, 'size-bytes': size_bytes } = response.result;
callback.call(context, path, size_bytes);
});
}
changeFileCommand(torrentId, fileIndices, command) {
const arguments_ = {
const params = {
ids: [torrentId],
};
arguments_[command] = fileIndices;
params[command] = fileIndices;
this.sendRequest(
{
arguments: arguments_,
jsonrpc: RPC._JsonRpcVersion,
method: 'torrent-set',
params,
},
() => {
this._controller.refreshTorrents([torrentId]);
@@ -173,14 +198,14 @@ export class Remote {
);
}
sendTorrentSetRequests(method, torrent_ids, arguments_, callback, context) {
if (!arguments_) {
arguments_ = {};
}
arguments_['ids'] = torrent_ids;
sendTorrentSetRequests(method, torrent_ids, params, callback, context) {
params ||= {};
params.ids = torrent_ids;
const o = {
arguments: arguments_,
id: 'webui',
jsonrpc: RPC._JsonRpcVersion,
method,
params,
};
this.sendRequest(o, callback, context);
}
@@ -217,16 +242,17 @@ export class Remote {
removeTorrents(torrents, trash) {
const o = {
arguments: {
jsonrpc: RPC._JsonRpcVersion,
method: 'torrent-remove',
params: {
'delete-local-data': trash,
ids: [],
},
method: 'torrent-remove',
};
if (torrents) {
for (let index = 0, length_ = torrents.length; index < length_; ++index) {
o.arguments.ids.push(torrents[index].getId());
o.params.ids.push(torrents[index].getId());
}
}
this.sendRequest(o, () => {
@@ -254,20 +280,22 @@ export class Remote {
url = `magnet:?xt=urn:btih:${url}`;
}
const o = {
arguments: {
jsonrpc: RPC._JsonRpcVersion,
method: 'torrent-add',
params: {
filename: url,
paused: options.paused,
},
method: 'torrent-add',
};
this.sendRequest(o, () => {
this._controller.refreshTorrents();
});
}
savePrefs(arguments_) {
savePrefs(params) {
const o = {
arguments: arguments_,
jsonrpc: RPC._JsonRpcVersion,
method: 'session-set',
params,
};
this.sendRequest(o, () => {
this._controller.loadDaemonPrefs();
@@ -275,6 +303,7 @@ export class Remote {
}
updateBlocklist() {
const o = {
jsonrpc: RPC._JsonRpcVersion,
method: 'blocklist-update',
};
this.sendRequest(o, () => {

View File

@@ -65,8 +65,8 @@ export class RenameDialog extends EventTarget {
file_path,
new_name,
(response) => {
if (response.result === 'success') {
const args = response.arguments;
if ('result' in response) {
const args = response.result;
if (handler) {
handler.subtree.name = args.name;
setTextContent(handler.name_container, args.name);
@@ -85,11 +85,13 @@ export class RenameDialog extends EventTarget {
} else {
tor.refresh(args);
}
} else if (response.result === 'Invalid argument') {
} else {
const error_obj = response.error;
const err_msg =
error_obj.data?.errorString ?? error_obj.message ?? '';
const connection_alert = new AlertDialog({
heading: `Error renaming "${file_path}"`,
message:
'Could not rename a torrent or file name. The path to file may have changed/not reflected correctly or the argument is invalid.',
message: `${err_msg} (${error_obj.code}`,
});
this.controller.setCurrentPopup(connection_alert);
}

View File

@@ -18,7 +18,7 @@ export class StatisticsDialog extends EventTarget {
this.remote = remote;
const updateDaemon = () =>
this.remote.loadDaemonStats((data) => this._update(data.arguments));
this.remote.loadDaemonStats((data) => this._update(data.result));
const delay_msec = 5000;
this.interval = setInterval(updateDaemon, delay_msec);
updateDaemon();

View File

@@ -353,7 +353,7 @@ export class Transmission extends EventTarget {
loadDaemonPrefs() {
this.remote.loadDaemonPrefs((data) => {
this.session_properties = data.arguments;
this.session_properties = data.result;
this._openTorrentFromUrl();
});
}