btpeer 0.7.0

Simple CLI tool and library to get peers from TCP/HTTP and UDP BitTorrent trackers
Documentation
pub mod proxy;
pub mod query;
mod response;

pub use proxy::Proxy;

use crate::Peer;
use bendy::decoding::FromBencode;
use cyphernet::addr::{HostName, PartialAddr, i2p::I2pAddr};
use std::{
    error::Error,
    net::{IpAddr, Ipv4Addr, Ipv6Addr},
    str::FromStr,
    time::Duration,
};

pub struct Response {
    pub interval: u32,
    pub leechers: u32,
    pub seeders: u32,
}

pub async fn announce(
    query: &query::Announce,
    timeout: Duration,
    proxy_url: Option<&str>,
    peers_buffer: Option<&mut Vec<Peer>>,
) -> Result<Response, Box<dyn Error>> {
    let client = reqwest::Client::builder().timeout(timeout);
    let response = match proxy_url {
        Some(p) => client.proxy(Proxy::from_str(p)?.0),
        None => client,
    }
    .build()?
    .get(query.to_string())
    .send()
    .await?;

    if !response.status().is_success() {
        return Err(format!("Tracker returned status: {}", response.status()).into());
    }

    // Handle response...
    let result = response::Response::from_bencode(&response.bytes().await?)?;

    if let Some(peers) = peers_buffer {
        if !result.peers.is_empty() {
            if query.url().host_str().is_some_and(|h| h.ends_with(".i2p")) {
                for chunk in result.peers.chunks_exact(32) {
                    peers.push(PartialAddr {
                        host: HostName::I2p(I2pAddr::from_str(&format!(
                            "{}.b32.i2p",
                            data_encoding::BASE32_NOPAD.encode(chunk)
                        ))?),
                        port: None,
                    })
                }
            } else {
                for chunk in result.peers.chunks_exact(6) {
                    let ip_bytes: [u8; 4] = chunk[0..4].try_into()?;
                    peers.push(PartialAddr {
                        host: HostName::Ip(IpAddr::V4(Ipv4Addr::from(ip_bytes))),
                        port: Some(u16::from_be_bytes(chunk[4..6].try_into()?)),
                    })
                }
            }
        }
        if !result.peers6.is_empty() {
            for chunk in result.peers6.chunks_exact(18) {
                let ip_bytes: [u8; 16] = chunk[0..16].try_into()?;
                peers.push(PartialAddr {
                    host: HostName::Ip(IpAddr::V6(Ipv6Addr::from(ip_bytes))),
                    port: Some(u16::from_be_bytes(chunk[16..18].try_into()?)),
                })
            }
        }
    }

    // Done.
    Ok(Response {
        interval: result.interval,
        leechers: result.incomplete,
        seeders: result.complete,
    })
}