Cleanup & Fixes

This commit is contained in:
Bommels05 2025-02-05 18:36:16 +01:00
parent 340b8db1b9
commit 68de25838c
6 changed files with 510 additions and 765 deletions

View File

@ -1,4 +1,5 @@
[workspace] [workspace]
resolver = "3"
members = [ "bankcli","banklib"] members = [ "bankcli","banklib"]
[workspace.package] [workspace.package]

View File

@ -17,6 +17,7 @@ clap.workspace = true
url.workspace = true url.workspace = true
lalrpop-util = "0.22.1" lalrpop-util = "0.22.1"
error-stack.workspace = true error-stack.workspace = true
flume.workspace = true
[build-dependencies] [build-dependencies]
lalrpop = "0.22.1" lalrpop = "0.22.1"

View File

@ -1,21 +1,18 @@
use std::future::pending; use banklib::config::{load_config, save_config};
use banklib::{AuthenticatedMessage, BankClient, BankError, BankResult, Response}; use banklib::extended::{Client, Credentials, State};
use banklib::{AuthenticatedMessage, BankClient, BankError, BankResult, ClientAction, Response, ResponseMessage};
use clap::Parser; use clap::Parser;
use std::ops::{Add, Deref};
use std::time::Duration;
use std::io;
use std::io::BufRead;
use std::thread::yield_now;
use error_stack::{report, Report}; use error_stack::{report, Report};
use tokio::io::BufReader; use flume::Receiver;
use std::io::stdin;
use std::ops::Add;
use std::time::Duration;
use std::thread;
use tokio::select; use tokio::select;
use tokio::task::JoinHandle;
use tracing::metadata::LevelFilter; use tracing::metadata::LevelFilter;
use tracing::{error, info, warn}; use tracing::{error, info, warn};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use url::Url; use url::Url;
use banklib::config::{load_config, save_config};
use banklib::extended::{Client, Credentials, State};
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
struct Args { struct Args {
@ -62,7 +59,8 @@ async fn main() {
url = arg; url = arg;
} else { } else {
if !config.server.last_server.is_empty() && config.general.use_last_server { if !config.server.last_server.is_empty() && config.general.use_last_server {
url = Url::parse(&config.server.last_server).expect("Invalid last server url in config"); url =
Url::parse(&config.server.last_server).expect("Invalid last server url in config");
} else { } else {
error!("Cannot use last server url - You need to provide one using --url"); error!("Cannot use last server url - You need to provide one using --url");
return; return;
@ -70,19 +68,31 @@ async fn main() {
} }
let client = BankClient::connect(url.clone()).await; let client = BankClient::connect(url.clone()).await;
let mut client = Client { client, config, state: State::default() }; let mut client = Client {
client,
config,
state: State::default(),
};
client.config.server.last_server = url.to_string(); client.config.server.last_server = url.to_string();
save_config(&client.config); save_config(&client.config);
if client.config.general.use_default_account { if client.config.general.use_default_account {
let password = client.config.accounts.accounts.get(&client.config.accounts.default_account); let password = client
.config
.accounts
.accounts
.get(&client.config.accounts.default_account);
if let Some(password) = password { if let Some(password) = password {
let username = client.config.accounts.default_account.clone(); let username = client.config.accounts.default_account.clone();
let password = password.clone(); let password = password.clone();
try_login(&mut client, Credentials { password, username }).await; try_login(&mut client, Credentials { password, username }).await;
} }
} else if client.config.general.use_last_account { } else if client.config.general.use_last_account {
let password = client.config.accounts.accounts.get(&client.config.accounts.last_account); let password = client
.config
.accounts
.accounts
.get(&client.config.accounts.last_account);
if let Some(password) = password { if let Some(password) = password {
let username = client.config.accounts.last_account.clone(); let username = client.config.accounts.last_account.clone();
let password = password.clone(); let password = password.clone();
@ -90,382 +100,27 @@ async fn main() {
} }
} }
let lines = read_lines();
let messages = client.client.receiver_msg().clone();
let actions = client.client.receiver_act().clone();
for _ in actions.try_iter() {}
loop { loop {
let line = read_line(Some(&mut client)).await; select! {
let mut split = line.split(' '); Ok(line) = lines.recv_async() => {
let command: &str = split.next().unwrap(); if handle_line(&mut client, &lines, line).await {
break;
match command {
"account" => {
let argument = split.next().or(Some("")).unwrap();
if argument == "add" {
let credentials = expect_credentials(split, &mut client, false).await;
if let Some(credentials) = credentials {
client.config.accounts.accounts.insert(credentials.username.clone(), credentials.password.clone());
if client.config.accounts.default_account.is_empty() {
client.config.accounts.default_account = credentials.username.clone();
}
save_config(&client.config);
if ask_confirmation("Do you want to test the account now?").await {
let old_credentials = client.state.last_credentials.clone();
let success = try_login(&mut client, credentials.clone()).await;
if success {
setup_new_account(&mut client, credentials.clone(), old_credentials).await;
} else {
if ask_confirmation("The login was not successful - Do you want to register this account instead?").await {
let result = client.register(credentials.clone()).await;
if result.is_ok() {
let result = client.login(credentials.clone()).await;
if result.is_ok() {
setup_new_account(&mut client, credentials, old_credentials).await;
} else {
handle_error(&result);
warn!("The login was not successful but the account was most likely still registered - Please try to login again")
}
} else {
handle_error(&result);
}
}
}
}
} else {
println!("You need to enter a username and password");
}
} else if argument == "use" {
let username: &str = split.next().or(Some("")).unwrap();
if client.config.accounts.accounts.contains_key(username) {
let password = client.config.accounts.accounts.get(username).unwrap().clone();
try_login(&mut client, Credentials { username: username.into(), password }).await;
} else {
println!("You need to enter a valid username")
}
} else if argument == "list" {
println!("--- Accounts ---");
let i: u8 = 0;
for (name, _) in &client.config.accounts.accounts {
if client.config.accounts.default_account.eq(name) {
println!("{}. {} (Default)", i + 1, name);
} else {
println!("{}. {}", i + 1, name);
}
}
println!("--- Accounts ---");
} else if argument == "remove" {
let username: &str = split.next().or(Some("")).unwrap();
if client.config.accounts.accounts.contains_key(username) {
client.config.accounts.accounts.remove(username);
println!("Account removed");
} else {
println!("You need to enter a valid username")
}
} else if argument == "help" {
println!("--- Available Commands ---");
println!("account add <username> <password> - Add a new account");
println!("account use [username] - Login with the default or the specified account");
println!("account remove <username> - Remove the account from the list");
println!("account default <username> - Make the account the default account");
println!("account list - List available accounts");
} else {
println!("Invalid subcommand! Use account help for a list of valid subcommands");
} }
} },
"settings" => { Ok(message) = messages.recv_async() => {
let argument = split.next().or(Some("")).unwrap(); handle_message(&mut client, message).await;
if argument == "set" { },
let setting = split.next(); Ok(action) = actions.recv_async() => {
let value = split.next(); handle_action(&mut client, action).await;
if setting.is_some() && value.is_some() { },
{
match setting.unwrap() {
"show_motd_after_login" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.show_motd_after_login = value;
} else {
break;
}
}
"show_inbox_count_after_login" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.show_inbox_count_after_login = value;
} else {
break;
}
}
"show_pm_inbox_count_after_login" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.show_pm_inbox_count_after_login = value;
} else {
break;
}
}
"show_balance_after_payment" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.show_balance_after_payment = value;
} else {
break;
}
}
"use_default_account" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.use_default_account = value;
if value {
client.config.general.use_last_account = false;
}
} else {
break;
}
}
"use_last_account" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.use_last_account = value;
if value {
client.config.general.use_default_account = false;
}
} else {
break;
}
}
"use_last_server" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.use_last_server = value;
} else {
break;
}
}
_ => {
error!("Invalid setting")
}
}
info!("Setting changed");
save_config(&client.config);
}
} else {
println!("You need to enter a setting and value");
}
} else if argument == "list" {
println!("--- Settings ---");
println!(
"show_motd_after_login: {}",
client.config.general.show_motd_after_login
);
println!(
"show_inbox_count_after_login: {}",
client.config.general.show_inbox_count_after_login
);
println!(
"show_pm_inbox_count_after_login: {}",
client.config.general.show_pm_inbox_count_after_login
);
println!(
"show_balance_after_payment: {}",
client.config.general.show_balance_after_payment
);
println!(
"use_default_account: {}",
client.config.general.use_default_account
);
println!(
"use_last_account: {}",
client.config.general.use_last_account
);
println!(
"use_last_server: {}",
client.config.general.use_last_server
);
} else if argument == "help" {
println!("--- Available Commands ---");
println!(
"settings set <setting> <value> - Set a setting to the specified value"
);
println!("settings list - List all settings");
} else {
println!("Invalid subcommand! Use settings help for a list of valid subcommands");
}
}
"register" => {
let credentials = expect_credentials(split, &mut client, true).await;
if let Some(credentials) = credentials {
let result = client.register(credentials.clone()).await;
if let Some(last_credentials) = &client.state.last_credentials {
handle_error(&client.client.set_credentials(last_credentials.username.clone(), last_credentials.password.clone()));
}
if let Err(error) = result {
match error.current_context() {
BankError::AuthenticationError => {
error!("Username is already taken");
if ask_confirmation("Do you want to login instead?").await {
try_login(&mut client, credentials).await;
}
}
_ => print_error(&error)
}
} else {
if ask_confirmation("Do you want to login with your new account?").await {
try_login(&mut client, credentials).await;
}
}
}
}
"login" => {
let credentials = expect_credentials(split, &mut client, true).await;
if let Some(credentials) = credentials {
try_login(&mut client, credentials).await;
}
}
"balance" => {
let balance = client.get_balance().await;
if let Ok(balance) = balance {
info!("You currently have a balance of {}₿", balance);
} else {
handle_error(&balance);
}
}
"pay" => {
let destination = split.next();
let amount = split.next();
if destination.is_none() || amount.is_none() || amount.unwrap().parse::<u32>().is_err() || amount.unwrap().parse::<u32>().unwrap() <= 0 {
error!("You need to enter a destination username and amount greater than 0");
} else {
let result = client.pay(destination.unwrap().into(), amount.unwrap().parse::<u32>().unwrap()).await;
handle_error(&result);
}
}
"inbox" => {
let size = client.get_inbox_size().await;
if let Ok(size) = size {
info!("--- {} Message{} ---", size, if size == 1 { "" } else { "s" });
for i in 0..size {
let msg = client.get_inbox_msg((i + 1) as i8).await;
if let Ok((id, msg)) = msg {
info!("{id}. {msg}");
} else {
handle_error(&msg);
}
}
info!("--- {} Message{} ---", size, if size == 1 { "" } else { "s" });
} else {
handle_error(&size);
}
}
"pm" => {
let username = split.next();
let message = split.next();
if username.is_some() && message.is_some() {
let mut message: String = message.unwrap().into();
let mut segment = split.next();
while segment.is_some() {
message = message.add(" ");
message = message.add(segment.unwrap().into());
segment = split.next();
}
let result = client.send_pm(username.unwrap().into(), message).await;
handle_error(&result);
} else if username.is_none() && username.is_none() {
let size = client.get_pm_size().await;
if let Ok(size) = size {
info!("--- {} Private Message{} ---", size, if size == 1 { "" } else { "s" });
for i in 0..size {
let msg = client.get_pm_msg((i + 1) as i8).await;
if let Ok((id, msg)) = msg {
info!("{}. {}", id, msg);
} else {
handle_error(&msg);
}
}
info!("--- {} Private Message{} ---", size, if size == 1 { "" } else { "s" });
} else {
handle_error(&size);
}
} else {
error!("You need to enter a username and message or nothing to view your PMs")
}
}
"delete" => {
let argument = split.next().or(Some("")).unwrap();
let id = split.next().or(Some("")).unwrap().parse();
if argument == "inbox" && id.is_ok() {
let result = client.delete_inbox_msg(id.unwrap()).await;
handle_error(&result);
} else if argument == "pm" && id.is_ok() {
let result = client.delete_pm_msg(id.unwrap()).await;
handle_error(&result);
} else {
println!("You need to specify inbox or pm and a message id");
}
}
"motd" => {
let motd = client.get_motd().await;
if let Ok(motd) = motd {
println!("--- {} ---", motd)
} else {
handle_error(&motd);
}
}
"logout" => {
if !client.state.logged_in {
warn!("You are already logged out")
} else {
let result = client.logout().await;
handle_error(&result);
}
}
"exit" => {
if client.state.logged_in {
let result = client.logout().await;
handle_error(&result);
}
client.client.close("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nBefator verabschiedet sich!\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n".into());
save_config(&client.config);
break;
}
"spam" => {
for _ in 0..100 {
for i in 0..5 {
let action: AuthenticatedMessage = match i {
0 => AuthenticatedMessage::Authenticate,
1 => AuthenticatedMessage::Motd,
2 => AuthenticatedMessage::GetBalance,
3 => AuthenticatedMessage::Register,
4 => AuthenticatedMessage::Pay {
destination: "thc".into(),
amount: 0,
},
_ => AuthenticatedMessage::Logout,
};
handle_error(&client.client.send_authenticated(action, |_| true).await);
}
}
}
"help" => {
let argument = split.next().or(Some("")).unwrap();
if argument.eq_ignore_ascii_case("extended") {
println!("--- Available extended Commands ---");
println!("register <username> <password> - Register a new account and log into that account");
println!("login [username] [password] - Log into the specified account or the account you were last logged into");
println!("spam - Sends various requests to the Server");
} else {
println!("--- Available Commands ---");
println!("account <help|add|use|list|remove> - Use account help for a description of all subcommands");
println!("balance - View your current balance");
println!("pay <username> <amount> - Pay someone Steam Coins");
println!("inbox - View your inbox");
println!("pm [destination] [message]- View your private messages or send one");
println!("delete <inbox|pm> <id> - Delete a message from your inbox or PMs");
println!("motd - View the current server motd");
println!("logout - Logout of your current account");
println!("exit - Logout from the server and exit");
println!("settings <list|set> - Manage settings");
println!("help [extended] - See a (extended) list of commands");
}
}
_ => {
println!("Unknown command - Use help for a list of commands")
}
} }
} }
tokio::time::sleep(Duration::from_secs(3)).await; tokio::time::sleep(Duration::from_secs(2)).await;
} }
async fn try_login(client: &mut Client, credentials: Credentials) -> bool { async fn try_login(client: &mut Client, credentials: Credentials) -> bool {
@ -480,7 +135,7 @@ async fn try_login(client: &mut Client, credentials: Credentials) -> bool {
} }
} }
} }
_ => handle_error(&result) _ => handle_error(&result),
} }
} else { } else {
return true; return true;
@ -488,13 +143,13 @@ async fn try_login(client: &mut Client, credentials: Credentials) -> bool {
false false
} }
async fn setup_new_account(client: &mut Client, credentials: Credentials, old_credentials: Option<Credentials>) { async fn setup_new_account(client: &mut Client, rx: &Receiver<String>, credentials: Credentials, old_credentials: Option<Credentials>) {
if !client.config.accounts.default_account.eq(&credentials.username) { if !client.config.accounts.default_account.eq(&credentials.username) {
if ask_confirmation("Do you want to make this account your default account?").await { if ask_confirmation(rx, "Do you want to make this account your default account?").await {
client.config.accounts.default_account = credentials.username.clone(); client.config.accounts.default_account = credentials.username.clone();
} }
} }
if !ask_confirmation("Do you want to use this account now?").await { if !ask_confirmation(rx, "Do you want to use this account now?").await {
if let Some(old_credentials) = old_credentials { if let Some(old_credentials) = old_credentials {
if !old_credentials.eq(&credentials) { if !old_credentials.eq(&credentials) {
let result = client.login(old_credentials).await; let result = client.login(old_credentials).await;
@ -510,51 +165,29 @@ async fn setup_new_account(client: &mut Client, credentials: Credentials, old_cr
} }
} }
fn read_lines() -> Receiver<String> {
async fn read_line(client: Option<&Client>) -> String { let (tx, rx) = flume::bounded::<String>(16);
let mut thread: Option<JoinHandle<_>> = None; thread::spawn(move || {
if let Some(client) = client { let mut input = stdin().lines();
thread = Some(tokio::spawn(async move { while let Some(line) = input.next() {
/*loop { match line {
if let Ok(message) = client.client.receiver().recv() { Ok(line) => {
match message.message { tx.send(line).unwrap();
Response::DisplayMessage { message, id, .. } => {
match id.as_str() {
"pay.received" => {
println!("{}", message);
if client.config.general.show_balance_after_payment {
let balance = client.get_balance().await;
if let Ok(balance) = balance {
info!("You now have a balance of {}₿", balance);
} else {
handle_error(&balance);
}
}
}
"pm_inbox.received" => {
println!("{}", message);
}
_ => { print_error(&report!(BankError::UnexpectedMessage)); }
}
}
_ => { print_error(&report!(BankError::UnexpectedMessage)); }
}
} }
}*/ Err(err) => {
})); error!("\n{:#?}", Report::new(err));
} break;
let mut line = String::new(); }
io::stdin().read_line(&mut line).unwrap(); }
let result = line.replace("\n", "").replace("\r", "");
if let Some(thread) = thread { }
thread.abort(); });
} rx
result
} }
async fn ask_confirmation(prompt: &str) -> bool { async fn ask_confirmation(rx: &Receiver<String>, prompt: &str) -> bool {
println!("{} (y/n): ", prompt); println!("{} (y/n): ", prompt);
let answer = read_line(None).await; let answer = rx.recv_async().await.unwrap();
answer.as_str() == "y" answer.as_str() == "y"
} }
@ -566,11 +199,7 @@ fn try_parse_bool(value: Option<&str>) -> Result<bool, ()> {
parsed.map_err(|_| ()) parsed.map_err(|_| ())
} }
async fn expect_credentials( async fn expect_credentials(mut split: std::str::Split<'_, char>, client: &mut Client, try_last_login: bool) -> Option<Credentials> {
mut split: std::str::Split<'_, char>,
client: &mut Client,
try_last_login: bool,
) -> Option<Credentials> {
let username = split.next(); let username = split.next();
let password = split.next(); let password = split.next();
if username.is_none() || password.is_none() { if username.is_none() || password.is_none() {
@ -599,5 +228,391 @@ fn handle_error<T>(result: &BankResult<T, BankError>) {
} }
fn print_error(error: &Report<BankError>) { fn print_error(error: &Report<BankError>) {
error!("\n{:?}", error); //error!("\n{:?}", error);
error!("{}", error.as_error().to_string());
} }
async fn handle_line(client: &mut Client, rx: &Receiver<String>, line: String) -> bool {
let mut split = line.split(' ');
let command: &str = split.next().unwrap();
match command {
"account" => {
let argument = split.next().or(Some("")).unwrap();
if argument == "add" {
let credentials = expect_credentials(split, client, false).await;
if let Some(credentials) = credentials {
client.config.accounts.accounts.insert(credentials.username.clone(), credentials.password.clone());
if client.config.accounts.default_account.is_empty() {
client.config.accounts.default_account = credentials.username.clone();
}
save_config(&client.config);
if ask_confirmation(rx, "Do you want to test the account now?").await {
let old_credentials = client.state.last_credentials.clone();
let success = try_login(client, credentials.clone()).await;
if success {
setup_new_account(client, rx, credentials.clone(), old_credentials).await;
} else {
if ask_confirmation(rx, "The login was not successful - Do you want to register this account instead?").await {
let result = client.register(credentials.clone()).await;
if let Some(last_credentials) = &client.state.last_credentials {
handle_error(&client.client.set_credentials(last_credentials.username.clone(), last_credentials.password.clone()));
}
if result.is_ok() {
let result = client.login(credentials.clone()).await;
if result.is_ok() {
setup_new_account(client, rx, credentials, old_credentials).await;
} else {
handle_error(&result);
warn!("The login was not successful but the account was most likely still registered - Please try to login again")
}
} else {
handle_error(&result);
}
}
}
}
} else {
println!("You need to enter a username and password");
}
} else if argument == "use" {
let username: &str = split.next().or(Some("")).unwrap();
if client.config.accounts.accounts.contains_key(username) {
let password = client.config.accounts.accounts.get(username).unwrap().clone();
try_login(client, Credentials { username: username.into(), password }).await;
} else {
println!("You need to enter a valid username")
}
} else if argument == "list" {
println!("--- Accounts ---");
let i: u8 = 0;
for (name, _) in &client.config.accounts.accounts {
if client.config.accounts.default_account.eq(name) {
println!("{}. {} (Default)", i + 1, name);
} else {
println!("{}. {}", i + 1, name);
}
}
println!("--- Accounts ---");
} else if argument == "remove" {
let username: &str = split.next().or(Some("")).unwrap();
if client.config.accounts.accounts.contains_key(username) {
client.config.accounts.accounts.remove(username);
println!("Account removed");
} else {
println!("You need to enter a valid username")
}
} else if argument == "help" {
println!("--- Available Commands ---");
println!("account add <username> <password> - Add a new account");
println!("account use [username] - Login with the default or the specified account");
println!("account remove <username> - Remove the account from the list");
println!("account default <username> - Make the account the default account");
println!("account list - List available accounts");
} else {
println!("Invalid subcommand! Use account help for a list of valid subcommands");
}
}
"settings" => {
let argument = split.next().or(Some("")).unwrap();
if argument == "set" {
let setting = split.next();
let value = split.next();
if setting.is_some() && value.is_some() {
match setting.unwrap() {
"show_motd_after_login" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.show_motd_after_login = value;
} else {
return false;
}
}
"show_inbox_count_after_login" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.show_inbox_count_after_login = value;
} else {
return false;
}
}
"show_pm_inbox_count_after_login" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.show_pm_inbox_count_after_login = value;
} else {
return false;
}
}
"show_balance_after_payment" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.show_balance_after_payment = value;
} else {
return false;
}
}
"use_default_account" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.use_default_account = value;
if value {
client.config.general.use_last_account = false;
}
} else {
return false;
}
}
"use_last_account" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.use_last_account = value;
if value {
client.config.general.use_default_account = false;
}
} else {
return false;
}
}
"use_last_server" => {
if let Ok(value) = try_parse_bool(value) {
client.config.general.use_last_server = value;
} else {
return false;
}
}
_ => { error!("Invalid setting") }
}
info!("Setting changed");
save_config(&client.config);
} else {
println!("You need to enter a setting and value");
}
} else if argument == "list" {
println!("--- Settings ---");
println!("show_motd_after_login: {}", client.config.general.show_motd_after_login);
println!("show_inbox_count_after_login: {}", client.config.general.show_inbox_count_after_login);
println!("show_pm_inbox_count_after_login: {}", client.config.general.show_pm_inbox_count_after_login);
println!("show_balance_after_payment: {}", client.config.general.show_balance_after_payment);
println!("use_default_account: {}", client.config.general.use_default_account);
println!("use_last_account: {}", client.config.general.use_last_account);
println!("use_last_server: {}", client.config.general.use_last_server);
} else if argument == "help" {
println!("--- Available Commands ---");
println!("settings set <setting> <value> - Set a setting to the specified value");
println!("settings list - List all settings");
} else {
println!("Invalid subcommand! Use settings help for a list of valid subcommands");
}
}
"register" => {
let credentials = expect_credentials(split, client, true).await;
if let Some(credentials) = credentials {
let result = client.register(credentials.clone()).await;
if let Some(last_credentials) = &client.state.last_credentials {
handle_error(&client.client.set_credentials(last_credentials.username.clone(), last_credentials.password.clone()));
}
if let Err(error) = result {
match error.current_context() {
BankError::AuthenticationError => {
error!("Username is already taken");
if ask_confirmation(rx, "Do you want to login instead?").await {
try_login(client, credentials).await;
}
}
_ => print_error(&error),
}
} else {
if ask_confirmation(rx, "Do you want to login with your new account?").await {
try_login(client, credentials).await;
}
}
}
}
"login" => {
let credentials = expect_credentials(split, client, true).await;
if let Some(credentials) = credentials {
try_login(client, credentials).await;
}
}
"balance" => {
let balance = client.get_balance().await;
if let Ok(balance) = balance {
info!("You currently have a balance of {}₿", balance);
} else {
handle_error(&balance);
}
}
"pay" => {
let destination = split.next();
let amount = split.next();
if destination.is_none() || amount.is_none() || amount.unwrap().parse::<u32>().is_err() || amount.unwrap().parse::<u32>().unwrap() <= 0 {
error!("You need to enter a destination username and amount greater than 0");
} else {
let result = client.pay(destination.unwrap().into(), amount.unwrap().parse::<u32>().unwrap()).await;
handle_error(&result);
}
}
"inbox" => {
let size = client.get_inbox_size().await;
if let Ok(size) = size {
info!("--- {} Message{} ---", size, if size == 1 { "" } else { "s" });
for i in 0..size {
let msg = client.get_inbox_msg((i + 1) as i8).await;
if let Ok((id, msg)) = msg {
info!("{id}. {msg}");
} else {
handle_error(&msg);
}
}
info!("--- {} Message{} ---", size, if size == 1 { "" } else { "s" });
} else {
handle_error(&size);
}
}
"pm" => {
let username = split.next();
let message = split.next();
if username.is_some() && message.is_some() {
let mut message: String = message.unwrap().into();
let mut segment = split.next();
while segment.is_some() {
message = message.add(" ");
message = message.add(segment.unwrap().into());
segment = split.next();
}
let result = client.send_pm(username.unwrap().into(), message).await;
handle_error(&result);
} else if username.is_none() && username.is_none() {
let size = client.get_pm_size().await;
if let Ok(size) = size {
info!("--- {} Private Message{} ---", size, if size == 1 { "" } else { "s" });
for i in 0..size {
let msg = client.get_pm_msg((i + 1) as i8).await;
if let Ok((id, msg)) = msg {
info!("{}. {}", id, msg);
} else {
handle_error(&msg);
}
}
info!("--- {} Private Message{} ---", size, if size == 1 { "" } else { "s" });
} else {
handle_error(&size);
}
} else {
error!("You need to enter a username and message or nothing to view your PMs")
}
}
"delete" => {
let argument = split.next().or(Some("")).unwrap();
let id = split.next().or(Some("")).unwrap().parse();
if argument == "inbox" && id.is_ok() {
let result = client.delete_inbox_msg(id.unwrap()).await;
handle_error(&result);
} else if argument == "pm" && id.is_ok() {
let result = client.delete_pm_msg(id.unwrap()).await;
handle_error(&result);
} else {
println!("You need to specify inbox or pm and a message id");
}
}
"motd" => {
let motd = client.get_motd().await;
if let Ok(motd) = motd {
println!("--- {} ---", motd)
} else {
handle_error(&motd);
}
}
"logout" => {
if !client.state.logged_in {
warn!("You are already logged out")
} else {
let result = client.logout().await;
handle_error(&result);
}
}
"exit" => {
if client.state.logged_in {
let result = client.logout().await;
handle_error(&result);
}
client.client.close("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nBefator verabschiedet sich!\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n".into());
save_config(&client.config);
return true;
}
"spam" => {
for _ in 0..100 {
for i in 0..5 {
let action: AuthenticatedMessage = match i {
0 => AuthenticatedMessage::Authenticate,
1 => AuthenticatedMessage::Motd,
2 => AuthenticatedMessage::GetBalance,
3 => AuthenticatedMessage::Register,
4 => AuthenticatedMessage::Pay {
destination: "thc".into(),
amount: 0,
},
_ => AuthenticatedMessage::Logout,
};
handle_error(&client.client.send_authenticated(action, |_| true).await);
}
}
}
"help" => {
let argument = split.next().or(Some("")).unwrap();
if argument.eq_ignore_ascii_case("extended") {
println!("--- Available extended Commands ---");
println!("register <username> <password> - Register a new account and log into that account");
println!("login [username] [password] - Log into the specified account or the account you were last logged into");
println!("spam - Sends various requests to the Server");
} else {
println!("--- Available Commands ---");
println!("account <help|add|use|list|remove> - Use account help for a description of all subcommands");
println!("balance - View your current balance");
println!("pay <username> <amount> - Pay someone Steam Coins");
println!("inbox - View your inbox");
println!("pm [destination] [message]- View your private messages or send one");
println!("delete <inbox|pm> <id> - Delete a message from your inbox or PMs");
println!("motd - View the current server motd");
println!("logout - Logout of your current account");
println!("exit - Logout from the server and exit");
println!("settings <list|set> - Manage settings");
println!("help [extended] - See a (extended) list of commands");
}
}
_ => { println!("Unknown command - Use help for a list of commands") }
}
false
}
async fn handle_message(client: &mut Client, response: ResponseMessage) {
match response.clone().message {
Response::DisplayMessage { message, id, .. } => match id.as_str() {
"pay.received" => {
println!("{}", message);
if client.config.general.show_balance_after_payment {
let balance = client.get_balance().await;
if let Ok(balance) = balance {
info!("You now have a balance of {}₿", balance);
} else {
handle_error(&balance);
}
}
}
"pm_inbox.received" => {
println!("{}", message);
}
_ => { print_error(&report!(BankError::UnexpectedMessage(response))); }
},
_ => { print_error(&report!(BankError::UnexpectedMessage(response))); }
}
}
async fn handle_action(client: &mut Client, action: ClientAction) {
match action {
ClientAction::Connected => {
if client.state.logged_in {
client.state.logged_in = false;
handle_error(&client.login(client.state.last_credentials.clone().unwrap()).await);
}
}
ClientAction::Disconnected => {}
}
}

