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",
]
[[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"

View file

@ -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",
] }

View file

@ -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<T> = result::Result<T, Error>;
type WsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;
#[derive(Clone)]
pub struct ConnTx {
peer_addr: SocketAddr,
tx: UnboundedSender<Message>,
}
@ -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<WebSocketStream<TcpStream>>,
ws_rx: SplitStream<WsStream>,
last_ping_payload: Arc<Mutex<Vec<u8>>>,
}
@ -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<Message>,
ws_tx: SplitSink<WebSocketStream<TcpStream>, Message>,
ws_tx: SplitSink<WsStream, Message>,
// Pinging and ponging
tx: UnboundedSender<Message>,
ping_delay: Duration,
@ -127,7 +125,7 @@ impl ConnMaintenance {
async fn shovel(
rx: UnboundedReceiver<Message>,
ws_tx: SplitSink<WebSocketStream<TcpStream>, Message>,
ws_tx: SplitSink<WsStream, Message>,
) -> Result<()> {
UnboundedReceiverStream::new(rx)
.map(Ok)
@ -163,22 +161,13 @@ impl ConnMaintenance {
}
}
pub fn new(
stream: WebSocketStream<TcpStream>,
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(),
};

View file

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

View file

@ -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<Option<(String, Session)>> {
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<Option<(String, Session)>> {
// 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::<u64>())),
nick: cmd.nick,
identity: Identity::of(&cmd.identity),
};
// let session = Session {
// id: SessionId::of(&format!("{}", rand::thread_rng().gen::<u64>())),
// 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<ServerSession> {
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<ServerSession> {
// 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::<Vec<_>>();
let last_message = room.last_message;
// let you = session.clone();
// let others = room
// .clients
// .values()
// .map(|client| client.session.clone())
// .collect::<Vec<_>>();
// 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))?;

View file

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

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 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<CrosstermBackend<Stdout>>) -> 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()?;

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::time;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("timed out")]
TimedOut,
#[error("canceled")]
Canceled,
}
@ -35,7 +38,14 @@ pub struct 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();
self.pending.insert(id, tx);
PendingReply {

View file

@ -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<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 {
name: String,
state: RoomState,
nick: Option<String>,
others: HashMap<SessionId, Session>,
stop: Sender<()>,
identity: String,
initial_nick: Option<String>,
status: Status,
connected: Option<Connected>,
present: Option<Present>,
still_alive: Sender<Never>,
}
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 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<Mutex<Self>>) {
todo!()
async fn bg_task(room: Arc<Mutex<Room>>, 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<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