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.
This commit is contained in:
Joscha 2022-02-19 01:01:52 +01:00
parent 31ffa5cd67
commit 992e84e67e
12 changed files with 791 additions and 141 deletions

349
Cargo.lock generated
View file

@ -28,6 +28,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.0" version = "0.13.0"
@ -58,6 +64,12 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bumpalo"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.4.3" version = "1.4.3"
@ -76,12 +88,64 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.0" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "cove-core" name = "cove-core"
version = "0.1.0" version = "0.1.0"
@ -121,9 +185,12 @@ name = "cove-tui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap",
"cove-core", "cove-core",
"crossterm", "crossterm",
"thiserror",
"tokio", "tokio",
"tokio-tungstenite",
"tui", "tui",
] ]
@ -328,6 +395,18 @@ dependencies = [
"wasi", "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]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.19" version = "0.1.19"
@ -380,6 +459,16 @@ dependencies = [
"unicode-normalization", "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]] [[package]]
name = "instant" name = "instant"
version = "0.1.12" version = "0.1.12"
@ -395,6 +484,21 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" 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]] [[package]]
name = "libc" name = "libc"
version = "0.2.117" version = "0.2.117"
@ -484,6 +588,21 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 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]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.11.2" version = "0.11.2"
@ -533,6 +652,30 @@ version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" 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]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.36" version = "1.0.36"
@ -617,18 +760,109 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" 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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.9" version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" 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]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 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]] [[package]]
name = "serde" name = "serde"
version = "1.0.136" version = "1.0.136"
@ -726,6 +960,18 @@ version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" 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]] [[package]]
name = "syn" name = "syn"
version = "1.0.86" version = "1.0.86"
@ -746,6 +992,12 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.30" version = "1.0.30"
@ -811,6 +1063,17 @@ dependencies = [
"syn", "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]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.8" version = "0.1.8"
@ -830,8 +1093,12 @@ checksum = "e80b39df6afcc12cdf752398ade96a6b9e99c903dfdc36e53ad10b9c366bca72"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
"rustls",
"rustls-native-certs",
"tokio", "tokio",
"tokio-rustls",
"tungstenite", "tungstenite",
"webpki",
] ]
[[package]] [[package]]
@ -860,10 +1127,12 @@ dependencies = [
"httparse", "httparse",
"log", "log",
"rand", "rand",
"rustls",
"sha-1", "sha-1",
"thiserror", "thiserror",
"url", "url",
"utf-8", "utf-8",
"webpki",
] ]
[[package]] [[package]]
@ -905,6 +1174,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "url" name = "url"
version = "2.2.2" 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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View file

@ -14,4 +14,6 @@ sha2 = "0.10.1"
thiserror = "1.0.30" thiserror = "1.0.30"
tokio = { version = "1.16.1", features = ["full"] } tokio = { version = "1.16.1", features = ["full"] }
tokio-stream = "0.1.8" tokio-stream = "0.1.8"
tokio-tungstenite = "0.16.1" tokio-tungstenite = { version = "0.16.1", features = [
"rustls-tls-native-roots",
] }

View file

