torrust_index/console/commands/seeder/
api.rs

1//! Action that a user can perform on a Index website.
2use thiserror::Error;
3use tracing::debug;
4
5use crate::web::api::client::v1::client::Client;
6use crate::web::api::client::v1::contexts::category::forms::AddCategoryForm;
7use crate::web::api::client::v1::contexts::category::responses::{ListItem, ListResponse};
8use crate::web::api::client::v1::contexts::torrent::forms::UploadTorrentMultipartForm;
9use crate::web::api::client::v1::contexts::torrent::responses::{UploadedTorrent, UploadedTorrentResponse};
10use crate::web::api::client::v1::contexts::user::forms::LoginForm;
11use crate::web::api::client::v1::contexts::user::responses::{LoggedInUserData, SuccessfulLoginResponse};
12use crate::web::api::client::v1::responses::TextResponse;
13
14#[derive(Error, Debug)]
15pub enum Error {
16    #[error("Torrent with the same info-hash already exist in the database")]
17    TorrentInfoHashAlreadyExists,
18    #[error("Torrent with the same title already exist in the database")]
19    TorrentTitleAlreadyExists,
20}
21
22/// It uploads a torrent file to the Torrust Index.
23///
24/// # Errors
25///
26/// It returns an error if the torrent already exists in the database.
27///
28/// # Panics
29///
30/// Panics if the response body is not a valid JSON.
31pub async fn upload_torrent(client: &Client, upload_torrent_form: UploadTorrentMultipartForm) -> Result<UploadedTorrent, Error> {
32    let categories = get_categories(client).await;
33
34    if !contains_category_with_name(&categories, &upload_torrent_form.category) {
35        add_category(client, &upload_torrent_form.category).await;
36    }
37
38    // todo: if we receive timeout error we should retry later. Otherwise we
39    // have to restart the seeder manually.
40
41    let response = client
42        .upload_torrent(upload_torrent_form.into())
43        .await
44        .expect("API should return a response");
45
46    debug!(target:"seeder", "response: {}", response.status);
47
48    if response.status == 400 {
49        if response.body.contains("This torrent already exists in our database") {
50            return Err(Error::TorrentInfoHashAlreadyExists);
51        }
52
53        if response.body.contains("This torrent title has already been used") {
54            return Err(Error::TorrentTitleAlreadyExists);
55        }
56    }
57
58    assert!(response.is_json_and_ok(), "Error uploading torrent: {}", response.body);
59
60    let uploaded_torrent_response: UploadedTorrentResponse =
61        serde_json::from_str(&response.body).expect("a valid JSON response should be returned from the Torrust Index API");
62
63    Ok(uploaded_torrent_response.data)
64}
65
66/// It logs in the user and returns the user data.
67///
68/// # Panics
69///
70/// Panics if the response body is not a valid JSON.
71pub async fn login(client: &Client, username: &str, password: &str) -> LoggedInUserData {
72    let response = client
73        .login_user(LoginForm {
74            login: username.to_owned(),
75            password: password.to_owned(),
76        })
77        .await
78        .expect("API should return a response");
79
80    let res: SuccessfulLoginResponse = serde_json::from_str(&response.body).unwrap_or_else(|_| {
81        panic!(
82            "a valid JSON response should be returned after login. Received: {}",
83            response.body
84        )
85    });
86
87    res.data
88}
89
90/// It returns all the index categories.
91///
92/// # Panics
93///
94/// Panics if the response body is not a valid JSON.
95pub async fn get_categories(client: &Client) -> Vec<ListItem> {
96    let response = client.get_categories().await.expect("API should return a response");
97
98    let res: ListResponse = serde_json::from_str(&response.body).unwrap();
99
100    res.data
101}
102
103/// It adds a new category.
104///
105/// # Panics
106///
107/// Will panic if it doesn't get a response form the API.
108pub async fn add_category(client: &Client, name: &str) -> TextResponse {
109    client
110        .add_category(AddCategoryForm {
111            name: name.to_owned(),
112            icon: None,
113        })
114        .await
115        .expect("API should return a response")
116}
117
118/// It checks if the category list contains the given category.
119fn contains_category_with_name(items: &[ListItem], category_name: &str) -> bool {
120    items.iter().any(|item| item.name == category_name)
121}