This repository has been archived on 2025-04-28. You can view files and clone it, but cannot push or open issues or pull requests.
rust-irc/src/client.rs
2023-11-30 21:38:56 -08:00

263 lines
9.4 KiB
Rust

use prompted::input;
use rust_irc::{clear, codes};
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
fn no_param_op(opcode: u8, stream: &mut TcpStream) {
stream.write(&[opcode]).unwrap();
}
fn one_param_op(opcode: u8, stream: &mut TcpStream, param: &str) {
let size: usize = param.to_string().capacity() + 1;
let mut out_buf: Vec<u8> = vec![0; size];
out_buf[0] = opcode;
for i in 1..param.len() + 1 {
out_buf[i] = *param.as_bytes().get(i - 1).unwrap();
}
stream.write(&out_buf).unwrap();
}
fn two_param_op(opcode: u8, stream: &mut TcpStream, param0: &str, param1: &str) {
let size: usize = param0.to_string().capacity() + param1.to_string().capacity() + 2;
let mut out_buf: Vec<u8> = vec![0; size];
let mut byte: usize = 0;
out_buf[byte] = opcode;
byte += 1;
for i in 0..param0.len() {
out_buf[byte] = *param0.as_bytes().get(i).unwrap();
byte += 1;
}
out_buf[byte] = 0x20;
byte += 1;
for i in 0..param1.len() {
out_buf[byte] = *param1.as_bytes().get(i).unwrap();
byte += 1;
}
stream.write(&out_buf).unwrap();
}
fn read_messages(mut stream: TcpStream, nick: &str, timestamp: &mut Arc<Mutex<Instant>>) {
let mut buffer: [u8; 1024] = [0; 1024];
loop {
match stream.read(&mut buffer) {
Ok(0) => {
println!("Server closed the connection. Shutting down client");
std::process::exit(0);
}
Ok(size) => {
let mut lock = timestamp.lock().unwrap();
*lock = Instant::now();
let msg_bytes: &[u8] = &buffer[..size];
process_message(msg_bytes, nick);
}
Err(_) => {
break;
}
}
}
}
fn process_message(msg_bytes: &[u8], nick: &str) {
match msg_bytes[0] {
codes::ERROR => match msg_bytes[1] {
codes::error::INVALID_ROOM => {
eprintln!("Operation Performed on an invalid room. Try again");
}
codes::error::NICKNAME_COLLISION => {
eprintln!("Nickname already in use on server. Connect again with a different one");
}
codes::error::SERVER_FULL => {
eprintln!("Server is full. Try again later");
}
codes::error::NOT_IN_ROOM => {
eprintln!("Cannot send a message before joining room. Use /join [room].")
}
codes::error::EMPTY_ROOM => {
eprintln!("Room is Empty");
}
codes::error::ALREADY_IN_ROOM => {
eprintln!("You are already in that room");
}
_ => {
eprintln!("Error code: {:x?}", msg_bytes[1]);
}
},
codes::client::MESSAGE => {
let message = String::from_utf8(msg_bytes[1..msg_bytes.len()].to_vec()).unwrap();
println!("[server]:{}", message);
}
codes::client::MESSAGE_ROOM => {
let params = String::from_utf8(msg_bytes[1..msg_bytes.len()].to_vec()).unwrap();
match params.split_once(" ") {
Some((room, remainder)) => match remainder.split_once(" ") {
Some((user, msg)) => {
if user != nick {
println!("[{}]:[{}]: {}", room, user, msg);
}
}
None => {
eprintln!("Malformed message recieved");
}
},
None => {
eprintln!("Malformed message recieved");
}
}
}
codes::RESPONSE_OK => {
// #[cfg(debug_assertions)]
// println!("RESPONSE_OK");
}
codes::RESPONSE => {
let message = String::from_utf8(msg_bytes[1..msg_bytes.len()].to_vec()).unwrap();
println!("{}", message);
}
codes::QUIT => {
println!("Server has closed the connection. Stopping client");
std::process::exit(0);
}
_ => {
#[cfg(debug_assertions)]
println!("BAD RESPONSE = {:x?} ", msg_bytes[0]);
}
}
}
fn disconnect(stream: &mut TcpStream) {
stream.write_all(&[codes::QUIT]).unwrap();
stream.shutdown(std::net::Shutdown::Both).unwrap();
}
fn help() {
clear();
println!("Available commands:");
println!("/quit <- Disconnect and stop the client");
println!("/rooms <- List all of the rooms on the server");
println!("/users <- List all of the user connected to the server");
println!("/list [room-name] <- List all of the users in the given room");
println!("/join [room-name] <- Join the given room. Create the room if it does not exist");
println!(
"/leave [room-name] <- Leave the given room. Error if the you are not already in the room"
);
}
pub fn start() {
clear();
println!("Starting the IRC client. No spaces allowed in nicknames or room names. /help to see available commands");
let mut nick: String;
loop {
nick = input!("Enter your nickname : ");
if nick.contains(" ") {
println!("May not contain spaces . Try again");
} else if nick.is_empty() {
println!("May not be empty . Try again");
} else {
break;
}
}
let host: String = input!("Enter the server host: ");
// let host: &str = "fab04.cecs.pdx.edu"; //hard coded host for testing
if let Ok(mut stream) = TcpStream::connect(host.to_owned() + ":6667") {
println!("Connected to {}", host);
//another stream for reading messages
let reader_clone: TcpStream = stream.try_clone().expect("Failed to clone stream");
let mut keepalive_clone = stream.try_clone().expect("failed to clone stream");
let nick_clone: String = nick.clone();
//timestamp for detecting unresponsive server
let timestamp = Arc::new(Mutex::new(Instant::now()));
let mut timestamp_clone: Arc<Mutex<Instant>> = Arc::clone(&timestamp);
thread::spawn(move || {
read_messages(reader_clone, &nick_clone, &mut timestamp_clone);
});
//watchdog to send keep_alive and stop client if server fails to respond
thread::spawn(move || loop {
thread::sleep(Duration::from_secs(5));
let lock = timestamp.lock().unwrap();
let now = Instant::now();
if now.duration_since(*lock) > Duration::from_secs(30) {
eprintln!("Server is unresponsive. Stopping client");
std::process::exit(1);
} else if now.duration_since(*lock) > Duration::from_secs(5) {
keepalive_clone.write_all(&[codes::KEEP_ALIVE]).unwrap();
}
});
//try to register the nickname
one_param_op(codes::client::REGISTER_NICK, &mut stream, &nick);
loop {
let inp = input!("");
match inp.split_once(" ") {
Some((cmd, param)) => match cmd {
"/quit" => {
disconnect(&mut stream);
break;
}
"/rooms" => no_param_op(codes::client::LIST_ROOMS, &mut stream),
"/users" => no_param_op(codes::client::LIST_USERS, &mut stream),
"/list" => match param.split_once(" ") {
Some((room, _)) => {
one_param_op(codes::client::LIST_USERS_IN_ROOM, &mut stream, room);
}
_ => {
println!("Malformaed. Try /list [room-name]");
}
},
"/join" => match param.split_once(" ") {
Some((_, _)) => {
println!("Malformed. Try /join [room-name]");
}
_ => {
one_param_op(codes::client::JOIN_ROOM, &mut stream, param);
}
},
"/leave" => match param.split_once(" ") {
Some((_, _)) => {
println!("Malformed. Try /leave [room-name]");
}
_ => {
one_param_op(codes::client::LEAVE_ROOM, &mut stream, param);
}
},
"/msg" => match param.split_once(" ") {
Some((room, msg)) => {
two_param_op(codes::client::MESSAGE_ROOM, &mut stream, room, msg);
}
_ => {
println!("Ufsage: /msg [room] [message]");
}
},
"/help" => {
help();
}
"/" => {
println!("Invalid command");
}
_ => {
one_param_op(codes::client::MESSAGE, &mut stream, &inp);
}
},
_ => {
one_param_op(codes::client::MESSAGE, &mut stream, &inp);
}
}
}
} else {
println!("Failed to connect to {} with nickname {} ", host, nick);
}
}