torrust_tracker/console/clients/http/
app.rs1use std::str::FromStr;
17use std::time::Duration;
18
19use anyhow::Context;
20use clap::{Parser, Subcommand};
21use reqwest::Url;
22use torrust_tracker_configuration::DEFAULT_TIMEOUT;
23use torrust_tracker_primitives::info_hash::InfoHash;
24
25use crate::shared::bit_torrent::tracker::http::client::requests::announce::QueryBuilder;
26use crate::shared::bit_torrent::tracker::http::client::responses::announce::Announce;
27use crate::shared::bit_torrent::tracker::http::client::responses::scrape;
28use crate::shared::bit_torrent::tracker::http::client::{requests, Client};
29
30#[derive(Parser, Debug)]
31#[command(author, version, about, long_about = None)]
32struct Args {
33 #[command(subcommand)]
34 command: Command,
35}
36
37#[derive(Subcommand, Debug)]
38enum Command {
39 Announce { tracker_url: String, info_hash: String },
40 Scrape { tracker_url: String, info_hashes: Vec<String> },
41}
42
43pub async fn run() -> anyhow::Result<()> {
47 let args = Args::parse();
48
49 match args.command {
50 Command::Announce { tracker_url, info_hash } => {
51 announce_command(tracker_url, info_hash, DEFAULT_TIMEOUT).await?;
52 }
53 Command::Scrape {
54 tracker_url,
55 info_hashes,
56 } => {
57 scrape_command(&tracker_url, &info_hashes, DEFAULT_TIMEOUT).await?;
58 }
59 }
60
61 Ok(())
62}
63
64async fn announce_command(tracker_url: String, info_hash: String, timeout: Duration) -> anyhow::Result<()> {
65 let base_url = Url::parse(&tracker_url).context("failed to parse HTTP tracker base URL")?;
66 let info_hash =
67 InfoHash::from_str(&info_hash).expect("Invalid infohash. Example infohash: `9c38422213e30bff212b30c360d26f9a02136422`");
68
69 let response = Client::new(base_url, timeout)?
70 .announce(&QueryBuilder::with_default_values().with_info_hash(&info_hash).query())
71 .await?;
72
73 let body = response.bytes().await?;
74
75 let announce_response: Announce = serde_bencode::from_bytes(&body)
76 .unwrap_or_else(|_| panic!("response body should be a valid announce response, got: \"{:#?}\"", &body));
77
78 let json = serde_json::to_string(&announce_response).context("failed to serialize scrape response into JSON")?;
79
80 println!("{json}");
81
82 Ok(())
83}
84
85async fn scrape_command(tracker_url: &str, info_hashes: &[String], timeout: Duration) -> anyhow::Result<()> {
86 let base_url = Url::parse(tracker_url).context("failed to parse HTTP tracker base URL")?;
87
88 let query = requests::scrape::Query::try_from(info_hashes).context("failed to parse infohashes")?;
89
90 let response = Client::new(base_url, timeout)?.scrape(&query).await?;
91
92 let body = response.bytes().await?;
93
94 let scrape_response = scrape::Response::try_from_bencoded(&body)
95 .unwrap_or_else(|_| panic!("response body should be a valid scrape response, got: \"{:#?}\"", &body));
96
97 let json = serde_json::to_string(&scrape_response).context("failed to serialize scrape response into JSON")?;
98
99 println!("{json}");
100
101 Ok(())
102}