@ -12,7 +12,7 @@ use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
use tokio_tungstenite::tungstenite::{self, Message}; use tokio_tungstenite::tungstenite::{self, Message};
use tokio_tungstenite::WebSocketStream; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream};
use crate::packets::Packet; use crate::packets::Packet;
@ -34,9 +34,10 @@ pub enum Error {
pub type Result<T> = result::Result<T, Error>; pub type Result<T> = result::Result<T, Error>;
type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
#[derive(Clone)] #[derive(Clone)]
pub struct ConnTx { pub struct ConnTx {
peer_addr: SocketAddr,
tx: UnboundedSender<Message>, tx: UnboundedSender<Message>,
} }
@ -49,16 +50,14 @@ impl fmt::Debug for ConnTx {
impl ConnTx { impl ConnTx {
pub fn send(&self, packet: &Packet) -> Result<()> { pub fn send(&self, packet: &Packet) -> Result<()> {
let str = serde_json::to_string(packet).expect("unserializable packet"); let str = serde_json::to_string(packet).expect("unserializable packet");
// TODO Format somewhat nicer? debug!("↑ {}", str.trim()); // TODO Format somewhat nicer?
debug!("<{}> ↑ {}", self.peer_addr, str.trim());
self.tx.send(Message::Text(str))?; self.tx.send(Message::Text(str))?;
Ok(()) Ok(())
} }
} }
pub struct ConnRx { pub struct ConnRx {
peer_addr: SocketAddr, ws_rx: SplitStream<WsStream>,
ws_rx: SplitStream<WebSocketStream<TcpStream>>,
last_ping_payload: Arc<Mutex<Vec<u8>>>, last_ping_payload: Arc<Mutex<Vec<u8>>>,
} }
@ -92,8 +91,7 @@ impl ConnRx {
let packet = serde_json::from_str(&str)?; let packet = serde_json::from_str(&str)?;
// TODO Format somewhat nicer? debug!("↓ {}", str.trim()); // TODO Format somewhat nicer?
debug!("<{}> ↓ {}", self.peer_addr, str.trim());
return Ok(Some(packet)); return Ok(Some(packet));
} }
@ -103,7 +101,7 @@ impl ConnRx {
pub struct ConnMaintenance { pub struct ConnMaintenance {
// Shoveling packets into the WS connection // Shoveling packets into the WS connection
rx: UnboundedReceiver<Message>, rx: UnboundedReceiver<Message>,
ws_tx: SplitSink<WebSocketStream<TcpStream>, Message>, ws_tx: SplitSink<WsStream, Message>,
// Pinging and ponging // Pinging and ponging
tx: UnboundedSender<Message>, tx: UnboundedSender<Message>,
ping_delay: Duration, ping_delay: Duration,
@ -127,7 +125,7 @@ impl ConnMaintenance {
async fn shovel( async fn shovel(
rx: UnboundedReceiver<Message>, rx: UnboundedReceiver<Message>,
ws_tx: SplitSink<WebSocketStream<TcpStream>, Message>, ws_tx: SplitSink<WsStream, Message>,
) -> Result<()> { ) -> Result<()> {
UnboundedReceiverStream::new(rx) UnboundedReceiverStream::new(rx)
.map(Ok) .map(Ok)
@ -163,22 +161,13 @@ impl ConnMaintenance {
} }
} }
pub fn new( pub fn new(stream: WsStream, ping_delay: Duration) -> Result<(ConnTx, ConnRx, ConnMaintenance)> {
stream: WebSocketStream<TcpStream>,
ping_delay: Duration,
) -> Result<(ConnTx, ConnRx, ConnMaintenance)> {
let peer_addr = stream.get_ref().peer_addr()?;
let (ws_tx, ws_rx) = stream.split(); let (ws_tx, ws_rx) = stream.split();
let (tx, rx) = mpsc::unbounded_channel(); let (tx, rx) = mpsc::unbounded_channel();
let last_ping_payload = Arc::new(Mutex::new(vec![])); let last_ping_payload = Arc::new(Mutex::new(vec![]));
let conn_tx = ConnTx { let conn_tx = ConnTx { tx: tx.clone() };
peer_addr,
tx: tx.clone(),
};
let conn_rx = ConnRx { let conn_rx = ConnRx {
peer_addr,
ws_rx, ws_rx,
last_ping_payload: last_ping_payload.clone(), last_ping_payload: last_ping_payload.clone(),
}; };

View file

@ -4,23 +4,30 @@ use crate::macros::packets;
use crate::{Message, MessageId, Session}; use crate::{Message, MessageId, Session};
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct HelloCmd { pub struct RoomCmd {
pub room: String, 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 nick: String,
pub identity: String, pub identity: String,
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum HelloRpl { pub enum IdentifyRpl {
Success { Success {
you: Session, you: Session,
others: Vec<Session>, others: Vec<Session>,
last_message: MessageId, last_message: MessageId,
}, },
InvalidRoom {
reason: String,
},
InvalidNick { InvalidNick {
reason: String, reason: String,
}, },
@ -37,7 +44,7 @@ pub struct NickCmd {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum NickRpl { pub enum NickRpl {
Success, Success { you: Session },
InvalidNick { reason: String }, 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 // 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. // for all notifications, as well as TryFrom impls for the individual structs.
packets! { packets! {
cmd Hello(HelloCmd, HelloRpl), cmd Room(RoomCmd, RoomRpl),
cmd Identify(IdentifyCmd, IdentifyRpl),
cmd Nick(NickCmd, NickRpl), cmd Nick(NickCmd, NickRpl),
cmd Send(SendCmd, SendRpl), cmd Send(SendCmd, SendRpl),
cmd Who(WhoCmd, WhoRpl), cmd Who(WhoCmd, WhoRpl),

View file

@ -9,14 +9,15 @@ use std::time::Duration;
use anyhow::anyhow; use anyhow::anyhow;
use cove_core::conn::{self, ConnMaintenance, ConnRx, ConnTx}; use cove_core::conn::{self, ConnMaintenance, ConnRx, ConnTx};
use cove_core::packets::{ use cove_core::packets::{
Cmd, HelloCmd, HelloRpl, JoinNtf, NickCmd, NickNtf, NickRpl, Packet, PartNtf, SendCmd, SendNtf, Cmd, IdentifyCmd, IdentifyRpl, JoinNtf, NickCmd, NickNtf, NickRpl, Packet, PartNtf, SendCmd,
SendRpl, WhoCmd, WhoRpl, SendNtf, SendRpl, WhoCmd, WhoRpl,
}; };
use cove_core::{Identity, Message, MessageId, Session, SessionId}; use cove_core::{Identity, Message, MessageId, Session, SessionId};
use log::{info, warn}; use log::{info, warn};
use rand::Rng; use rand::Rng;
use tokio::net::{TcpListener, TcpStream}; use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio_tungstenite::MaybeTlsStream;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Client { struct Client {
@ -155,7 +156,12 @@ impl ServerSession {
} }
self.session.nick = cmd.nick.clone(); 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); self.room.lock().await.nick(self.session.id, cmd.nick);
Ok(()) Ok(())
@ -189,7 +195,8 @@ impl ServerSession {
async fn handle_packet(&mut self, packet: Packet) -> anyhow::Result<()> { async fn handle_packet(&mut self, packet: Packet) -> anyhow::Result<()> {
match packet { match packet {
Packet::Cmd { id, cmd } => match cmd { 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::Nick(cmd) => self.handle_nick(id, cmd).await,
Cmd::Send(cmd) => self.handle_send(id, cmd).await, Cmd::Send(cmd) => self.handle_send(id, cmd).await,
Cmd::Who(cmd) => self.handle_who(id, cmd).await, Cmd::Who(cmd) => self.handle_who(id, cmd).await,
@ -228,90 +235,92 @@ impl Server {
.clone() .clone()
} }
async fn handle_hello( // async fn handle_hello(
&self, // &self,
tx: &ConnTx, // tx: &ConnTx,
id: u64, // id: u64,
cmd: HelloCmd, // cmd: IdentifyCmd,
) -> anyhow::Result<Option<(String, Session)>> { // ) -> anyhow::Result<Option<(String, Session)>> {
if let Some(reason) = util::check_room(&cmd.room) { // if let Some(reason) = util::check_room(&cmd.room) {
tx.send(&Packet::rpl(id, HelloRpl::InvalidRoom { reason }))?; // tx.send(&Packet::rpl(id, IdentifyRpl::InvalidRoom { reason }))?;
return Ok(None); // return Ok(None);
} // }
if let Some(reason) = util::check_nick(&cmd.nick) { // if let Some(reason) = util::check_nick(&cmd.nick) {
tx.send(&Packet::rpl(id, HelloRpl::InvalidNick { reason }))?; // tx.send(&Packet::rpl(id, IdentifyRpl::InvalidNick { reason }))?;
return Ok(None); // return Ok(None);
} // }
if let Some(reason) = util::check_identity(&cmd.identity) { // if let Some(reason) = util::check_identity(&cmd.identity) {
tx.send(&Packet::rpl(id, HelloRpl::InvalidIdentity { reason }))?; // tx.send(&Packet::rpl(id, IdentifyRpl::InvalidIdentity { reason }))?;
return Ok(None); // return Ok(None);
} // }
let session = Session { // let session = Session {
id: SessionId::of(&format!("{}", rand::thread_rng().gen::<u64>())), // id: SessionId::of(&format!("{}", rand::thread_rng().gen::<u64>())),
nick: cmd.nick, // nick: cmd.nick,
identity: Identity::of(&cmd.identity), // 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<ServerSession> { // async fn greet(&self, tx: ConnTx, mut rx: ConnRx) -> anyhow::Result<ServerSession> {
let (id, room, session) = loop { // let (id, room, session) = loop {
let (id, cmd) = match rx.recv().await? { // let (id, cmd) = match rx.recv().await? {
Some(Packet::Cmd { // Some(Packet::Cmd {
id, // id,
cmd: Cmd::Hello(cmd), // cmd: Cmd::Hello(cmd),
}) => (id, cmd), // }) => (id, cmd),
Some(_) => return Err(anyhow!("not a Hello packet")), // Some(_) => return Err(anyhow!("not a Hello packet")),
None => return Err(anyhow!("connection closed during greeting")), // None => return Err(anyhow!("connection closed during greeting")),
}; // };
if let Some((room, session)) = self.handle_hello(&tx, id, cmd).await? { // if let Some((room, session)) = self.handle_hello(&tx, id, cmd).await? {
break (id, room, session); // 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 you = session.clone();
let others = room // let others = room
.clients // .clients
.values() // .values()
.map(|client| client.session.clone()) // .map(|client| client.session.clone())
.collect::<Vec<_>>(); // .collect::<Vec<_>>();
let last_message = room.last_message; // let last_message = room.last_message;
tx.send(&Packet::rpl( // tx.send(&Packet::rpl(
id, // id,
HelloRpl::Success { // IdentifyRpl::Success {
you, // you,
others, // others,
last_message, // last_message,
}, // },
))?; // ))?;
room.join(Client { // room.join(Client {
session: session.clone(), // session: session.clone(),
send: tx.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<()> { async fn greet_and_run(&self, tx: ConnTx, rx: ConnRx) -> anyhow::Result<()> {
let mut session = self.greet(tx, rx).await?; // let mut session = self.greet(tx, rx).await?;
let result = session.run().await; // let result = session.run().await;
session.room.lock().await.part(session.session.id); // session.room.lock().await.part(session.session.id);
result // result
todo!()
} }
/// Wrapper for [`ConnMaintenance::perform`] so it returns an /// Wrapper for [`ConnMaintenance::perform`] so it returns an
@ -322,6 +331,7 @@ impl Server {
} }
async fn handle_conn(&self, stream: TcpStream) -> anyhow::Result<()> { async fn handle_conn(&self, stream: TcpStream) -> anyhow::Result<()> {
let stream = MaybeTlsStream::Plain(stream);
let stream = tokio_tungstenite::accept_async(stream).await?; let stream = tokio_tungstenite::accept_async(stream).await?;
let (tx, rx, maintenance) = conn::new(stream, Duration::from_secs(10))?; let (tx, rx, maintenance) = conn::new(stream, Duration::from_secs(10))?;
tokio::try_join!(self.greet_and_run(tx, rx), Self::maintain(maintenance))?; tokio::try_join!(self.greet_and_run(tx, rx), Self::maintain(maintenance))?;

View file

@ -5,12 +5,15 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.53" anyhow = "1.0.53"
clap = { version = "3.1.0", features = ["derive"] }
cove-core = { path = "../cove-core" } cove-core = { path = "../cove-core" }
crossterm = "0.22.1" crossterm = "0.22.1"
# futures = "0.3.21" # futures = "0.3.21"
# serde_json = "1.0.78" # serde_json = "1.0.78"
# thiserror = "1.0.30" thiserror = "1.0.30"
tokio = { version = "1.16.1", features = ["full"] } tokio = { version = "1.16.1", features = ["full"] }
# tokio-stream = "0.1.8" # 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" tui = "0.17.0"

24
cove-tui/src/config.rs Normal file
View file

@ -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),
}
}
}

View file

@ -1,9 +1,12 @@
mod replies; mod replies;
mod room; mod room;
mod config;
mod never;
use std::io::{self, Stdout}; use std::io::{self, Stdout};
use std::time::Duration; use std::time::Duration;
use config::Config;
use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
use crossterm::execute; use crossterm::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen}; use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
@ -23,6 +26,8 @@ async fn run(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> anyhow::Resul
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let config = Config::load();
let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?; let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
crossterm::terminal::enable_raw_mode()?; crossterm::terminal::enable_raw_mode()?;

2
cove-tui/src/never.rs Normal file
View file

@ -0,0 +1,2 @@
// TODO Replace with `!` when it is stabilised
pub enum Never {}

View file

@ -6,8 +6,11 @@ use std::time::Duration;
use tokio::sync::oneshot::{self, Receiver, Sender}; use tokio::sync::oneshot::{self, Receiver, Sender};
use tokio::time; use tokio::time;
#[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("timed out")]
TimedOut, TimedOut,
#[error("canceled")]
Canceled, Canceled,
} }
@ -35,7 +38,14 @@ pub struct Replies<I, R> {
} }
impl<I: Eq + Hash, R> Replies<I, R> { impl<I: Eq + Hash, R> Replies<I, R> {
pub async fn wait_for(&mut self, id: I) -> PendingReply<R> { pub fn new(timeout: Duration) -> Self {
Self {
timeout,
pending: HashMap::new(),
}
}
pub fn wait_for(&mut self, id: I) -> PendingReply<R> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
self.pending.insert(id, tx); self.pending.insert(id, tx);
PendingReply { PendingReply {

View file

@ -1,63 +1,280 @@
use std::any;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; 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 cove_core::{Session, SessionId};
use tokio::sync::oneshot::{self, Sender}; use tokio::sync::oneshot::{self, Sender};
use tokio::sync::Mutex; use tokio::sync::Mutex;
pub enum ConnectedState { use crate::config::Config;
ChoosingNick, use crate::never::Never;
Identifying, use crate::replies::{self, Replies};
Online,
#[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 { /// State for when a websocket connection exists.
Connecting, struct Connected {
Reconnecting, tx: ConnTx,
Connected { state: ConnectedState, tx: ConnTx }, next_id: u64,
DoesNotExist, replies: Replies<u64, Rpl>,
}
/// State for when a client has fully joined a room.
struct Present {
session: Session,
others: HashMap<SessionId, Session>,
}
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 { pub struct Room {
name: String, name: String,
state: RoomState, identity: String,
nick: Option<String>, initial_nick: Option<String>,
others: HashMap<SessionId, Session>, status: Status,
stop: Sender<()>, connected: Option<Connected>,
present: Option<Present>,
still_alive: Sender<Never>,
} }
impl Room { impl Room {
pub async fn create(name: String) -> Arc<Mutex<Self>> { pub async fn new(
name: String,
identity: String,
initial_nick: Option<String>,
config: &'static Config,
) -> Arc<Mutex<Self>> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let room = Self { let room = Arc::new(Mutex::new(Self {
name, name,
state: RoomState::Connecting, identity,
nick: None, initial_nick,
others: HashMap::new(), status: Status::Nominal,
stop: tx, connected: None,
}; present: None,
let room = Arc::new(Mutex::new(room)); still_alive: tx,
}));
let room_clone = room.clone(); let room_clone = room.clone();
tokio::spawn(async { tokio::spawn(async move {
tokio::select! { tokio::select! {
_ = rx => {}, _ = rx => {}
_ = Self::connect(room_clone) => {} _ = Self::bg_task(room_clone, config) => {}
} }
}); });
room room
} }
async fn connect(room: Arc<Mutex<Self>>) { async fn bg_task(room: Arc<Mutex<Room>>, config: &'static Config) {
todo!() 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),
});
} }
pub fn stop(self) { tokio::select! {
// If the send goes wrong because the other end has hung up, it's _ = mt.perform() => {}
// already stopped and there's nothing to do. _ = Self::receive(room.clone(), rx, &mut room_verified) => {}
let _ = self.stop.send(()); }
}
if !room_verified {
room.lock().await.status = Status::CouldNotConnect;
return;
}
}
}
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<Mutex<Room>>,
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<Mutex<Room>>,
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<C, R>(room: &Mutex<Room>, cmd: C) -> Result<R, Error>
where
C: Into<Cmd>,
Rpl: TryInto<R>,
{
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<Mutex<Room>>, nick: String, identity: String) -> Result<(), Error> {
let result: IdentifyRpl = Self::cmd(&room, IdentifyCmd { nick, identity }).await?;
Ok(())
} }
} }

31
room.md Normal file
View file

@ -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