websocket
This commit is contained in:
@@ -12,9 +12,12 @@ 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"] }
|
||||
tauri-winrt-notification = "0.6.0"
|
||||
open = "5.3.0"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.8", default-features = false, features = [
|
||||
@@ -24,9 +27,13 @@ reqwest = { version = "0.12.8", default-features = false, features = [
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_qs = "0.13.0"
|
||||
sha256 = "1.5.0"
|
||||
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"] }
|
||||
|
@@ -56,10 +56,7 @@ impl App {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
oauth::open_browser(
|
||||
c.code_verifier().unwrap(),
|
||||
c.code_challenge_method().unwrap(),
|
||||
)?;
|
||||
oauth::open_browser(c.clone())?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -70,7 +67,7 @@ impl App {
|
||||
c.set_token(None)?;
|
||||
c.set_open_browser(false)?;
|
||||
|
||||
let _ = s.send(Event::Ready { config: c.clone() });
|
||||
let _ = s.send(Event::Logout);
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -108,7 +105,7 @@ impl ApplicationHandler for App {
|
||||
|
||||
if let Ok(event) = self.receiver.try_recv() {
|
||||
match event {
|
||||
Event::Ready { .. } => {
|
||||
Event::Logout | Event::Ready => {
|
||||
self.tray_icon
|
||||
.set_text(self.items.get("login").unwrap(), "Login")
|
||||
.unwrap();
|
||||
@@ -117,7 +114,7 @@ impl ApplicationHandler for App {
|
||||
.set_enabled(self.items.get("forget").unwrap(), false)
|
||||
.unwrap();
|
||||
}
|
||||
Event::TokenReceived { .. } => {
|
||||
Event::TokenReceived { .. } | Event::Connected => {
|
||||
self.tray_icon
|
||||
.set_text(self.items.get("login").unwrap(), "Open Avam")
|
||||
.unwrap();
|
||||
@@ -138,9 +135,7 @@ impl ApplicationHandler for App {
|
||||
fn resumed(&mut self, _: &winit::event_loop::ActiveEventLoop) {
|
||||
let _ = self.tray_icon.build();
|
||||
|
||||
let _ = self.sender.send(Event::Ready {
|
||||
config: self.config.clone(),
|
||||
});
|
||||
let _ = self.sender.send(Event::Ready);
|
||||
|
||||
if !self.config.toast_shown() {
|
||||
let _ = Toast::new(crate::AVAM_APP_ID)
|
||||
|
97
avam-client/src/client.rs
Normal file
97
avam-client/src/client.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use std::{borrow::Cow, time::Duration};
|
||||
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use reqwest::StatusCode;
|
||||
use tokio::{
|
||||
sync::broadcast::{Receiver, Sender},
|
||||
time::sleep,
|
||||
};
|
||||
use tokio_tungstenite::{
|
||||
connect_async,
|
||||
tungstenite::{
|
||||
self,
|
||||
protocol::{frame::coding::CloseCode, CloseFrame},
|
||||
ClientRequestBuilder,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{state_machine::Event, BASE_URL};
|
||||
|
||||
pub async fn start(
|
||||
event_sender: Sender<Event>,
|
||||
mut event_receiver: Receiver<Event>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut writer = None;
|
||||
|
||||
let uri: tungstenite::http::Uri = format!("{}/ws", BASE_URL.replace("https", "wss")).parse()?;
|
||||
|
||||
loop {
|
||||
if let Ok(event) = &event_receiver.try_recv() {
|
||||
match event {
|
||||
Event::TokenReceived { token } => {
|
||||
let builder = ClientRequestBuilder::new(uri.clone())
|
||||
.with_header("Authorization", format!("Bearer {}", token));
|
||||
|
||||
let (socket, response) = connect_async(builder).await?;
|
||||
|
||||
if response.status() != StatusCode::SWITCHING_PROTOCOLS {
|
||||
tracing::error!("{:#?}", response);
|
||||
continue;
|
||||
}
|
||||
|
||||
let (write, mut read) = socket.split();
|
||||
writer = Some(write);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let message = match read.next().await {
|
||||
Some(data) => match data {
|
||||
Ok(message) => message,
|
||||
Err(e) => {
|
||||
tracing::error!("{:?}", e);
|
||||
return;
|
||||
}
|
||||
},
|
||||
None => return,
|
||||
};
|
||||
|
||||
let data = message.to_text();
|
||||
tracing::debug!("{:?}", data);
|
||||
});
|
||||
|
||||
tracing::info!("Connected");
|
||||
let _ = event_sender.send(Event::Connected);
|
||||
}
|
||||
Event::Logout => {
|
||||
if let Some(mut write) = writer {
|
||||
write
|
||||
.send(tungstenite::Message::Close(Some(CloseFrame {
|
||||
code: CloseCode::Normal,
|
||||
reason: Cow::from("User Logout"),
|
||||
})))
|
||||
.await?;
|
||||
writer = None;
|
||||
tracing::debug!("Disconnected");
|
||||
event_sender.send(Event::Disconnected)?;
|
||||
}
|
||||
}
|
||||
Event::Quit => {
|
||||
tracing::info!("Shutting down Client");
|
||||
if let Some(mut write) = writer {
|
||||
write
|
||||
.send(tungstenite::Message::Close(Some(CloseFrame {
|
||||
code: CloseCode::Normal,
|
||||
reason: Cow::from("Application Shutdown"),
|
||||
})))
|
||||
.await?;
|
||||
tracing::debug!("Disconnected");
|
||||
}
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
tracing::info!("Client Shutdown");
|
||||
Ok(())
|
||||
}
|
@@ -101,10 +101,6 @@ impl Config {
|
||||
pub fn code_verifier(&self) -> Option<CodeVerifier> {
|
||||
self.code_verifier.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn code_challenge_method(&self) -> Option<CodeChallengeMethod> {
|
||||
self.code_challenge_method.read().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
@@ -2,6 +2,7 @@
|
||||
#![allow(clippy::needless_return)]
|
||||
|
||||
mod app;
|
||||
mod client;
|
||||
mod config;
|
||||
mod dirs;
|
||||
mod icon;
|
||||
@@ -18,7 +19,8 @@ use oauth::{start_code_listener, start_code_to_token};
|
||||
use pipe::Pipe;
|
||||
use state_machine::Event;
|
||||
use tokio::task::JoinSet;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
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";
|
||||
@@ -42,7 +44,8 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
|
||||
init_logging()?;
|
||||
|
||||
let (event_sender, event_receiver) = tokio::sync::broadcast::channel(1);
|
||||
// let (socket_sender, socket_receiver) = tokio::sync::broadcast::channel(1);
|
||||
let (event_sender, event_receiver) = tokio::sync::broadcast::channel(10);
|
||||
let args = Arguments::parse();
|
||||
|
||||
if handle_single_instance(&args).await? {
|
||||
@@ -81,7 +84,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
|
||||
// // Start the code listener
|
||||
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));
|
||||
|
||||
// Start token listener
|
||||
@@ -90,6 +93,20 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
let receiver = event_receiver.resubscribe();
|
||||
futures.spawn(start_code_to_token(c, pipe_receiver, sender, receiver));
|
||||
|
||||
// Start the websocket client
|
||||
// The socket client will just sit there until TokenReceivedEvent comes in to authenticate with the socket server
|
||||
// The server needs to not accept any messages until the authentication is verified
|
||||
let sender = event_sender.clone();
|
||||
let receiver = event_receiver.resubscribe();
|
||||
futures.spawn(client::start(sender, receiver));
|
||||
|
||||
// We need 2 way channels (2 channels, both with tx/rx) to send data from the socket to simconnect and back
|
||||
|
||||
// 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
|
||||
|
||||
// Start the Tray Icon
|
||||
let c = config.clone();
|
||||
let sender = event_sender.clone();
|
||||
@@ -230,7 +247,9 @@ fn init_logging() -> Result<(), anyhow::Error> {
|
||||
#[cfg(not(debug_assertions))]
|
||||
let file = File::options().append(true).open(&log_file)?;
|
||||
|
||||
let fmt = tracing_subscriber::fmt::layer();
|
||||
let fmt = tracing_subscriber::fmt::layer().with_filter(tracing_subscriber::filter::filter_fn(
|
||||
|metadata| metadata.level() < &Level::TRACE,
|
||||
));
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
let fmt = fmt.with_ansi(false).with_writer(Arc::new(file));
|
||||
|
@@ -1,4 +1,5 @@
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
use tokio::{
|
||||
sync::broadcast::{Receiver, Sender},
|
||||
@@ -6,13 +7,30 @@ use tokio::{
|
||||
};
|
||||
|
||||
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(
|
||||
code_verifier: CodeVerifier,
|
||||
code_challenge_method: CodeChallengeMethod,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
#[derive(Debug, Error)]
|
||||
pub enum OpenBrowserError {
|
||||
#[error(transparent)]
|
||||
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 {
|
||||
CodeChallengeMethod::Plain => {
|
||||
use base64::prelude::*;
|
||||
@@ -84,8 +102,11 @@ pub async fn start_code_to_token(
|
||||
.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 { token })?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
29
avam-client/src/simconnect.rs
Normal file
29
avam-client/src/simconnect.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
pub struct Client {
|
||||
// whatever we need
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
// websocket receiver
|
||||
// websocket sender
|
||||
// simconnect client handle
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> Result<(), anyhow::Error> {
|
||||
loop {
|
||||
tokio::select! {
|
||||
// we can either get a message from the websocket to pass to simconnect
|
||||
|
||||
// we can get a message from simconnect to pass to the websocket
|
||||
|
||||
// or we get a quit event from the event channel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start() -> Result<(), anyhow::Error> {
|
||||
Client::new().run().await?
|
||||
}
|
@@ -1,4 +1,9 @@
|
||||
use tokio::sync::broadcast::{Receiver, Sender};
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::{
|
||||
sync::broadcast::{Receiver, Sender},
|
||||
time::sleep,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
@@ -6,117 +11,75 @@ use crate::{
|
||||
oauth,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum State {
|
||||
Init,
|
||||
AppStart {
|
||||
config: Config,
|
||||
},
|
||||
Authenticate {
|
||||
open_browser: bool,
|
||||
code_verifier: CodeVerifier,
|
||||
code_challenge_method: CodeChallengeMethod,
|
||||
},
|
||||
Connect {
|
||||
token: String,
|
||||
},
|
||||
AppStart,
|
||||
Authenticate,
|
||||
Connect { token: String },
|
||||
WaitForSim,
|
||||
InSim,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Event {
|
||||
Ready {
|
||||
config: Config,
|
||||
},
|
||||
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
|
||||
Ready,
|
||||
StartAuthenticate, // 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
|
||||
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
|
||||
SimDisconnected, // SimConnect is disconnected, we've finished the flight and exited back to the menu, transition back to WaitForSim
|
||||
Logout,
|
||||
Quit,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn next(self, event: Event) -> State {
|
||||
match (self, event) {
|
||||
match (self.clone(), event.clone()) {
|
||||
// (Current State, SomeEvent) => NextState
|
||||
(_, Event::Ready { config }) => State::AppStart { config },
|
||||
(
|
||||
State::AppStart { .. },
|
||||
Event::StartAuthenticate {
|
||||
open_browser,
|
||||
code_verifier,
|
||||
code_challenge_method,
|
||||
},
|
||||
) => Self::Authenticate {
|
||||
open_browser,
|
||||
code_verifier,
|
||||
code_challenge_method,
|
||||
}, // Goto Authenticate
|
||||
(_, Event::Ready) => State::AppStart,
|
||||
(_, Event::Logout) => State::AppStart,
|
||||
(_, Event::StartAuthenticate) => Self::Authenticate, // Goto Authenticate
|
||||
|
||||
(State::AppStart { .. }, Event::TokenReceived { token }) => State::Connect { token },
|
||||
(State::Authenticate { .. }, Event::TokenReceived { token }) => {
|
||||
State::Connect { token }
|
||||
}
|
||||
(_, Event::TokenReceived { token }) => State::Connect { token },
|
||||
|
||||
(State::Connect { .. }, Event::Connected) => todo!(), // Goto WaitForSim
|
||||
(_, Event::Connected) => State::WaitForSim, // Goto WaitForSim
|
||||
|
||||
(State::WaitForSim, Event::SimConnected) => todo!(), // Goto InSim
|
||||
(_, Event::SimConnected) => todo!(), // Goto InSim
|
||||
|
||||
(_, Event::Disconnected) => todo!(), // Goto Connect
|
||||
(State::InSim, Event::SimDisconnected) => todo!(), // Goto WaitForSim
|
||||
(_, Event::Disconnected) => State::AppStart, // Goto Connect
|
||||
(_, Event::SimDisconnected) => State::WaitForSim, // 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 {
|
||||
State::Init => Ok(()),
|
||||
State::AppStart { config } => {
|
||||
State::AppStart => {
|
||||
if let Some(token) = config.token() {
|
||||
signal.send(Event::TokenReceived {
|
||||
token: token.to_string(),
|
||||
})?;
|
||||
} else {
|
||||
let open_browser = config.open_browser();
|
||||
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,
|
||||
})?;
|
||||
signal.send(Event::StartAuthenticate)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
State::Authenticate {
|
||||
open_browser,
|
||||
code_verifier,
|
||||
code_challenge_method,
|
||||
} => {
|
||||
if *open_browser {
|
||||
oauth::open_browser(code_verifier.clone(), code_challenge_method.clone())?;
|
||||
State::Authenticate => {
|
||||
if config.open_browser() {
|
||||
oauth::open_browser(config.clone())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
State::Connect { .. } => Ok(()),
|
||||
State::WaitForSim => Ok(()),
|
||||
State::WaitForSim => {
|
||||
tracing::info!("Waiting for sim!");
|
||||
Ok(())
|
||||
}
|
||||
State::InSim => Ok(()),
|
||||
}
|
||||
}
|
||||
@@ -129,25 +92,20 @@ pub async fn start(
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut state = State::Init;
|
||||
|
||||
state.run(event_sender.clone()).await?;
|
||||
state.run(event_sender.clone(), config.clone()).await?;
|
||||
|
||||
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 {
|
||||
tracing::info!("Shutting down State Machine");
|
||||
break;
|
||||
}
|
||||
|
||||
state = state.next(event).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?;
|
||||
}
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
}
|
||||
|
||||
tracing::info!("State Machine Shutdown");
|
||||
|
Reference in New Issue
Block a user