Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d917e14
flight tracker regex check
julestheshiba Nov 12, 2025
b5af9ea
flight tracker started
julestheshiba Nov 12, 2025
cde4df3
flight tracker refactored and properly setup
julestheshiba Nov 13, 2025
8b2fd11
more flight tracker
julestheshiba Nov 23, 2025
d3f1dba
basic flight tracking
julestheshiba Nov 24, 2025
352f9ba
planeinfo command working
julestheshiba Nov 24, 2025
1b0e7f8
started flight progress
julestheshiba Nov 24, 2025
a8f6046
current version of flight tracker
julestheshiba Nov 26, 2025
16ca9e5
Add schema and fix config file finding.
coravacav Nov 27, 2025
f89eb64
Updated and fixed errors
julestheshiba Nov 27, 2025
c897916
flight tracker regex check
julestheshiba Nov 12, 2025
1bb08b9
flight tracker started
julestheshiba Nov 12, 2025
f6ef285
flight tracker refactored and properly setup
julestheshiba Nov 13, 2025
72fa72c
more flight tracker
julestheshiba Nov 23, 2025
e3030ba
basic flight tracking
julestheshiba Nov 24, 2025
16e9cf9
planeinfo command working
julestheshiba Nov 24, 2025
e1fa542
started flight progress
julestheshiba Nov 24, 2025
ae3b1c2
current version of flight tracker
julestheshiba Nov 26, 2025
2614a4b
Updated and fixed errors
julestheshiba Nov 27, 2025
b09a95e
Merge branch 'main' of https://github.com/julestheshiba/uofu-cs-disco…
julestheshiba Nov 27, 2025
dd8a452
switched to embed discord prints
julestheshiba Nov 27, 2025
c2167f2
bot working v1
julestheshiba Nov 27, 2025
40987e1
removed guild id
julestheshiba Nov 27, 2025
a79676f
removed local config
julestheshiba Nov 27, 2025
356553e
actually removed
julestheshiba Nov 27, 2025
660407e
added newline in Cargo.toml
julestheshiba Nov 27, 2025
bbb6032
moved airport lookup to if statement to reduce api calls
julestheshiba Nov 28, 2025
c54177f
removed log file
julestheshiba Nov 29, 2025
9651596
deleted
julestheshiba Nov 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1 +1 @@

guild_id = "538917688311939072"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ db/
all_backups/

debug.json
secrets.properties
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ bot-lib = { path = 'bot-lib' }
bot-traits = { path = 'bot-traits' }
bot-db = { path = 'bot-db' }
rand = "0.9"
serde = { version = "1", default-features = false, features = ["derive", "rc"] }
serde = { version = "1", default-features = false, features = ["derive", "rc", "std"] }
itertools = "0.14"
notify = { version = "8", default-features = false, features = [
"macos_kqueue",
Expand Down Expand Up @@ -59,4 +59,4 @@ opt-level = 3
opt-level = 1

[profile.test.package."*"]
opt-level = 3
opt-level = 3
2 changes: 1 addition & 1 deletion bot-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ poise = { workspace = true }
tracing = { workspace = true }
bot-traits = { workspace = true }
rand = { workspace = true }
serde = { workspace = true }
bot-db = { workspace = true }
itertools = { workspace = true }
notify = { workspace = true }
Expand All @@ -20,6 +19,7 @@ chrono = "0.4"
toml = { default-features = false, version = "0.9", features = ["parse", "serde", "std"] }
regex = "1"
nom = "7"
serde = { workspace = true }
serde_with = { version = "3.7", features = ["chrono"] }
futures = "0.3"
parking_lot = "0.12"
Expand Down
2 changes: 2 additions & 0 deletions bot-lib/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod set_bot_role;
mod set_dog_role;
mod timeout;
mod yeet;
mod track_flight;

pub use admin::*;
pub use anon_notify::*;
Expand All @@ -37,6 +38,7 @@ pub use set_bot_role::*;
pub use set_dog_role::*;
pub use timeout::*;
pub use yeet::*;
pub use track_flight::*;

use crate::data::PoiseContext;
use color_eyre::eyre::{Context, ContextCompat, OptionExt, Result};
Expand Down
309 changes: 309 additions & 0 deletions bot-lib/src/commands/track_flight.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
use std::f64::consts::PI;
use crate::data::PoiseContext;
use color_eyre::eyre::{Result, eyre};
use poise::{CreateReply};
use regex::Regex;
use serde::Deserialize;
use chrono::{Local, Datelike};

#[derive(Debug, Deserialize)]
struct AirportResponse {
response: Option<AirportData>,
error: Option<AirlabsError>,
}

#[derive(Debug, Deserialize)]
struct AirportData {
lat: Option<f64>,
lng: Option<f64>,
}

#[derive(Debug, Deserialize)]
struct FlightResponse {
response: Option<FlightData>,
error: Option<AirlabsError>,
}

#[derive(Debug, Deserialize)]
struct FlightData {
flight_iata: Option<String>,
airline_iata: Option<String>,
dep_iata: Option<String>,
arr_iata: Option<String>,
status: Option<String>,
duration: Option<i64>,
model: Option<String>,
manufacture: Option<String>,
dep_time: Option<String>,
arr_time: Option<String>,
engine: Option<String>,
age: Option<i64>,
built: Option<i64>,
arr_estimated: Option<String>,
dep_estimated: Option<String>,
flight_icao: Option<String>,
airline_icao: Option<String>,
dep_icao: Option<String>,
arr_icao: Option<String>,
airline_name: Option<String>,
lat: Option<f64>,
long: Option<f64>,
}

#[derive(Debug, Deserialize)]
struct AirlabsError {
message: String,
}

static IATA_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(r"^[A-Z]{2}[0-9]{1,4}").unwrap()
});

