websocket

This commit is contained in:
2024-10-18 18:20:44 +02:00
parent ea88c755b5
commit 5e651b382d
19 changed files with 654 additions and 131 deletions

View File

@@ -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"] }

View File

@@ -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
View 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(())
}

View File

@@ -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 {

View File

@@ -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));

View 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 })?;
}
}
}

View 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?
}

View File

@@ -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");