torrust-actix 4.2.3

A rich, fast and efficient Bittorrent Tracker.
use crate::api::api::{
    api_parse_body,
    api_service_token,
    api_validation,
    parse_info_hash
};
use crate::api::structs::api_service_data::ApiServiceData;
use crate::api::structs::query_token::QueryToken;
use crate::tracker::enums::updates_action::UpdatesAction;
use crate::tracker::structs::peer_id::PeerId;
use crate::tracker::structs::torrent_entry::TorrentEntry;
use crate::tracker::types::ahash_map::AHashMap;
use actix_web::http::header::ContentType;
use actix_web::web::Data;
use actix_web::{
    web,
    HttpRequest,
    HttpResponse
};
use serde_json::{
    json,
    Value
};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{
    SystemTime,
    UNIX_EPOCH
};

pub async fn api_service_torrent_get(request: HttpRequest, path: web::Path<String>, data: Data<Arc<ApiServiceData>>) -> HttpResponse
{
    if let Some(error_return) = api_validation(&request, &data).await { return error_return; }
    let params = web::Query::<QueryToken>::from_query(request.query_string()).unwrap();
    if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; }
    let info = path.into_inner();
    let info_hash = match parse_info_hash(&info) {
        Ok(h) => h,
        Err(r) => return r,
    };
    match data.torrent_tracker.get_torrent(info_hash) {
        None => HttpResponse::NotFound().content_type(ContentType::json()).json(json!({"status": "unknown info_hash"})),
        Some(torrent) => HttpResponse::Ok().content_type(ContentType::json()).json(api_service_torrents_return_torrent_json(torrent)),
    }
}

pub async fn api_service_torrents_get(request: HttpRequest, payload: web::Payload, data: Data<Arc<ApiServiceData>>) -> HttpResponse
{
    if let Some(error_return) = api_validation(&request, &data).await { return error_return; }
    let params = web::Query::<QueryToken>::from_query(request.query_string()).unwrap();
    if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; }
    let body = match api_parse_body(payload).await {
        Ok(data) => data,
        Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})),
    };
    let info_hashes = match serde_json::from_slice::<Vec<String>>(&body) {
        Ok(hash) => hash,
        Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})),
    };
    let mut torrents_output = HashMap::with_capacity(info_hashes.len());
    for info in info_hashes {
        if info.len() == 40 {
            match parse_info_hash(&info) {
                Ok(info_hash) => {
                    if let Some(torrent) = data.torrent_tracker.get_torrent(info_hash) {
                        torrents_output.insert(info, api_service_torrents_return_torrent_json(torrent));
                    }
                }
                Err(_) => {
                    return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"}))
                }
            }
        }
    }
    HttpResponse::Ok().content_type(ContentType::json()).json(json!({
        "status": "ok",
        "torrents": torrents_output
    }))
}

pub async fn api_service_torrent_post(request: HttpRequest, path: web::Path<(String, u64)>, data: Data<Arc<ApiServiceData>>) -> HttpResponse
{
    if let Some(error_return) = api_validation(&request, &data).await { return error_return; }
    let params = web::Query::<QueryToken>::from_query(request.query_string()).unwrap();
    if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; }
    let (info, completed) = path.into_inner();
    let info_hash = match parse_info_hash(&info) {
        Ok(h) => h,
        Err(r) => return r,
    };
    let torrent_entry = TorrentEntry {
        seeds: AHashMap::default(),
        seeds_ipv6: AHashMap::default(),
        peers: AHashMap::default(),
        peers_ipv6: AHashMap::default(),
        rtc_seeds: AHashMap::default(),
        rtc_peers: AHashMap::default(),
        completed,
        updated: std::time::Instant::now(),
    };
    if data.torrent_tracker.config.database.persistent {
        let _ = data.torrent_tracker.add_torrent_update(info_hash, torrent_entry.clone(), UpdatesAction::Add);
    }
    match data.torrent_tracker.add_torrent(info_hash, torrent_entry) {
        (_, true) => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})),
        (_, false) => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "info_hash updated"})),
    }
}

pub async fn api_service_torrents_post(request: HttpRequest, payload: web::Payload, data: Data<Arc<ApiServiceData>>) -> HttpResponse
{
    if let Some(error_return) = api_validation(&request, &data).await { return error_return; }
    let params = web::Query::<QueryToken>::from_query(request.query_string()).unwrap();
    if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; }
    let body = match api_parse_body(payload).await {
        Ok(data) => data,
        Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})),
    };
    let info_hashmap = match serde_json::from_slice::<HashMap<String, u64>>(&body) {
        Ok(data) => data,
        Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})),
    };
    let mut torrents_output = HashMap::with_capacity(info_hashmap.len());
    for (info, completed) in info_hashmap {
        if info.len() == 40 {
            match parse_info_hash(&info) {
                Ok(info_hash) => {
                    let torrent_entry = TorrentEntry {
                        seeds: AHashMap::default(),
                        seeds_ipv6: AHashMap::default(),
                        peers: AHashMap::default(),
                        peers_ipv6: AHashMap::default(),
                        rtc_seeds: AHashMap::default(),
                        rtc_peers: AHashMap::default(),
                        completed,
                        updated: std::time::Instant::now(),
                    };
                    if data.torrent_tracker.config.database.persistent {
                        let _ = data.torrent_tracker.add_torrent_update(info_hash, torrent_entry.clone(), UpdatesAction::Add);
                    }
                    let status = match data.torrent_tracker.add_torrent(info_hash, torrent_entry) {
                        (_, true) => json!({"status": "ok"}),
                        (_, false) => json!({"status": "info_hash updated"}),
                    };
                    torrents_output.insert(info, status);
                }
                Err(_) => {
                    return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"}))
                }
            }
        }
    }
    HttpResponse::Ok().content_type(ContentType::json()).json(json!({
        "status": "ok",
        "torrents": torrents_output
    }))
}

