Compare commits
9 Commits
d5bc2a700f
...
main
Author | SHA1 | Date | |
---|---|---|---|
ccab4b2590 | |||
6e4a52c9c2 | |||
6d65463286 | |||
cd7d9fa3b7 | |||
b94b3cf44f | |||
5e651b382d | |||
ea88c755b5 | |||
4c88ca0685 | |||
4e8ef3c0b4 |
552
Cargo.lock
generated
552
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
46
Cargo.toml
46
Cargo.toml
@@ -1,5 +1,5 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [".", "avam-client", "avam-wasm"]
|
members = [".", "avam-client", "avam-protocol", "avam-wasm"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
@@ -28,23 +28,31 @@ panic = "abort"
|
|||||||
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
|
||||||
|
|
||||||
ssr = [
|
ssr = [
|
||||||
|
"dep:avam-protocol",
|
||||||
|
|
||||||
"dep:argon2",
|
"dep:argon2",
|
||||||
"dep:dotenvy",
|
"dep:dashmap",
|
||||||
"dep:rand",
|
|
||||||
"dep:sha256",
|
|
||||||
"dep:jsonwebtoken",
|
|
||||||
"dep:tokio",
|
|
||||||
"dep:time",
|
|
||||||
"dep:tracing-subscriber",
|
|
||||||
"dep:leptos_axum",
|
|
||||||
"dep:lettre",
|
|
||||||
"dep:tera",
|
|
||||||
"dep:sqlx",
|
|
||||||
"dep:axum",
|
"dep:axum",
|
||||||
|
"dep:axum-extra",
|
||||||
"dep:axum-macros",
|
"dep:axum-macros",
|
||||||
"dep:axum_session",
|
"dep:axum_session",
|
||||||
"dep:axum_session_sqlx",
|
"dep:axum_session_sqlx",
|
||||||
|
|
||||||
|
"dep:dotenvy",
|
||||||
|
"dep:futures",
|
||||||
|
"dep:jsonwebtoken",
|
||||||
|
"dep:leptos_axum",
|
||||||
|
"dep:lettre",
|
||||||
|
|
||||||
|
"dep:rand",
|
||||||
|
"dep:sha256",
|
||||||
|
"dep:sqlx",
|
||||||
|
"dep:tokio",
|
||||||
|
"dep:time",
|
||||||
|
"dep:tracing-subscriber",
|
||||||
|
"dep:tera",
|
||||||
|
|
||||||
"dep:tower",
|
"dep:tower",
|
||||||
"dep:tower-http",
|
"dep:tower-http",
|
||||||
"dep:tower-layer",
|
"dep:tower-layer",
|
||||||
@@ -55,16 +63,19 @@ ssr = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
avam-protocol = { path = "./avam-protocol", optional = true }
|
||||||
# Utilities
|
# Utilities
|
||||||
anyhow = { version = "1.0.89", optional = false }
|
anyhow = { version = "1.0.89", optional = false }
|
||||||
argon2 = { version = "0.5.3", optional = true }
|
argon2 = { version = "0.5.3", optional = true }
|
||||||
derive_more = { version = "1.0.0", features = ["full"], optional = false }
|
derive_more = { version = "1.0.0", features = ["full"], optional = false }
|
||||||
|
dashmap = { version = "6.1.0", optional = true }
|
||||||
dotenvy = { version = "0.15.7", optional = true }
|
dotenvy = { version = "0.15.7", optional = true }
|
||||||
|
futures = { version = "0.3.31", optional = true }
|
||||||
rand = { version = "0.8.5", optional = true }
|
rand = { version = "0.8.5", optional = true }
|
||||||
serde = { version = "1.0.210", features = ["std", "derive"], optional = false }
|
serde = { version = "1.0.210", features = ["std", "derive"], optional = false }
|
||||||
thiserror = { version = "1.0.64", optional = false }
|
thiserror = { version = "1.0.64", optional = false }
|
||||||
tokio = { version = "1.40.0", features = ["full"], optional = true }
|
|
||||||
time = { version = "0.3.36", optional = true }
|
time = { version = "0.3.36", optional = true }
|
||||||
|
tokio = { version = "1.40.0", features = ["full"], optional = true }
|
||||||
tracing = { version = "0.1.40", optional = false }
|
tracing = { version = "0.1.40", optional = false }
|
||||||
tracing-subscriber = { version = "0.3.18", features = [
|
tracing-subscriber = { version = "0.3.18", features = [
|
||||||
"env-filter",
|
"env-filter",
|
||||||
@@ -102,21 +113,24 @@ sqlx = { version = "0.8.2", default-features = false, features = [
|
|||||||
], optional = true }
|
], optional = true }
|
||||||
|
|
||||||
# Web
|
# Web
|
||||||
axum = { version = "0.7.7", optional = true }
|
axum = { version = "0.7.7", optional = true, features = ["ws"] }
|
||||||
|
axum-extra = { version = "0.9.4", optional = true, features = ["typed-header"] }
|
||||||
axum-macros = { version = "0.4.2", optional = true }
|
axum-macros = { version = "0.4.2", optional = true }
|
||||||
axum_session = { version = "0.14.0", optional = true }
|
axum_session = { version = "0.14.0", optional = true }
|
||||||
axum_session_sqlx = { version = "0.3.0", optional = true }
|
axum_session_sqlx = { version = "0.3.0", optional = true }
|
||||||
|
http = "1"
|
||||||
tower = { version = "0.4", optional = true, features = ["util"] }
|
tower = { version = "0.4", optional = true, features = ["util"] }
|
||||||
tower-http = { version = "0.6.1", features = ["trace", "fs"], optional = true }
|
tower-http = { version = "0.6.1", features = ["trace", "fs"], optional = true }
|
||||||
tower-layer = { version = "0.3.3", optional = true }
|
tower-layer = { version = "0.3.3", optional = true }
|
||||||
http = "1"
|
|
||||||
validator = "0.18.1"
|
validator = "0.18.1"
|
||||||
|
|
||||||
# OAuth2
|
# OAuth2
|
||||||
base64 = { version = "0.22.1", default-features = false }
|
base64 = { version = "0.22.1", default-features = false }
|
||||||
sha256 = { version = "1.5.0", optional = true } # this fucker has a dependency on tokio?!
|
|
||||||
jsonwebtoken = { version = "9.3.0", optional = true }
|
jsonwebtoken = { version = "9.3.0", optional = true }
|
||||||
serde_qs = "0.13.0"
|
serde_qs = "0.13.0"
|
||||||
|
sha256 = { version = "1.5.0", optional = true } # this fucker has a dependency on tokio?!
|
||||||
|
sqids = "0.4.1"
|
||||||
|
hash-ids = "0.3.1"
|
||||||
|
|
||||||
[[workspace.metadata.leptos]]
|
[[workspace.metadata.leptos]]
|
||||||
name = "avam"
|
name = "avam"
|
||||||
|
BIN
SimConnect.dll
Normal file
BIN
SimConnect.dll
Normal file
Binary file not shown.
@@ -4,34 +4,47 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.40.0", features = ["full"] }
|
|
||||||
|
|
||||||
tray-icon = "0.19"
|
|
||||||
image = "0.25"
|
|
||||||
winit = "0.30"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
config = "0.14.0"
|
|
||||||
toml = "0.8"
|
|
||||||
|
|
||||||
directories = "5.0"
|
|
||||||
|
|
||||||
anyhow = { version = "1.0" }
|
anyhow = { version = "1.0" }
|
||||||
thiserror = { version = "1.0" }
|
avam-protocol = { path = "../avam-protocol" }
|
||||||
winreg = "0.52.0"
|
base64 = { version = "0.22.1", default-features = false }
|
||||||
|
clap = { version = "4.5.20", features = ["derive"] }
|
||||||
|
config = "0.14.0"
|
||||||
|
ctrlc = "3.4.5"
|
||||||
|
derive_more = { version = "1.0", features = ["full"] }
|
||||||
|
directories = "5.0"
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
futures-util = { version = "0.3.31", default-features = false, features = [
|
||||||
|
"sink",
|
||||||
|
"std",
|
||||||
|
] }
|
||||||
|
image = "0.25"
|
||||||
interprocess = { version = "2.2.1", features = ["tokio"] }
|
interprocess = { version = "2.2.1", features = ["tokio"] }
|
||||||
open = "5.3.0"
|
open = "5.3.0"
|
||||||
clap = { version = "4.5.20", features = ["derive"] }
|
rand = "0.8.5"
|
||||||
derive_more = { version = "1.0", features = ["full"] }
|
|
||||||
uuid = { version = "1.10.0", features = ["fast-rng", "serde", "v4"] }
|
|
||||||
serde_qs = "0.13.0"
|
|
||||||
ctrlc = "3.4.5"
|
|
||||||
reqwest = { version = "0.12.8", default-features = false, features = [
|
reqwest = { version = "0.12.8", default-features = false, features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
"json",
|
"json",
|
||||||
] }
|
] }
|
||||||
rand = "0.8.5"
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_qs = "0.13.0"
|
||||||
sha256 = "1.5.0"
|
sha256 = "1.5.0"
|
||||||
base64 = { version = "0.22.1", default-features = false }
|
simconnect-sdk = { path = "D:/source/MSFS/simconnect-sdk-rs/simconnect-sdk", features = [
|
||||||
|
"derive",
|
||||||
|
] }
|
||||||
|
tauri-winrt-notification = "0.6.0"
|
||||||
|
thiserror = { version = "1.0" }
|
||||||
|
time = "0.3.36"
|
||||||
|
tokio = { version = "1.40.0", features = ["full"] }
|
||||||
|
tokio-tungstenite = { version = "0.24.0", features = [
|
||||||
|
"rustls-tls-webpki-roots",
|
||||||
|
] }
|
||||||
|
toml = "0.8"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["time", "fmt", "std"] }
|
||||||
|
tray-icon = "0.19"
|
||||||
|
uuid = { version = "1.10.0", features = ["fast-rng", "serde", "v4"] }
|
||||||
|
winit = "0.30"
|
||||||
|
winreg = "0.52.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
windres = "0.2"
|
windres = "0.2"
|
||||||
|
@@ -1,5 +1,27 @@
|
|||||||
use windres::Build;
|
fn main() -> std::io::Result<()> {
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
fn main() {
|
let target_os = std::env::var("CARGO_CFG_TARGET_OS");
|
||||||
Build::new().compile("icon.rc").unwrap();
|
if target_os.as_deref() == Ok("windows") {
|
||||||
|
let name = "Avii's Virtual Airline Manager";
|
||||||
|
let version = env!("CARGO_PKG_VERSION");
|
||||||
|
let mut sv = version.split('.').collect::<Vec<_>>();
|
||||||
|
while sv.len() < 4 {
|
||||||
|
sv.push("0");
|
||||||
|
}
|
||||||
|
let file_version = format!("{}, {}, {}, {}", sv[0], sv[1], sv[2], sv[3]);
|
||||||
|
windres::Build::new()
|
||||||
|
.define(
|
||||||
|
"THE_FILE",
|
||||||
|
Some(format!(r#""{name} Client Module""#).as_str()),
|
||||||
|
)
|
||||||
|
.define("THE_PROJECT", Some(format!(r#""{name}""#).as_str()))
|
||||||
|
.define("THE_VERSION", Some(format!(r#""{version}""#).as_str()))
|
||||||
|
.define("THE_FILEVERSION", Some(file_version.as_str()))
|
||||||
|
.compile("res/resource.rc")?;
|
||||||
|
for entry in std::fs::read_dir("res")? {
|
||||||
|
let entry = entry?;
|
||||||
|
println!("cargo:rerun-if-changed={}", entry.path().display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
1 ICON "icon.ico"
|
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
28
avam-client/res/resource.rc
Normal file
28
avam-client/res/resource.rc
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma code_page(65001)
|
||||||
|
|
||||||
|
1 VERSIONINFO
|
||||||
|
FILEVERSION THE_FILEVERSION
|
||||||
|
PRODUCTVERSION THE_FILEVERSION
|
||||||
|
FILEFLAGSMASK 0x0000003Fl //VS_FFI_FILEFLAGSMASK
|
||||||
|
FILEFLAGS 0x0
|
||||||
|
FILEOS 0x00040004l //VOS_NT_WINDOWS32
|
||||||
|
FILETYPE 0x00000001l //VFT_APP
|
||||||
|
FILESUBTYPE 0x00000000l //VFT2_UNKNOWN
|
||||||
|
{
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
{
|
||||||
|
BLOCK "040904B0"
|
||||||
|
{
|
||||||
|
VALUE "FileDescription", THE_FILE
|
||||||
|
VALUE "FileVersion", THE_VERSION
|
||||||
|
VALUE "ProductVersion", THE_VERSION
|
||||||
|
VALUE "ProductName", THE_PROJECT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
{
|
||||||
|
VALUE "Translation", 0x409, 0x4B0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
1 ICON "res/icon.ico"
|
@@ -1,15 +1,30 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use tauri_winrt_notification::Toast;
|
||||||
use tokio::sync::broadcast::{Receiver, Sender};
|
use tokio::sync::broadcast::{Receiver, Sender};
|
||||||
use tray_icon::menu::{MenuId, MenuItem};
|
use tray_icon::menu::{MenuId, MenuItem};
|
||||||
use winit::{
|
use winit::{
|
||||||
application::ApplicationHandler,
|
application::ApplicationHandler,
|
||||||
event::StartCause,
|
event::StartCause,
|
||||||
event_loop::{ActiveEventLoop, ControlFlow},
|
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{config::Config, icon::TrayIcon, oauth, state_machine::Event, BASE_URL};
|
use crate::{config::Config, icon::TrayIcon, oauth, state_machine::Event, BASE_URL};
|
||||||
|
|
||||||
|
pub async fn start(
|
||||||
|
config: Config,
|
||||||
|
sender: Sender<Event>,
|
||||||
|
receiver: Receiver<Event>,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let mut app = App::new(config, sender, receiver)?;
|
||||||
|
let event_loop = EventLoop::new()?;
|
||||||
|
|
||||||
|
event_loop.run_app(&mut app)?;
|
||||||
|
tracing::info!("EventLoop Shutdown");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
config: Config,
|
config: Config,
|
||||||
tray_icon: TrayIcon,
|
tray_icon: TrayIcon,
|
||||||
@@ -41,10 +56,7 @@ impl App {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
oauth::open_browser(
|
oauth::open_browser(c.clone())?;
|
||||||
c.code_verifier().unwrap(),
|
|
||||||
c.code_challenge_method().unwrap(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
@@ -55,7 +67,7 @@ impl App {
|
|||||||
c.set_token(None)?;
|
c.set_token(None)?;
|
||||||
c.set_open_browser(false)?;
|
c.set_open_browser(false)?;
|
||||||
|
|
||||||
let _ = s.send(Event::Ready { config: c.clone() });
|
let _ = s.send(Event::Logout);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
@@ -93,7 +105,7 @@ impl ApplicationHandler for App {
|
|||||||
|
|
||||||
if let Ok(event) = self.receiver.try_recv() {
|
if let Ok(event) = self.receiver.try_recv() {
|
||||||
match event {
|
match event {
|
||||||
Event::Ready { .. } => {
|
Event::Logout | Event::Ready => {
|
||||||
self.tray_icon
|
self.tray_icon
|
||||||
.set_text(self.items.get("login").unwrap(), "Login")
|
.set_text(self.items.get("login").unwrap(), "Login")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -102,7 +114,7 @@ impl ApplicationHandler for App {
|
|||||||
.set_enabled(self.items.get("forget").unwrap(), false)
|
.set_enabled(self.items.get("forget").unwrap(), false)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
Event::TokenReceived { .. } => {
|
Event::TokenReceived { .. } | Event::Connected => {
|
||||||
self.tray_icon
|
self.tray_icon
|
||||||
.set_text(self.items.get("login").unwrap(), "Open Avam")
|
.set_text(self.items.get("login").unwrap(), "Open Avam")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -112,7 +124,7 @@ impl ApplicationHandler for App {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
Event::Quit => {
|
Event::Quit => {
|
||||||
println!("Shutting down EventLoop");
|
tracing::info!("Shutting down EventLoop");
|
||||||
event_loop.exit()
|
event_loop.exit()
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
@@ -123,9 +135,15 @@ impl ApplicationHandler for App {
|
|||||||
fn resumed(&mut self, _: &winit::event_loop::ActiveEventLoop) {
|
fn resumed(&mut self, _: &winit::event_loop::ActiveEventLoop) {
|
||||||
let _ = self.tray_icon.build();
|
let _ = self.tray_icon.build();
|
||||||
|
|
||||||
let _ = self.sender.send(Event::Ready {
|
let _ = self.sender.send(Event::Ready);
|
||||||
config: self.config.clone(),
|
|
||||||
});
|
if !self.config.toast_shown() {
|
||||||
|
let _ = Toast::new(crate::AVAM_APP_ID)
|
||||||
|
.title("Avam is running in your system tray!")
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = self.config.set_toast_shown(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window_event(
|
fn window_event(
|
||||||
|
187
avam-client/src/client.rs
Normal file
187
avam-client/src/client.rs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use avam_protocol::{Packet, Packets, SimConnectPacket, SystemPacket};
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use reqwest::StatusCode;
|
||||||
|
use tokio::{
|
||||||
|
sync::broadcast::{channel, Receiver, Sender},
|
||||||
|
task::JoinSet,
|
||||||
|
time::{sleep, timeout},
|
||||||
|
};
|
||||||
|
use tokio_tungstenite::{
|
||||||
|
connect_async,
|
||||||
|
tungstenite::{
|
||||||
|
self,
|
||||||
|
protocol::{frame::coding::CloseCode, CloseFrame},
|
||||||
|
ClientRequestBuilder, Message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{config::Config, state_machine::Event, BASE_URL};
|
||||||
|
|
||||||
|
pub async fn start(
|
||||||
|
config: Config,
|
||||||
|
simconnect_sender: Sender<SimConnectPacket>,
|
||||||
|
socket_receiver: Receiver<SimConnectPacket>,
|
||||||
|
event_sender: Sender<Event>,
|
||||||
|
mut event_receiver: Receiver<Event>,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let uri: tungstenite::http::Uri = format!("{}/ws", BASE_URL.replace("https", "wss")).parse()?;
|
||||||
|
|
||||||
|
let mut futures = JoinSet::new();
|
||||||
|
|
||||||
|
let (ia_sender, ia_receiver) = channel(10);
|
||||||
|
loop {
|
||||||
|
if let Ok(event) = &event_receiver.try_recv() {
|
||||||
|
match event {
|
||||||
|
Event::TokenReceived => {
|
||||||
|
let Some(token) = config.token() else {
|
||||||
|
let _ = event_sender.send(Event::Logout);
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let builder = ClientRequestBuilder::new(uri.clone())
|
||||||
|
.with_header("Authorization", format!("Bearer {}", token));
|
||||||
|
|
||||||
|
tracing::info!("Connecting");
|
||||||
|
let Ok(Ok((socket, response))) =
|
||||||
|
timeout(Duration::from_secs(10), connect_async(builder)).await
|
||||||
|
else {
|
||||||
|
tracing::error!("Unable to connect");
|
||||||
|
let _ = event_sender.send(Event::Disconnected);
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if response.status() != StatusCode::SWITCHING_PROTOCOLS {
|
||||||
|
tracing::error!("Unable to connect: {:#?}", response);
|
||||||
|
let _ = event_sender.send(Event::Disconnected);
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut write, mut read) = socket.split();
|
||||||
|
|
||||||
|
let mut ia_receiver: Receiver<Packets> = ia_receiver.resubscribe();
|
||||||
|
futures.spawn(async move {
|
||||||
|
loop {
|
||||||
|
if let Ok(d) = ia_receiver.try_recv() {
|
||||||
|
let message = match d {
|
||||||
|
Packets::System(SystemPacket::Close { reason }) => {
|
||||||
|
Message::Close(Some(CloseFrame {
|
||||||
|
code: CloseCode::Normal,
|
||||||
|
reason: reason.into(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
Packets::System(SystemPacket::Pong) => {
|
||||||
|
let Ok(encoded_message) = d.encode() else {
|
||||||
|
tracing::error!(
|
||||||
|
"Unable to encode message for sending: {:#?}",
|
||||||
|
d
|
||||||
|
);
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
Message::Binary(encoded_message)
|
||||||
|
}
|
||||||
|
d => {
|
||||||
|
// tracing::info!("sending packet: {:#?}", &d);
|
||||||
|
let Ok(encoded_message) = d.encode() else {
|
||||||
|
tracing::error!(
|
||||||
|
"Unable to encode message for sending: {:#?}",
|
||||||
|
d
|
||||||
|
);
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
Message::Binary(encoded_message)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match write.send(message).await {
|
||||||
|
Err(tungstenite::Error::AlreadyClosed) => break,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Error writing to socket: {:?}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(()) => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let ias = ia_sender.clone();
|
||||||
|
let es = event_sender.clone();
|
||||||
|
let scs = simconnect_sender.clone();
|
||||||
|
futures.spawn(async move {
|
||||||
|
loop {
|
||||||
|
let message = match read.next().await {
|
||||||
|
Some(data) => match data {
|
||||||
|
Ok(message) => message,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("{:?}", e);
|
||||||
|
let _ = es.send(Event::Disconnected);
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(data) = Packets::decode(&message.into_data()) {
|
||||||
|
if data == Packets::System(SystemPacket::Ping) {
|
||||||
|
let _ = ias.send(Packets::System(SystemPacket::Pong));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From Socket -> SimConnect
|
||||||
|
if let Packets::SimConnect(sim_connect_packet) = data {
|
||||||
|
tracing::info!("packet received: {:?}", &sim_connect_packet);
|
||||||
|
let _ = scs.send(sim_connect_packet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Data from simconnect -> Socket
|
||||||
|
let mut socket_receiver = socket_receiver.resubscribe();
|
||||||
|
let ias = ia_sender.clone();
|
||||||
|
futures.spawn(async move {
|
||||||
|
loop {
|
||||||
|
if let Ok(message) = socket_receiver.try_recv() {
|
||||||
|
let _ = ias.send(Packets::SimConnect(message));
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tracing::info!("Connected");
|
||||||
|
let _ = event_sender.send(Event::Connected);
|
||||||
|
}
|
||||||
|
Event::Logout => {
|
||||||
|
let _ = ia_sender.send(Packets::System(SystemPacket::Close {
|
||||||
|
reason: "User Logout".to_string(),
|
||||||
|
}));
|
||||||
|
sleep(Duration::from_millis(200)).await;
|
||||||
|
futures.abort_all();
|
||||||
|
}
|
||||||
|
Event::Quit => {
|
||||||
|
tracing::info!("Shutting down Client");
|
||||||
|
let _ = ia_sender.send(Packets::System(SystemPacket::Close {
|
||||||
|
reason: "Quit".to_string(),
|
||||||
|
}));
|
||||||
|
sleep(Duration::from_millis(200)).await;
|
||||||
|
futures.abort_all();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
tracing::info!("Client Shutdown");
|
||||||
|
Ok(())
|
||||||
|
}
|
@@ -15,6 +15,8 @@ use crate::{
|
|||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[serde(with = "arc_rwlock_serde")]
|
#[serde(with = "arc_rwlock_serde")]
|
||||||
token: Arc<RwLock<Option<String>>>,
|
token: Arc<RwLock<Option<String>>>,
|
||||||
|
#[serde(with = "arc_rwlock_serde")]
|
||||||
|
toast_shown: Arc<RwLock<bool>>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
code_verifier: Arc<RwLock<Option<CodeVerifier>>>,
|
code_verifier: Arc<RwLock<Option<CodeVerifier>>>,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
@@ -27,6 +29,7 @@ impl Default for Config {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
token: Default::default(),
|
token: Default::default(),
|
||||||
|
toast_shown: Default::default(),
|
||||||
code_verifier: Default::default(),
|
code_verifier: Default::default(),
|
||||||
code_challenge_method: Default::default(),
|
code_challenge_method: Default::default(),
|
||||||
open_browser: Arc::new(RwLock::new(true)),
|
open_browser: Arc::new(RwLock::new(true)),
|
||||||
@@ -49,6 +52,8 @@ pub enum ConfigError {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Toml(#[from] toml::ser::Error),
|
Toml(#[from] toml::ser::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
Config(#[from] config::ConfigError),
|
||||||
|
#[error(transparent)]
|
||||||
Unknown(#[from] anyhow::Error),
|
Unknown(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +69,18 @@ impl Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub(crate) fn toast_shown(&self) -> bool {
|
||||||
|
*self.toast_shown.read().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_toast_shown(&self, value: bool) -> Result<(), ConfigError> {
|
||||||
|
*self.toast_shown.write().unwrap() = value;
|
||||||
|
self.write()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn set_code_verifier(
|
pub fn set_code_verifier(
|
||||||
&self,
|
&self,
|
||||||
@@ -84,10 +101,6 @@ impl Config {
|
|||||||
pub fn code_verifier(&self) -> Option<CodeVerifier> {
|
pub fn code_verifier(&self) -> Option<CodeVerifier> {
|
||||||
self.code_verifier.read().unwrap().clone()
|
self.code_verifier.read().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn code_challenge_method(&self) -> Option<CodeChallengeMethod> {
|
|
||||||
self.code_challenge_method.read().unwrap().clone()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -110,7 +123,12 @@ impl Config {
|
|||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let config: Self = config.try_deserialize().unwrap_or_default();
|
let token = config.get_string("token").ok();
|
||||||
|
let toast_shown = config.get_bool("toast_shown").unwrap_or_default();
|
||||||
|
let config = Self::default();
|
||||||
|
|
||||||
|
config.set_token(token)?;
|
||||||
|
config.set_toast_shown(toast_shown)?;
|
||||||
|
|
||||||
config.write()?;
|
config.write()?;
|
||||||
|
|
||||||
|
@@ -40,4 +40,23 @@ impl Dirs {
|
|||||||
c.push(".lock");
|
c.push(".lock");
|
||||||
Ok(c)
|
Ok(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
pub fn get_log_dir() -> Result<PathBuf, DirsError> {
|
||||||
|
let c = Self::get_config_dir()?;
|
||||||
|
let mut log_dir = c.parent().unwrap().to_path_buf();
|
||||||
|
log_dir.push("logs");
|
||||||
|
if !log_dir.exists() {
|
||||||
|
std::fs::create_dir_all(&log_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(log_dir.to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
pub fn get_log_file() -> Result<PathBuf, DirsError> {
|
||||||
|
let mut l = Self::get_log_dir()?;
|
||||||
|
l.push("avam.log");
|
||||||
|
Ok(l)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -97,7 +97,7 @@ impl TrayIcon {
|
|||||||
if let Some(item) = self.menu_items.get(id) {
|
if let Some(item) = self.menu_items.get(id) {
|
||||||
if let Some(i) = self.menu.items().iter().find(|i| i.id() == id) {
|
if let Some(i) = self.menu.items().iter().find(|i| i.id() == id) {
|
||||||
if let Err(e) = item(i) {
|
if let Err(e) = item(i) {
|
||||||
eprintln!("{:#?}", e);
|
tracing::error!("{:#?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
#![allow(clippy::needless_return)]
|
#![allow(clippy::needless_return)]
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
mod client;
|
||||||
mod config;
|
mod config;
|
||||||
mod dirs;
|
mod dirs;
|
||||||
mod icon;
|
mod icon;
|
||||||
@@ -8,6 +10,7 @@ mod lock;
|
|||||||
mod models;
|
mod models;
|
||||||
mod oauth;
|
mod oauth;
|
||||||
mod pipe;
|
mod pipe;
|
||||||
|
mod simconnect;
|
||||||
mod state_machine;
|
mod state_machine;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
@@ -16,8 +19,11 @@ use lock::Lock;
|
|||||||
use oauth::{start_code_listener, start_code_to_token};
|
use oauth::{start_code_listener, start_code_to_token};
|
||||||
use pipe::Pipe;
|
use pipe::Pipe;
|
||||||
use state_machine::Event;
|
use state_machine::Event;
|
||||||
use tokio::task::JoinSet;
|
use tokio::{sync::broadcast::channel, task::JoinSet};
|
||||||
|
use tracing::Level;
|
||||||
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, Layer};
|
||||||
|
|
||||||
|
pub static AVAM_APP_ID: &str = "AvamToast-ECEB71694A5E6105";
|
||||||
pub static BASE_URL: &str = "https://avam.avii.nl";
|
pub static BASE_URL: &str = "https://avam.avii.nl";
|
||||||
pub static PROJECT_NAME: &str = "Avii's Virtual Airline Manager";
|
pub static PROJECT_NAME: &str = "Avii's Virtual Airline Manager";
|
||||||
pub static COPYRIGHT: &str = "Avii's Virtual Airline Manager © 2024";
|
pub static COPYRIGHT: &str = "Avii's Virtual Airline Manager © 2024";
|
||||||
@@ -35,7 +41,11 @@ pub struct Arguments {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), anyhow::Error> {
|
async fn main() -> Result<(), anyhow::Error> {
|
||||||
let (event_sender, event_receiver) = tokio::sync::broadcast::channel(1);
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
|
init_logging()?;
|
||||||
|
|
||||||
|
let (event_sender, event_receiver) = channel(10);
|
||||||
let args = Arguments::parse();
|
let args = Arguments::parse();
|
||||||
|
|
||||||
if handle_single_instance(&args).await? {
|
if handle_single_instance(&args).await? {
|
||||||
@@ -43,6 +53,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
register_url_scheme()?;
|
register_url_scheme()?;
|
||||||
|
register_notification_handler()?;
|
||||||
|
|
||||||
let config = Config::new()?;
|
let config = Config::new()?;
|
||||||
|
|
||||||
@@ -52,7 +63,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
let sender = event_sender.clone();
|
let sender = event_sender.clone();
|
||||||
let mut ctrl_c_counter = 0;
|
let mut ctrl_c_counter = 0;
|
||||||
ctrlc::set_handler(move || {
|
ctrlc::set_handler(move || {
|
||||||
println!("CTRL_C: Quit singal sent");
|
tracing::info!("CTRL_C: Quit singal sent");
|
||||||
ctrl_c_counter += 1;
|
ctrl_c_counter += 1;
|
||||||
if ctrl_c_counter >= 3 {
|
if ctrl_c_counter >= 3 {
|
||||||
let _ = unregister_url_scheme();
|
let _ = unregister_url_scheme();
|
||||||
@@ -61,7 +72,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = sender.send(Event::Quit) {
|
if let Err(e) = sender.send(Event::Quit) {
|
||||||
println!("{:#?}", e)
|
tracing::error!("{:#?}", e)
|
||||||
};
|
};
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
@@ -73,7 +84,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
|
|
||||||
// // Start the code listener
|
// // Start the code listener
|
||||||
let receiver = event_receiver.resubscribe();
|
let receiver = event_receiver.resubscribe();
|
||||||
let (pipe_sender, pipe_receiver) = tokio::sync::broadcast::channel(100);
|
let (pipe_sender, pipe_receiver) = tokio::sync::broadcast::channel(10);
|
||||||
futures.spawn(start_code_listener(pipe_sender, receiver));
|
futures.spawn(start_code_listener(pipe_sender, receiver));
|
||||||
|
|
||||||
// Start token listener
|
// Start token listener
|
||||||
@@ -82,11 +93,40 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
let receiver = event_receiver.resubscribe();
|
let receiver = event_receiver.resubscribe();
|
||||||
futures.spawn(start_code_to_token(c, pipe_receiver, sender, receiver));
|
futures.spawn(start_code_to_token(c, pipe_receiver, sender, receiver));
|
||||||
|
|
||||||
|
// Prepare channels for socket <-> simconnect
|
||||||
|
let (simconnect_sender, simconnect_receiver) = channel(10);
|
||||||
|
let (socket_sender, socket_receiver) = channel(10);
|
||||||
|
|
||||||
|
// Start the websocket client
|
||||||
|
let c = config.clone();
|
||||||
|
let sender = event_sender.clone();
|
||||||
|
let receiver = event_receiver.resubscribe();
|
||||||
|
futures.spawn(client::start(
|
||||||
|
c,
|
||||||
|
simconnect_sender,
|
||||||
|
socket_receiver,
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Start the simconnect listener
|
||||||
|
// The simconnect sends data to the webscoket
|
||||||
|
// It also receives data from the websocket to do things like set plane id and fuel and such things
|
||||||
|
// If possible even position
|
||||||
|
let sender = event_sender.clone();
|
||||||
|
let receiver = event_receiver.resubscribe();
|
||||||
|
futures.spawn(simconnect::start(
|
||||||
|
simconnect_receiver,
|
||||||
|
socket_sender,
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
));
|
||||||
|
|
||||||
// Start the Tray Icon
|
// Start the Tray Icon
|
||||||
let c = config.clone();
|
let c = config.clone();
|
||||||
let sender = event_sender.clone();
|
let sender = event_sender.clone();
|
||||||
let receiver = event_receiver.resubscribe();
|
let receiver = event_receiver.resubscribe();
|
||||||
start_tray_icon(c, sender, receiver).await?;
|
app::start(c, sender, receiver).await?;
|
||||||
|
|
||||||
// Wait for everything to finish
|
// Wait for everything to finish
|
||||||
while let Some(result) = futures.join_next().await {
|
while let Some(result) = futures.join_next().await {
|
||||||
@@ -97,28 +137,12 @@ async fn main() -> Result<(), anyhow::Error> {
|
|||||||
|
|
||||||
// Cleanup
|
// Cleanup
|
||||||
unregister_url_scheme()?;
|
unregister_url_scheme()?;
|
||||||
|
unregister_notification_handler()?;
|
||||||
Lock::unlock();
|
Lock::unlock();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
use app::App;
|
|
||||||
use tokio::sync::broadcast::{Receiver, Sender};
|
|
||||||
use winit::event_loop::EventLoop;
|
|
||||||
async fn start_tray_icon(
|
|
||||||
config: Config,
|
|
||||||
sender: Sender<Event>,
|
|
||||||
receiver: Receiver<Event>,
|
|
||||||
) -> Result<(), anyhow::Error> {
|
|
||||||
let mut app = App::new(config, sender, receiver)?;
|
|
||||||
let event_loop = EventLoop::new()?;
|
|
||||||
|
|
||||||
event_loop.run_app(&mut app)?;
|
|
||||||
println!("EventLoop Shutdonw");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
@@ -178,3 +202,76 @@ fn unregister_url_scheme() -> Result<(), anyhow::Error> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn register_notification_handler() -> Result<(), anyhow::Error> {
|
||||||
|
use winreg::enums::*;
|
||||||
|
use winreg::RegKey;
|
||||||
|
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
let avam_schema_root = hkcu.create_subkey(format!(
|
||||||
|
"Software\\Classes\\AppUserModelId\\{}",
|
||||||
|
AVAM_APP_ID
|
||||||
|
))?;
|
||||||
|
let current_exec = std::env::current_exe()?;
|
||||||
|
let mut icon = current_exec.parent().unwrap().to_path_buf();
|
||||||
|
icon.push("icon.png");
|
||||||
|
|
||||||
|
avam_schema_root
|
||||||
|
.0
|
||||||
|
.set_value("DisplayName", &crate::PROJECT_NAME)?;
|
||||||
|
avam_schema_root.0.set_value("IconBackgroundColor", &"0")?;
|
||||||
|
avam_schema_root
|
||||||
|
.0
|
||||||
|
.set_value("IconUri", &icon.to_str().unwrap())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unregister_notification_handler() -> Result<(), anyhow::Error> {
|
||||||
|
use winreg::enums::*;
|
||||||
|
use winreg::RegKey;
|
||||||
|
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
hkcu.delete_subkey_all(format!(
|
||||||
|
"Software\\Classes\\AppUserModelId\\{}",
|
||||||
|
AVAM_APP_ID
|
||||||
|
))
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_logging() -> Result<(), anyhow::Error> {
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
use dirs::Dirs;
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let log_file = Dirs::get_log_file()?;
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
if !log_file.exists() {
|
||||||
|
fs::write(&log_file, "")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let file = File::options().append(true).open(&log_file)?;
|
||||||
|
|
||||||
|
let fmt = tracing_subscriber::fmt::layer();
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
let fmt = fmt.with_ansi(false).with_writer(Arc::new(file));
|
||||||
|
|
||||||
|
let fmt = fmt.with_filter(tracing_subscriber::filter::filter_fn(|metadata| {
|
||||||
|
metadata.level() < &Level::TRACE
|
||||||
|
}));
|
||||||
|
|
||||||
|
tracing_subscriber::registry().with(fmt).init();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@@ -194,34 +194,6 @@ impl AuthorizeRequest {
|
|||||||
scope,
|
scope,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn client_id(&self) -> uuid::Uuid {
|
|
||||||
// self.client_id
|
|
||||||
// }
|
|
||||||
|
|
||||||
// pub fn response_type(&self) -> ResponseType {
|
|
||||||
// self.response_type.clone()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// pub fn state(&self) -> Option<String> {
|
|
||||||
// self.state.clone()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// pub fn code_challenge(&self) -> String {
|
|
||||||
// self.code_challenge.clone()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// pub fn code_challenge_method(&self) -> Option<CodeChallengeMethod> {
|
|
||||||
// self.code_challenge_method.clone()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// pub fn redirect_uri(&self) -> String {
|
|
||||||
// self.redirect_uri.clone()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// pub fn scope(&self) -> Option<String> {
|
|
||||||
// self.scope.clone()
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::broadcast::{Receiver, Sender},
|
sync::broadcast::{Receiver, Sender},
|
||||||
@@ -6,13 +7,30 @@ use tokio::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::Config, models::*, pipe::Pipe, state_machine::Event, BASE_URL, CLIENT_ID, REDIRECT_URI,
|
config::{Config, ConfigError},
|
||||||
|
models::*,
|
||||||
|
pipe::Pipe,
|
||||||
|
state_machine::Event,
|
||||||
|
BASE_URL, CLIENT_ID, REDIRECT_URI,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn open_browser(
|
#[derive(Debug, Error)]
|
||||||
code_verifier: CodeVerifier,
|
pub enum OpenBrowserError {
|
||||||
code_challenge_method: CodeChallengeMethod,
|
#[error(transparent)]
|
||||||
) -> Result<(), anyhow::Error> {
|
SerdeQs(#[from] serde_qs::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Config(#[from] ConfigError),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_browser(config: Config) -> Result<(), OpenBrowserError> {
|
||||||
|
let code_verifier = CodeVerifier::new();
|
||||||
|
let code_challenge_method = CodeChallengeMethod::Sha256;
|
||||||
|
|
||||||
|
config.set_code_verifier(Some(code_verifier.clone()))?;
|
||||||
|
config.set_code_challenge_method(Some(code_challenge_method.clone()))?;
|
||||||
|
|
||||||
let code_challenge = match code_challenge_method {
|
let code_challenge = match code_challenge_method {
|
||||||
CodeChallengeMethod::Plain => {
|
CodeChallengeMethod::Plain => {
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
@@ -60,7 +78,7 @@ pub async fn start_code_to_token(
|
|||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = sleep(Duration::from_millis(100)) => {
|
_ = sleep(Duration::from_millis(100)) => {
|
||||||
if let Ok(Event::Quit) = event_receiver.try_recv() {
|
if let Ok(Event::Quit) = event_receiver.try_recv() {
|
||||||
println!("Shutting down Code Transformer");
|
tracing::info!("Shutting down Code Transformer");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,12 +102,15 @@ pub async fn start_code_to_token(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let response: AuthorizationCodeResponse = response.json().await?;
|
let response: AuthorizationCodeResponse = response.json().await?;
|
||||||
|
let token = response.token();
|
||||||
|
|
||||||
event_sender.send(Event::TokenReceived { token: response.token() })?;
|
config.set_token(Some(token.clone()))?;
|
||||||
|
|
||||||
|
event_sender.send(Event::TokenReceived)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Code Transformer Shutdown");
|
tracing::info!("Code Transformer Shutdown");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@@ -61,20 +61,20 @@ impl Pipe {
|
|||||||
let new_sender = pipe_sender.clone();
|
let new_sender = pipe_sender.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = Self::handle_conn(conn, new_sender).await {
|
if let Err(e) = Self::handle_conn(conn, new_sender).await {
|
||||||
eprintln!("error while handling connection: {e}");
|
tracing::error!("error while handling connection: {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
_ = sleep(Duration::from_millis(100)) => {
|
_ = sleep(Duration::from_millis(100)) => {
|
||||||
if let Ok(Event::Quit) = quit_signal.try_recv() {
|
if let Ok(Event::Quit) = quit_signal.try_recv() {
|
||||||
println!("Shutting down Code Listener");
|
tracing::info!("Shutting down Code Listener");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Code Listener Shutdown");
|
tracing::info!("Code Listener Shutdown");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
312
avam-client/src/simconnect.rs
Normal file
312
avam-client/src/simconnect.rs
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
mod models;
|
||||||
|
|
||||||
|
use avam_protocol::SimConnectPacket;
|
||||||
|
use core::ops::ControlFlow;
|
||||||
|
use models::*;
|
||||||
|
use simconnect_sdk::{Notification, Object, SimConnect as SC, SystemEvent, SystemEventRequest};
|
||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
use tokio::{
|
||||||
|
sync::{
|
||||||
|
broadcast::{Receiver, Sender},
|
||||||
|
RwLock,
|
||||||
|
},
|
||||||
|
time::sleep,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::state_machine::Event;
|
||||||
|
|
||||||
|
pub struct SimState {
|
||||||
|
atc_id: String,
|
||||||
|
// gps location
|
||||||
|
// fuel state
|
||||||
|
// cargo state
|
||||||
|
// is landed
|
||||||
|
// is stopped
|
||||||
|
// etc
|
||||||
|
// basically, all the data we wanna send to socket
|
||||||
|
|
||||||
|
// we also need to know if we're in a flight here, and what flight,
|
||||||
|
// mission parameters etc maybe
|
||||||
|
// unless we just wanna keep that purely serverside..
|
||||||
|
// and only process incoming packets if there is an activem mission.
|
||||||
|
|
||||||
|
// the thing i _dont_ want, is to be invasive with alerts and notifications
|
||||||
|
// that the user is "forgetting" to start a flight etc with AVAM running
|
||||||
|
// basically, avam should be able to be running always without the user noticing
|
||||||
|
// any interaction is done via the dashboard
|
||||||
|
// starting, stopping, status etc
|
||||||
|
|
||||||
|
// eventually even with "realistic" flightplans, from time of takeoff, estimate time of arrival,
|
||||||
|
// and even have time-critical jobs that need to get finished before a set time
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SimState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
atc_id: String::from("AVAM"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SimConnect {
|
||||||
|
simconnect_receiver: Receiver<SimConnectPacket>, // Data from the socket
|
||||||
|
socket_sender: Sender<SimConnectPacket>, // Data to the socket
|
||||||
|
sender: Sender<Event>,
|
||||||
|
receiver: Receiver<Event>,
|
||||||
|
state: Arc<RwLock<SimState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimConnect {
|
||||||
|
async fn open(&self, client: &mut SC) -> Result<(), anyhow::Error> {
|
||||||
|
client.subscribe_to_system_event(SystemEventRequest::AircraftLoaded)?;
|
||||||
|
client.subscribe_to_system_event(SystemEventRequest::SimStart)?;
|
||||||
|
|
||||||
|
let id = client.register_object::<AtcID>()?;
|
||||||
|
client.register_object::<Airplane>()?;
|
||||||
|
client.register_object::<Fuel>()?;
|
||||||
|
client.register_object::<Gps>()?;
|
||||||
|
client.register_object::<OnGround>()?;
|
||||||
|
client.register_object::<IsParked>()?;
|
||||||
|
|
||||||
|
let value = self.state.read().await.atc_id.clone();
|
||||||
|
tracing::info!("Updating ATC_ID: {}", &value);
|
||||||
|
client.set_data_on_sim_object_with_id(id, &mut atc_id(&value)?)?;
|
||||||
|
|
||||||
|
let _ = self.sender.send(Event::SimConnected);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn object(&self, _: &mut SC, object: &Object) -> Result<(), anyhow::Error> {
|
||||||
|
if let Ok(data) = Airplane::try_from(object) {
|
||||||
|
self.socket_sender
|
||||||
|
.send(SimConnectPacket::Airplane(avam_protocol::Airplane {
|
||||||
|
atc_type: data.atc_type,
|
||||||
|
atc_model: data.atc_model,
|
||||||
|
title: data.title,
|
||||||
|
category: data.category,
|
||||||
|
}))?;
|
||||||
|
// We've already got our data, there's no point in trying another in this iteration
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(data) = Fuel::try_from(object) {
|
||||||
|
self.socket_sender
|
||||||
|
.send(SimConnectPacket::Fuel(avam_protocol::Fuel {
|
||||||
|
weight: data.weight,
|
||||||
|
center_quantity: data.center_quantity,
|
||||||
|
center_capacity: data.center_capacity,
|
||||||
|
center2_quantity: data.center2_quantity,
|
||||||
|
center2_capacity: data.center2_capacity,
|
||||||
|
center3_quantity: data.center3_quantity,
|
||||||
|
center3_capacity: data.center3_capacity,
|
||||||
|
left_main_quantity: data.left_main_quantity,
|
||||||
|
left_main_capacity: data.left_main_capacity,
|
||||||
|
left_aux_quantity: data.left_aux_quantity,
|
||||||
|
left_aux_capacity: data.left_aux_capacity,
|
||||||
|
left_tip_quantity: data.left_tip_quantity,
|
||||||
|
left_tip_capacity: data.left_tip_capacity,
|
||||||
|
right_main_quantity: data.right_main_quantity,
|
||||||
|
right_main_capacity: data.right_main_capacity,
|
||||||
|
right_aux_quantity: data.right_aux_quantity,
|
||||||
|
right_aux_capacity: data.right_aux_capacity,
|
||||||
|
right_tip_quantity: data.right_tip_quantity,
|
||||||
|
right_tip_capacity: data.right_tip_capacity,
|
||||||
|
external1_quantity: data.external1_quantity,
|
||||||
|
external1_capacity: data.external1_capacity,
|
||||||
|
external2_quantity: data.external2_quantity,
|
||||||
|
external2_capacity: data.external2_capacity,
|
||||||
|
}))?;
|
||||||
|
// We've already got our data, there's no point in trying another in this iteration
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(data) = Gps::try_from(object) {
|
||||||
|
self.socket_sender
|
||||||
|
.send(SimConnectPacket::Gps(avam_protocol::Gps {
|
||||||
|
lat: data.lat,
|
||||||
|
lon: data.lon,
|
||||||
|
alt: data.alt,
|
||||||
|
}))?;
|
||||||
|
// We've already got our data, there's no point in trying another in this iteration
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(data) = OnGround::try_from(object) {
|
||||||
|
self.socket_sender
|
||||||
|
.send(SimConnectPacket::OnGround(avam_protocol::OnGround {
|
||||||
|
sim_on_ground: data.sim_on_ground,
|
||||||
|
}))?;
|
||||||
|
// We've already got our data, there's no point in trying another in this iteration
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change to parking break state or something, unless we're a helicopter, then we dont have one
|
||||||
|
if let Ok(data) = IsParked::try_from(object) {
|
||||||
|
self.socket_sender
|
||||||
|
.send(SimConnectPacket::IsParked(avam_protocol::IsParked {
|
||||||
|
is_parked: data.is_parked,
|
||||||
|
}))?;
|
||||||
|
// We've already got our data, there's no point in trying another in this iteration
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn system_event(
|
||||||
|
&self,
|
||||||
|
client: &mut SC,
|
||||||
|
event: &SystemEvent,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
match event {
|
||||||
|
SystemEvent::AircraftLoaded { .. } => {
|
||||||
|
tracing::info!("Aircraft Loaded!");
|
||||||
|
let id = client.get_object_id::<AtcID>()?;
|
||||||
|
|
||||||
|
let value = self.state.read().await.atc_id.clone();
|
||||||
|
tracing::info!("Updating ATC_ID: {}", &value);
|
||||||
|
for _ in 1..=10 {
|
||||||
|
client.set_data_on_sim_object_with_id(id, &mut atc_id(&value)?)?;
|
||||||
|
sleep(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SystemEvent::SimStart => {
|
||||||
|
tracing::info!("Sim Start");
|
||||||
|
}
|
||||||
|
|
||||||
|
// SystemEvent::OneSecond => todo!(),
|
||||||
|
// SystemEvent::FourSeconds => todo!(),
|
||||||
|
// SystemEvent::SixTimesPerSecond => todo!(),
|
||||||
|
// SystemEvent::AircraftLoaded { file_name } => todo!(),
|
||||||
|
// SystemEvent::Crashed => todo!(),
|
||||||
|
// SystemEvent::CrashReset => todo!(),
|
||||||
|
// SystemEvent::FlightSaved { file_name } => todo!(),
|
||||||
|
// SystemEvent::FlightPlanActivated { file_name } => todo!(),
|
||||||
|
// SystemEvent::FlightPlanDeactivated => todo!(),
|
||||||
|
// SystemEvent::Frame {
|
||||||
|
// frame_rate,
|
||||||
|
// sim_speed,
|
||||||
|
// } => todo!(),
|
||||||
|
// SystemEvent::Pause { state } => todo!(),
|
||||||
|
// SystemEvent::Paused => todo!(),
|
||||||
|
// SystemEvent::PauseFrame {
|
||||||
|
// frame_rate,
|
||||||
|
// sim_speed,
|
||||||
|
// } => todo!(),
|
||||||
|
// SystemEvent::PositionChanged => todo!(),
|
||||||
|
// SystemEvent::Sim { state } => todo!(),
|
||||||
|
// SystemEvent::SimStart => todo!(),
|
||||||
|
// SystemEvent::SimStop => todo!(),
|
||||||
|
// SystemEvent::Sound { state } => todo!(),
|
||||||
|
// SystemEvent::Unpaused => todo!(),
|
||||||
|
// SystemEvent::View { view } => todo!(),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimConnect {
|
||||||
|
pub fn new(
|
||||||
|
simconnect_receiver: Receiver<SimConnectPacket>,
|
||||||
|
socket_sender: Sender<SimConnectPacket>,
|
||||||
|
sender: Sender<Event>,
|
||||||
|
receiver: Receiver<Event>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
simconnect_receiver,
|
||||||
|
socket_sender,
|
||||||
|
sender,
|
||||||
|
receiver,
|
||||||
|
state: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run(&self) -> Result<(), anyhow::Error> {
|
||||||
|
let mut receiver = self.receiver.resubscribe();
|
||||||
|
let mut simconnect_receiver = self.simconnect_receiver.resubscribe();
|
||||||
|
let state = self.state.clone();
|
||||||
|
let mut sc: Option<SC> = None;
|
||||||
|
loop {
|
||||||
|
if let Ok(Event::Quit) = receiver.try_recv() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(packet) = simconnect_receiver.try_recv() {
|
||||||
|
match packet {
|
||||||
|
SimConnectPacket::AtcID(value) => {
|
||||||
|
state.write().await.atc_id = value.clone();
|
||||||
|
if let Some(client) = sc.as_mut() {
|
||||||
|
if let Ok(id) = client.get_object_id::<AtcID>() {
|
||||||
|
tracing::info!("Updating ATC_ID: {}", &value);
|
||||||
|
client.set_data_on_sim_object_with_id(id, &mut atc_id(&value)?)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SimConnectPacket::Fuel(_) => unreachable!(), // Outgoing packet, not handled here (yet) we probably need to set fuel state somehow
|
||||||
|
SimConnectPacket::Airplane(_) => unreachable!(),
|
||||||
|
SimConnectPacket::Gps(_) => unreachable!(), // Outgoing packet, not handled here (yet) we probably need to set position somehow
|
||||||
|
SimConnectPacket::OnGround(_) => unreachable!(), // Outgoing packet, not handled here
|
||||||
|
SimConnectPacket::IsParked(_) => unreachable!(), // Outgoing packet, not handled here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sc.is_none() {
|
||||||
|
if let Ok(simconnect) = SC::new("Avii's Virtual Airline Manager") {
|
||||||
|
sc = Some(simconnect);
|
||||||
|
tracing::info!("Connected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(client) = sc.as_mut() {
|
||||||
|
if self.handle(client).await.is_break() {
|
||||||
|
tracing::info!("Disconnected");
|
||||||
|
sc = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle(&self, client: &mut SC) -> ControlFlow<(), ()> {
|
||||||
|
if let Ok(Some(event)) = client.get_next_dispatch() {
|
||||||
|
if let Err(e) = match event {
|
||||||
|
Notification::Open => self.open(client).await,
|
||||||
|
Notification::Object(ref object) => self.object(client, object).await,
|
||||||
|
Notification::SystemEvent(ref system_event) => {
|
||||||
|
self.system_event(client, system_event).await
|
||||||
|
}
|
||||||
|
// Notification::ClientEvent(ref client_event) => todo!(),
|
||||||
|
// Notification::AirportList(ref vec) => todo!(),
|
||||||
|
// Notification::WaypointList(ref vec) => todo!(),
|
||||||
|
// Notification::NdbList(ref vec) => todo!(),
|
||||||
|
// Notification::VorList(ref vec) => todo!(),
|
||||||
|
Notification::Quit => {
|
||||||
|
let _ = self.sender.send(Event::SimDisconnected);
|
||||||
|
return ControlFlow::Break(());
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
} {
|
||||||
|
tracing::error!("Error handling SimConnect::Notification: {:#?}", e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ControlFlow::Continue(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start(
|
||||||
|
simconnect_receiver: Receiver<SimConnectPacket>,
|
||||||
|
socket_sender: Sender<SimConnectPacket>,
|
||||||
|
sender: Sender<Event>,
|
||||||
|
receiver: Receiver<Event>,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
SimConnect::new(simconnect_receiver, socket_sender, sender, receiver)
|
||||||
|
.run()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
123
avam-client/src/simconnect/models.rs
Normal file
123
avam-client/src/simconnect/models.rs
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use std::ffi::CString;
|
||||||
|
|
||||||
|
use simconnect_sdk::SimConnectObject;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Clone, SimConnectObject)]
|
||||||
|
#[simconnect(period = "second", condition = "changed")]
|
||||||
|
pub(super) struct AtcID {
|
||||||
|
#[simconnect(name = "ATC ID")]
|
||||||
|
pub value: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn atc_id(value: &str) -> Result<AtcIDCPacked, anyhow::Error> {
|
||||||
|
if value.len() > 10 {
|
||||||
|
return Err(anyhow::anyhow!("ATC_ID exceeding 10 characters"));
|
||||||
|
}
|
||||||
|
let cs = CString::new(value.to_uppercase())?;
|
||||||
|
let mut buffer = [0i8; 256];
|
||||||
|
for (i, b) in cs.to_bytes_with_nul().iter().enumerate() {
|
||||||
|
buffer[i] = *b as i8;
|
||||||
|
}
|
||||||
|
Ok(AtcIDCPacked { value: buffer })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, SimConnectObject)]
|
||||||
|
#[simconnect(period = "second", condition = "changed")]
|
||||||
|
pub(super) struct Airplane {
|
||||||
|
#[simconnect(name = "ATC TYPE")]
|
||||||
|
pub atc_type: String,
|
||||||
|
#[simconnect(name = "ATC MODEL")]
|
||||||
|
pub atc_model: String,
|
||||||
|
#[simconnect(name = "TITLE")]
|
||||||
|
pub title: String,
|
||||||
|
#[simconnect(name = "CATEGORY")]
|
||||||
|
pub category: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, SimConnectObject)]
|
||||||
|
#[simconnect(period = "second", condition = "changed")]
|
||||||
|
pub(super) struct Fuel {
|
||||||
|
#[simconnect(name = "FUEL TANK CENTER QUANTITY", unit = "gallons")]
|
||||||
|
pub center_quantity: f64,
|
||||||
|
#[simconnect(name = "FUEL TANK CENTER CAPACITY", unit = "gallons")]
|
||||||
|
pub center_capacity: f64,
|
||||||
|
|
||||||
|
#[simconnect(name = "FUEL TANK CENTER2 QUANTITY", unit = "gallons")]
|
||||||
|
pub center2_quantity: f64,
|
||||||
|
#[simconnect(name = "FUEL TANK CENTER2 CAPACITY", unit = "gallons")]
|
||||||
|
pub center2_capacity: f64,
|
||||||
|
|
||||||
|
#[simconnect(name = "FUEL TANK CENTER3 QUANTITY", unit = "gallons")]
|
||||||
|
pub center3_quantity: f64,
|
||||||
|
#[simconnect(name = "FUEL TANK CENTER3 CAPACITY", unit = "gallons")]
|
||||||
|
pub center3_capacity: f64,
|
||||||
|
|
||||||
|
#[simconnect(name = "FUEL TANK LEFT MAIN QUANTITY", unit = "gallons")]
|
||||||
|
pub left_main_quantity: f64,
|
||||||
|
#[simconnect(name = "FUEL TANK LEFT MAIN CAPACITY", unit = "gallons")]
|
||||||
|
pub left_main_capacity: f64,
|
||||||
|
|
||||||
|
#[simconnect(name = "FUEL TANK LEFT AUX QUANTITY", unit = "gallons")]
|
||||||
|
pub left_aux_quantity: f64,
|
||||||
|
#[simconnect(name = "FUEL TANK LEFT AUX CAPACITY", unit = "gallons")]
|
||||||
|
pub left_aux_capacity: f64,
|
||||||
|
|
||||||
|
#[simconnect(name = "FUEL TANK LEFT TIP QUANTITY", unit = "gallons")]
|
||||||
|
pub left_tip_quantity: f64,
|
||||||
|
#[simconnect(name = "FUEL TANK LEFT TIP CAPACITY", unit = "gallons")]
|
||||||
|
pub left_tip_capacity: f64,
|
||||||
|
|
||||||
|
#[simconnect(name = "FUEL TANK RIGHT MAIN QUANTITY", unit = "gallons")]
|
||||||
|
pub right_main_quantity: f64,
|
||||||
|
#[simconnect(name = "FUEL TANK RIGHT MAIN CAPACITY", unit = "gallons")]
|
||||||
|
pub right_main_capacity: f64,
|
||||||
|
|
||||||
|
#[simconnect(name = "FUEL TANK RIGHT AUX QUANTITY", unit = "gallons")]
|
||||||
|
pub right_aux_quantity: f64,
|
||||||
|
#[simconnect(name = "FUEL TANK RIGHT AUX CAPACITY", unit = "gallons")]
|
||||||
|
pub right_aux_capacity: f64,
|
||||||
|
|
||||||
|
#[simconnect(name = "FUEL TANK RIGHT TIP QUANTITY", unit = "gallons")]
|
||||||
|
pub right_tip_quantity: f64,
|
||||||
|
#[simconnect(name = "FUEL TANK RIGHT TIP CAPACITY", unit = "gallons")]
|
||||||
|
pub right_tip_capacity: f64,
|
||||||
|
|
||||||
|
#[simconnect(name = "FUEL TANK EXTERNAL1 QUANTITY", unit = "gallons")]
|
||||||
|
pub external1_quantity: f64,
|
||||||
|
#[simconnect(name = "FUEL TANK EXTERNAL1 CAPACITY", unit = "gallons")]
|
||||||
|
pub external1_capacity: f64,
|
||||||
|
|
||||||
|
#[simconnect(name = "FUEL TANK EXTERNAL2 QUANTITY", unit = "gallons")]
|
||||||
|
pub external2_quantity: f64,
|
||||||
|
#[simconnect(name = "FUEL TANK EXTERNAL2 CAPACITY", unit = "gallons")]
|
||||||
|
pub external2_capacity: f64,
|
||||||
|
|
||||||
|
#[simconnect(name = "FUEL WEIGHT PER GALLON", unit = "pounds")]
|
||||||
|
pub weight: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, SimConnectObject)]
|
||||||
|
#[simconnect(period = "second", condition = "changed")]
|
||||||
|
pub(super) struct Gps {
|
||||||
|
#[simconnect(name = "PLANE LATITUDE", unit = "degrees")]
|
||||||
|
pub lat: f64,
|
||||||
|
#[simconnect(name = "PLANE LONGITUDE", unit = "degrees")]
|
||||||
|
pub lon: f64,
|
||||||
|
#[simconnect(name = "PLANE ALTITUDE", unit = "feet")]
|
||||||
|
pub alt: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, SimConnectObject)]
|
||||||
|
#[simconnect(period = "second", condition = "changed")]
|
||||||
|
pub(super) struct OnGround {
|
||||||
|
#[simconnect(name = "SIM ON GROUND")]
|
||||||
|
pub sim_on_ground: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, SimConnectObject)]
|
||||||
|
#[simconnect(period = "second", condition = "changed")]
|
||||||
|
pub(super) struct IsParked {
|
||||||
|
#[simconnect(name = "PLANE IN PARKING STATE")]
|
||||||
|
pub is_parked: bool,
|
||||||
|
}
|
@@ -1,126 +1,83 @@
|
|||||||
use tokio::sync::broadcast::{Receiver, Sender};
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::{
|
use tokio::{
|
||||||
config::Config,
|
sync::broadcast::{Receiver, Sender},
|
||||||
models::{CodeChallengeMethod, CodeVerifier},
|
time::sleep,
|
||||||
oauth,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
use crate::{config::Config, oauth};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
Init,
|
Init,
|
||||||
AppStart {
|
AppStart,
|
||||||
config: Config,
|
Shutdown,
|
||||||
},
|
Authenticate,
|
||||||
Authenticate {
|
Connect,
|
||||||
open_browser: bool,
|
|
||||||
code_verifier: CodeVerifier,
|
|
||||||
code_challenge_method: CodeChallengeMethod,
|
|
||||||
},
|
|
||||||
Connect {
|
|
||||||
token: String,
|
|
||||||
},
|
|
||||||
WaitForSim,
|
WaitForSim,
|
||||||
Running,
|
InSim,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Ready {
|
Ready,
|
||||||
config: Config,
|
StartAuthenticate, // should not be string
|
||||||
},
|
TokenReceived, // AppStart and Authenticate can fire off TokenReceived to transition into Connect
|
||||||
StartAuthenticate {
|
|
||||||
open_browser: bool,
|
|
||||||
code_verifier: CodeVerifier,
|
|
||||||
code_challenge_method: CodeChallengeMethod,
|
|
||||||
}, // should not be string
|
|
||||||
TokenReceived {
|
|
||||||
token: String,
|
|
||||||
}, // AppStart and Authenticate can fire off TokenReceived to transition into Connect
|
|
||||||
Connected, // Once connected to the socket, and properly authenticated, fire off Connected to transition to WaitForSim
|
Connected, // Once connected to the socket, and properly authenticated, fire off Connected to transition to WaitForSim
|
||||||
Disconnected, // If for whatever reason we're disconnected from the backend, we need to transition back to Connect
|
Disconnected, // If for whatever reason we're disconnected from the backend, we need to transition back to Connect
|
||||||
SimConnected, // SimConnect is connected, we're in the world and ready to send data, transition to Running
|
SimConnected, // SimConnect is connected, we're in the world and ready to send data, transition to Running
|
||||||
SimDisconnected, // SimConnect is disconnected, we've finished the flight and exited back to the menu, transition back to WaitForSim
|
SimDisconnected, // SimConnect is disconnected, we've finished the flight and exited back to the menu, transition back to WaitForSim
|
||||||
|
Logout,
|
||||||
Quit,
|
Quit,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
pub async fn next(self, event: Event) -> State {
|
pub async fn next(self, event: Event) -> State {
|
||||||
match (self, event) {
|
match (self.clone(), event.clone()) {
|
||||||
// (Current State, SomeEvent) => NextState
|
// (Current State, SomeEvent) => NextState
|
||||||
(_, Event::Ready { config }) => State::AppStart { config },
|
(_, Event::Ready) => State::AppStart,
|
||||||
(
|
(_, Event::Logout) => State::AppStart,
|
||||||
State::AppStart { .. },
|
(_, Event::StartAuthenticate) => Self::Authenticate, // Goto Authenticate
|
||||||
Event::StartAuthenticate {
|
|
||||||
open_browser,
|
|
||||||
code_verifier,
|
|
||||||
code_challenge_method,
|
|
||||||
},
|
|
||||||
) => Self::Authenticate {
|
|
||||||
open_browser,
|
|
||||||
code_verifier,
|
|
||||||
code_challenge_method,
|
|
||||||
}, // Goto Authenticate
|
|
||||||
|
|
||||||
(State::AppStart { .. }, Event::TokenReceived { token }) => State::Connect { token },
|
(_, Event::TokenReceived) => State::Connect,
|
||||||
(State::Authenticate { .. }, Event::TokenReceived { token }) => {
|
|
||||||
State::Connect { token }
|
(_, Event::Connected) => State::WaitForSim, // Goto WaitForSim
|
||||||
|
|
||||||
|
(_, Event::Disconnected) => {
|
||||||
|
sleep(Duration::from_secs(5)).await; // wait 5 seconds before reconnecting
|
||||||
|
tracing::info!("Attempting reconnect");
|
||||||
|
State::AppStart // Goto Connect
|
||||||
}
|
}
|
||||||
|
|
||||||
(State::Connect { .. }, Event::Connected) => todo!(), // Goto WaitForSim
|
(_, Event::SimConnected) => State::InSim, // Goto InSim
|
||||||
|
(_, Event::SimDisconnected) => State::WaitForSim, // Goto WaitForSim
|
||||||
|
|
||||||
(State::WaitForSim, Event::SimConnected) => todo!(), // Goto Running
|
(_, Event::Quit) => State::Shutdown, // All events can go into quit, to shutdown the application
|
||||||
|
|
||||||
(State::Running, Event::Disconnected) => todo!(), // Goto Connect
|
|
||||||
(State::Running, Event::SimDisconnected) => todo!(), // Goto WaitForSim
|
|
||||||
|
|
||||||
(_, Event::Quit) => todo!(), // All events can go into quit, to shutdown the application
|
|
||||||
|
|
||||||
_ => panic!("Invalid state transition"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run(&self, signal: Sender<Event>) -> Result<(), anyhow::Error> {
|
pub async fn run(&self, signal: Sender<Event>, config: Config) -> Result<(), anyhow::Error> {
|
||||||
match self {
|
match self {
|
||||||
State::Init => Ok(()),
|
State::Init => Ok(()),
|
||||||
State::AppStart { config } => {
|
State::AppStart => {
|
||||||
if let Some(token) = config.token() {
|
if config.token().is_some() {
|
||||||
signal.send(Event::TokenReceived {
|
signal.send(Event::TokenReceived)?;
|
||||||
token: token.to_string(),
|
|
||||||
})?;
|
|
||||||
} else {
|
} else {
|
||||||
let open_browser = config.open_browser();
|
signal.send(Event::StartAuthenticate)?;
|
||||||
let code_verifier = CodeVerifier::new();
|
|
||||||
let code_challenge_method = CodeChallengeMethod::Sha256;
|
|
||||||
|
|
||||||
config.set_code_verifier(Some(code_verifier.clone()))?;
|
|
||||||
config.set_code_challenge_method(Some(code_challenge_method.clone()))?;
|
|
||||||
|
|
||||||
signal.send(Event::StartAuthenticate {
|
|
||||||
open_browser,
|
|
||||||
code_verifier,
|
|
||||||
code_challenge_method,
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
State::Authenticate {
|
State::Authenticate => {
|
||||||
open_browser,
|
if config.open_browser() {
|
||||||
code_verifier,
|
oauth::open_browser(config.clone())?;
|
||||||
code_challenge_method,
|
|
||||||
} => {
|
|
||||||
if *open_browser {
|
|
||||||
oauth::open_browser(code_verifier.clone(), code_challenge_method.clone())?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
State::Connect { token } => {
|
State::Connect => Ok(()),
|
||||||
println!("Holyshit we've got a token: {}", token);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
State::WaitForSim => Ok(()),
|
State::WaitForSim => Ok(()),
|
||||||
State::Running => Ok(()),
|
State::InSim => Ok(()),
|
||||||
|
State::Shutdown => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,27 +89,22 @@ pub async fn start(
|
|||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), anyhow::Error> {
|
||||||
let mut state = State::Init;
|
let mut state = State::Init;
|
||||||
|
|
||||||
state.run(event_sender.clone()).await?;
|
state.run(event_sender.clone(), config.clone()).await?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Ok(event) = event_receiver.recv().await {
|
if let Ok(event) = event_receiver.try_recv() {
|
||||||
|
state = state.next(event.clone()).await;
|
||||||
|
|
||||||
|
state.run(event_sender.clone(), config.clone()).await?;
|
||||||
|
|
||||||
if event == Event::Quit {
|
if event == Event::Quit {
|
||||||
println!("Shutting down State Machine");
|
tracing::info!("Shutting down State Machine");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
state = state.next(event).await;
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
|
||||||
// before run
|
|
||||||
if let State::Connect { token } = &state {
|
|
||||||
// before run Connect, save the given token in config
|
|
||||||
config.set_token(Some(token.clone()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.run(event_sender.clone()).await?;
|
tracing::info!("State Machine Shutdown");
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("State Machine Shutdown");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
12
avam-protocol/Cargo.toml
Normal file
12
avam-protocol/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "avam-protocol"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.90"
|
||||||
|
bincode = "1.3.3"
|
||||||
|
derive_more = "1.0.0"
|
||||||
|
flate2 = { version = "1.0.34", features = ["zlib-rs"] }
|
||||||
|
serde = { version = "1.0.210", features = ["derive"] }
|
||||||
|
thiserror = "1.0.64"
|
194
avam-protocol/src/lib.rs
Normal file
194
avam-protocol/src/lib.rs
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
use derive_more::derive::Display;
|
||||||
|
use serde::{de, Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Display, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub enum SystemPacket {
|
||||||
|
Ping,
|
||||||
|
Pong,
|
||||||
|
Close { reason: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Display, Clone, Deserialize, Serialize)]
|
||||||
|
pub enum SimConnectPacket {
|
||||||
|
AtcID(String),
|
||||||
|
Airplane(Airplane),
|
||||||
|
Fuel(Fuel),
|
||||||
|
Gps(Gps),
|
||||||
|
OnGround(OnGround),
|
||||||
|
IsParked(IsParked),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Display, Deserialize, Serialize)]
|
||||||
|
#[display("[{atc_type} - {atc_model}] {title} ({category})")]
|
||||||
|
pub struct Airplane {
|
||||||
|
pub atc_type: String,
|
||||||
|
pub atc_model: String,
|
||||||
|
pub title: String,
|
||||||
|
pub category: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Fuel {
|
||||||
|
pub center_quantity: f64,
|
||||||
|
pub center_capacity: f64,
|
||||||
|
|
||||||
|
pub center2_quantity: f64,
|
||||||
|
pub center2_capacity: f64,
|
||||||
|
|
||||||
|
pub center3_quantity: f64,
|
||||||
|
pub center3_capacity: f64,
|
||||||
|
|
||||||
|
pub left_main_quantity: f64,
|
||||||
|
pub left_main_capacity: f64,
|
||||||
|
|
||||||
|
pub left_aux_quantity: f64,
|
||||||
|
pub left_aux_capacity: f64,
|
||||||
|
|
||||||
|
pub left_tip_quantity: f64,
|
||||||
|
pub left_tip_capacity: f64,
|
||||||
|
|
||||||
|
pub right_main_quantity: f64,
|
||||||
|
pub right_main_capacity: f64,
|
||||||
|
|
||||||
|
pub right_aux_quantity: f64,
|
||||||
|
pub right_aux_capacity: f64,
|
||||||
|
|
||||||
|
pub right_tip_quantity: f64,
|
||||||
|
pub right_tip_capacity: f64,
|
||||||
|
|
||||||
|
pub external1_quantity: f64,
|
||||||
|
pub external1_capacity: f64,
|
||||||
|
|
||||||
|
pub external2_quantity: f64,
|
||||||
|
pub external2_capacity: f64,
|
||||||
|
|
||||||
|
pub weight: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fuel {
|
||||||
|
pub fn total_quantity(&self) -> f64 {
|
||||||
|
self.center_quantity
|
||||||
|
+ self.center2_quantity
|
||||||
|
+ self.center3_quantity
|
||||||
|
+ self.left_main_quantity
|
||||||
|
+ self.left_aux_quantity
|
||||||
|
+ self.left_tip_quantity
|
||||||
|
+ self.right_main_quantity
|
||||||
|
+ self.right_aux_quantity
|
||||||
|
+ self.right_tip_quantity
|
||||||
|
+ self.external1_quantity
|
||||||
|
+ self.external2_quantity
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn total_capacity(&self) -> f64 {
|
||||||
|
self.center_capacity
|
||||||
|
+ self.center2_capacity
|
||||||
|
+ self.center3_capacity
|
||||||
|
+ self.left_main_capacity
|
||||||
|
+ self.left_aux_capacity
|
||||||
|
+ self.left_tip_capacity
|
||||||
|
+ self.right_main_capacity
|
||||||
|
+ self.right_aux_capacity
|
||||||
|
+ self.right_tip_capacity
|
||||||
|
+ self.external1_capacity
|
||||||
|
+ self.external2_capacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for Fuel {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let percent = (100.0 / self.total_capacity()) * self.total_quantity();
|
||||||
|
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{}GL/{}GL ({}%)",
|
||||||
|
self.total_quantity(),
|
||||||
|
self.total_capacity(),
|
||||||
|
percent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Display, Deserialize, Serialize)]
|
||||||
|
#[display("{lat}, {lon} at {alt}ft")]
|
||||||
|
pub struct Gps {
|
||||||
|
pub lat: f64,
|
||||||
|
pub lon: f64,
|
||||||
|
pub alt: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Display, Deserialize, Serialize)]
|
||||||
|
pub struct OnGround {
|
||||||
|
pub sim_on_ground: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Display, Deserialize, Serialize)]
|
||||||
|
pub struct IsParked {
|
||||||
|
pub is_parked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
// #[derive(Debug, Display, Clone, Deserialize, Serialize)]
|
||||||
|
// #[display("[{icao}] {lat} {lon} {alt}")]
|
||||||
|
// pub struct Airport {
|
||||||
|
// pub icao: String,
|
||||||
|
// pub region: String,
|
||||||
|
// pub lat: f64,
|
||||||
|
// pub lon: f64,
|
||||||
|
// pub alt: f64,
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl PartialEq for SimConnectPacket {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
matches!(
|
||||||
|
(self, other),
|
||||||
|
(Self::AtcID(_), Self::AtcID(_)) | (Self::Fuel(_), Self::Fuel(_))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Eq for SimConnectPacket {}
|
||||||
|
|
||||||
|
#[derive(Debug, Display, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub enum Packets {
|
||||||
|
System(SystemPacket),
|
||||||
|
SimConnect(SimConnectPacket),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Packet for Packets {}
|
||||||
|
|
||||||
|
pub trait Packet: Serialize + de::DeserializeOwned + Sized {
|
||||||
|
fn encode(&self) -> Result<Vec<u8>, anyhow::Error> {
|
||||||
|
use flate2::write::GzEncoder;
|
||||||
|
use flate2::Compression;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let encoded = bincode::serialize(&self)?;
|
||||||
|
let mut compressor = GzEncoder::new(Vec::new(), Compression::best());
|
||||||
|
compressor.write_all(&encoded)?;
|
||||||
|
|
||||||
|
Ok(compressor.finish()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode(data: &[u8]) -> Result<Self, anyhow::Error> {
|
||||||
|
use flate2::read::GzDecoder;
|
||||||
|
use std::io::Read;
|
||||||
|
let mut decompressor = GzDecoder::new(data);
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
decompressor.read_to_end(&mut buffer)?;
|
||||||
|
|
||||||
|
Ok(bincode::deserialize(&buffer)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_works() {
|
||||||
|
let msg = Packets::System(SystemPacket::Ping);
|
||||||
|
|
||||||
|
let decoded = Packet::decode(&msg.encode().unwrap()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(msg, decoded);
|
||||||
|
}
|
||||||
|
}
|
2
migrations/20241027203520_airport.down.sql
Normal file
2
migrations/20241027203520_airport.down.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add down migration script here
|
||||||
|
DROP TABLE "airports";
|
42844
migrations/20241027203520_airport.up.sql
Normal file
42844
migrations/20241027203520_airport.up.sql
Normal file
File diff suppressed because it is too large
Load Diff
3
migrations/20241028150902_aircraft.down.sql
Normal file
3
migrations/20241028150902_aircraft.down.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
-- Add down migration script here
|
||||||
|
DROP TABLE "ower_history";
|
||||||
|
DROP TABLE "aircrafts";
|
66
migrations/20241028150902_aircraft.up.sql
Normal file
66
migrations/20241028150902_aircraft.up.sql
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
-- Add up migration script here
|
||||||
|
CREATE TABLE "aircrafts" (
|
||||||
|
"id" uuid NOT NULL,
|
||||||
|
"registration" text NOT NULL,
|
||||||
|
"category" text NOT NULL,
|
||||||
|
|
||||||
|
"manufacturer" text NOT NULL,
|
||||||
|
"model" text NOT NULL,
|
||||||
|
|
||||||
|
"on_ground" boolean NOT NULL DEFAULT 1,
|
||||||
|
|
||||||
|
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "aircrafts_pkey" PRIMARY KEY ("id"),
|
||||||
|
CONSTRAINT "aircrafts_registration_unique" UNIQUE ("registration")
|
||||||
|
) WITH (oids = false);
|
||||||
|
|
||||||
|
CREATE TABLE "gps" (
|
||||||
|
"aircraft_id" uuid NOT NULL,
|
||||||
|
|
||||||
|
"latitude_deg" numeric NOT NULL,
|
||||||
|
"longitude_deg" numeric NOT NULL,
|
||||||
|
"elevation_ft" numeric NOT NULL,
|
||||||
|
|
||||||
|
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "gps_pkey" PRIMARY KEY ("aircraft_id")
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE TABLE "fuel" (
|
||||||
|
"aircraft_id" uuid NOT NULL,
|
||||||
|
"center_quantity" numeric,
|
||||||
|
"center_capacity" numeric,
|
||||||
|
"center2_quantity" numeric,
|
||||||
|
"center2_capacity" numeric,
|
||||||
|
"center3_quantity" numeric,
|
||||||
|
"center3_capacity" numeric,
|
||||||
|
"left_main_quantity" numeric,
|
||||||
|
"left_main_capacity" numeric,
|
||||||
|
"left_aux_quantity" numeric,
|
||||||
|
"left_aux_capacity" numeric,
|
||||||
|
"left_tip_quantity" numeric,
|
||||||
|
"left_tip_capacity" numeric,
|
||||||
|
"right_main_quantity" numeric,
|
||||||
|
"right_main_capacity" numeric,
|
||||||
|
"right_aux_quantity" numeric,
|
||||||
|
"right_aux_capacity" numeric,
|
||||||
|
"right_tip_quantity" numeric,
|
||||||
|
"right_tip_capacity" numeric,
|
||||||
|
"external1_quantity" numeric,
|
||||||
|
"external1_capacity" numeric,
|
||||||
|
"external2_quantity" numeric,
|
||||||
|
"external2_capacity" numeric,
|
||||||
|
"fuel_weight" numeric,
|
||||||
|
|
||||||
|
"created_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "fuel_pkey" PRIMARY KEY ("aircraft_id")
|
||||||
|
)
|
||||||
|
|
||||||
|
-- If a player is in an aircraft that doesnt exist on our end, ask them to "purchace" it from the factory
|
||||||
|
-- What's the pricing going to look like though... hmm
|
||||||
|
|
||||||
|
-- If a pilot buys a new aircraft they need to get in it at least once to get all the fuel-cell data, we don't know beforehand how many tanks it has
|
||||||
|
-- after updating, the capacity should be > 0 to indicate which ones are used
|
2
migrations/20241028214336_pilot.down.sql
Normal file
2
migrations/20241028214336_pilot.down.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- Add down migration script here
|
||||||
|
DROP TABLE "pilots";
|
25
migrations/20241028214336_pilot.up.sql
Normal file
25
migrations/20241028214336_pilot.up.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-- Add up migration script here
|
||||||
|
CREATE TABLE "pilots" (
|
||||||
|
"id" uuid NOT NULL, -- use the random-person-generator's id?
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
|
||||||
|
"full_name" text NOT NULL,
|
||||||
|
"date_of_birth" timestamp NOT NULL,
|
||||||
|
"gender" text NOT NULL,
|
||||||
|
"nationality" text NOT NULL,
|
||||||
|
"photo" text NOT NULL, -- base64 encoded image from RPG?
|
||||||
|
|
||||||
|
-- Pilot also needs to keep track of career progress
|
||||||
|
-- Flight lessons etc?
|
||||||
|
|
||||||
|
-- current location, i think we can use lat/lon to infer an airport location
|
||||||
|
"latitude_deg" numeric NOT NULL,
|
||||||
|
"longitude_deg" numeric NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "pilots_pkey" PRIMARY KEY ("id")
|
||||||
|
) WITH (oids = false);
|
||||||
|
|
||||||
|
-- Creating a new pilot from the following inputs:
|
||||||
|
-- Age (will determine year, but pick a random date) - depending if the RPG's API supports this
|
||||||
|
-- Gender
|
||||||
|
-- Nationality
|
4
migrations/20241028221013_pivot_tables.down.sql
Normal file
4
migrations/20241028221013_pivot_tables.down.sql
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
-- Add down migration script here
|
||||||
|
DROP TABLE "owner_history";
|
||||||
|
DROP TABLE "pilot_aircraft";
|
||||||
|
DROP TABLE "flight_log";
|
31
migrations/20241028221013_pivot_tables.up.sql
Normal file
31
migrations/20241028221013_pivot_tables.up.sql
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
-- Add up migration script here
|
||||||
|
|
||||||
|
CREATE TABLE "owner_history" (
|
||||||
|
"id" BIGSERIAL NOT NULL,
|
||||||
|
"aircraft_id" BIGSERIAL NOT NULL,
|
||||||
|
"pilot_id" uuid NOT NULL,
|
||||||
|
"since" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "owner_history_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Pilot currently occupying/flying aircraft
|
||||||
|
CREATE TABLE "pilot_current_aircraft" (
|
||||||
|
"pilot_id" uuid NOT NULL,
|
||||||
|
"aircraft_id" uuid NOT NULL,
|
||||||
|
CONSTRAINT "pilot_aircraft_pkey" PRIMARY KEY ("pilot_id", "aircraft_id"),
|
||||||
|
) WITH (oids = false);
|
||||||
|
|
||||||
|
CREATE TABLE "flight_log" (
|
||||||
|
"id" BIGSERIAL NOT NULL,
|
||||||
|
"pilot_id" uuid NOT NULL,
|
||||||
|
"aircraft_id" uuid NOT NULL,
|
||||||
|
"takeoff_time" timestamp DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"takeoff_latitude_deg" NUMERIC NOT NULL,
|
||||||
|
"takeoff_longitude_deg" NUMERIC NOT NULL,
|
||||||
|
"takeoff_elevation_ft" NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
"landing" timestamp,
|
||||||
|
"landing_latitude_deg" NUMERIC NOT NULL,
|
||||||
|
"landing_longitude_deg" NUMERIC NOT NULL,
|
||||||
|
"landing_elevation_ft" NUMERIC NOT NULL DEFAULT 0,
|
||||||
|
CONSTRAINT "flight_log_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
@@ -30,7 +30,7 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
let api_service = api::Service::new(postgres.clone(), dangerous_lettre);
|
let api_service = api::Service::new(postgres.clone(), dangerous_lettre);
|
||||||
|
|
||||||
let app_state = AppState::new(api_service).await;
|
let app_state = AppState::new(api_service, config.clone()).await;
|
||||||
|
|
||||||
HttpServer::new(app_state, postgres.pool())
|
HttpServer::new(app_state, postgres.pool())
|
||||||
.await?
|
.await?
|
||||||
|
@@ -9,6 +9,7 @@ const SMTP_USERNAME: &str = "SMTP_USERNAME";
|
|||||||
const SMTP_PASSWORD: &str = "SMTP_PASSWORD";
|
const SMTP_PASSWORD: &str = "SMTP_PASSWORD";
|
||||||
const SMTP_SENDER: &str = "SMTP_SENDER";
|
const SMTP_SENDER: &str = "SMTP_SENDER";
|
||||||
const JWT_SECRET: &str = "JWT_SECRET";
|
const JWT_SECRET: &str = "JWT_SECRET";
|
||||||
|
const HASH_ID: &str = "HASH_ID";
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@@ -21,6 +22,7 @@ pub struct Config {
|
|||||||
pub smtp_sender: String,
|
pub smtp_sender: String,
|
||||||
|
|
||||||
pub jwt_secret: String,
|
pub jwt_secret: String,
|
||||||
|
pub hash_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
@@ -32,6 +34,7 @@ impl Config {
|
|||||||
let smtp_password = load_env(SMTP_PASSWORD)?;
|
let smtp_password = load_env(SMTP_PASSWORD)?;
|
||||||
let smtp_sender = load_env(SMTP_SENDER)?;
|
let smtp_sender = load_env(SMTP_SENDER)?;
|
||||||
let jwt_secret = load_env(JWT_SECRET)?;
|
let jwt_secret = load_env(JWT_SECRET)?;
|
||||||
|
let hash_id = load_env(HASH_ID)?;
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
database_url,
|
database_url,
|
||||||
@@ -41,6 +44,7 @@ impl Config {
|
|||||||
smtp_password,
|
smtp_password,
|
||||||
smtp_sender,
|
smtp_sender,
|
||||||
jwt_secret,
|
jwt_secret,
|
||||||
|
hash_id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,12 +23,16 @@ pub mod prelude {
|
|||||||
pub use super::ports::*;
|
pub use super::ports::*;
|
||||||
pub use super::service::*;
|
pub use super::service::*;
|
||||||
|
|
||||||
|
pub use crate::domain::leptos::check_user;
|
||||||
|
|
||||||
pub use crate::domain::leptos::flashbag::Alert;
|
pub use crate::domain::leptos::flashbag::Alert;
|
||||||
pub use crate::domain::leptos::flashbag::Flash;
|
pub use crate::domain::leptos::flashbag::Flash;
|
||||||
pub use crate::domain::leptos::flashbag::FlashBag;
|
pub use crate::domain::leptos::flashbag::FlashBag;
|
||||||
pub use crate::domain::leptos::flashbag::FlashMessage;
|
pub use crate::domain::leptos::flashbag::FlashMessage;
|
||||||
|
|
||||||
pub use axum_session::SessionAnySession as Session;
|
pub use axum_session::SessionAnySession as Session;
|
||||||
|
|
||||||
|
pub use avam_protocol::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "ssr"))]
|
#[cfg(not(feature = "ssr"))]
|
||||||
|
32
src/lib/domain/api/models/aircraft.rs
Normal file
32
src/lib/domain/api/models/aircraft.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// max limit: 1.336.335
|
||||||
|
|
||||||
|
// Ok so i kinda wanna go all out on this one
|
||||||
|
|
||||||
|
// The basic things like
|
||||||
|
// Aircraft Manufacturer
|
||||||
|
// Aircraft Type
|
||||||
|
// Registration Number (up to 1.336.335, generated with auto incremented ids and hash_id)
|
||||||
|
// - Custom registration nr should be possible, "auto" increment will be a manual thing, check if it exists, if so, skip
|
||||||
|
// Build date
|
||||||
|
|
||||||
|
// Usage things
|
||||||
|
// Current fuel level in all possible tanks
|
||||||
|
// Current location and altitude
|
||||||
|
// Passangers and Cargo
|
||||||
|
|
||||||
|
// But also Logs
|
||||||
|
// Owner history
|
||||||
|
// Flight Logs which contains weights and balances, to and from etc
|
||||||
|
// Crashes and maintanance logs
|
||||||
|
|
||||||
|
// Most of these things can and will be automated / generated without the need for user input
|
||||||
|
// Maybe maintanance can be a thing where a plane can't be used for x time
|
||||||
|
|
||||||
|
// -----------------------
|
||||||
|
|
||||||
|
// The thing we actually need first is a factory
|
||||||
|
// Using the webinterface, we actually need to go to a factory to build us a new aircraft
|
||||||
|
|
||||||
|
// Hangars
|
||||||
|
// Hangars are on airports
|
||||||
|
// so we need those too
|
@@ -1,11 +1,30 @@
|
|||||||
|
use avam_protocol::Packets;
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
domain::api::models::oauth::*, inbound::http::handlers::oauth::AuthorizationCodeRequest,
|
domain::api::models::oauth::*,
|
||||||
|
inbound::http::handlers::oauth::{AuthorizationCodeRequest, VerifyClientAuthorizationRequest},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::super::models::user::*;
|
use super::super::models::user::*;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
pub trait ApiService: Clone + Send + Sync + 'static {
|
pub trait ApiService: Clone + Send + Sync + 'static {
|
||||||
|
// -- -
|
||||||
|
// Websocket
|
||||||
|
// ---
|
||||||
|
fn add_connection(&self, user: &User, sender: Sender<Packets>);
|
||||||
|
|
||||||
|
fn has_connection(&self, user: &User) -> bool;
|
||||||
|
|
||||||
|
fn remove_connection(&self, user: &User);
|
||||||
|
|
||||||
|
fn send(
|
||||||
|
&self,
|
||||||
|
user: &User,
|
||||||
|
packet: Packets,
|
||||||
|
) -> impl Future<Output = Result<(), anyhow::Error>> + Send;
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
// USER
|
// USER
|
||||||
// ---
|
// ---
|
||||||
@@ -56,5 +75,13 @@ pub trait ApiService: Clone + Send + Sync + 'static {
|
|||||||
fn create_token(
|
fn create_token(
|
||||||
&self,
|
&self,
|
||||||
req: AuthorizationCodeRequest,
|
req: AuthorizationCodeRequest,
|
||||||
) -> impl Future<Output = Result<Option<TokenSubject>, TokenError>> + Send;
|
) -> impl Future<Output = Result<TokenSubject, TokenError>> + Send;
|
||||||
|
|
||||||
|
/// ---
|
||||||
|
/// WS
|
||||||
|
/// ---
|
||||||
|
fn verify_client_authorization(
|
||||||
|
&self,
|
||||||
|
req: VerifyClientAuthorizationRequest,
|
||||||
|
) -> impl Future<Output = Result<User, anyhow::Error>> + Send;
|
||||||
}
|
}
|
||||||
|
@@ -2,10 +2,14 @@
|
|||||||
Module `service` provides the canonical implementation of the [ApiService] port. All
|
Module `service` provides the canonical implementation of the [ApiService] port. All
|
||||||
user-domain logic is defined here.
|
user-domain logic is defined here.
|
||||||
*/
|
*/
|
||||||
|
use avam_protocol::Packets;
|
||||||
use axum_session::SessionAnySession;
|
use axum_session::SessionAnySession;
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use tokio::sync::mpsc::Sender;
|
||||||
|
|
||||||
use crate::inbound::http::handlers::oauth::AuthorizationCodeRequest;
|
use crate::inbound::http::handlers::oauth::AuthorizationCodeRequest;
|
||||||
use crate::inbound::http::handlers::oauth::GrantType;
|
use crate::inbound::http::handlers::oauth::GrantType;
|
||||||
|
use crate::inbound::http::handlers::oauth::VerifyClientAuthorizationRequest;
|
||||||
|
|
||||||
use super::models::oauth::Client;
|
use super::models::oauth::Client;
|
||||||
use super::models::oauth::*;
|
use super::models::oauth::*;
|
||||||
@@ -24,6 +28,7 @@ where
|
|||||||
{
|
{
|
||||||
repo: R,
|
repo: R,
|
||||||
notifier: N,
|
notifier: N,
|
||||||
|
connections: DashMap<uuid::Uuid, Sender<Packets>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, N> Service<R, N>
|
impl<R, N> Service<R, N>
|
||||||
@@ -32,7 +37,11 @@ where
|
|||||||
N: Email,
|
N: Email,
|
||||||
{
|
{
|
||||||
pub fn new(repo: R, notifier: N) -> Self {
|
pub fn new(repo: R, notifier: N) -> Self {
|
||||||
Self { repo, notifier }
|
Self {
|
||||||
|
repo,
|
||||||
|
notifier,
|
||||||
|
connections: DashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +50,25 @@ where
|
|||||||
R: UserRepository + OAuthRepository,
|
R: UserRepository + OAuthRepository,
|
||||||
N: Email,
|
N: Email,
|
||||||
{
|
{
|
||||||
|
fn add_connection(&self, user: &User, sender: Sender<Packets>) {
|
||||||
|
self.connections.insert(user.id(), sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_connection(&self, user: &User) -> bool {
|
||||||
|
self.connections.contains_key(&user.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_connection(&self, user: &User) {
|
||||||
|
self.connections.remove(&user.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send(&self, user: &User, packet: Packets) -> Result<(), anyhow::Error> {
|
||||||
|
if let Some(sender) = self.connections.get(&user.id()) {
|
||||||
|
sender.send(packet).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError> {
|
async fn create_user(&self, req: CreateUserRequest) -> Result<User, CreateUserError> {
|
||||||
let result = self.repo.create_user(req).await;
|
let result = self.repo.create_user(req).await;
|
||||||
|
|
||||||
@@ -228,7 +256,7 @@ where
|
|||||||
async fn create_token(
|
async fn create_token(
|
||||||
&self,
|
&self,
|
||||||
req: AuthorizationCodeRequest,
|
req: AuthorizationCodeRequest,
|
||||||
) -> Result<Option<TokenSubject>, TokenError> {
|
) -> Result<TokenSubject, TokenError> {
|
||||||
if req.grant_type() != GrantType::AuthorizationCode {
|
if req.grant_type() != GrantType::AuthorizationCode {
|
||||||
return Err(TokenError::InvalidRequest);
|
return Err(TokenError::InvalidRequest);
|
||||||
}
|
}
|
||||||
@@ -265,6 +293,24 @@ where
|
|||||||
|
|
||||||
let _ = self.repo.delete_token(req.code()).await;
|
let _ = self.repo.delete_token(req.code()).await;
|
||||||
|
|
||||||
Ok(Some(token))
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify_client_authorization(
|
||||||
|
&self,
|
||||||
|
req: VerifyClientAuthorizationRequest,
|
||||||
|
) -> Result<User, anyhow::Error> {
|
||||||
|
let user_id = req.user_id();
|
||||||
|
let client_id = req.client_id();
|
||||||
|
|
||||||
|
if !self.repo.is_authorized_client(user_id, client_id).await? {
|
||||||
|
return Err(anyhow::anyhow!("Unauthorized"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(user) = self.repo.find_user_by_id(user_id).await? else {
|
||||||
|
return Err(anyhow::anyhow!("Unauthorized"));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,46 +1,54 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
|
use leptos_router::ActionForm;
|
||||||
|
|
||||||
use crate::domain::api::prelude::User;
|
use crate::domain::api::prelude::*;
|
||||||
|
#[server]
|
||||||
|
async fn login_action(ident: String) -> Result<(), ServerFnError<String>> {
|
||||||
|
use crate::domain::api::prelude::*;
|
||||||
|
|
||||||
|
if ident.len() > 10 {
|
||||||
|
return Err(ServerFnError::WrappedServerError(
|
||||||
|
"ATC_ID exceeding 10 characters".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(user) = check_user().await? else {
|
||||||
|
return Err(ServerFnError::WrappedServerError("No user".into()));
|
||||||
|
};
|
||||||
|
|
||||||
|
let app = use_context::<AppService>().unwrap();
|
||||||
|
|
||||||
|
app.send(&user, Packets::SimConnect(SimConnectPacket::AtcID(ident)))
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("{:#?}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Renders the home page of your application.
|
/// Renders the home page of your application.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn DashboardPage(user: User) -> impl IntoView {
|
pub fn DashboardPage(user: User) -> impl IntoView {
|
||||||
view! {
|
let submit = Action::<LoginAction, _>::server();
|
||||||
<section class="login is-fullheight">
|
// let response = submit.value().read_only();
|
||||||
<div class="columns is-fullheight">
|
|
||||||
<div class="column is-one-third-fullhd is-half-widescreen">
|
|
||||||
<div class="login_container">
|
|
||||||
<div style="margin: auto 0">
|
|
||||||
<div class="has-text-centered">
|
|
||||||
<img src="/android-chrome-192x192.png" alt={ crate::PROJECT_NAME }/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="p-10">
|
||||||
<pre>Hello, { user.email().to_string() }!</pre>
|
<pre>Hello, { user.email().to_string() }!</pre>
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered link">
|
||||||
<a href="/auth/logout">Logout</a>
|
<a href="/auth/logout">Logout</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<ActionForm action=submit>
|
||||||
|
<label class="input input-bordered flex items-center gap-2">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
<input type="text" placeholder="Registration Number" maxlength="10" name="ident" />
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<input type="submit" value="Update" class="btn btn-block" />
|
||||||
</div>
|
</div>
|
||||||
|
</ActionForm>
|
||||||
<footer>
|
|
||||||
<div class="content has-text-centered">
|
|
||||||
<p>
|
|
||||||
{ crate::PROJECT_NAME }
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span class="icon"><a href="https://git.avii.nl/AVAM/avam" class="is-link" target="_BLANK"><i class="fab fa-git-alt"></i></a></span>
|
|
||||||
<span class="icon"><a href="#" class="is-link" target="_BLANK"><i class="fab fa-discord"></i></a></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="column is-fullheight background is-hidden-mobile has-background-primary has-text-primary-invert">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
}
|
||||||
}.into_view()
|
.into_view()
|
||||||
}
|
}
|
||||||
|
@@ -5,14 +5,12 @@ use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
|||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::ConnectInfo,
|
extract::ConnectInfo, routing::{any, get, post}, Extension
|
||||||
routing::{get, post},
|
|
||||||
Extension,
|
|
||||||
};
|
};
|
||||||
use axum_session::{SessionAnyPool, SessionConfig, SessionLayer, SessionStore};
|
use axum_session::{SessionAnyPool, SessionConfig, SessionLayer, SessionStore};
|
||||||
use axum_session_sqlx::SessionPgPool;
|
use axum_session_sqlx::SessionPgPool;
|
||||||
use handlers::{
|
use handlers::{
|
||||||
fileserv::file_and_error_handler, leptos::{leptos_routes_handler, server_fn_handler}, oauth, user::activate_account
|
fileserv::file_and_error_handler, leptos::{leptos_routes_handler, server_fn_handler}, oauth, user::activate_account, websocket::ws_handler
|
||||||
};
|
};
|
||||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
@@ -57,6 +55,7 @@ impl HttpServer {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let router = axum::Router::new()
|
let router = axum::Router::new()
|
||||||
|
.route("/ws", any(ws_handler))
|
||||||
.nest("/oauth2", oauth::routes())
|
.nest("/oauth2", oauth::routes())
|
||||||
.route("/auth/activate/:token", get(activate_account))
|
.route("/auth/activate/:token", get(activate_account))
|
||||||
.route("/api/*fn_name", post(server_fn_handler))
|
.route("/api/*fn_name", post(server_fn_handler))
|
||||||
@@ -87,3 +86,4 @@ impl HttpServer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,3 +2,4 @@ pub mod fileserv;
|
|||||||
pub mod leptos;
|
pub mod leptos;
|
||||||
pub mod oauth;
|
pub mod oauth;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
pub mod websocket;
|
||||||
|
@@ -120,6 +120,26 @@ impl AuthorizationCodeRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct VerifyClientAuthorizationRequest {
|
||||||
|
user_id: uuid::Uuid,
|
||||||
|
client_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VerifyClientAuthorizationRequest {
|
||||||
|
pub fn new(user_id: uuid::Uuid, client_id: uuid::Uuid) -> Self {
|
||||||
|
Self { client_id, user_id }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn user_id(&self) -> uuid::Uuid {
|
||||||
|
self.user_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client_id(&self) -> uuid::Uuid {
|
||||||
|
self.client_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct TokenClaims<T>
|
pub struct TokenClaims<T>
|
||||||
where
|
where
|
||||||
@@ -180,7 +200,11 @@ where
|
|||||||
let iat = now.unix_timestamp() as usize;
|
let iat = now.unix_timestamp() as usize;
|
||||||
let exp = (now + time::Duration::days(30)).unix_timestamp() as usize;
|
let exp = (now + time::Duration::days(30)).unix_timestamp() as usize;
|
||||||
|
|
||||||
let claims = TokenClaims { sub, iat, exp };
|
let claims = TokenClaims {
|
||||||
|
sub: serde_qs::to_string(&sub)?,
|
||||||
|
iat,
|
||||||
|
exp,
|
||||||
|
};
|
||||||
|
|
||||||
let token = encode(
|
let token = encode(
|
||||||
&Header::default(),
|
&Header::default(),
|
||||||
|
250
src/lib/inbound/http/handlers/websocket.rs
Normal file
250
src/lib/inbound/http/handlers/websocket.rs
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
use std::{net::SocketAddr, ops::ControlFlow, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use avam_protocol::{Packet, Packets, SimConnectPacket, SystemPacket};
|
||||||
|
use axum::{
|
||||||
|
extract::{
|
||||||
|
ws::{Message, WebSocket},
|
||||||
|
ConnectInfo, State, WebSocketUpgrade,
|
||||||
|
},
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use axum_extra::{
|
||||||
|
headers::{self, authorization::Bearer},
|
||||||
|
TypedHeader,
|
||||||
|
};
|
||||||
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use http::StatusCode;
|
||||||
|
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||||
|
use tokio::{
|
||||||
|
sync::mpsc,
|
||||||
|
time::{sleep, Instant},
|
||||||
|
};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
domain::api::{
|
||||||
|
ports::ApiService,
|
||||||
|
prelude::{TokenSubject, User},
|
||||||
|
},
|
||||||
|
inbound::http::{
|
||||||
|
handlers::oauth::{TokenClaims, VerifyClientAuthorizationRequest},
|
||||||
|
state::AppState,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn ws_handler<S: ApiService>(
|
||||||
|
State(app_state): State<AppState<S>>,
|
||||||
|
ws: WebSocketUpgrade,
|
||||||
|
auth_token: Option<TypedHeader<headers::Authorization<Bearer>>>,
|
||||||
|
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||||
|
) -> Result<impl IntoResponse, StatusCode> {
|
||||||
|
let auth_token = match auth_token {
|
||||||
|
Some(TypedHeader(token)) => Some(token.token().to_string()),
|
||||||
|
None => {
|
||||||
|
tracing::error!("No Authorization Header Supplied");
|
||||||
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(auth_token) = auth_token else {
|
||||||
|
tracing::error!("No Token Supplied");
|
||||||
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
|
};
|
||||||
|
|
||||||
|
let jwt_secret = &app_state.config().jwt_secret;
|
||||||
|
|
||||||
|
let claims = decode::<TokenClaims<String>>(
|
||||||
|
&auth_token,
|
||||||
|
&DecodingKey::from_secret(jwt_secret.as_ref()),
|
||||||
|
&Validation::new(Algorithm::HS256),
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("Unable to decode token: {}\n{:?}", auth_token, e);
|
||||||
|
StatusCode::UNAUTHORIZED
|
||||||
|
})?
|
||||||
|
.claims;
|
||||||
|
|
||||||
|
let token_subject: TokenSubject = serde_qs::from_str(&claims.sub).map_err(|e| {
|
||||||
|
tracing::error!("Unable to parse Token Subject: {}\n{:?}", &claims.sub, e);
|
||||||
|
StatusCode::BAD_REQUEST
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let user_id = token_subject.user_id();
|
||||||
|
let client_id = token_subject.client_id();
|
||||||
|
|
||||||
|
let app = app_state.api_service();
|
||||||
|
let Ok(user) = app
|
||||||
|
.verify_client_authorization(VerifyClientAuthorizationRequest::new(user_id, client_id))
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
tracing::error!("Client {} not authorized by {}", client_id, user_id);
|
||||||
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
|
};
|
||||||
|
|
||||||
|
if app_state.api_service().has_connection(&user) {
|
||||||
|
return Err(StatusCode::CONFLICT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ws.on_upgrade(move |socket| handle_socket(app_state.clone(), socket, user, addr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Actual websocket statemachine (one will be spawned per connection)
|
||||||
|
async fn handle_socket<S: ApiService>(
|
||||||
|
app_state: AppState<S>,
|
||||||
|
socket: WebSocket,
|
||||||
|
user: User,
|
||||||
|
who: SocketAddr,
|
||||||
|
) {
|
||||||
|
// AppState needs to store user to channel handles or something
|
||||||
|
// It'd know who's connected at all times and be able to send messages
|
||||||
|
|
||||||
|
let (sender, mut receiver) = mpsc::channel(10);
|
||||||
|
app_state.api_service().add_connection(&user, sender);
|
||||||
|
|
||||||
|
// This can probably be hella-abstracted away and be made a lot cleaner
|
||||||
|
let (mut writer, mut reader) = socket.split();
|
||||||
|
// This probably needs a (mpsc?) channel, then a dedicated thread to send ping on that channel
|
||||||
|
let mut ping_timer = Instant::now();
|
||||||
|
ping_timer.checked_add(Duration::from_secs(15)); // add 15 seconds so we instantly trigger a ping
|
||||||
|
let u = user.clone();
|
||||||
|
let writer_handle = tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
let msg = match receiver.try_recv() {
|
||||||
|
Ok(msg) => Some(msg),
|
||||||
|
Err(mpsc::error::TryRecvError::Disconnected) => break,
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(msg) = msg {
|
||||||
|
let Ok(encoded_message) = msg.encode() else {
|
||||||
|
tracing::error!("Unable to encode message for sending: {:#?}", msg);
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
tracing::info!("> [{}]: {:?}", &u.email(), msg);
|
||||||
|
let _ = writer.send(Message::Binary(encoded_message)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ping_timer.elapsed() >= Duration::from_secs(15) {
|
||||||
|
tracing::trace!(
|
||||||
|
"> [{}]: {:?}",
|
||||||
|
&u.email(),
|
||||||
|
Packets::System(SystemPacket::Ping)
|
||||||
|
);
|
||||||
|
let _ = writer
|
||||||
|
.send(Message::Binary(
|
||||||
|
Packets::System(SystemPacket::Ping).encode().unwrap(),
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
ping_timer = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let u = user.clone();
|
||||||
|
let a_s = app_state.clone();
|
||||||
|
let reader_handle = tokio::spawn(async move {
|
||||||
|
loop {
|
||||||
|
if let Some(Ok(data)) = reader.next().await {
|
||||||
|
let Ok(packet) = Packets::decode(&data.clone().into_data()) else {
|
||||||
|
if let Message::Close(Some(c)) = data {
|
||||||
|
tracing::info!("{} disconnected: {}", u.email(), c.reason);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tracing::error!("Invalid packet received {:#?}", data);
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
match packet {
|
||||||
|
Packets::System(system_packet) => {
|
||||||
|
process_system_packet(a_s.clone(), &u, system_packet).await;
|
||||||
|
}
|
||||||
|
Packets::SimConnect(sim_connect_packet) => {
|
||||||
|
process_message(a_s.api_service().clone(), &u, sim_connect_packet).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = reader_handle => {
|
||||||
|
tracing::debug!("reader closed");
|
||||||
|
}
|
||||||
|
_ = writer_handle => {
|
||||||
|
tracing::debug!("writer closed");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// remove the user/channel from AppState
|
||||||
|
app_state.api_service().remove_connection(&user);
|
||||||
|
|
||||||
|
// returning from the handler closes the websocket connection
|
||||||
|
tracing::debug!("Websocket context {who} destroyed");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_system_packet<S>(
|
||||||
|
app_state: AppState<S>,
|
||||||
|
user: &User,
|
||||||
|
packet: SystemPacket,
|
||||||
|
) -> ControlFlow<(), ()>
|
||||||
|
where
|
||||||
|
S: ApiService,
|
||||||
|
{
|
||||||
|
tracing::trace!("< [{}]: {:?}", user.email(), packet);
|
||||||
|
match packet {
|
||||||
|
SystemPacket::Ping => {
|
||||||
|
// send back pong
|
||||||
|
let _ = app_state
|
||||||
|
.api_service()
|
||||||
|
.send(user, Packets::System(SystemPacket::Pong))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
SystemPacket::Pong => {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
SystemPacket::Close { reason } => {
|
||||||
|
tracing::debug!("{} disconnect: {}", user.email(), reason);
|
||||||
|
return ControlFlow::Break(()); // whatever this means
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ControlFlow::Continue(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn process_message<S>(
|
||||||
|
api_service: Arc<S>,
|
||||||
|
user: &User,
|
||||||
|
packet: SimConnectPacket,
|
||||||
|
) -> ControlFlow<(), ()>
|
||||||
|
where
|
||||||
|
S: ApiService,
|
||||||
|
{
|
||||||
|
// This needs to be abstracted away from here like actually
|
||||||
|
match packet {
|
||||||
|
SimConnectPacket::AtcID(id) => info!("[{}] Registration to {}", user.email(), id),
|
||||||
|
SimConnectPacket::Fuel(fuel) => info!("[{}] Fuel state: {}", user.email(), fuel),
|
||||||
|
SimConnectPacket::Airplane(airplane) => info!("[{}] Airplane: {}", user.email(), airplane),
|
||||||
|
SimConnectPacket::Gps(gps) => info!("[{}] Location: {}", user.email(), gps),
|
||||||
|
SimConnectPacket::OnGround(on_ground) => {
|
||||||
|
info!("[{}] On Ground: {}", user.email(), on_ground)
|
||||||
|
}
|
||||||
|
SimConnectPacket::IsParked(is_parked) => {
|
||||||
|
info!("[{}] Is Parked: {}", user.email(), is_parked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The airplane variant checks if the player is in the correct airplane for the selected job etc
|
||||||
|
// Send error if not and don't handle the flight
|
||||||
|
|
||||||
|
// tracing::info!("< [{}]: {:?}", user.email(), packet);
|
||||||
|
|
||||||
|
// On incoming packets, we use the internal api to store stuff to the database
|
||||||
|
// We'll use Server Side Events (SSE) to keep the dashboard up to date with the state of the database
|
||||||
|
|
||||||
|
ControlFlow::Continue(())
|
||||||
|
}
|
@@ -3,15 +3,16 @@ use std::sync::Arc;
|
|||||||
use axum::extract::FromRef;
|
use axum::extract::FromRef;
|
||||||
use leptos::get_configuration;
|
use leptos::get_configuration;
|
||||||
|
|
||||||
use crate::domain::api::ports::ApiService;
|
use crate::{config::Config, domain::api::ports::ApiService};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Clone)]
|
||||||
/// The global application state shared between all request handlers.
|
/// The global application state shared between all request handlers.
|
||||||
pub struct AppState<S>
|
pub struct AppState<S>
|
||||||
where
|
where
|
||||||
S: ApiService,
|
S: ApiService,
|
||||||
{
|
{
|
||||||
pub leptos_options: leptos::LeptosOptions,
|
pub leptos_options: leptos::LeptosOptions,
|
||||||
|
config: Arc<Config>,
|
||||||
api_service: Arc<S>,
|
api_service: Arc<S>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,13 +20,18 @@ impl<S> AppState<S>
|
|||||||
where
|
where
|
||||||
S: ApiService,
|
S: ApiService,
|
||||||
{
|
{
|
||||||
pub async fn new(api_service: S) -> Self {
|
pub async fn new(api_service: S, config: Config) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
config: Arc::new(config),
|
||||||
leptos_options: get_configuration(None).await.unwrap().leptos_options,
|
leptos_options: get_configuration(None).await.unwrap().leptos_options,
|
||||||
api_service: Arc::new(api_service),
|
api_service: Arc::new(api_service),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn config(&self) -> Arc<Config> {
|
||||||
|
self.config.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn api_service(&self) -> Arc<S> {
|
pub fn api_service(&self) -> Arc<S> {
|
||||||
self.api_service.clone()
|
self.api_service.clone()
|
||||||
}
|
}
|
||||||
|
@@ -968,34 +968,6 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: grid;
|
|
||||||
width: 100%;
|
|
||||||
grid-auto-flow: row;
|
|
||||||
place-items: start;
|
|
||||||
-moz-column-gap: 1rem;
|
|
||||||
column-gap: 1rem;
|
|
||||||
row-gap: 2.5rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer > * {
|
|
||||||
display: grid;
|
|
||||||
place-items: start;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 48rem) {
|
|
||||||
.footer {
|
|
||||||
grid-auto-flow: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-center {
|
|
||||||
grid-auto-flow: row dense;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
display: flex;
|
display: flex;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
@@ -1500,6 +1472,10 @@ html {
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-10 {
|
||||||
|
padding: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.px-1 {
|
.px-1 {
|
||||||
padding-left: 0.25rem;
|
padding-left: 0.25rem;
|
||||||
padding-right: 0.25rem;
|
padding-right: 0.25rem;
|
||||||
|
Reference in New Issue
Block a user