use crate::error::FlightRadarError;
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
pub struct FlightRadarClient {
client: Client,
base_url: String,
api_key: String,
}
impl FlightRadarClient {
pub fn new(api_key: String) -> Self {
FlightRadarClient {
client: Client::new(),
base_url: "https://fr24api.flightradar24.com/api/".to_string(),
api_key,
}
}
pub fn update_base_url(&mut self, base_url: String) {
self.base_url = base_url;
}
fn build_query_params(params: &FullLiveFlightQuery) -> Result<String, FlightRadarError> {
let mut url: String = String::new();
if let Some(bounds) = ¶ms.bounds {
let bounds_str = format!(
"?bounds={},{},{},{}",
bounds.north, bounds.south, bounds.west, bounds.east
);
url.push_str(&bounds_str);
}
if let Some(flights) = ¶ms.flights {
url.push_str("&flights=");
for flight in flights {
if flight.chars().all(char::is_alphanumeric) && flight.len() > 2 {
url.push_str(&flight.to_string());
url.push(',');
} else {
return Err(FlightRadarError::Parameter(format!("Flight #: {}", flight)));
}
}
url.pop();
}
if let Some(callsigns) = ¶ms.callsigns {
url.push_str("&callsigns=");
for callsign in callsigns {
if callsign.chars().all(char::is_alphanumeric)
&& callsign.len() > 2
&& callsign.len() <= 8
{
url.push_str(&callsign.to_string());
url.push(',');
} else {
return Err(FlightRadarError::Parameter(format!(
"Callsign: {}",
callsign
)));
}
}
url.pop();
}
if let Some(registrations) = ¶ms.registrations {
url.push_str("®istrations=");
for registration in registrations {
if registration
.chars()
.all(|c| c.is_alphanumeric() || c == '-')
&& registration.len() > 1
&& registration.len() <= 12
{
url.push_str(®istration.to_string());
url.push(',');
} else {
return Err(FlightRadarError::Parameter(format!(
"Registration #: {}",
registration
)));
}
}
url.pop();
}
if let Some(painted_as) = ¶ms.painted_as {
url.push_str("&painted_as=");
for painted in painted_as {
if painted.chars().all(char::is_alphabetic) && painted.len() == 3 {
url.push_str(&painted.to_string());
url.push(',');
} else {
return Err(FlightRadarError::Parameter(format!(
"Painted As: {}",
painted
)));
}
}
url.pop();
}
if let Some(operating_as) = ¶ms.operating_as {
url.push_str("&operating_as=");
for operating in operating_as {
if operating.chars().all(char::is_alphabetic) && operating.len() == 3 {
url.push_str(&operating.to_string());
url.push(',');
} else {
return Err(FlightRadarError::Parameter(format!(
"Operating As: {}",
operating
)));
}
}
url.pop();
}
if let Some(airports) = ¶ms.airports {
url.push_str("&airports=");
for airport in airports {
if airport
.chars()
.all(|c| (c.is_alphabetic() && (c != 'i' || c != 'q' || c != 'x')) || c == ':')
{
url.push_str(&airport.to_string());
url.push(',');
} else {
return Err(FlightRadarError::Parameter(format!("Airport: {}", airport)));
}
}
url.pop();
}
if let Some(routes) = ¶ms.routes {
url.push_str("&routes=");
for route in routes {
if route
.chars()
.all(|c| (c.is_alphabetic() && (c != 'i' || c != 'q' || c != 'x')) || c == '-')
{
url.push_str(&route.to_string());
url.push(',');
} else {
return Err(FlightRadarError::Parameter(format!("Route: {}", route)));
}
}
url.pop();
}
if let Some(aircraft) = ¶ms.aircraft {
url.push_str("&aircraft=");
for aircraft_iter in aircraft {
if aircraft_iter
.chars()
.all(|c| c.is_alphanumeric() || c == '*')
{
if aircraft_iter.chars().filter(|c| c == &'*').count() == 1 {
url.push_str(&aircraft_iter.to_string());
url.push(',');
}
} else {
return Err(FlightRadarError::Parameter(format!(
"Aircraft: {}",
aircraft_iter
)));
}
}
url.pop();
}
if let Some(altitude_ranges) = ¶ms.altitude_ranges {
url.push_str("&altitude_ranges=");
for altitude_range in altitude_ranges {
url.push_str(&altitude_range.min.to_string());
url.push('-');
url.push_str(&altitude_range.max.to_string());
url.push(',');
}
url.pop();
}
if let Some(squawks) = ¶ms.squawks {
url.push_str("&squawks=");
for squawk in squawks {
if squawk <= &7777 {
url.push_str(&squawk.to_string());
url.push(',');
} else {
return Err(FlightRadarError::Parameter(format!("Squawk: {}", squawk)));
}
}
url.pop();
}
if let Some(categories) = ¶ms.categories {
url.push_str("&categories=");
for category in categories {
if "PCMJTHBGDVON".contains(*category) {
url.push(*category);
url.push(',');
} else {
return Err(FlightRadarError::Parameter(format!(
"Category: {}",
category
)));
}
}
url.pop();
}
if let Some(data_sources) = ¶ms.data_sources {
let valid_data_sources = [
"ADSB".to_string(),
"MLAT".to_string(),
"ESTIMATED".to_string(),
];
url.push_str("&data_sources=");
for data_source in data_sources {
if valid_data_sources.contains(data_source) {
url.push_str(data_source);
url.push(',');
} else {
return Err(FlightRadarError::Parameter(format!(
"Data Source: {}",
data_source
)));
}
}
url.pop();
}
if let Some(airspaces) = ¶ms.airspaces {
url.push_str("&airspaces=");
for airspace in airspaces {
if airspace.chars().all(char::is_alphabetic) {
url.push_str(&airspace.to_string());
url.push(',');
} else {
return Err(FlightRadarError::Parameter(format!(
"Airspace: {}",
airspace
)));
}
}
url.pop();
}
if let Some(gspeed) = ¶ms.gspeed {
match gspeed {
ApiRangeEnum::U32(val) => {
if val > &5000 {
return Err(FlightRadarError::Parameter(format!("GSpeed: {}", val)));
}
url.push_str("&gspeed=");
url.push_str(&val.to_string());
}
ApiRangeEnum::ApiRange(gspeed) => {
if gspeed.max > 5000 {
return Err(FlightRadarError::Parameter(format!(
"GSpeed: {}",
gspeed.max
)));
}
url.push_str("&gspeed=");
url.push_str(&gspeed.min.to_string());
url.push('-');
url.push_str(&gspeed.max.to_string());
}
}
};
if let Some(limit) = ¶ms.limit {
url.push_str("&limit=");
url.push_str(&limit.to_string());
}
Ok(url)
}
pub fn check_live_parameters(query_in: &FullLiveFlightQuery) -> bool {
if query_in.aircraft.is_some()
|| query_in.airports.is_some()
|| query_in.airspaces.is_some()
|| query_in.altitude_ranges.is_some()
|| query_in.bounds.is_some()
|| query_in.callsigns.is_some()
|| query_in.categories.is_some()
|| query_in.data_sources.is_some()
|| query_in.flights.is_some()
|| query_in.gspeed.is_some()
|| query_in.operating_as.is_some()
|| query_in.painted_as.is_some()
|| query_in.registrations.is_some()
|| query_in.routes.is_some()
|| query_in.squawks.is_some()
{
return true;
};
false
}
pub fn check_historic_parameters(query_in: &FullLiveFlightQuery) -> bool {
if query_in.aircraft.is_some()
|| query_in.airports.is_some()
|| query_in.altitude_ranges.is_some()
|| query_in.bounds.is_some()
|| query_in.callsigns.is_some()
|| query_in.categories.is_some()
|| query_in.data_sources.is_some()
|| query_in.flights.is_some()
|| query_in.gspeed.is_some()
|| query_in.operating_as.is_some()
|| query_in.painted_as.is_some()
|| query_in.registrations.is_some()
|| query_in.routes.is_some()
|| query_in.squawks.is_some()
{
return true;
};
false
}
pub fn query_endpoint(&self, url: String) -> Result<String, FlightRadarError> {
let response = self
.client
.get(&url)
.header("Accept-Version", "v1")
.bearer_auth(&self.api_key)
.send()?; let response_text = response.text()?; Ok(response_text)
}
pub fn get_airline_by_icao(&self, icao: &str) -> Result<Airline, FlightRadarError> {
let url = format!("{}static/airlines/{}/light", self.base_url, icao);
let text = match self.query_endpoint(url) {
Ok(data) => data,
Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
};
let airline: Airline = serde_json::from_str(&text)
.map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
Ok(airline)
}
pub fn get_airport_by_code(&self, code: &str) -> Result<Airport, FlightRadarError> {
let url = format!("{}static/airports/{}/full", self.base_url, code);
let text = match self.query_endpoint(url) {
Ok(data) => data,
Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
};
let airport: Airport = serde_json::from_str(&text)
.map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
Ok(airport)
}
pub fn get_airport_lite_by_code(&self, code: &str) -> Result<AirportLite, FlightRadarError> {
let url = format!("{}static/airports/{}/light", self.base_url, code);
let text = match self.query_endpoint(url) {
Ok(data) => data,
Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
};
let airport: AirportLite = serde_json::from_str(&text)
.map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
Ok(airport)
}
pub fn get_live_flight(
&self,
other_queries: Option<&FullLiveFlightQuery>,
) -> Result<FullLiveFlightResponse, FlightRadarError> {
let defualt_query_in = &FullLiveFlightQuery::default();
let other_query_in = match other_queries {
Some(data) => data,
_ => defualt_query_in,
};
if !Self::check_live_parameters(other_query_in) {
return Err(FlightRadarError::Parameter(
"Missing One Of Required Parameters".to_string(),
));
}
let params_back = Self::build_query_params(other_query_in)?;
let endpoint = format!("{}live/flight-positions/full{}", self.base_url, params_back);
let text = match self.query_endpoint(endpoint) {
Ok(data) => data,
Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
};
let live_data: FullLiveFlightResponse = serde_json::from_str(&text)
.map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
Ok(live_data)
}
pub fn get_live_flight_light(
&self,
other_queries: Option<&FullLiveFlightQuery>,
) -> Result<LightLiveFlightResponse, FlightRadarError> {
let defualt_query_in = &FullLiveFlightQuery::default();
let other_query_in = match other_queries {
Some(data) => data,
_ => defualt_query_in,
};
if !Self::check_live_parameters(other_query_in) {
return Err(FlightRadarError::Parameter(
"Missing One Of Required Parameters".to_string(),
));
}
let params_back = Self::build_query_params(other_query_in)?;
let endpoint = format!("{}live/flight-positions/full{}", self.base_url, params_back);
let text = match self.query_endpoint(endpoint) {
Ok(data) => data,
Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
};
let live_data: LightLiveFlightResponse = serde_json::from_str(&text)
.map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
Ok(live_data)
}
pub fn get_historic_flight(
&self,
timestamp: &u64,
other_queries: Option<&FullLiveFlightQuery>,
) -> Result<FullLiveFlightResponse, FlightRadarError> {
const MIN_TIMESTAMP: u64 = 1462924800;
if timestamp < &MIN_TIMESTAMP {
return Err(FlightRadarError::Parameter(format!(
"Invalid Timestamp: {}",
timestamp
)));
};
let stuff = match other_queries {
Some(stuff) => stuff,
None => &FullLiveFlightQuery::default(),
};
if !Self::check_historic_parameters(stuff) {
return Err(FlightRadarError::Parameter(
"Missing One Of Required Parameters".to_string(),
));
}
let params_back = if let Some(other_queries) = other_queries {
match Self::build_query_params(other_queries) {
Ok(param_str) => param_str[1..param_str.len()].to_string(),
Err(_) => String::from(""),
}
} else {
String::from("")
};
let endpoint = format!(
"{}historic/flight-positions/full?timestamp={}&{}",
self.base_url, timestamp, params_back
);
let text = match self.query_endpoint(endpoint) {
Ok(data) => data,
Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
};
let historic_data: FullLiveFlightResponse = serde_json::from_str(&text)
.map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
Ok(historic_data)
}
pub fn get_historic_flight_light(
&self,
timestamp: &u64,
other_queries: Option<&FullLiveFlightQuery>,
) -> Result<LightLiveFlightResponse, FlightRadarError> {
const MIN_TIMESTAMP: u64 = 1462924800;
if timestamp < &MIN_TIMESTAMP {
return Err(FlightRadarError::Parameter(format!(
"Invalid Timestamp: {}",
timestamp
)));
};
let stuff = match other_queries {
Some(stuff) => stuff,
None => &FullLiveFlightQuery::default(),
};
if !Self::check_historic_parameters(stuff) {
return Err(FlightRadarError::Parameter(
"Missing One Of Required Parameters".to_string(),
));
}
let params_back = if let Some(other_queries) = other_queries {
match Self::build_query_params(other_queries) {
Ok(param_str) => param_str[1..param_str.len()].to_string(),
Err(_) => String::from(""),
}
} else {
String::from("")
};
let endpoint = format!(
"{}historic/flight-positions/light?timestamp={}&{}",
self.base_url, timestamp, params_back
);
let text = match self.query_endpoint(endpoint) {
Ok(data) => data,
Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
};
let historic_data: LightLiveFlightResponse = serde_json::from_str(&text)
.map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
Ok(historic_data)
}
pub fn get_flight_tracks_by_id(
&self,
flight_id: &str,
) -> Result<Vec<Flight>, FlightRadarError> {
if u64::from_str_radix(flight_id, 16).is_err() {
return Err(FlightRadarError::Parameter(format!(
"Flight ID Not Hexadecimal: {}",
flight_id
)));
}
let url = format!("{}flight-tracks?flight_id={}", self.base_url, flight_id);
let text = match self.query_endpoint(url) {
Ok(data) => data,
Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
};
let flights: Vec<Flight> = serde_json::from_str(&text)
.map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
Ok(flights)
}
pub fn get_api_usage(&self, period: &str) -> Result<ApiUsageResponse, FlightRadarError> {
(match period {
"24h" | "7d" | "30d" | "1y" => Ok(()),
_ => Err(FlightRadarError::Parameter(format!(
"Period: {}. Should be: 24h|7d|30d|1y",
period
))),
})?;
let url = format!("{}usage?period={}", self.base_url, period);
let text = match self.query_endpoint(url) {
Ok(data) => data,
Err(_) => return Err(FlightRadarError::General("GET Request Failed".to_string())),
};
let usage: ApiUsageResponse = serde_json::from_str(&text)
.map_err(|e| FlightRadarError::Parsing(format!("{}\nResponse: {}", e, text)))?;
Ok(usage)
}
}
#[derive(Debug, Deserialize, Default)]
pub struct Track {
pub timestamp: String,
pub lat: f64,
pub lon: f64,
pub alt: u32,
pub gspeed: u32,
pub vspeed: u32,
pub track: u32,
pub squawk: String,
pub callsign: String,
pub source: String,
}
#[derive(Debug, Deserialize, Default)]
pub struct Flight {
#[serde(rename = "fr24_id")]
pub id: String,
pub tracks: Vec<Track>,
}
#[derive(Debug, Deserialize, Default)]
pub struct ApiUsageResponse {
pub data: Vec<ApiEndpointUsage>,
}
#[derive(Debug, Deserialize, Default)]
pub struct ApiEndpointUsage {
pub endpoint: String,
pub metadata: String,
pub request_count: u32,
pub results: u32,
pub credits: u32,
}
#[derive(Debug, Deserialize, Default)]
pub struct Airline {
pub name: String,
pub iata: Option<String>,
pub icao: String,
}
#[derive(Debug, Deserialize, Default)]
pub struct Airport {
pub name: String,
pub iata: String,
pub icao: String,
pub lon: f64,
pub lat: f64,
pub elevation: i32,
pub country: Country,
pub city: String,
pub state: Option<String>,
pub timezone: Timezone,
}
#[derive(Debug, Deserialize, Default)]
pub struct Country {
pub code: String,
pub name: String,
}
#[derive(Debug, Deserialize, Default)]
pub struct Timezone {
pub name: String,
pub offset: i32,
}
#[derive(Debug, Deserialize, Default)]
pub struct AirportLite {
pub name: String,
pub iata: String,
pub icao: String,
}
#[derive(Debug, Deserialize, Default)]
pub struct FullLiveFlightQuery {
pub bounds: Option<Bounds>,
pub flights: Option<Vec<String>>,
pub callsigns: Option<Vec<String>>,
pub registrations: Option<Vec<String>>,
pub painted_as: Option<Vec<String>>,
pub operating_as: Option<Vec<String>>,
pub airports: Option<Vec<String>>,
pub routes: Option<Vec<String>>,
pub aircraft: Option<Vec<String>>,
pub altitude_ranges: Option<Vec<ApiRange>>,
pub squawks: Option<Vec<u16>>,
pub categories: Option<Vec<char>>,
pub data_sources: Option<Vec<String>>,
pub airspaces: Option<Vec<String>>,
pub gspeed: Option<ApiRangeEnum>,
pub limit: Option<u32>,
}
#[derive(Debug, Deserialize, Default, Serialize)]
pub struct Bounds {
pub north: f64,
pub south: f64,
pub west: f64,
pub east: f64,
}
#[derive(Debug, Deserialize, Default)]
pub struct ApiRange {
pub min: u32,
pub max: u32,
}
#[derive(Debug, Deserialize)]
pub enum ApiRangeEnum {
U32(u32),
ApiRange(ApiRange),
}
#[derive(Deserialize, Debug, Default)]
pub struct FullLiveFlightResponse {
pub data: Vec<FullLiveFlightData>,
}
#[derive(Deserialize, Debug, Default)]
pub struct FullLiveFlightData {
pub fr24_id: String,
pub flight: String,
pub callsign: String,
pub lat: f64,
pub lon: f64,
pub track: u32,
pub alt: u32,
pub gspeed: u32,
pub vspeed: i32, pub squawk: String,
pub timestamp: String,
pub source: String,
pub hex: String,
#[serde(rename = "type")]
pub type_field: String,
pub reg: String,
pub painted_as: String,
pub operating_as: String,
pub orig_iata: String,
pub orig_icao: String,
pub dest_iata: String,
pub dest_icao: String,
pub eta: String,
}
#[derive(Deserialize, Debug, Default)]
pub struct LightLiveFlightResponse {
pub data: Vec<LightLiveFlightData>,
}
#[derive(Deserialize, Debug, Default)]
pub struct LightLiveFlightData {
pub fr24_id: String,
pub hex: String,
pub callsign: String,
pub lat: f64,
pub lon: f64,
pub track: u32,
pub alt: u32,
pub gspeed: u32,
pub vspeed: i32, pub squawk: String,
pub timestamp: String,
pub source: String,
}