pub async fn api_service_torrent_delete(request: HttpRequest, path: web::Path<String>, data: Data<Arc<ApiServiceData>>) -> HttpResponse
{
    if let Some(error_return) = api_validation(&request, &data).await { return error_return; }
    let params = web::Query::<QueryToken>::from_query(request.query_string()).unwrap();
    if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; }
    let info = path.into_inner();
    let info_hash = match parse_info_hash(&info) {
        Ok(h) => h,
        Err(r) => return r,
    };
    if data.torrent_tracker.config.database.persistent {
        let _ = data.torrent_tracker.add_torrent_update(info_hash, TorrentEntry::default(), UpdatesAction::Remove);
    }
    match data.torrent_tracker.remove_torrent(info_hash) {
        None => HttpResponse::NotModified().content_type(ContentType::json()).json(json!({"status": "unknown info_hash"})),
        Some(_) => HttpResponse::Ok().content_type(ContentType::json()).json(json!({"status": "ok"})),
    }
}

pub async fn api_service_torrents_delete(request: HttpRequest, payload: web::Payload, data: Data<Arc<ApiServiceData>>) -> HttpResponse
{
    if let Some(error_return) = api_validation(&request, &data).await { return error_return; }
    let params = web::Query::<QueryToken>::from_query(request.query_string()).unwrap();
    if let Some(response) = api_service_token(params.token.clone(), Arc::clone(&data.torrent_tracker.config)).await { return response; }
    let body = match api_parse_body(payload).await {
        Ok(data) => data,
        Err(error) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": error.to_string()})),
    };
    let hashes = match serde_json::from_slice::<Vec<String>>(&body) {
        Ok(data) => data,
        Err(_) => return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "bad json body"})),
    };
    let mut torrents_output = HashMap::with_capacity(hashes.len());
    for info in hashes {
        if info.len() == 40 {
            match parse_info_hash(&info) {
                Ok(info_hash) => {
                    if data.torrent_tracker.config.database.persistent {
                        let _ = data.torrent_tracker.add_torrent_update(info_hash, TorrentEntry::default(), UpdatesAction::Remove);
                    }
                    let status = match data.torrent_tracker.remove_torrent(info_hash) {
                        None => json!({"status": "unknown info_hash"}),
                        Some(_) => json!({"status": "ok"}),
                    };
                    torrents_output.insert(info, status);
                }
                Err(_) => {
                    return HttpResponse::BadRequest().content_type(ContentType::json()).json(json!({"status": "invalid info_hash"}))
                }
            }
        }
    }
    HttpResponse::Ok().content_type(ContentType::json()).json(json!({
        "status": "ok",
        "torrents": torrents_output
    }))
}

pub fn api_service_torrents_return_torrent_json(torrent: TorrentEntry) -> Value
{
    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
    let process_peer_entry = |(peer_id, torrent_peer): (&PeerId, &crate::tracker::structs::torrent_peer::TorrentPeer)| {
        let elapsed_ms = torrent_peer.updated.elapsed().as_millis();
        let timestamp = now.saturating_sub(elapsed_ms);
        let timestamp_final = ((timestamp as f64 / 2.0).round() * 2.0) as u64;
        json!({
            "peer_id": peer_id.0,
            "peer_addr": torrent_peer.peer_addr,
            "updated": timestamp_final,
            "uploaded": torrent_peer.uploaded.0 as u64,
            "downloaded": torrent_peer.downloaded.0 as u64,
            "left": torrent_peer.left.0 as u64,
        })
    };
    let seeds: Vec<Value> = torrent.seeds.iter()
        .chain(torrent.seeds_ipv6.iter())
        .map(process_peer_entry)
        .collect();
    let peers: Vec<Value> = torrent.peers.iter()
        .chain(torrent.peers_ipv6.iter())
        .map(process_peer_entry)
        .collect();
    let elapsed_ms = torrent.updated.elapsed().as_millis();
    let timestamp = now.saturating_sub(elapsed_ms);
    let timestamp_final = ((timestamp as f64 / 2.0).round() * 2.0) as u64;
    json!({
        "status": "ok",
        "seeds": seeds,
        "peers": peers,
        "completed": torrent.completed,
        "updated": timestamp_final
    })
}