View File

@ -19,6 +19,6 @@ tokio-util.workspace = true
tracing.workspace = true tracing.workspace = true
rand.workspace = true rand.workspace = true
url.workspace = true url.workspace = true
flume.workspace =true flume.workspace = true
error-stack.workspace = true error-stack.workspace = true
thiserror.workspace = true thiserror.workspace = true

View File

@ -23,11 +23,12 @@ pub mod config;
pub struct BankClient { pub struct BankClient {
client: Client<Handler>, client: Client<Handler>,
rx: Receiver<ResponseMessage>, rx_msg: Receiver<ResponseMessage>,
rx_act: Receiver<ClientAction>,
handle: JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>> handle: JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>>
} }
#[derive(Error, Debug)] #[derive(Error, Debug, Clone)]
pub enum BankError { pub enum BankError {
#[error("Request timed out")] #[error("Request timed out")]
TimedOut, TimedOut,
@ -41,8 +42,8 @@ pub enum BankError {
DestinationUnknown, DestinationUnknown,
#[error("You don't have enough Steam Coins")] #[error("You don't have enough Steam Coins")]
NotEnoughMoney, NotEnoughMoney,
#[error("Unknown unexpected message received")] #[error("Unknown unexpected message received {_0:?}")]
UnexpectedMessage UnexpectedMessage(ResponseMessage)
} }
pub type BankResult<T, E = BankError> = error_stack::Result<T, E>; pub type BankResult<T, E = BankError> = error_stack::Result<T, E>;
@ -52,42 +53,38 @@ impl BankClient {
let url = url.into(); let url = url.into();
let client_config = ClientConfig::new(url); let client_config = ClientConfig::new(url);
let random: u8 = random(); let random: u8 = random();
let (tx, rx) = flume::unbounded(); let (tx_msg, rx_msg) = flume::unbounded();
let (tx_act, rx_act) = flume::unbounded();
let (client, future) = ezsockets::connect( let (client, future) = ezsockets::connect(
move |client| Handler { move |client| Handler {
client, client,
tx, tx_msg,
tx_act,
username: String::new(), username: String::new(),
password: String::new(), password: String::new(),
id: random as i32, id: random as i32,
receivers: VecDeque::new(), receivers: VecDeque::new(),
}, }, client_config,
client_config,
).await; ).await;
let handle = tokio::spawn(future); let handle = tokio::spawn(future);
Self { Self { client, rx_msg, rx_act, handle }
client,
rx,
handle,
}
} }
pub async fn send_authenticated( pub async fn send_authenticated(&self, message: AuthenticatedMessage, accept: fn(&ResponseMessage) -> bool) -> BankResult<Response> {
&self,
message: AuthenticatedMessage,
accept: fn(&ResponseMessage) -> bool,
) -> BankResult<Response> {
self.check_running()?; self.check_running()?;
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.client.call(ClientCommand::SendAuthenticatedNew {message, accept, tx}).map_err(|_| report!(BankError::InternalError))?; self.client.call(ClientCommand::SendAuthenticatedNew {message, accept, tx}).map_err(|_| report!(BankError::InternalError))?;
tokio::time::timeout(Duration::from_secs(2), rx.into_future()).await.map_err(|_| report!(BankError::TimedOut))?.map_err(|_| report!(BankError::InternalError))?.map_err(|err| report!(err)) tokio::time::timeout(Duration::from_secs(5), rx.into_future()).await.map_err(|_| report!(BankError::TimedOut))?.map_err(|_| report!(BankError::InternalError))?.map_err(|err| report!(err))
} }
pub fn set_credentials(&self, username: String, password: String) -> BankResult<()> { pub fn set_credentials(&self, username: String, password: String) -> BankResult<()> {
self.client.call(ClientCommand::ChangeCredentials {username, password}).map_err(|_| report!(BankError::InternalError)) self.client.call(ClientCommand::ChangeCredentials {username, password}).map_err(|_| report!(BankError::InternalError))
} }
pub fn receiver(&self) -> &Receiver<ResponseMessage> { pub fn receiver_msg(&self) -> &Receiver<ResponseMessage> {
&self.rx &self.rx_msg
}
pub fn receiver_act(&self) -> &Receiver<ClientAction> {
&self.rx_act
} }
fn check_running(&self) -> BankResult<(), BankError>{ fn check_running(&self) -> BankResult<(), BankError>{
@ -108,22 +105,11 @@ impl BankClient {
struct Handler { struct Handler {
client: Client<Handler>, client: Client<Handler>,
//config: Arc<Mutex<Config>>, tx_msg: Sender<ResponseMessage>,
tx: Sender<ResponseMessage>, tx_act: Sender<ClientAction>,
username: String, username: String,
password: String, password: String,
//last_username: String,
//last_password: String,
id: i32, id: i32,
//logged_in: bool,
/*message_queue: VecDeque<(
AuthenticatedMessage,
Box<dyn Future<Output = ()> + Send + Unpin>,
)>,
send_time: SystemTime,
retry_count: u8,
inbox_message_count: u8,
pm_message_count: u8,*/
receivers: VecDeque<(fn(&ResponseMessage) -> bool, oneshot::Sender<Result<Response, BankError>>)> receivers: VecDeque<(fn(&ResponseMessage) -> bool, oneshot::Sender<Result<Response, BankError>>)>
} }
@ -135,34 +121,7 @@ impl Handler {
} }
} }
/*fn last_credentials(&self) -> Credentials { fn send_authenticated(&mut self, message: AuthenticatedMessage) {
Credentials {
username: &self.last_username,
password: &self.last_password,
}
}*/
/*fn send_authenticated(&mut self, message: AuthenticatedMessage) {
self.send_authenticated_callback(message);
}*/
/*fn send_authenticated_callback(
&mut self,
message: AuthenticatedMessage,
callback: Box<dyn Future<Output = ()> + Send + Unpin>,
) {
if self.message_queue.is_empty() {
self.send_authenticated_now(message, callback);
} else {
self.message_queue.push_back((message, callback));
}
}*/
fn send_authenticated(
&mut self,
message: AuthenticatedMessage,
//callback: Box<dyn Future<Output = ()> + Send + Unpin>,
) {
#[derive(Serialize)] #[derive(Serialize)]
struct WithCredentials<'a> { struct WithCredentials<'a> {
#[serde(flatten)] #[serde(flatten)]
@ -172,40 +131,20 @@ impl Handler {
id: i32, id: i32,
} }
/*if message == AuthenticatedMessage::Authenticate && self.logged_in {
self.send_authenticated_now(AuthenticatedMessage::Logout, Box::new(Box::pin(async {})));
self.send_authenticated(message);
return;
}*/
//self.send_time = SystemTime::now();
//self.message_queue.push_front((message.clone(), callback));
debug!("Sending: {message:?}"); debug!("Sending: {message:?}");
let credentials: Credentials = self.credentials(); let credentials: Credentials = self.credentials();
self.client self.client.text(serde_json::to_string(&WithCredentials {
.text( credentials,
serde_json::to_string(&WithCredentials { message,
credentials, id: self.id,
message, }).unwrap(), ).expect("Could not send authenticated message");
id: self.id,
})
.unwrap(),
)
.expect("Could not send authenticated message");
} }
/*fn backup_auth(&mut self) { fn clear_receivers(&mut self, error: BankError) {
if !self.last_username.is_empty() && !self.last_password.is_empty() { for (_, tx) in self.receivers.drain(..) {
self.username = self.last_username.clone(); let _ = tx.send(Err(error.clone()));
self.password = self.last_password.clone();
self.send_authenticated(AuthenticatedMessage::Authenticate)
} }
} }
fn auth_error(&self) {
warn!("Authentication error while logged in. Did the password change?");
}*/
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -254,7 +193,7 @@ pub enum AuthenticatedMessage {
Motd, Motd,
} }
#[derive(Deserialize)] #[derive(Deserialize, Clone, Debug)]
#[serde(tag = "action", rename_all = "camelCase")] #[serde(tag = "action", rename_all = "camelCase")]
pub enum Response { pub enum Response {
DisplayMessage { DisplayMessage {
@ -286,7 +225,7 @@ pub enum Response {
}, },
} }
#[derive(Deserialize)] #[derive(Deserialize, Clone, Debug)]
pub struct ResponseMessage { pub struct ResponseMessage {
#[serde(flatten)] #[serde(flatten)]
pub message: Response, pub message: Response,
@ -309,7 +248,6 @@ impl ezsockets::ClientExt for Handler {
async fn on_text(&mut self, text: String) -> Result<(), Error> { async fn on_text(&mut self, text: String) -> Result<(), Error> {
debug!("Received: {text}"); debug!("Received: {text}");
let message: ResponseMessage = let message: ResponseMessage =
serde_json::from_str(text.as_str()).expect("Error decoding message"); serde_json::from_str(text.as_str()).expect("Error decoding message");
if message.id == self.id || message.id == -1 { if message.id == self.id || message.id == -1 {
@ -325,163 +263,21 @@ impl ezsockets::ClientExt for Handler {
_ => {} _ => {}
} }
if let Some((index, (_, _))) = self.receivers.iter().enumerate().find(|(_, (accept, _))| accept(&message)) { let mut found = false;
while let Some((index, (_, _))) = self.receivers.iter().enumerate().find(|(_, (accept, _))| accept(&message)) {
let (_, tx) = self.receivers.remove(index).unwrap(); let (_, tx) = self.receivers.remove(index).unwrap();
if let Err(_) = tx.send(Ok(message.message)) { found = true;
warn!("Message receiver dropped");
if let Err(message) = tx.send(Ok(message.message.clone())) {
warn!("Message receiver dropped {:?}", message);
continue;
} }
return Ok(()); return Ok(());
} }
let _ = self.tx.send(message);
/*if self.message_queue.is_empty() { if !found {
warn!("Received unexpected message from server"); let _ = self.tx_msg.send(message);
} }
match message.message {
Response::DisplayMessage { message, id, value } => {
if !message.is_empty() {
println!("{message}");
}
let config = self.config.lock().unwrap();
let show_motd_after_login = config.general.show_motd_after_login;
let show_inbox_count_after_login = config.general.show_inbox_count_after_login;
let show_pm_inbox_count_after_login =
config.general.show_pm_inbox_count_after_login;
let show_balance_after_payment = config.general.show_balance_after_payment;
drop(config);
match id.as_str() {
"auth.success" => {
self.id = value as i32;
self.last_username = self.username.clone();
self.last_password = self.password.clone();
self.logged_in = true;
if show_motd_after_login {
self.send_authenticated(AuthenticatedMessage::Motd);
}
if show_inbox_count_after_login {
self.send_authenticated(AuthenticatedMessage::GetInbox {
message_id: -1,
})
}
if show_pm_inbox_count_after_login {
self.send_authenticated(AuthenticatedMessage::GetPmInbox {
message_id: -1,
})
}
println!("(Client Id: {})", self.id);
}
"auth.fail.credentials" => {
self.backup_auth();
}
"register.success" => {
self.send_authenticated(AuthenticatedMessage::Authenticate);
}
"register.fail.usernameTaken" => {
self.backup_auth();
}
"logout.success" => {
self.logged_in = false;
}
"logout.fail.credentials" => {
self.auth_error();
}
"logout.fail.notloggedin" => {
self.logged_in = false;
}
"balance.success" => {}
"balance.fail.credentials" => {
self.auth_error();
}
"pay.fail.negative_amount" => {
panic!("Should not be able to send a negative amount")
}
"pay.fail.orig_user_unknown" => {
self.auth_error();
}
"pay.fail.credentials" => {
self.auth_error();
}
"pay.fail.unknown_dest" => {}
"pay.fail.not_enough_money" => {
if show_balance_after_payment {
self.send_authenticated(AuthenticatedMessage::GetBalance);
}
}
"pay.fail.unknown_error" => {}
"pay.success" => {
if show_balance_after_payment {
self.send_authenticated(AuthenticatedMessage::GetBalance);
}
}
"pay.received" => {
if show_balance_after_payment {
self.send_authenticated(AuthenticatedMessage::GetBalance);
}
}
"message_count" => {
self.inbox_message_count = value as u8;
println!(
"You have {} unread message{}",
self.inbox_message_count,
if self.inbox_message_count == 1 {
""
} else {
"s"
}
)
}
"pm_message_count" => {
self.pm_message_count = value as u8;
println!(
"You have {} unread private message{}",
self.pm_message_count,
if self.pm_message_count == 1 { "" } else { "s" }
)
}
"pm_inbox.send.success" => {}
"pm_inbox.dest.unkown" => {}
"read_inbox.success" => {}
"read_inbox.fail.credentials" => {
self.auth_error();
}
"read_pm.success" => {}
"read_pm.fail.credentials" => {
self.auth_error();
}
_ => {
warn!("Unknown message id: {}", id)
}
}
}
Response::Inbox {
message_id,
message,
} => {
println!("{}. {}", message_id, message);
}
Response::PmInbox {
message_id,
message,
} => {
println!("{}. {}", message_id, message);
}
Response::Motd { motd } => {
println!("--- {motd} ---");
}
}
if !self.message_queue.is_empty() {
self.message_queue.pop_front().unwrap().1.await;
if !self.message_queue.is_empty() {
let queued = self.message_queue.pop_front().unwrap();
self.send_authenticated_now(queued.0, queued.1);
}
}*/
} else { } else {
debug!("ignoring message for other id") debug!("ignoring message for other id")
} }
@ -495,11 +291,7 @@ impl ezsockets::ClientExt for Handler {
async fn on_call(&mut self, call: Self::Call) -> Result<(), Error> { async fn on_call(&mut self, call: Self::Call) -> Result<(), Error> {
match call { match call {
ClientCommand::SendAuthenticatedNew { ClientCommand::SendAuthenticatedNew { message, accept, tx, } => {
message,
accept,
tx,
} => {
self.send_authenticated(message); self.send_authenticated(message);
self.receivers.push_back((accept, tx)); self.receivers.push_back((accept, tx));
} }
@ -510,116 +302,50 @@ impl ezsockets::ClientExt for Handler {
self.username = username; self.username = username;
self.password = password; self.password = password;
} }
/*ClientCommand::TryLastLogin => {
if self.last_username.is_empty() || self.last_password.is_empty() {
println!("You need to enter a username and password");
} else {
self.send_authenticated(AuthenticatedMessage::Authenticate);
}
}
ClientCommand::CheckTimeout => {
if !self.message_queue.is_empty() && self.send_time.elapsed().unwrap().as_secs() > 5
{
if self.retry_count >= 3 {
self.message_queue.pop_front();
self.retry_count = 0;
println!("Request Timeout - All Retries failed")
} else {
self.retry_count += 1;
println!("Request Timeout - Retrying {}/3", self.retry_count);
}
if !self.message_queue.is_empty() {
let queued = self.message_queue.pop_front().unwrap();
self.send_authenticated_now(queued.0, queued.1);
}
}
}
ClientCommand::SendWithCallback(message, callback) => {
self.send_authenticated_callback(message, callback);
}
ClientCommand::PrintInbox => {
for i in 0..self.inbox_message_count {
self.send_authenticated(AuthenticatedMessage::GetInbox {
message_id: (i + 1) as i8,
})
}
}
ClientCommand::PrintPmInbox => {
for i in 0..self.pm_message_count {
self.send_authenticated(AuthenticatedMessage::GetPmInbox {
message_id: (i + 1) as i8,
})
}
}
ClientCommand::TryAccount(success, error) => {
//let last_username = self.last_username.clone();
//let last_password = self.last_password.clone();
self.last_username = String::new();
self.last_password = String::new();
/*self.send_authenticated_callback(AuthenticatedMessage::Authenticate, Box::new(Box::pin(async move {
if !self.logged_in {
self.send_authenticated_callback(AuthenticatedMessage::Register, Box::new(Box::pin(async move {
if !self.logged_in {
error.await;
} else {
success.await;
}
})));
} else {
success.await;
}
})));
*/
}*/
} }
Ok(()) Ok(())
} }
async fn on_connect(&mut self) -> Result<(), Error> { async fn on_connect(&mut self) -> Result<(), Error> {
info!("Successfully connected to the Server"); info!("Successfully connected to the Server");
if !self.username.is_empty() && !self.password.is_empty() { let _ = self.tx_act.send(ClientAction::Connected);
self.send_authenticated(AuthenticatedMessage::Authenticate);
}
Ok(()) Ok(())
} }
async fn on_connect_fail(&mut self, _error: WSError) -> Result<ClientCloseMode, Error> { async fn on_connect_fail(&mut self, _error: WSError) -> Result<ClientCloseMode, Error> {
info!("Disconnected, reconnecting..."); info!("Connection failed, reconnecting...");
Ok(ClientCloseMode::Reconnect) Ok(ClientCloseMode::Reconnect)
} }
async fn on_close(&mut self, _frame: Option<CloseFrame>) -> Result<ClientCloseMode, Error> { async fn on_close(&mut self, _frame: Option<CloseFrame>) -> Result<ClientCloseMode, Error> {
self.clear_receivers(BankError::Disconnected);
let _ = self.tx_act.send(ClientAction::Disconnected);
info!("Disconnected, reconnecting..."); info!("Disconnected, reconnecting...");
Ok(ClientCloseMode::Reconnect) Ok(ClientCloseMode::Reconnect)
} }
async fn on_disconnect(&mut self) -> Result<ClientCloseMode, Error> { async fn on_disconnect(&mut self) -> Result<ClientCloseMode, Error> {
self.clear_receivers(BankError::Disconnected);
let _ = self.tx_act.send(ClientAction::Disconnected);
info!("Disconnected, reconnecting..."); info!("Disconnected, reconnecting...");
Ok(ClientCloseMode::Reconnect) Ok(ClientCloseMode::Reconnect)
} }
} }
pub enum ClientAction {
Connected,
Disconnected
}
pub enum ClientCommand { pub enum ClientCommand {
SendAuthenticatedNew { SendAuthenticatedNew {
message: AuthenticatedMessage, message: AuthenticatedMessage,
accept: fn(&ResponseMessage) -> bool, accept: fn(&ResponseMessage) -> bool,
tx: tokio::sync::oneshot::Sender<Result<Response, BankError>>, tx: oneshot::Sender<Result<Response, BankError>>,
}, },
SendAuthenticated(AuthenticatedMessage), SendAuthenticated(AuthenticatedMessage),
/*SendWithCallback(
AuthenticatedMessage,
Box<dyn Future<Output = ()> + Send + Unpin>,
),
PrintInbox,
PrintPmInbox,*/
ChangeCredentials { ChangeCredentials {
username: String, username: String,
password: String, password: String,
}, },
/*TryLastLogin,
CheckTimeout,
TryAccount(
Box<dyn Future<Output = ()> + Send + Unpin>,
Box<dyn Future<Output = ()> + Send + Unpin>,
),*/
} }

2
rust-toolchain.toml Normal file
View File

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"