Skip to content

Commit a1ded65

Browse files
committed
feat: [#1159] extract new package tracker api client
1 parent f6aca40 commit a1ded65

File tree

12 files changed

+345
-1
lines changed

12 files changed

+345
-1
lines changed

.github/workflows/deployment.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ jobs:
5858
cargo publish -p bittorrent-http-protocol
5959
cargo publish -p bittorrent-tracker-client
6060
cargo publish -p torrust-tracker
61+
cargo publish -p torrust-tracker-api-client
6162
cargo publish -p torrust-tracker-client
6263
cargo publish -p torrust-tracker-clock
6364
cargo publish -p torrust-tracker-configuration

Cargo.lock

+10
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ rust-version.workspace = true
1616
version.workspace = true
1717

1818
[lib]
19-
name = "torrust_tracker_lib"
19+
name = "torrust_tracker_lib"
2020

2121
[workspace.package]
2222
authors = ["Nautilus Cyberneering <info@nautilus-cyberneering.de>, Mick van Dijke <mick@dutchbits.nl>"]
@@ -108,6 +108,7 @@ members = [
108108
"packages/primitives",
109109
"packages/test-helpers",
110110
"packages/torrent-repository",
111+
"packages/tracker-api-client",
111112
"packages/tracker-client",
112113
]
113114

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
description = "A library to interact with the Torrust Tracker REST API."
3+
keywords = ["bittorrent", "client", "tracker"]
4+
license = "LGPL-3.0"
5+
name = "torrust-tracker-api-client"
6+
readme = "README.md"
7+
8+
authors.workspace = true
9+
documentation.workspace = true
10+
edition.workspace = true
11+
homepage.workspace = true
12+
publish.workspace = true
13+
repository.workspace = true
14+
rust-version.workspace = true
15+
version.workspace = true
16+
17+
[dependencies]
18+
hyper = "1"
19+
reqwest = { version = "0", features = ["json"] }
20+
serde = { version = "1", features = ["derive"] }
21+
uuid = { version = "1", features = ["v4"] }

packages/tracker-api-client/README.md

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Torrust Tracker API Client
2+
3+
A library to interact with the Torrust Tracker REST API.
4+
5+
## License
6+
7+
**Copyright (c) 2024 The Torrust Developers.**
8+
9+
This program is free software: you can redistribute it and/or modify it under the terms of the [GNU Lesser General Public License][LGPL_3_0] as published by the [Free Software Foundation][FSF], version 3.
10+
11+
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the [GNU Lesser General Public License][LGPL_3_0] for more details.
12+
13+
You should have received a copy of the *GNU Lesser General Public License* along with this program. If not, see <https://www.gnu.org/licenses/>.
14+
15+
Some files include explicit copyright notices and/or license notices.
16+
17+
### Legacy Exception
18+
19+
For prosperity, versions of Torrust BitTorrent Tracker Client that are older than five years are automatically granted the [MIT-0][MIT_0] license in addition to the existing [LGPL-3.0-only][LGPL_3_0] license.
20+
21+
[LGPL_3_0]: ./LICENSE
22+
[MIT_0]: ./docs/licenses/LICENSE-MIT_0
23+
[FSF]: https://www.fsf.org/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
MIT No Attribution
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
4+
software and associated documentation files (the "Software"), to deal in the Software
5+
without restriction, including without limitation the rights to use, copy, modify,
6+
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
7+
permit persons to whom the Software is furnished to do so.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
10+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
11+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
12+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
13+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
14+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
pub type ReqwestQuery = Vec<ReqwestQueryParam>;
2+
pub type ReqwestQueryParam = (String, String);
3+
4+
/// URL Query component
5+
#[derive(Default, Debug)]
6+
pub struct Query {
7+
params: Vec<QueryParam>,
8+
}
9+
10+
impl Query {
11+
#[must_use]
12+
pub fn empty() -> Self {
13+
Self { params: vec![] }
14+
}
15+
16+
#[must_use]
17+
pub fn params(params: Vec<QueryParam>) -> Self {
18+
Self { params }
19+
}
20+
21+
pub fn add_param(&mut self, param: QueryParam) {
22+
self.params.push(param);
23+
}
24+
}
25+
26+
impl From<Query> for ReqwestQuery {
27+
fn from(url_search_params: Query) -> Self {
28+
url_search_params
29+
.params
30+
.iter()
31+
.map(|param| ReqwestQueryParam::from((*param).clone()))
32+
.collect()
33+
}
34+
}
35+
36+
/// URL query param
37+
#[derive(Clone, Debug)]
38+
pub struct QueryParam {
39+
name: String,
40+
value: String,
41+
}
42+
43+
impl QueryParam {
44+
#[must_use]
45+
pub fn new(name: &str, value: &str) -> Self {
46+
Self {
47+
name: name.to_string(),
48+
value: value.to_string(),
49+
}
50+
}
51+
}
52+
53+
impl From<QueryParam> for ReqwestQueryParam {
54+
fn from(param: QueryParam) -> Self {
55+
(param.name, param.value)
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod http;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#[must_use]
2+
pub fn connection_with_invalid_token(bind_address: &str) -> ConnectionInfo {
3+
ConnectionInfo::authenticated(bind_address, "invalid token")
4+
}
5+
6+
#[must_use]
7+
pub fn connection_with_no_token(bind_address: &str) -> ConnectionInfo {
8+
ConnectionInfo::anonymous(bind_address)
9+
}
10+
11+
#[derive(Clone)]
12+
pub struct ConnectionInfo {
13+
pub bind_address: String,
14+
pub api_token: Option<String>,
15+
}
16+
17+
impl ConnectionInfo {
18+
#[must_use]
19+
pub fn authenticated(bind_address: &str, api_token: &str) -> Self {
20+
Self {
21+
bind_address: bind_address.to_string(),
22+
api_token: Some(api_token.to_string()),
23+
}
24+
}
25+
26+
#[must_use]
27+
pub fn anonymous(bind_address: &str) -> Self {
28+
Self {
29+
bind_address: bind_address.to_string(),
30+
api_token: None,
31+
}
32+
}
33+
}
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod common;
2+
pub mod connection_info;
3+
pub mod v1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use hyper::HeaderMap;
2+
use reqwest::Response;
3+
use serde::Serialize;
4+
use uuid::Uuid;
5+
6+
use crate::common::http::{Query, QueryParam, ReqwestQuery};
7+
use crate::connection_info::ConnectionInfo;
8+
9+
/// API Client
10+
pub struct Client {
11+
connection_info: ConnectionInfo,
12+
base_path: String,
13+
}
14+
15+
impl Client {
16+
#[must_use]
17+
pub fn new(connection_info: ConnectionInfo) -> Self {
18+
Self {
19+
connection_info,
20+
base_path: "/api/v1/".to_string(),
21+
}
22+
}
23+
24+
pub async fn generate_auth_key(&self, seconds_valid: i32, headers: Option<HeaderMap>) -> Response {
25+
self.post_empty(&format!("key/{}", &seconds_valid), headers).await
26+
}
27+
28+
pub async fn add_auth_key(&self, add_key_form: AddKeyForm, headers: Option<HeaderMap>) -> Response {
29+
self.post_form("keys", &add_key_form, headers).await
30+
}
31+
32+
pub async fn delete_auth_key(&self, key: &str, headers: Option<HeaderMap>) -> Response {
33+
self.delete(&format!("key/{}", &key), headers).await
34+
}
35+
36+
pub async fn reload_keys(&self, headers: Option<HeaderMap>) -> Response {
37+
self.get("keys/reload", Query::default(), headers).await
38+
}
39+
40+
pub async fn whitelist_a_torrent(&self, info_hash: &str, headers: Option<HeaderMap>) -> Response {
41+
self.post_empty(&format!("whitelist/{}", &info_hash), headers).await
42+
}
43+
44+
pub async fn remove_torrent_from_whitelist(&self, info_hash: &str, headers: Option<HeaderMap>) -> Response {
45+
self.delete(&format!("whitelist/{}", &info_hash), headers).await
46+
}
47+
48+
pub async fn reload_whitelist(&self, headers: Option<HeaderMap>) -> Response {
49+
self.get("whitelist/reload", Query::default(), headers).await
50+
}
51+
52+
pub async fn get_torrent(&self, info_hash: &str, headers: Option<HeaderMap>) -> Response {
53+
self.get(&format!("torrent/{}", &info_hash), Query::default(), headers).await
54+
}
55+
56+
pub async fn get_torrents(&self, params: Query, headers: Option<HeaderMap>) -> Response {
57+
self.get("torrents", params, headers).await
58+
}
59+
60+
pub async fn get_tracker_statistics(&self, headers: Option<HeaderMap>) -> Response {
61+
self.get("stats", Query::default(), headers).await
62+
}
63+
64+
pub async fn get(&self, path: &str, params: Query, headers: Option<HeaderMap>) -> Response {
65+
let mut query: Query = params;
66+
67+
if let Some(token) = &self.connection_info.api_token {
68+
query.add_param(QueryParam::new("token", token));
69+
};
70+
71+
self.get_request_with_query(path, query, headers).await
72+
}
73+
74+
/// # Panics
75+
///
76+
/// Will panic if the request can't be sent
77+
pub async fn post_empty(&self, path: &str, headers: Option<HeaderMap>) -> Response {
78+
let builder = reqwest::Client::new()
79+
.post(self.base_url(path).clone())
80+
.query(&ReqwestQuery::from(self.query_with_token()));
81+
82+
let builder = match headers {
83+
Some(headers) => builder.headers(headers),
84+
None => builder,
85+
};
86+
87+
builder.send().await.unwrap()
88+
}
89+
90+
/// # Panics
91+
///
92+
/// Will panic if the request can't be sent
93+
pub async fn post_form<T: Serialize + ?Sized>(&self, path: &str, form: &T, headers: Option<HeaderMap>) -> Response {
94+
let builder = reqwest::Client::new()
95+
.post(self.base_url(path).clone())
96+
.query(&ReqwestQuery::from(self.query_with_token()))
97+
.json(&form);
98+
99+
let builder = match headers {
100+
Some(headers) => builder.headers(headers),
101+
None => builder,
102+
};
103+
104+
builder.send().await.unwrap()
105+
}
106+
107+
/// # Panics
108+
///
109+
/// Will panic if the request can't be sent
110+
async fn delete(&self, path: &str, headers: Option<HeaderMap>) -> Response {
111+
let builder = reqwest::Client::new()
112+
.delete(self.base_url(path).clone())
113+
.query(&ReqwestQuery::from(self.query_with_token()));
114+
115+
let builder = match headers {
116+
Some(headers) => builder.headers(headers),
117+
None => builder,
118+
};
119+
120+
builder.send().await.unwrap()
121+
}
122+
123+
pub async fn get_request_with_query(&self, path: &str, params: Query, headers: Option<HeaderMap>) -> Response {
124+
get(&self.base_url(path), Some(params), headers).await
125+
}
126+
127+
pub async fn get_request(&self, path: &str) -> Response {
128+
get(&self.base_url(path), None, None).await
129+
}
130+
131+
fn query_with_token(&self) -> Query {
132+
match &self.connection_info.api_token {
133+
Some(token) => Query::params([QueryParam::new("token", token)].to_vec()),
134+
None => Query::default(),
135+
}
136+
}
137+
138+
fn base_url(&self, path: &str) -> String {
139+
format!("http://{}{}{path}", &self.connection_info.bind_address, &self.base_path)
140+
}
141+
}
142+
143+
/// # Panics
144+
///
145+
/// Will panic if the request can't be sent
146+
pub async fn get(path: &str, query: Option<Query>, headers: Option<HeaderMap>) -> Response {
147+
let builder = reqwest::Client::builder().build().unwrap();
148+
149+
let builder = match query {
150+
Some(params) => builder.get(path).query(&ReqwestQuery::from(params)),
151+
None => builder.get(path),
152+
};
153+
154+
let builder = match headers {
155+
Some(headers) => builder.headers(headers),
156+
None => builder,
157+
};
158+
159+
builder.send().await.unwrap()
160+
}
161+
162+
/// Returns a `HeaderMap` with a request id header
163+
///
164+
/// # Panics
165+
///
166+
/// Will panic if the request ID can't be parsed into a string.
167+
#[must_use]
168+
pub fn headers_with_request_id(request_id: Uuid) -> HeaderMap {
169+
let mut headers = HeaderMap::new();
170+
headers.insert("x-request-id", request_id.to_string().parse().unwrap());
171+
headers
172+
}
173+
174+
#[derive(Serialize, Debug)]
175+
pub struct AddKeyForm {
176+
#[serde(rename = "key")]
177+
pub opt_key: Option<String>,
178+
pub seconds_valid: Option<u64>,
179+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod client;

0 commit comments

Comments
 (0)