From 992e84e67e6673b76da064dc3dcdca9c27732ee8 Mon Sep 17 00:00:00 2001 From: Joscha Date: Sat, 19 Feb 2022 01:01:52 +0100 Subject: [PATCH] Continue implementing rooms Changing lots of things along the way... But that's how it is: Make one change, make more changes to fix the resulting errors and so on. --- Cargo.lock | 349 +++++++++++++++++++++++++++++++++++++++ cove-core/Cargo.toml | 4 +- cove-core/src/conn.rs | 31 ++-- cove-core/src/packets.rs | 24 ++- cove-server/src/main.rs | 164 +++++++++--------- cove-tui/Cargo.toml | 7 +- cove-tui/src/config.rs | 24 +++ cove-tui/src/main.rs | 5 + cove-tui/src/never.rs | 2 + cove-tui/src/replies.rs | 12 +- cove-tui/src/room.rs | 279 +++++++++++++++++++++++++++---- room.md | 31 ++++ 12 files changed, 791 insertions(+), 141 deletions(-) create mode 100644 cove-tui/src/config.rs create mode 100644 cove-tui/src/never.rs create mode 100644 room.md diff --git a/Cargo.lock b/Cargo.lock index 8b26d40..73b94e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,6 +28,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "base64" version = "0.13.0" @@ -58,6 +64,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + [[package]] name = "byteorder" version = "1.4.3" @@ -76,12 +88,64 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f1fea81f183005ced9e59cdb01737ef2423956dac5a6d731b06b2ecfaa3467" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd1122e63869df2cb309f449da1ad54a7c6dfeb7c7e6ccd8e0825d9eb93bb72" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "cove-core" version = "0.1.0" @@ -121,9 +185,12 @@ name = "cove-tui" version = "0.1.0" dependencies = [ "anyhow", + "clap", "cove-core", "crossterm", + "thiserror", "tokio", + "tokio-tungstenite", "tui", ] @@ -328,6 +395,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -380,6 +459,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown", +] + [[package]] name = "instant" version = "0.1.12" @@ -395,6 +484,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +[[package]] +name = "js-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" version = "0.2.117" @@ -484,6 +588,21 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -533,6 +652,30 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.36" @@ -617,18 +760,109 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustls" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b323592e3164322f5b193dc4302e4e36cd8d37158a712d664efae1a5c2791700" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca9ebdfa27d3fc180e42879037b5338ab1c040c06affd00d8338598e7800943" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64", +] + [[package]] name = "ryu" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.136" @@ -726,6 +960,18 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "syn" version = "1.0.86" @@ -746,6 +992,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + [[package]] name = "thiserror" version = "1.0.30" @@ -811,6 +1063,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27d5f2b839802bd8267fa19b0530f5a08b9c08cd417976be2a65d130fe1c11b" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + [[package]] name = "tokio-stream" version = "0.1.8" @@ -830,8 +1093,12 @@ checksum = "e80b39df6afcc12cdf752398ade96a6b9e99c903dfdc36e53ad10b9c366bca72" dependencies = [ "futures-util", "log", + "rustls", + "rustls-native-certs", "tokio", + "tokio-rustls", "tungstenite", + "webpki", ] [[package]] @@ -860,10 +1127,12 @@ dependencies = [ "httparse", "log", "rand", + "rustls", "sha-1", "thiserror", "url", "utf-8", + "webpki", ] [[package]] @@ -905,6 +1174,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.2.2" @@ -935,6 +1210,80 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasm-bindgen" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" + +[[package]] +name = "web-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/cove-core/Cargo.toml b/cove-core/Cargo.toml index df24bb7..356ff5d 100644 --- a/cove-core/Cargo.toml +++ b/cove-core/Cargo.toml @@ -14,4 +14,6 @@ sha2 = "0.10.1" thiserror = "1.0.30" tokio = { version = "1.16.1", features = ["full"] } tokio-stream = "0.1.8" -tokio-tungstenite = "0.16.1" +tokio-tungstenite = { version = "0.16.1", features = [ + "rustls-tls-native-roots", +] } diff --git a/cove-core/src/conn.rs b/cove-core/src/conn.rs index 15700fd..23967de 100644 --- a/cove-core/src/conn.rs +++ b/cove-core/src/conn.rs @@ -12,7 +12,7 @@ use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; use tokio::sync::Mutex; use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_tungstenite::tungstenite::{self, Message}; -use tokio_tungstenite::WebSocketStream; +use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; use crate::packets::Packet; @@ -34,9 +34,10 @@ pub enum Error { pub type Result = result::Result; +type WsStream = WebSocketStream>; + #[derive(Clone)] pub struct ConnTx { - peer_addr: SocketAddr, tx: UnboundedSender, } @@ -49,16 +50,14 @@ impl fmt::Debug for ConnTx { impl ConnTx { pub fn send(&self, packet: &Packet) -> Result<()> { let str = serde_json::to_string(packet).expect("unserializable packet"); - // TODO Format somewhat nicer? - debug!("<{}> ↑ {}", self.peer_addr, str.trim()); + debug!("↑ {}", str.trim()); // TODO Format somewhat nicer? self.tx.send(Message::Text(str))?; Ok(()) } } pub struct ConnRx { - peer_addr: SocketAddr, - ws_rx: SplitStream>, + ws_rx: SplitStream, last_ping_payload: Arc>>, } @@ -92,8 +91,7 @@ impl ConnRx { let packet = serde_json::from_str(&str)?; - // TODO Format somewhat nicer? - debug!("<{}> ↓ {}", self.peer_addr, str.trim()); + debug!("↓ {}", str.trim()); // TODO Format somewhat nicer? return Ok(Some(packet)); } @@ -103,7 +101,7 @@ impl ConnRx { pub struct ConnMaintenance { // Shoveling packets into the WS connection rx: UnboundedReceiver, - ws_tx: SplitSink, Message>, + ws_tx: SplitSink, // Pinging and ponging tx: UnboundedSender, ping_delay: Duration, @@ -127,7 +125,7 @@ impl ConnMaintenance { async fn shovel( rx: UnboundedReceiver, - ws_tx: SplitSink, Message>, + ws_tx: SplitSink, ) -> Result<()> { UnboundedReceiverStream::new(rx) .map(Ok) @@ -163,22 +161,13 @@ impl ConnMaintenance { } } -pub fn new( - stream: WebSocketStream, - ping_delay: Duration, -) -> Result<(ConnTx, ConnRx, ConnMaintenance)> { - let peer_addr = stream.get_ref().peer_addr()?; - +pub fn new(stream: WsStream, ping_delay: Duration) -> Result<(ConnTx, ConnRx, ConnMaintenance)> { let (ws_tx, ws_rx) = stream.split(); let (tx, rx) = mpsc::unbounded_channel(); let last_ping_payload = Arc::new(Mutex::new(vec![])); - let conn_tx = ConnTx { - peer_addr, - tx: tx.clone(), - }; + let conn_tx = ConnTx { tx: tx.clone() }; let conn_rx = ConnRx { - peer_addr, ws_rx, last_ping_payload: last_ping_payload.clone(), }; diff --git a/cove-core/src/packets.rs b/cove-core/src/packets.rs index 0e179fc..588b3b8 100644 --- a/cove-core/src/packets.rs +++ b/cove-core/src/packets.rs @@ -4,23 +4,30 @@ use crate::macros::packets; use crate::{Message, MessageId, Session}; #[derive(Debug, Deserialize, Serialize)] -pub struct HelloCmd { - pub room: String, +pub struct RoomCmd { + pub name: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub enum RoomRpl { + Success, + InvalidRoom { reason: String }, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct IdentifyCmd { pub nick: String, pub identity: String, } #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type")] -pub enum HelloRpl { +pub enum IdentifyRpl { Success { you: Session, others: Vec, last_message: MessageId, }, - InvalidRoom { - reason: String, - }, InvalidNick { reason: String, }, @@ -37,7 +44,7 @@ pub struct NickCmd { #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "type")] pub enum NickRpl { - Success, + Success { you: Session }, InvalidNick { reason: String }, } @@ -86,7 +93,8 @@ pub struct SendNtf { // Create a Cmd enum for all commands, a Rpl enum for all replies and a Ntf enum // for all notifications, as well as TryFrom impls for the individual structs. packets! { - cmd Hello(HelloCmd, HelloRpl), + cmd Room(RoomCmd, RoomRpl), + cmd Identify(IdentifyCmd, IdentifyRpl), cmd Nick(NickCmd, NickRpl), cmd Send(SendCmd, SendRpl), cmd Who(WhoCmd, WhoRpl), diff --git a/cove-server/src/main.rs b/cove-server/src/main.rs index 5e10f50..be47f5e 100644 --- a/cove-server/src/main.rs +++ b/cove-server/src/main.rs @@ -9,14 +9,15 @@ use std::time::Duration; use anyhow::anyhow; use cove_core::conn::{self, ConnMaintenance, ConnRx, ConnTx}; use cove_core::packets::{ - Cmd, HelloCmd, HelloRpl, JoinNtf, NickCmd, NickNtf, NickRpl, Packet, PartNtf, SendCmd, SendNtf, - SendRpl, WhoCmd, WhoRpl, + Cmd, IdentifyCmd, IdentifyRpl, JoinNtf, NickCmd, NickNtf, NickRpl, Packet, PartNtf, SendCmd, + SendNtf, SendRpl, WhoCmd, WhoRpl, }; use cove_core::{Identity, Message, MessageId, Session, SessionId}; use log::{info, warn}; use rand::Rng; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::Mutex; +use tokio_tungstenite::MaybeTlsStream; #[derive(Debug, Clone)] struct Client { @@ -155,7 +156,12 @@ impl ServerSession { } self.session.nick = cmd.nick.clone(); - self.tx.send(&Packet::rpl(id, NickRpl::Success))?; + self.tx.send(&Packet::rpl( + id, + NickRpl::Success { + you: self.session.clone(), + }, + ))?; self.room.lock().await.nick(self.session.id, cmd.nick); Ok(()) @@ -189,7 +195,8 @@ impl ServerSession { async fn handle_packet(&mut self, packet: Packet) -> anyhow::Result<()> { match packet { Packet::Cmd { id, cmd } => match cmd { - Cmd::Hello(_) => Err(anyhow!("unexpected Hello cmd")), + Cmd::Room(_) => Err(anyhow!("unexpected Room cmd")), + Cmd::Identify(_) => Err(anyhow!("unexpected Identify cmd")), Cmd::Nick(cmd) => self.handle_nick(id, cmd).await, Cmd::Send(cmd) => self.handle_send(id, cmd).await, Cmd::Who(cmd) => self.handle_who(id, cmd).await, @@ -228,90 +235,92 @@ impl Server { .clone() } - async fn handle_hello( - &self, - tx: &ConnTx, - id: u64, - cmd: HelloCmd, - ) -> anyhow::Result> { - if let Some(reason) = util::check_room(&cmd.room) { - tx.send(&Packet::rpl(id, HelloRpl::InvalidRoom { reason }))?; - return Ok(None); - } - if let Some(reason) = util::check_nick(&cmd.nick) { - tx.send(&Packet::rpl(id, HelloRpl::InvalidNick { reason }))?; - return Ok(None); - } - if let Some(reason) = util::check_identity(&cmd.identity) { - tx.send(&Packet::rpl(id, HelloRpl::InvalidIdentity { reason }))?; - return Ok(None); - } + // async fn handle_hello( + // &self, + // tx: &ConnTx, + // id: u64, + // cmd: IdentifyCmd, + // ) -> anyhow::Result> { + // if let Some(reason) = util::check_room(&cmd.room) { + // tx.send(&Packet::rpl(id, IdentifyRpl::InvalidRoom { reason }))?; + // return Ok(None); + // } + // if let Some(reason) = util::check_nick(&cmd.nick) { + // tx.send(&Packet::rpl(id, IdentifyRpl::InvalidNick { reason }))?; + // return Ok(None); + // } + // if let Some(reason) = util::check_identity(&cmd.identity) { + // tx.send(&Packet::rpl(id, IdentifyRpl::InvalidIdentity { reason }))?; + // return Ok(None); + // } - let session = Session { - id: SessionId::of(&format!("{}", rand::thread_rng().gen::())), - nick: cmd.nick, - identity: Identity::of(&cmd.identity), - }; + // let session = Session { + // id: SessionId::of(&format!("{}", rand::thread_rng().gen::())), + // nick: cmd.nick, + // identity: Identity::of(&cmd.identity), + // }; - Ok(Some((cmd.room, session))) - } + // Ok(Some((cmd.room, session))) + // } - async fn greet(&self, tx: ConnTx, mut rx: ConnRx) -> anyhow::Result { - let (id, room, session) = loop { - let (id, cmd) = match rx.recv().await? { - Some(Packet::Cmd { - id, - cmd: Cmd::Hello(cmd), - }) => (id, cmd), - Some(_) => return Err(anyhow!("not a Hello packet")), - None => return Err(anyhow!("connection closed during greeting")), - }; + // async fn greet(&self, tx: ConnTx, mut rx: ConnRx) -> anyhow::Result { + // let (id, room, session) = loop { + // let (id, cmd) = match rx.recv().await? { + // Some(Packet::Cmd { + // id, + // cmd: Cmd::Hello(cmd), + // }) => (id, cmd), + // Some(_) => return Err(anyhow!("not a Hello packet")), + // None => return Err(anyhow!("connection closed during greeting")), + // }; - if let Some((room, session)) = self.handle_hello(&tx, id, cmd).await? { - break (id, room, session); - } - }; + // if let Some((room, session)) = self.handle_hello(&tx, id, cmd).await? { + // break (id, room, session); + // } + // }; - let room = self.room(room).await; + // let room = self.room(room).await; - { - let mut room = room.lock().await; + // { + // let mut room = room.lock().await; - let you = session.clone(); - let others = room - .clients - .values() - .map(|client| client.session.clone()) - .collect::>(); - let last_message = room.last_message; + // let you = session.clone(); + // let others = room + // .clients + // .values() + // .map(|client| client.session.clone()) + // .collect::>(); + // let last_message = room.last_message; - tx.send(&Packet::rpl( - id, - HelloRpl::Success { - you, - others, - last_message, - }, - ))?; + // tx.send(&Packet::rpl( + // id, + // IdentifyRpl::Success { + // you, + // others, + // last_message, + // }, + // ))?; - room.join(Client { - session: session.clone(), - send: tx.clone(), - }); - } + // room.join(Client { + // session: session.clone(), + // send: tx.clone(), + // }); + // } + + // Ok(ServerSession { + // tx, + // rx, + // room, + // session, + // }) + // } - Ok(ServerSession { - tx, - rx, - room, - session, - }) - } async fn greet_and_run(&self, tx: ConnTx, rx: ConnRx) -> anyhow::Result<()> { - let mut session = self.greet(tx, rx).await?; - let result = session.run().await; - session.room.lock().await.part(session.session.id); - result + // let mut session = self.greet(tx, rx).await?; + // let result = session.run().await; + // session.room.lock().await.part(session.session.id); + // result + todo!() } /// Wrapper for [`ConnMaintenance::perform`] so it returns an @@ -322,6 +331,7 @@ impl Server { } async fn handle_conn(&self, stream: TcpStream) -> anyhow::Result<()> { + let stream = MaybeTlsStream::Plain(stream); let stream = tokio_tungstenite::accept_async(stream).await?; let (tx, rx, maintenance) = conn::new(stream, Duration::from_secs(10))?; tokio::try_join!(self.greet_and_run(tx, rx), Self::maintain(maintenance))?; diff --git a/cove-tui/Cargo.toml b/cove-tui/Cargo.toml index cc712b9..28dde89 100644 --- a/cove-tui/Cargo.toml +++ b/cove-tui/Cargo.toml @@ -5,12 +5,15 @@ edition = "2021" [dependencies] anyhow = "1.0.53" +clap = { version = "3.1.0", features = ["derive"] } cove-core = { path = "../cove-core" } crossterm = "0.22.1" # futures = "0.3.21" # serde_json = "1.0.78" -# thiserror = "1.0.30" +thiserror = "1.0.30" tokio = { version = "1.16.1", features = ["full"] } # tokio-stream = "0.1.8" -# tokio-tungstenite = "0.16.1" +tokio-tungstenite = { version = "0.16.1", features = [ + "rustls-tls-native-roots", +] } tui = "0.17.0" diff --git a/cove-tui/src/config.rs b/cove-tui/src/config.rs new file mode 100644 index 0000000..5ec701b --- /dev/null +++ b/cove-tui/src/config.rs @@ -0,0 +1,24 @@ +use std::time::Duration; + +use clap::Parser; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long, default_value_t = String::from("wss://plugh.de/cove/"))] + cove_url: String, +} + +pub struct Config { + pub cove_url: String, + pub timeout: Duration, +} + +impl Config { + pub fn load() -> Self { + let args = Args::parse(); + Self { + cove_url: args.cove_url, + timeout: Duration::from_secs(10), + } + } +} diff --git a/cove-tui/src/main.rs b/cove-tui/src/main.rs index 363e229..25ba203 100644 --- a/cove-tui/src/main.rs +++ b/cove-tui/src/main.rs @@ -1,9 +1,12 @@ mod replies; mod room; +mod config; +mod never; use std::io::{self, Stdout}; use std::time::Duration; +use config::Config; use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::execute; use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; @@ -23,6 +26,8 @@ async fn run(terminal: &mut Terminal>) -> anyhow::Resul #[tokio::main] async fn main() -> anyhow::Result<()> { + let config = Config::load(); + let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?; crossterm::terminal::enable_raw_mode()?; diff --git a/cove-tui/src/never.rs b/cove-tui/src/never.rs new file mode 100644 index 0000000..dffbe50 --- /dev/null +++ b/cove-tui/src/never.rs @@ -0,0 +1,2 @@ +// TODO Replace with `!` when it is stabilised +pub enum Never {} diff --git a/cove-tui/src/replies.rs b/cove-tui/src/replies.rs index 564922c..31a15f9 100644 --- a/cove-tui/src/replies.rs +++ b/cove-tui/src/replies.rs @@ -6,8 +6,11 @@ use std::time::Duration; use tokio::sync::oneshot::{self, Receiver, Sender}; use tokio::time; +#[derive(Debug, thiserror::Error)] pub enum Error { + #[error("timed out")] TimedOut, + #[error("canceled")] Canceled, } @@ -35,7 +38,14 @@ pub struct Replies { } impl Replies { - pub async fn wait_for(&mut self, id: I) -> PendingReply { + pub fn new(timeout: Duration) -> Self { + Self { + timeout, + pending: HashMap::new(), + } + } + + pub fn wait_for(&mut self, id: I) -> PendingReply { let (tx, rx) = oneshot::channel(); self.pending.insert(id, tx); PendingReply { diff --git a/cove-tui/src/room.rs b/cove-tui/src/room.rs index aa53e69..3ba7929 100644 --- a/cove-tui/src/room.rs +++ b/cove-tui/src/room.rs @@ -1,63 +1,280 @@ +use std::any; use std::collections::HashMap; use std::sync::Arc; +use std::time::Duration; -use cove_core::conn::ConnTx; +use anyhow::bail; +use cove_core::conn::{self, ConnMaintenance, ConnRx, ConnTx}; +use cove_core::packets::{ + Cmd, IdentifyCmd, IdentifyRpl, JoinNtf, NickRpl, Ntf, Packet, RoomCmd, RoomRpl, Rpl, SendRpl, + WhoRpl, +}; use cove_core::{Session, SessionId}; use tokio::sync::oneshot::{self, Sender}; use tokio::sync::Mutex; -pub enum ConnectedState { - ChoosingNick, - Identifying, - Online, +use crate::config::Config; +use crate::never::Never; +use crate::replies::{self, Replies}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("not connected")] + NotConnected, + #[error("not present")] + NotPresent, + #[error("incorrect reply type")] + IncorrectReplyType, + #[error("{0}")] + Conn(#[from] conn::Error), + #[error("{0}")] + Replies(#[from] replies::Error), } -pub enum RoomState { - Connecting, - Reconnecting, - Connected { state: ConnectedState, tx: ConnTx }, - DoesNotExist, +/// State for when a websocket connection exists. +struct Connected { + tx: ConnTx, + next_id: u64, + replies: Replies, +} + +/// State for when a client has fully joined a room. +struct Present { + session: Session, + others: HashMap, +} + +enum Status { + /// No action required by the UI. + Nominal, + /// User must enter a nick. + NickRequired, + /// Identifying to the server. No action required by the UI. + Identifying, + CouldNotConnect, + InvalidRoom(String), + InvalidNick(String), + InvalidIdentity(String), + InvalidContent(String), } pub struct Room { name: String, - state: RoomState, - nick: Option, - others: HashMap, - stop: Sender<()>, + identity: String, + initial_nick: Option, + status: Status, + connected: Option, + present: Option, + still_alive: Sender, } impl Room { - pub async fn create(name: String) -> Arc> { + pub async fn new( + name: String, + identity: String, + initial_nick: Option, + config: &'static Config, + ) -> Arc> { let (tx, rx) = oneshot::channel(); - let room = Self { + let room = Arc::new(Mutex::new(Self { name, - state: RoomState::Connecting, - nick: None, - others: HashMap::new(), - stop: tx, - }; - let room = Arc::new(Mutex::new(room)); + identity, + initial_nick, + status: Status::Nominal, + connected: None, + present: None, + still_alive: tx, + })); let room_clone = room.clone(); - tokio::spawn(async { + tokio::spawn(async move { tokio::select! { - _ = rx => {}, - _ = Self::connect(room_clone) => {} + _ = rx => {} + _ = Self::bg_task(room_clone, config) => {} } }); room } - async fn connect(room: Arc>) { - todo!() + async fn bg_task(room: Arc>, config: &'static Config) { + let mut room_verified = false; + loop { + if let Ok((tx, rx, mt)) = Self::connect(&config.cove_url, config.timeout).await { + { + let mut room = room.lock().await; + room.status = Status::Nominal; + room.connected = Some(Connected { + tx, + next_id: 0, + replies: Replies::new(config.timeout), + }); + } + + tokio::select! { + _ = mt.perform() => {} + _ = Self::receive(room.clone(), rx, &mut room_verified) => {} + } + } + + if !room_verified { + room.lock().await.status = Status::CouldNotConnect; + return; + } + } } - pub fn stop(self) { - // If the send goes wrong because the other end has hung up, it's - // already stopped and there's nothing to do. - let _ = self.stop.send(()); + async fn connect( + url: &str, + timeout: Duration, + ) -> anyhow::Result<(ConnTx, ConnRx, ConnMaintenance)> { + let stream = tokio_tungstenite::connect_async(url).await?.0; + let conn = conn::new(stream, timeout)?; + Ok(conn) + } + + async fn receive( + room: Arc>, + mut rx: ConnRx, + room_verified: &mut bool, + ) -> anyhow::Result<()> { + while let Some(packet) = rx.recv().await? { + match packet { + Packet::Cmd { .. } => {} // Ignore, the server never sends commands + Packet::Rpl { id, rpl } => { + room.lock().await.on_rpl(&room, id, rpl, room_verified)?; + } + Packet::Ntf { ntf } => room.lock().await.on_ntf(ntf), + } + } + Ok(()) + } + + fn on_rpl( + &mut self, + room: &Arc>, + id: u64, + rpl: Rpl, + room_verified: &mut bool, + ) -> anyhow::Result<()> { + match &rpl { + Rpl::Room(RoomRpl::Success) => { + *room_verified = true; + if let Some(nick) = &self.initial_nick { + tokio::spawn(Self::identify( + room.clone(), + nick.clone(), + self.identity.clone(), + )); + } else { + self.status = Status::NickRequired; + } + } + Rpl::Room(RoomRpl::InvalidRoom { reason }) => { + self.status = Status::InvalidRoom(reason.clone()); + anyhow::bail!("invalid room"); + } + Rpl::Identify(IdentifyRpl::Success { + you, + others, + last_message, + }) => { + let others = others + .iter() + .map(|session| (session.id, session.clone())) + .collect(); + self.present = Some(Present { + session: you.clone(), + others, + }); + // TODO Send last message to store + } + Rpl::Identify(IdentifyRpl::InvalidNick { reason }) => { + self.status = Status::InvalidNick(reason.clone()); + } + Rpl::Identify(IdentifyRpl::InvalidIdentity { reason }) => { + self.status = Status::InvalidIdentity(reason.clone()); + } + Rpl::Nick(NickRpl::Success { you }) => { + if let Some(present) = &mut self.present { + present.session = you.clone(); + } + } + Rpl::Nick(NickRpl::InvalidNick { reason }) => { + self.status = Status::InvalidNick(reason.clone()); + } + Rpl::Send(SendRpl::Success { message }) => { + // TODO Send message to store + } + Rpl::Send(SendRpl::InvalidContent { reason }) => { + self.status = Status::InvalidContent(reason.clone()); + } + Rpl::Who(WhoRpl { you, others }) => { + if let Some(present) = &mut self.present { + present.session = you.clone(); + present.others = others + .iter() + .map(|session| (session.id, session.clone())) + .collect(); + } + } + } + + if let Some(connected) = &mut self.connected { + connected.replies.complete(&id, rpl); + } + + Ok(()) + } + + fn on_ntf(&mut self, ntf: Ntf) { + match ntf { + Ntf::Join(join) => { + if let Some(present) = &mut self.present { + present.others.insert(join.who.id, join.who); + } + } + Ntf::Nick(nick) => { + if let Some(present) = &mut self.present { + present.others.insert(nick.who.id, nick.who); + } + } + Ntf::Part(part) => { + if let Some(present) = &mut self.present { + present.others.remove(&part.who.id); + } + } + Ntf::Send(_) => { + // TODO Send message to store + } + } + } + + async fn cmd(room: &Mutex, cmd: C) -> Result + where + C: Into, + Rpl: TryInto, + { + let token = { + let mut room = room.lock().await; + let connected = room.connected.as_mut().ok_or(Error::NotConnected)?; + + let id = connected.next_id; + connected.next_id += 1; + + let pending_reply = connected.replies.wait_for(id); + connected.tx.send(&Packet::cmd(id, cmd.into()))?; + pending_reply + }; + + let rpl = token.get().await?; + let rpl = rpl.try_into().map_err(|_| Error::IncorrectReplyType)?; + Ok(rpl) + } + + async fn identify(room: Arc>, nick: String, identity: String) -> Result<(), Error> { + let result: IdentifyRpl = Self::cmd(&room, IdentifyCmd { nick, identity }).await?; + Ok(()) } } diff --git a/room.md b/room.md new file mode 100644 index 0000000..524fe1d --- /dev/null +++ b/room.md @@ -0,0 +1,31 @@ +- Determine room name +- Connect for the first time + - If connection fails: Show error, done + - Set room + - If room is invalid: Show error, done + - If no nick is set by default: Let user choose nick + - Identify yourself + - If nick is invalid: Show error, let user edit nick + - If identity is invalid: Show error, done + - Listen to events, send commands +- Reconnect + - If connection fails: Show error, done + - Set room + - If room is invalid: Show error, done + - Identify yourself + - If nick is invalid: Show error, let user edit nick + - If identity is invalid: Show error, done + - Listen to events, send commands + +General state: +- Initial nick (optional) +- A way to stop the entire room + +State present when WS connection exists: +- Connection itself +- Next command id +- Replies + +State present when fully connected: +- Own session +- Others