diff --git a/Cargo.lock b/Cargo.lock index 2b488c9..91744ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.95" @@ -27,6 +77,7 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" name = "bRAC" version = "0.1.0+1.99.2" dependencies = [ + "clap", "colored", "crossterm", "homedir", @@ -61,6 +112,52 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clap" +version = "4.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + [[package]] name = "colored" version = "3.0.0" @@ -129,6 +226,12 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "homedir" version = "0.3.4" @@ -151,6 +254,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.14" @@ -231,6 +340,12 @@ dependencies = [ "libc", ] +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + [[package]] name = "parking_lot" version = "0.12.3" @@ -446,6 +561,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.98" @@ -463,6 +584,12 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index db9e7b9..a796eac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,5 @@ lazy_static = "1.5.0" crossterm = "0.28.1" serde = { version = "1.0.217", features = ["serde_derive"] } serde_yml = "0.0.12" -homedir = "0.3.4" \ No newline at end of file +homedir = "0.3.4" +clap = { version = "4.5.28", features = ["derive"] } diff --git a/README.md b/README.md index 39dbd58..ecf0b9c 100644 --- a/README.md +++ b/README.md @@ -30,11 +30,37 @@ cargo build --release # build release (target/release/bRAC) cargo run # run (builds and runs bRAC itself) ``` +## config + +```yml +host: meex.lol:11234 # server host +name: null # user name +message_format: 리㹰<{name}> {text} # message format +update_time: 50 # update chat interval +max_messages: 100 # chat messages limit +``` + +## command args + +``` + -p, --config-path Print config path + -H, --host Use specified host + -n, --name Use specified name + -F, --message-format Use specified message format + -r, --read-messages Print unformatted messages from chat and exit + -s, --send-message Send unformatted message to chat and exit + -f, --disable-formatting Disable message formatting and sanitizing + -c, --disable-commands Disable slash commands + -i, --disable-ip-hiding Disable ip hiding + -h, --help Print help + -V, --version Print version +``` + ## commands +`/help` - show help message \ `/clear` - clear chat \ -`/spam *args` - spam with text \ -`/help` - show help message +`/spam *args` - spam with text ## colored usernames diff --git a/src/config.rs b/src/config.rs index 4ae6782..1489b26 100644 --- a/src/config.rs +++ b/src/config.rs @@ -16,8 +16,8 @@ pub struct Config { } pub fn load_config(path: PathBuf) -> Config { - println!("Config path: {}", path.to_string_lossy()); - println!("Loading config..."); + // println!("Config path: {}", path.to_string_lossy()); + // println!("Loading config..."); let config = if !fs::exists(&path).unwrap_or_default() { let host = get_input("Host (default: meex.lol:11234) > ").unwrap_or("meex.lol:11234".to_string()); @@ -41,7 +41,7 @@ pub fn load_config(path: PathBuf) -> Config { serde_yml::from_str(config).expect("Config load error") }; - println!("Config loaded successfully!"); + // println!("Config loaded successfully!"); config } diff --git a/src/main.rs b/src/main.rs index 9e2f77f..d3e614c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,25 +1,20 @@ use std::{ - error::Error, io::{stdin, stdout, BufRead, Write}, sync::{Arc, RwLock}, thread, time::{Duration, SystemTime, UNIX_EPOCH} + error::Error, io::{stdin, stdout, BufRead, Write}, sync::{Arc, RwLock} }; use colored::Color; use config::{get_config_path, load_config, Config}; -use rac::{run_recv_loop, send_message}; +use rac::{read_messages, run_recv_loop, send_message}; use rand::random; use regex::Regex; use lazy_static::lazy_static; use term::run_main_loop; +use clap::Parser; const ADVERTISEMENT: &str = "\r\x1B[1A use bRAC client! https://github.com/MeexReay/bRAC \x1B[1B"; const ADVERTISEMENT_ENABLED: bool = false; - -mod config; -mod term; -mod rac; - - lazy_static! { static ref DATE_REGEX: Regex = Regex::new(r"\[(.*?)\] (.*)").unwrap(); static ref COLORED_USERNAMES: Vec<(Regex, Color)> = vec![ @@ -31,6 +26,10 @@ lazy_static! { } +mod config; +mod term; +mod rac; + fn get_input(prompt: &str) -> Option { let mut out = stdout().lock(); @@ -47,31 +46,86 @@ fn get_input(prompt: &str) -> Option { } } - -fn on_command(config: Arc, host: &str, command: &str) -> Result<(), Box> { +fn on_command(config: Arc, host: &str, disable_hiding_ip: bool, command: &str) -> Result<(), Box> { let command = command.trim_start_matches("/"); let (command, args) = command.split_once(" ").unwrap_or((&command, "")); let args = args.split(" ").collect::>(); if command == "clear" { - send_message(host, &format!("\r\x1B[1A{}", " ".repeat(64)).repeat(config.max_messages))?; + send_message(host, &format!("\r\x1B[1A{}", " ".repeat(64)).repeat(config.max_messages), disable_hiding_ip)?; // *input.write().unwrap() = "/ заспамлено)))".to_string(); } else if command == "spam" { - send_message(host, &format!("\r\x1B[1A{}{}", args.join(" "), " ".repeat(10)).repeat(config.max_messages))?; + send_message(host, &format!("\r\x1B[1A{}{}", args.join(" "), " ".repeat(10)).repeat(config.max_messages), disable_hiding_ip)?; // *input.write().unwrap() = "/ заспамлено)))".to_string(); } else if command == "help" { - write!(stdout(), "/clear - clear console; /spam *args - spam console with text; /help - show help message")?; + write!(stdout(), "Help message:\r +/clear - clear console\r +/spam *args - spam console with text\r +/help - show help message\r +\r +Press enter to close")?; stdout().flush()?; } Ok(()) } -fn main() { - let start_time = SystemTime::now(); - let config = load_config(get_config_path()); - let name = match config.name.clone() { +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Print config path + #[arg(short='p', long)] + config_path: bool, + + /// Use specified host + #[arg(short='H', long)] + host: Option, + + /// Use specified name + #[arg(short, long)] + name: Option, + + /// Use specified message format + #[arg(short='F', long)] + message_format: Option, + + /// Print unformatted messages from chat and exit + #[arg(short, long)] + read_messages: bool, + + /// Send unformatted message to chat and exit + #[arg(short, long, value_name="MESSAGE")] + send_message: Option, + + /// Disable message formatting and sanitizing + #[arg(short='f', long)] + disable_formatting: bool, + + /// Disable slash commands + #[arg(short='c', long)] + disable_commands: bool, + + /// Disable ip hiding + #[arg(short='i', long)] + disable_ip_hiding: bool, +} + + +fn main() { + let args = Args::parse(); + + let config_path = get_config_path(); + + if args.config_path { + print!("{}", config_path.to_string_lossy()); + return; + } + + // let start_time = SystemTime::now(); + let mut config = load_config(config_path); + + let name = match args.name.clone().or(config.name.clone()) { Some(i) => i, None => { let anon_name = format!("Anon#{:X}", random::()); @@ -79,15 +133,40 @@ fn main() { }, }; + if let Some(host) = args.host { + config.host = host; + } + + if let Some(message_format) = args.message_format { + config.message_format = message_format; + } + + let disable_hiding_ip = args.disable_ip_hiding; + + if let Some(message) = args.send_message { + send_message(&config.host, &message, disable_hiding_ip).expect("Error sending message"); + return; + } + + if args.read_messages { + print!("{}", read_messages(&config.host).expect("Error reading messages")); + return; + } + + let disable_formatting = args.disable_formatting; + let disable_commands = args.disable_commands; + let messages = Arc::new(RwLock::new(String::new())); let input = Arc::new(RwLock::new(String::new())); let config = Arc::new(config); - let elapsed = start_time.elapsed().unwrap().as_millis(); - if elapsed < 1500 { - thread::sleep(Duration::from_millis((1500 - elapsed) as u64)); - } - run_recv_loop(config.clone(), config.host.clone(), messages.clone(), input.clone()); - run_main_loop(config.clone(), messages.clone(), input.clone(), config.host.clone(), name.clone()); + + // let elapsed = start_time.elapsed().unwrap().as_millis(); + // if elapsed < 1500 { + // thread::sleep(Duration::from_millis((1500 - elapsed) as u64)); + // } + + run_recv_loop(config.clone(), config.host.clone(), messages.clone(), input.clone(), disable_formatting); + run_main_loop(config.clone(), messages.clone(), input.clone(), config.host.clone(), name.clone(), disable_formatting, disable_commands, disable_hiding_ip); } diff --git a/src/rac.rs b/src/rac.rs index 6f4c5dd..1298891 100644 --- a/src/rac.rs +++ b/src/rac.rs @@ -2,12 +2,17 @@ use std::{error::Error, io::{Read, Write}, net::TcpStream, sync::{Arc, RwLock}, use crate::{config::Config, term::print_console, ADVERTISEMENT, ADVERTISEMENT_ENABLED}; -pub fn send_message(host: &str, message: &str) -> Result<(), Box> { +pub fn send_message(host: &str, message: &str, disable_hiding_ip: bool) -> Result<(), Box> { let mut stream = TcpStream::connect(host)?; stream.write_all(&[0x01])?; - let data = format!("\r\x07{}{}{}", + let data = format!("{}{}{}{}", + if disable_hiding_ip { + "\r\x07" + } else { + "" + }, message, - if message.chars().count() < 39 { + if !disable_hiding_ip && message.chars().count() < 39 { " ".repeat(39-message.chars().count()) } else { String::new() @@ -28,7 +33,7 @@ fn skip_null(stream: &mut TcpStream) -> Result, Box> { } } -fn read_messages(host: &str) -> Result> { +pub fn read_messages(host: &str) -> Result> { let mut stream = TcpStream::connect(host)?; stream.write_all(&[0x00])?; @@ -67,23 +72,23 @@ fn read_messages(host: &str) -> Result> { Ok(packet_data) } -fn recv_loop(config: Arc, host: &str, cache: Arc>, input: Arc>) -> Result<(), Box> { +fn recv_loop(config: Arc, host: &str, cache: Arc>, input: Arc>, disable_formatting: bool) -> Result<(), Box> { while let Ok(data) = read_messages(host) { if data == cache.read().unwrap().clone() { continue } *cache.write().unwrap() = data; - print_console(config.clone(), &cache.read().unwrap(), &input.read().unwrap())?; + print_console(config.clone(), &cache.read().unwrap(), &input.read().unwrap(), disable_formatting)?; thread::sleep(Duration::from_millis(config.update_time as u64)); } Ok(()) } -pub fn run_recv_loop(config: Arc, host: String, messages: Arc>, input: Arc>) { +pub fn run_recv_loop(config: Arc, host: String, messages: Arc>, input: Arc>, disable_formatting: bool) { thread::spawn({ move || { - let _ = recv_loop(config.clone(), &host, messages, input); + let _ = recv_loop(config.clone(), &host, messages, input, disable_formatting); println!("Connection closed"); } }); diff --git a/src/term.rs b/src/term.rs index d968dfc..7447ba2 100644 --- a/src/term.rs +++ b/src/term.rs @@ -6,14 +6,18 @@ use regex::Regex; use crate::{config::Config, on_command, rac::send_message, ADVERTISEMENT, COLORED_USERNAMES, DATE_REGEX}; -pub fn print_console(config: Arc, messages: &str, input: &str) -> Result<(), Box> { +pub fn print_console(config: Arc, messages: &str, input: &str, disable_formatting: bool) -> Result<(), Box> { let mut messages = messages.split("\n") .map(|o| o.to_string()) .collect::>(); messages.reverse(); messages.truncate(config.max_messages); messages.reverse(); - let messages: Vec = messages.into_iter().filter_map(format_message).collect(); + let messages: Vec = if disable_formatting { + messages + } else { + messages.into_iter().filter_map(format_message).collect() + }; let text = format!( "{}{}\n> {}", "\n".repeat(config.max_messages - messages.len()), @@ -80,7 +84,7 @@ fn find_username_color(message: &str) -> Option<(String, String, Color)> { None } -fn poll_events(config: Arc, input: Arc>, messages: Arc>, host: String, name: String) { +fn poll_events(config: Arc, input: Arc>, messages: Arc>, host: String, name: String, disable_formatting: bool, disable_commands: bool, disable_hiding_ip: bool) { loop { if !event::poll(Duration::from_millis(50)).unwrap_or(false) { continue } @@ -106,17 +110,18 @@ fn poll_events(config: Arc, input: Arc>, messages: Arc, input: Arc>, messages: Arc, messages: Arc>, input: Arc>, host: String, name: String) { +pub fn run_main_loop(config: Arc, messages: Arc>, input: Arc>, host: String, name: String, disable_formatting: bool, disable_commands: bool, disable_hiding_ip: bool) { enable_raw_mode().unwrap(); // thread::spawn({ @@ -167,5 +172,5 @@ pub fn run_main_loop(config: Arc, messages: Arc>, input: // } // }); - poll_events(config, input.clone(), messages.clone(), host, name); + poll_events(config, input.clone(), messages.clone(), host, name, disable_formatting, disable_commands, disable_hiding_ip); } \ No newline at end of file