static ICAO_RE: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
regex::Regex::new(r"^[A-Z]{3}[0-9]{1,4}").unwrap()
});

fn format_time(label: &str, scheduled: &Option<String>, estimated: &Option<String>) -> String {
match (scheduled, estimated) {
(_, Some(est)) => format!("Est {label}: {est}"),
(Some(sched), None) => format!("{label}: {sched}"),
_ => format!("{label}: N/A"),
}
}

fn minutes_to_hours(duration: Option<i64>) -> String {
let time = duration.unwrap_or(0);
let hours = time/60;
let minutes = time%60;

format!("Flight Time: {hours}:{minutes}")
}

fn degree_to_rad(deg :f64) -> f64{
deg * PI / 180.0
}

fn haversine_distance(lat1: f64, long1: f64, lat2: f64, long2: f64) -> f64 {
let r = 6371.0;
let dlat = degree_to_rad(lat2 - lat1);
let dlon = degree_to_rad(long2 - long1);
let lat1 = degree_to_rad(lat1);
let lat2 = degree_to_rad(lat2);

let a = (dlat/2.0).sin().powi(2) +
lat1.cos() * lat2.cos() * (dlon/2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
r * c
}

fn flight_progess(plane_lat: f64, plane_long: f64, source_lat: f64, source_long: f64, dst_lat: f64, dst_long: f64) -> f64{
let d_star_dest = haversine_distance(source_lat, source_long, dst_lat, dst_long);
let d_start_airp = haversine_distance(source_lat, source_long, plane_lat, plane_long);
d_star_dest / d_start_airp
}

async fn airport_lookup(ctx: PoiseContext<'_>, code: String) -> Option<AirportData> {
let api_key = std::env::var("API_KEY").map_err(|_| eyre!("API_KEY missing from environment")).ok()?;
let searched_iata = IATA_RE.is_match(&code);
let searched_icao = ICAO_RE.is_match(&code);

let url = if searched_iata {
format!(
"https://airlabs.co/api/v9/airport?iata_code={}&api_key={}",
code, api_key
)
} else if searched_icao {
format!(
"https://airlabs.co/api/v9/airport?icao_code={}&api_key={}",
code, api_key
)
} else {
ctx.reply("Please provide a valid airport ident(IATA or ICAO)").await.ok()?;
return None;
};

let client = reqwest::Client::new();
let response: AirportResponse = client
.get(url)
.send()
.await
.ok()?
.json()
.await
.ok()?;

if let Some(err) = response.error {
ctx.reply(format!("API Error: {}", err.message)).await.ok()?;
return None;
}

let Some(airport) = response.response else {
ctx.reply("No airport data found for that code.").await.ok()?;
return None;
};

Some(airport)
}

async fn flight_lookup(ctx: PoiseContext<'_>, code: String) -> Option<FlightData> {
let api_key = std::env::var("API_KEY").map_err(|_| eyre!("API_KEY missing from environment")).ok()?;
let date = Local::now().format("%Y-%m-%d").to_string();

let searched_iata = IATA_RE.is_match(&code);
let searched_icao = ICAO_RE.is_match(&code);

let url = if searched_iata {
format!(
"https://airlabs.co/api/v9/flight?flight_iata={}&api_key={}&flight_date={}",
code, api_key, date
)
} else if searched_icao {
format!(
"https://airlabs.co/api/v9/flight?flight_icao={}&api_key={}&flight_date={}",
code, api_key, date
)
} else {
ctx.reply("Please provide a valid flight number (IATA or ICAO)").await.ok()?;
return None;
};

let client = reqwest::Client::new();
let response: FlightResponse = client
.get(url)
.send()
.await.ok()?
.json()
.await.ok()?;

if let Some(err) = response.error {
ctx.reply(format!("API Error: {}", err.message)).await.ok()?;
return None;
}

let Some(flight) = response.response else {
ctx.reply("No flight data found for that number.").await.ok()?;
return None;
};

Some(flight)
}

///get information on a specified flight
#[poise::command(slash_command, rename = "trackflight")]
pub async fn track_flight(
ctx: PoiseContext<'_>,

search: String,
) -> Result<()> {
ctx.defer().await?;

let search: String = search
.chars()
.filter(|c| c.is_alphanumeric())
.collect::<String>()
.to_uppercase();

let searched_iata = IATA_RE.is_match(&search);
let searched_icao = ICAO_RE.is_match(&search);

let flight = match flight_lookup(ctx, search).await{
Some(flight) => flight,
None => return Ok(()),
};

let (flight_label, airline_standard, dep_airport, arr_airport) = if searched_iata {
(
flight.flight_iata.clone().or(flight.flight_icao.clone()).unwrap_or_default(),
flight.airline_iata.clone().or(flight.airline_iata.clone()).unwrap_or_else(|| "N/A".to_string()),
flight.dep_iata.clone().or(flight.dep_icao.clone()).unwrap_or_else(|| "N/A".to_string()),
flight.arr_iata.clone().or(flight.arr_icao.clone()).unwrap_or_else(|| "N/A".to_string()),
)
} else if searched_icao {
(
flight.flight_icao.clone().or(flight.flight_iata.clone()).unwrap_or_default(),
flight.airline_icao.clone().or(flight.airline_icao.clone()).unwrap_or_else(|| "N/A".to_string()),
flight.dep_icao.clone().or(flight.dep_iata.clone()).unwrap_or_else(|| "N/A".to_string()),
flight.arr_icao.clone().or(flight.arr_iata.clone()).unwrap_or_else(|| "N/A".to_string()),
)
} else {
(
flight.flight_iata.clone().or(flight.flight_icao.clone()).unwrap_or_default(),
flight.airline_iata.clone().or(flight.airline_iata.clone()).unwrap_or_else(|| "N/A".to_string()),
flight.dep_iata.clone().or(flight.dep_icao.clone()).unwrap_or_else(|| "N/A".to_string()),
flight.arr_iata.clone().or(flight.arr_icao.clone()).unwrap_or_else(|| "N/A".to_string()),
)
};

let dep_time_display = format_time("Departure Time", &flight.dep_time, &flight.dep_estimated);
let arr_time_display = format_time("Arrival Time", &flight.arr_time, &flight.arr_estimated);
let airline = flight.airline_name.clone().unwrap_or_else(|| "Unknown".to_string());
let status = flight.status.clone().unwrap_or_else(|| "Unknown".to_string());
let duration = minutes_to_hours(flight.duration.clone());

let reply_msg = format!(
"**Flight {}\n**Airline: {}\nStatus: {}\n{} -> {}\n{}\n{}\n{}",
flight_label, airline, status, dep_airport, arr_airport, duration, dep_time_display, arr_time_display
);

ctx.send(CreateReply::default().content(reply_msg)).await?;

Ok(())
}

///get information on an aircraft
#[poise::command(slash_command, rename = "planeinfo")]
pub async fn plane_details(
ctx: PoiseContext<'_>,

search: String,
) -> Result<()> {
ctx.defer().await?;

let search: String = search
.chars()
.filter(|c| c.is_alphanumeric())
.collect::<String>()
.to_uppercase();

let searched_iata = IATA_RE.is_match(&search);
let searched_icao = ICAO_RE.is_match(&search);

let flight = match flight_lookup(ctx, search).await{
Some(flight) => flight,
None => return Ok(()),
};

let (flight_label, airline) = if searched_iata {
(
flight.flight_iata.clone().or(flight.flight_icao.clone()).unwrap_or_default(),
flight.airline_iata.clone().or(flight.airline_iata.clone()).unwrap_or_else(|| "N/A".to_string()),
)
} else if searched_icao {
(
flight.flight_icao.clone().or(flight.flight_iata.clone()).unwrap_or_default(),
flight.airline_icao.clone().or(flight.airline_icao.clone()).unwrap_or_else(|| "N/A".to_string()),
)
} else {
(
flight.flight_iata.clone().or(flight.flight_icao.clone()).unwrap_or_default(),
flight.airline_iata.clone().or(flight.airline_iata.clone()).unwrap_or_else(|| "N/A".to_string()),
)
};

let status = flight.status.clone().unwrap_or_else(|| "Unknown".to_string());
let aircraft = flight.model.clone().unwrap_or_else(|| "BoingBus 67420 Max".to_string());
let manufacture = flight.manufacture.clone().unwrap_or_else(|| "BoingBus".to_string());
let engine = flight.engine.clone().unwrap_or_else(|| "FartJet".to_string());
let built = flight.built.clone().unwrap_or_else(|| 0);
let current_date = chrono::Utc::now();
let age = current_date.year() - built as i32;

let reply_msg = format!(
"**Flight {}\n**Airline: {}\nStatus: {}\nAircraft Type: {}\nManufacture: {}\nEngine Type: {}\nAge: {}\nDate of Manufacture: {}",
flight_label, airline, status, aircraft, manufacture, engine, age, built
);

ctx.send(CreateReply::default().content(reply_msg)).await?;

Ok(())
}
6 changes: 3 additions & 3 deletions bot-lib/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ pub async fn setup_db() {
.await
.expect("Failed to select namespace and database");

DB.query(include_str!("../../schema.surrealql"))
.await
.expect("Failed to execute schema query");
//DB.query(include_str!("../../schema.surrealql"))
//.await
//.expect("Failed to execute schema query");
}

/// The global state of the bot
Expand Down
Loading