avam-client and oauth2
This commit is contained in:
144
avam-client/src/app.rs
Normal file
144
avam-client/src/app.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use tokio::sync::broadcast::{Receiver, Sender};
|
||||
use tray_icon::menu::{MenuId, MenuItem};
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
event::StartCause,
|
||||
event_loop::{ActiveEventLoop, ControlFlow},
|
||||
};
|
||||
|
||||
use crate::{config::Config, icon::TrayIcon, oauth, state_machine::Event, BASE_URL};
|
||||
|
||||
pub struct App {
|
||||
config: Config,
|
||||
tray_icon: TrayIcon,
|
||||
sender: Sender<Event>,
|
||||
receiver: Receiver<Event>,
|
||||
items: HashMap<&'static str, MenuId>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(
|
||||
config: Config,
|
||||
sender: Sender<Event>,
|
||||
receiver: Receiver<Event>,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let mut tray_icon = TrayIcon::new(load_icon(), crate::PROJECT_NAME);
|
||||
|
||||
let login = MenuItem::new("Login", true, None);
|
||||
let forget = MenuItem::new("Logout and Exit", false, None);
|
||||
let quit = MenuItem::new("Exit", true, None);
|
||||
|
||||
let mut items = HashMap::new();
|
||||
|
||||
let c = config.clone();
|
||||
let login_id = tray_icon.add_menu_item(&login, move |item| {
|
||||
// ..
|
||||
let item = item.as_menuitem().unwrap();
|
||||
if item.text() != "Login" {
|
||||
open::that(BASE_URL)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
oauth::open_browser(
|
||||
c.code_verifier().unwrap(),
|
||||
c.code_challenge_method().unwrap(),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
tray_icon.add_seperator()?;
|
||||
|
||||
let c = config.clone();
|
||||
let s = sender.clone();
|
||||
let forget_id = tray_icon.add_menu_item(&forget, move |_| {
|
||||
c.set_token(None)?;
|
||||
s.send(Event::Quit)?;
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let s = sender.clone();
|
||||
tray_icon.add_menu_item(&quit, move |_| {
|
||||
s.send(Event::Quit)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
items.insert("login", login_id);
|
||||
items.insert("forget", forget_id);
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
tray_icon,
|
||||
sender,
|
||||
receiver,
|
||||
items,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for App {
|
||||
fn new_events(&mut self, event_loop: &ActiveEventLoop, _: StartCause) {
|
||||
event_loop.set_control_flow(ControlFlow::WaitUntil(
|
||||
std::time::Instant::now() + std::time::Duration::from_millis(16),
|
||||
));
|
||||
|
||||
if let Ok(event) = tray_icon::menu::MenuEvent::receiver().try_recv() {
|
||||
self.tray_icon.handle(event.id());
|
||||
}
|
||||
|
||||
if let Ok(event) = self.receiver.try_recv() {
|
||||
match event {
|
||||
Event::Quit => {
|
||||
println!("Shutting down EventLoop");
|
||||
event_loop.exit()
|
||||
}
|
||||
Event::TokenReceived { .. } => {
|
||||
self.tray_icon
|
||||
.set_text(self.items.get("login").unwrap(), "Open Avam")
|
||||
.unwrap();
|
||||
|
||||
self.tray_icon
|
||||
.set_enabled(self.items.get("forget").unwrap(), true)
|
||||
.unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resumed(&mut self, _: &winit::event_loop::ActiveEventLoop) {
|
||||
let _ = self.tray_icon.build();
|
||||
|
||||
let _ = self.sender.send(Event::Ready {
|
||||
config: self.config.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
_: &winit::event_loop::ActiveEventLoop,
|
||||
_: winit::window::WindowId,
|
||||
_: winit::event::WindowEvent,
|
||||
) {
|
||||
// We don't have a window
|
||||
}
|
||||
}
|
||||
|
||||
fn load_icon() -> tray_icon::Icon {
|
||||
let icon = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../public/favicon-32x32.png"
|
||||
));
|
||||
let (icon_rgba, icon_width, icon_height) = {
|
||||
let image = image::load_from_memory(icon)
|
||||
.expect("Failed to open icon path")
|
||||
.into_rgba8();
|
||||
let (width, height) = image.dimensions();
|
||||
let rgba = image.into_raw();
|
||||
(rgba, width, height)
|
||||
};
|
||||
tray_icon::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
|
||||
}
|
125
avam-client/src/config.rs
Normal file
125
avam-client/src/config.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use std::{
|
||||
io::Write,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
dirs::{Dirs, DirsError},
|
||||
models::{CodeChallengeMethod, CodeVerifier},
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(with = "arc_rwlock_serde")]
|
||||
token: Arc<RwLock<Option<String>>>,
|
||||
#[serde(skip)]
|
||||
code_verifier: Arc<RwLock<Option<CodeVerifier>>>,
|
||||
#[serde(skip)]
|
||||
code_challenge_method: Arc<RwLock<Option<CodeChallengeMethod>>>,
|
||||
}
|
||||
|
||||
impl PartialEq for Config {
|
||||
fn eq(&self, _: &Self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Dirs(#[from] DirsError),
|
||||
#[error(transparent)]
|
||||
Toml(#[from] toml::ser::Error),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn set_token(&self, token: Option<String>) -> Result<(), ConfigError> {
|
||||
*self.token.write().unwrap() = token;
|
||||
self.write()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn token(&self) -> Option<String> {
|
||||
self.token.read().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn set_code_verifier(
|
||||
&self,
|
||||
code_verifier: Option<CodeVerifier>,
|
||||
) -> Result<(), ConfigError> {
|
||||
*self.code_verifier.write().unwrap() = code_verifier;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_code_challenge_method(
|
||||
&self,
|
||||
code_challenge_method: Option<CodeChallengeMethod>,
|
||||
) -> Result<(), ConfigError> {
|
||||
*self.code_challenge_method.write().unwrap() = code_challenge_method;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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 {
|
||||
pub(crate) fn new() -> Result<Self, ConfigError> {
|
||||
let config = config::Config::builder()
|
||||
.add_source(
|
||||
config::File::with_name(Dirs::get_config_file()?.to_str().unwrap()).required(false),
|
||||
)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let config: Self = config.try_deserialize().unwrap_or_default();
|
||||
|
||||
config.write()?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn write(&self) -> Result<(), ConfigError> {
|
||||
let toml = toml::to_string_pretty(self)?;
|
||||
let mut file = std::fs::File::create(&Dirs::get_config_file()?)?;
|
||||
file.write_all(toml.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
mod arc_rwlock_serde {
|
||||
use serde::de::Deserializer;
|
||||
use serde::ser::Serializer;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
pub fn serialize<S, T>(val: &Arc<RwLock<T>>, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
T: Serialize,
|
||||
{
|
||||
T::serialize(&*val.read().unwrap(), s)
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D, T>(d: D) -> Result<Arc<RwLock<T>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
T: Deserialize<'de>,
|
||||
{
|
||||
Ok(Arc::new(RwLock::new(T::deserialize(d)?)))
|
||||
}
|
||||
}
|
43
avam-client/src/dirs.rs
Normal file
43
avam-client/src/dirs.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use directories::ProjectDirs;
|
||||
use thiserror::Error;
|
||||
|
||||
pub struct Dirs {}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DirsError {
|
||||
#[error("Unable to get Project Directories")]
|
||||
ProjectDirs,
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl Dirs {
|
||||
pub fn get_config_dir() -> Result<PathBuf, DirsError> {
|
||||
let Some(proj_dirs) = ProjectDirs::from("nl", "Avii", "Avam") else {
|
||||
return Err(DirsError::ProjectDirs);
|
||||
};
|
||||
|
||||
let config_dir = proj_dirs.config_local_dir();
|
||||
|
||||
if !config_dir.exists() {
|
||||
std::fs::create_dir_all(config_dir)?;
|
||||
}
|
||||
Ok(config_dir.to_path_buf())
|
||||
}
|
||||
|
||||
pub fn get_config_file() -> Result<PathBuf, DirsError> {
|
||||
let mut c = Self::get_config_dir()?;
|
||||
c.push("config.toml");
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
pub fn get_lock_file() -> Result<PathBuf, DirsError> {
|
||||
let mut c = Self::get_config_dir()?;
|
||||
c.push(".lock");
|
||||
Ok(c)
|
||||
}
|
||||
}
|
117
avam-client/src/icon.rs
Normal file
117
avam-client/src/icon.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use tray_icon::{
|
||||
menu::{IsMenuItem, Menu, MenuId, MenuItemKind, PredefinedMenuItem},
|
||||
TrayIconBuilder,
|
||||
};
|
||||
|
||||
// The app winit thing can listen in on the same events
|
||||
// coming from the state machine to give feedback to the user
|
||||
// changing the icon, the alt texts, and the context menu
|
||||
// based on the last received event
|
||||
|
||||
type Callback = Box<dyn Fn(&MenuItemKind) -> Result<(), anyhow::Error> + 'static>;
|
||||
|
||||
pub struct TrayIcon {
|
||||
icon: tray_icon::Icon,
|
||||
title: String,
|
||||
menu_items: HashMap<MenuId, Callback>,
|
||||
menu: Menu,
|
||||
tray_icon: Option<tray_icon::TrayIcon>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl TrayIcon {
|
||||
pub fn new(icon: tray_icon::Icon, title: &str) -> Self {
|
||||
Self {
|
||||
icon,
|
||||
title: title.to_string(),
|
||||
menu_items: HashMap::new(),
|
||||
menu: Menu::new(),
|
||||
tray_icon: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_seperator(&mut self) -> Result<(), anyhow::Error> {
|
||||
self.menu.append(&PredefinedMenuItem::separator())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_menu_item<F>(
|
||||
&mut self,
|
||||
item: &dyn IsMenuItem,
|
||||
callback: F,
|
||||
) -> Result<MenuId, anyhow::Error>
|
||||
where
|
||||
F: Fn(&MenuItemKind) -> Result<(), anyhow::Error> + 'static,
|
||||
{
|
||||
let id = item.id().clone();
|
||||
self.menu.append(item)?;
|
||||
|
||||
self.menu_items
|
||||
.insert(id.clone(), Box::new(callback) as Callback);
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn set_enabled(&self, id: &MenuId, enabled: bool) -> Result<(), anyhow::Error> {
|
||||
if let Some(item) = self.menu.items().iter().find(|i| i.id() == id) {
|
||||
if let Some(menuitem) = item.as_menuitem() {
|
||||
menuitem.set_enabled(enabled);
|
||||
}
|
||||
if let Some(menuitem) = item.as_check_menuitem() {
|
||||
menuitem.set_enabled(enabled);
|
||||
}
|
||||
if let Some(menuitem) = item.as_icon_menuitem() {
|
||||
menuitem.set_enabled(enabled);
|
||||
}
|
||||
if let Some(menuitem) = item.as_submenu() {
|
||||
menuitem.set_enabled(enabled);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_text(&self, id: &MenuId, text: &str) -> Result<(), anyhow::Error> {
|
||||
if let Some(item) = self.menu.items().iter().find(|i| i.id() == id) {
|
||||
if let Some(menuitem) = item.as_menuitem() {
|
||||
menuitem.set_text(text);
|
||||
}
|
||||
if let Some(menuitem) = item.as_check_menuitem() {
|
||||
menuitem.set_text(text);
|
||||
}
|
||||
if let Some(menuitem) = item.as_icon_menuitem() {
|
||||
menuitem.set_text(text);
|
||||
}
|
||||
if let Some(menuitem) = item.as_predefined_menuitem() {
|
||||
menuitem.set_text(text);
|
||||
}
|
||||
if let Some(menuitem) = item.as_submenu() {
|
||||
menuitem.set_text(text);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle(&self, id: &MenuId) {
|
||||
if let Some(item) = self.menu_items.get(id) {
|
||||
if let Some(i) = self.menu.items().iter().find(|i| i.id() == id) {
|
||||
if let Err(e) = item(i) {
|
||||
eprintln!("{:#?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build(&mut self) -> Result<(), anyhow::Error> {
|
||||
self.tray_icon = Some(
|
||||
TrayIconBuilder::new()
|
||||
.with_tooltip(self.title.clone())
|
||||
.with_menu(Box::new(self.menu.clone()))
|
||||
.with_icon(self.icon.clone())
|
||||
.build()?,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
48
avam-client/src/lock.rs
Normal file
48
avam-client/src/lock.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use crate::dirs::{Dirs, DirsError};
|
||||
use thiserror::Error;
|
||||
|
||||
pub struct Lock {
|
||||
force: bool,
|
||||
lock_file: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LockError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Dirs(#[from] DirsError),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
impl Lock {
|
||||
pub fn new(force: bool) -> Result<Self, LockError> {
|
||||
let lock_file = Dirs::get_lock_file()?;
|
||||
|
||||
Ok(Self { lock_file, force })
|
||||
}
|
||||
|
||||
pub fn lock() {
|
||||
if let Ok(lock_file) = Dirs::get_lock_file() {
|
||||
if !fs::exists(&lock_file).unwrap() {
|
||||
fs::write(&lock_file, "").expect("there to not be issues with the filesystem");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unlock() {
|
||||
if let Ok(lock_file) = Dirs::get_lock_file() {
|
||||
if fs::exists(&lock_file).unwrap() {
|
||||
let _ = fs::remove_file(lock_file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_locked(&self) -> bool {
|
||||
!self.force
|
||||
&& fs::exists(&self.lock_file).expect("there to not be issues with the filesystem")
|
||||
}
|
||||
}
|
180
avam-client/src/main.rs
Normal file
180
avam-client/src/main.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
#![allow(clippy::needless_return)]
|
||||
|
||||
mod app;
|
||||
mod config;
|
||||
mod dirs;
|
||||
mod icon;
|
||||
mod lock;
|
||||
mod models;
|
||||
mod oauth;
|
||||
mod pipe;
|
||||
mod state_machine;
|
||||
|
||||
use crate::config::Config;
|
||||
use clap::Parser;
|
||||
use lock::Lock;
|
||||
use oauth::{start_code_listener, start_code_to_token};
|
||||
use pipe::Pipe;
|
||||
use state_machine::Event;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
pub static BASE_URL: &str = "https://avam.avii.nl";
|
||||
pub static PROJECT_NAME: &str = "Avii's Virtual Airline Manager";
|
||||
pub static COPYRIGHT: &str = "Avii's Virtual Airline Manager © 2024";
|
||||
pub static CLIENT_ID: uuid::Uuid = uuid::uuid!("f9525060-0a34-4233-87e2-0f9990b7c6db");
|
||||
pub static REDIRECT_URI: &str = "avam:token";
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Arguments {
|
||||
#[arg(short, long)]
|
||||
code: Option<String>,
|
||||
#[arg(short, long, action)]
|
||||
force: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
let (event_sender, event_receiver) = tokio::sync::broadcast::channel(1);
|
||||
let args = Arguments::parse();
|
||||
|
||||
if handle_single_instance(&args).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
register_url_scheme()?;
|
||||
|
||||
let config = Config::new()?;
|
||||
|
||||
let mut futures = JoinSet::new();
|
||||
|
||||
// Register Quit handler
|
||||
let sender = event_sender.clone();
|
||||
let mut ctrl_c_counter = 0;
|
||||
ctrlc::set_handler(move || {
|
||||
println!("CTRL_C: Quit singal sent");
|
||||
ctrl_c_counter += 1;
|
||||
if ctrl_c_counter >= 3 {
|
||||
let _ = unregister_url_scheme();
|
||||
Lock::unlock();
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if let Err(e) = sender.send(Event::Quit) {
|
||||
println!("{:#?}", e)
|
||||
};
|
||||
})?;
|
||||
|
||||
// Start the State Machine
|
||||
let c = config.clone();
|
||||
let sender = event_sender.clone();
|
||||
let receiver = event_receiver.resubscribe();
|
||||
futures.spawn(state_machine::start(c, sender, receiver));
|
||||
|
||||
// // Start the code listener
|
||||
let receiver = event_receiver.resubscribe();
|
||||
let (pipe_sender, pipe_receiver) = tokio::sync::broadcast::channel(100);
|
||||
futures.spawn(start_code_listener(pipe_sender, receiver));
|
||||
|
||||
// Start token listener
|
||||
let c = config.clone();
|
||||
let sender = event_sender.clone();
|
||||
let receiver = event_receiver.resubscribe();
|
||||
futures.spawn(start_code_to_token(c, pipe_receiver, sender, receiver));
|
||||
|
||||
// Start the Tray Icon
|
||||
let c = config.clone();
|
||||
let sender = event_sender.clone();
|
||||
let receiver = event_receiver.resubscribe();
|
||||
start_tray_icon(c, sender, receiver).await?;
|
||||
|
||||
// Wait for everything to finish
|
||||
while let Some(result) = futures.join_next().await {
|
||||
if let Ok(Err(e)) = result {
|
||||
panic!("{:#?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
unregister_url_scheme()?;
|
||||
Lock::unlock();
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
/// returns `Ok(true)` if we need to quit gracefully
|
||||
/// returns `Err(e)` if we're already running
|
||||
async fn handle_single_instance(args: &Arguments) -> Result<bool, anyhow::Error> {
|
||||
let lock = Lock::new(args.force)?;
|
||||
|
||||
if lock.is_locked() && args.code.is_none() {
|
||||
return Err(anyhow::anyhow!("Lockfile exists, exiting."));
|
||||
}
|
||||
|
||||
if let Some(code) = &args.code {
|
||||
Pipe::send(code).await?;
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
Lock::lock();
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn register_url_scheme() -> Result<(), anyhow::Error> {
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let avam_schema_root = hkcu.create_subkey("Software\\Classes\\avam")?;
|
||||
avam_schema_root.0.set_value("URL Protocol", &"")?;
|
||||
let command = avam_schema_root.0.create_subkey("shell\\open\\command")?;
|
||||
|
||||
let current_exec = std::env::current_exe()?;
|
||||
|
||||
command.0.set_value(
|
||||
"",
|
||||
&format!("\"{}\" -c \"%1\"", current_exec.to_str().unwrap()),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unregister_url_scheme() -> Result<(), anyhow::Error> {
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
hkcu.delete_subkey_all("Software\\Classes\\avam").ok();
|
||||
|
||||
Ok(())
|
||||
}
|
300
avam-client/src/models.rs
Normal file
300
avam-client/src/models.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_more::{derive::From, Display};
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::REDIRECT_URI;
|
||||
|
||||
#[derive(
|
||||
Clone, Debug, Display, From, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
pub struct RedirectUri(String);
|
||||
|
||||
impl RedirectUri {
|
||||
pub fn new(uri: &str) -> Self {
|
||||
Self(uri.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for RedirectUri {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Display, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[serde(from = "String")]
|
||||
pub struct CodeVerifier(String);
|
||||
|
||||
impl From<String> for CodeVerifier {
|
||||
fn from(value: String) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for CodeVerifier {
|
||||
fn from(value: &str) -> Self {
|
||||
Self(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeVerifier {
|
||||
pub fn new() -> Self {
|
||||
let token: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(120)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
|
||||
Self(token)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CodeVerifier {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CodeChallengeMethodError {
|
||||
#[error("Code challenge method is not valid.")]
|
||||
Invalid,
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
// to be extended as new error scenarios are introduced
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub enum CodeChallengeMethod {
|
||||
#[serde(rename = "plain")]
|
||||
Plain,
|
||||
#[serde(rename = "S256")]
|
||||
Sha256,
|
||||
}
|
||||
|
||||
impl FromStr for CodeChallengeMethod {
|
||||
type Err = CodeChallengeMethodError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"plain" => Ok(Self::Plain),
|
||||
"S256" => Ok(Self::Sha256),
|
||||
_ => Err(CodeChallengeMethodError::Invalid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ResponseTypeError {
|
||||
#[error("The response type is not valid.")]
|
||||
Invalid,
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
// to be extended as new error scenarios are introduced
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub enum ResponseType {
|
||||
#[serde(rename = "code")]
|
||||
Code,
|
||||
}
|
||||
|
||||
impl Display for ResponseType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
ResponseType::Code => "code",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ResponseType {
|
||||
type Err = ResponseTypeError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"code" => Ok(Self::Code),
|
||||
_ => Err(ResponseTypeError::Invalid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum GrantTypeError {
|
||||
#[error("The grant type is not valid.")]
|
||||
Invalid,
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
// to be extended as new error scenarios are introduced
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub enum GrantType {
|
||||
#[serde(rename = "authorization_code")]
|
||||
AuthorizationCode,
|
||||
}
|
||||
|
||||
impl Display for GrantType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
GrantType::AuthorizationCode => "authorization_code",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for GrantType {
|
||||
type Err = GrantTypeError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"authorization_code" => Ok(Self::AuthorizationCode),
|
||||
_ => Err(GrantTypeError::Invalid),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AuthorizeRequest {
|
||||
client_id: uuid::Uuid,
|
||||
response_type: ResponseType, // Make type (enum:code,)
|
||||
state: Option<String>, // random string for CSRF protection
|
||||
code_challenge: String, // pkce
|
||||
code_challenge_method: Option<CodeChallengeMethod>, // Make type (enum:sha256,) hashing algo
|
||||
redirect_uri: RedirectUri, // Make type
|
||||
scope: Option<String>, // space seperated string with permissions
|
||||
}
|
||||
|
||||
impl AuthorizeRequest {
|
||||
pub fn new(
|
||||
client_id: uuid::Uuid,
|
||||
response_type: ResponseType,
|
||||
state: Option<String>,
|
||||
code_challenge: String,
|
||||
code_challenge_method: Option<CodeChallengeMethod>,
|
||||
redirect_uri: RedirectUri,
|
||||
scope: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
client_id,
|
||||
response_type,
|
||||
state,
|
||||
code_challenge,
|
||||
code_challenge_method,
|
||||
redirect_uri,
|
||||
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)]
|
||||
pub struct AuthorizationResponse {
|
||||
code: String,
|
||||
state: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<String> for AuthorizationResponse {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
let mut parts = s.split('?');
|
||||
let Some(protocol) = parts.next() else {
|
||||
return Err(anyhow::anyhow!("Invalid AuthorizationResponse"));
|
||||
};
|
||||
|
||||
if protocol != REDIRECT_URI {
|
||||
return Err(anyhow::anyhow!("Invalid AuthorizationResponse"));
|
||||
}
|
||||
|
||||
let Some(qs) = parts.next() else {
|
||||
return Err(anyhow::anyhow!("Invalid AuthorizationResponse"));
|
||||
};
|
||||
|
||||
if parts.count() > 0 {
|
||||
return Err(anyhow::anyhow!("Invalid AuthorizationResponse"));
|
||||
}
|
||||
|
||||
Ok(serde_qs::from_str(qs)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthorizationResponse {
|
||||
pub fn code(&self) -> String {
|
||||
self.code.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AuthorizationCodeRequest {
|
||||
grant_type: GrantType,
|
||||
code: String,
|
||||
redirect_uri: RedirectUri,
|
||||
client_id: uuid::Uuid,
|
||||
code_verifier: CodeVerifier,
|
||||
}
|
||||
|
||||
impl AuthorizationCodeRequest {
|
||||
pub fn new(
|
||||
grant_type: GrantType,
|
||||
code: String,
|
||||
redirect_uri: RedirectUri,
|
||||
client_id: uuid::Uuid,
|
||||
code_verifier: CodeVerifier,
|
||||
) -> Self {
|
||||
Self {
|
||||
grant_type,
|
||||
code,
|
||||
redirect_uri,
|
||||
client_id,
|
||||
code_verifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AuthorizationCodeResponse {
|
||||
token: String,
|
||||
}
|
||||
|
||||
impl AuthorizationCodeResponse {
|
||||
pub fn token(&self) -> String {
|
||||
self.token.clone()
|
||||
}
|
||||
}
|
95
avam-client/src/oauth.rs
Normal file
95
avam-client/src/oauth.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::{
|
||||
sync::broadcast::{Receiver, Sender},
|
||||
time::sleep,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
config::Config, 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> {
|
||||
let code_challenge = match code_challenge_method {
|
||||
CodeChallengeMethod::Plain => {
|
||||
use base64::prelude::*;
|
||||
BASE64_URL_SAFE_NO_PAD.encode(code_verifier.to_string())
|
||||
}
|
||||
CodeChallengeMethod::Sha256 => {
|
||||
use base64::prelude::*;
|
||||
BASE64_URL_SAFE_NO_PAD.encode(sha256::digest(code_verifier.to_string()))
|
||||
}
|
||||
};
|
||||
|
||||
let request = AuthorizeRequest::new(
|
||||
CLIENT_ID,
|
||||
ResponseType::Code,
|
||||
None,
|
||||
code_challenge.clone(),
|
||||
Some(code_challenge_method.clone()),
|
||||
RedirectUri::new(REDIRECT_URI),
|
||||
None,
|
||||
);
|
||||
|
||||
let qs = serde_qs::to_string(&request)?;
|
||||
|
||||
open::that(format!("{}/oauth2/authorize?{}", BASE_URL, qs))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start_code_listener(
|
||||
pipe_sender: Sender<AuthorizationResponse>,
|
||||
event_receiver: Receiver<Event>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let pipe = Pipe::new(event_receiver.resubscribe());
|
||||
pipe.listen(pipe_sender).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn start_code_to_token(
|
||||
config: Config,
|
||||
mut pipe_receiver: Receiver<AuthorizationResponse>,
|
||||
event_sender: Sender<Event>,
|
||||
mut event_receiver: Receiver<Event>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = sleep(Duration::from_millis(100)) => {
|
||||
if let Ok(Event::Quit) = event_receiver.try_recv() {
|
||||
println!("Shutting down Code Transformer");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(response) = pipe_receiver.recv() => {
|
||||
let r = AuthorizationCodeRequest::new(
|
||||
GrantType::AuthorizationCode,
|
||||
response.code(),
|
||||
REDIRECT_URI.into(),
|
||||
CLIENT_ID,
|
||||
config.code_verifier().unwrap()
|
||||
);
|
||||
|
||||
let qs = serde_qs::to_string(&r)?;
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let response = client
|
||||
.post(format!("{}/oauth2/token", BASE_URL))
|
||||
.body(qs)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let response: AuthorizationCodeResponse = response.json().await?;
|
||||
|
||||
event_sender.send(Event::TokenReceived { token: response.token() })?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Code Transformer Shutdown");
|
||||
Ok(())
|
||||
}
|
97
avam-client/src/pipe.rs
Normal file
97
avam-client/src/pipe.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
use interprocess::os::windows::named_pipe::{pipe_mode, tokio::*, PipeListenerOptions};
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::broadcast::{Receiver, Sender};
|
||||
use tokio::time::sleep;
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
select,
|
||||
};
|
||||
|
||||
use crate::models::AuthorizationResponse;
|
||||
use crate::state_machine::Event;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PipeError {
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Unknown(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
pub struct Pipe {
|
||||
quit_signal: Receiver<Event>,
|
||||
}
|
||||
|
||||
const PIPE_NAME: &str = "AvamICP";
|
||||
|
||||
impl Pipe {
|
||||
pub fn new(quit_signal: Receiver<Event>) -> Self {
|
||||
Self { quit_signal }
|
||||
}
|
||||
|
||||
pub async fn send(msg: &str) -> Result<(), PipeError> {
|
||||
let conn = DuplexPipeStream::<pipe_mode::Bytes>::connect_by_path(format!(
|
||||
r"\\.\pipe\{}",
|
||||
PIPE_NAME
|
||||
))
|
||||
.await?;
|
||||
let (_, mut sender) = conn.split();
|
||||
|
||||
sender.write_all(msg.as_bytes()).await?;
|
||||
sender.shutdown().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn listen(
|
||||
&self,
|
||||
pipe_sender: Sender<AuthorizationResponse>,
|
||||
) -> Result<(), PipeError> {
|
||||
// todo: give state so we can gracefully shutdown the pipe
|
||||
let mut quit_signal = self.quit_signal.resubscribe();
|
||||
let listener = PipeListenerOptions::new()
|
||||
.path(Path::new(&format!(r"\\.\pipe\{}", PIPE_NAME)))
|
||||
.create_tokio_duplex::<pipe_mode::Bytes>()?;
|
||||
|
||||
loop {
|
||||
select! {
|
||||
Ok(conn) = listener.accept() => {
|
||||
let new_sender = pipe_sender.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = Self::handle_conn(conn, new_sender).await {
|
||||
eprintln!("error while handling connection: {e}");
|
||||
}
|
||||
});
|
||||
},
|
||||
_ = sleep(Duration::from_millis(100)) => {
|
||||
if let Ok(Event::Quit) = quit_signal.try_recv() {
|
||||
println!("Shutting down Code Listener");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("Code Listener Shutdown");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_conn(
|
||||
conn: DuplexPipeStream<pipe_mode::Bytes>,
|
||||
pipe_sender: Sender<AuthorizationResponse>,
|
||||
) -> Result<(), PipeError> {
|
||||
let (mut recver, _) = conn.split();
|
||||
|
||||
let mut buffer = Vec::with_capacity(512);
|
||||
|
||||
recver.read_buf(&mut buffer).await?;
|
||||
|
||||
let as_string = String::from_utf8_lossy(&buffer).to_string();
|
||||
let response: AuthorizationResponse = as_string.try_into()?;
|
||||
|
||||
let _ = pipe_sender.send(response);
|
||||
Ok(())
|
||||
}
|
||||
}
|
149
avam-client/src/state_machine.rs
Normal file
149
avam-client/src/state_machine.rs
Normal file
@@ -0,0 +1,149 @@
|
||||
use tokio::sync::broadcast::{Receiver, Sender};
|
||||
|
||||
use crate::{
|
||||
config::Config,
|
||||
models::{CodeChallengeMethod, CodeVerifier},
|
||||
oauth,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum State {
|
||||
Init,
|
||||
AppStart {
|
||||
config: Config,
|
||||
},
|
||||
Authenticate {
|
||||
code_verifier: CodeVerifier,
|
||||
code_challenge_method: CodeChallengeMethod,
|
||||
},
|
||||
Connect {
|
||||
token: String,
|
||||
},
|
||||
WaitForSim,
|
||||
Running,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Event {
|
||||
Ready {
|
||||
config: Config,
|
||||
},
|
||||
StartAuthenticate {
|
||||
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
|
||||
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
|
||||
Quit,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub async fn next(self, event: Event) -> State {
|
||||
match (self, event) {
|
||||
// (Current State, SomeEvent) => NextState
|
||||
(State::Init, Event::Ready { config }) => State::AppStart { config },
|
||||
(
|
||||
State::AppStart { .. },
|
||||
Event::StartAuthenticate {
|
||||
code_verifier: code_challenge,
|
||||
code_challenge_method,
|
||||
},
|
||||
) => Self::Authenticate {
|
||||
code_verifier: code_challenge,
|
||||
code_challenge_method,
|
||||
}, // Goto Authenticate
|
||||
|
||||
(State::AppStart { .. }, Event::TokenReceived { token }) => State::Connect { token },
|
||||
(State::Authenticate { .. }, Event::TokenReceived { token }) => {
|
||||
State::Connect { token }
|
||||
}
|
||||
|
||||
(State::Connect { .. }, Event::Connected) => todo!(), // Goto WaitForSim
|
||||
|
||||
(State::WaitForSim, Event::SimConnected) => todo!(), // Goto Running
|
||||
|
||||
(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> {
|
||||
match self {
|
||||
State::Init => Ok(()),
|
||||
State::AppStart { config } => {
|
||||
if let Some(token) = config.token() {
|
||||
signal.send(Event::TokenReceived {
|
||||
token: token.to_string(),
|
||||
})?;
|
||||
} else {
|
||||
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 {
|
||||
code_verifier,
|
||||
code_challenge_method,
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
State::Authenticate {
|
||||
code_verifier,
|
||||
code_challenge_method,
|
||||
} => {
|
||||
oauth::open_browser(code_verifier.clone(), code_challenge_method.clone())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
State::Connect { token } => {
|
||||
println!("Holyshit we've got a token: {}", token);
|
||||
Ok(())
|
||||
}
|
||||
State::WaitForSim => Ok(()),
|
||||
State::Running => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(
|
||||
config: Config,
|
||||
event_sender: Sender<Event>,
|
||||
mut event_receiver: Receiver<Event>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let mut state = State::Init;
|
||||
|
||||
state.run(event_sender.clone()).await?;
|
||||
|
||||
loop {
|
||||
if let Ok(event) = event_receiver.recv().await {
|
||||
if event == Event::Quit {
|
||||
println!("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?;
|
||||
}
|
||||
}
|
||||
|
||||
println!("State Machine Shutdown");
|
||||
Ok(())
|
||||
}
|
Reference in New Issue
Block a user