From c662b64f11a174bc2138c2b6de57c28331bf79e3 Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Thu, 14 Sep 2023 18:55:03 +0200 Subject: [PATCH 01/17] disable tls support in sqlx, remove webpki-roots --- Cargo.lock | 26 -------------------------- Cargo.toml | 2 +- deny.toml | 8 ++++++-- 3 files changed, 7 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69c4337..94d6a3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1603,19 +1603,14 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls", - "rustls-pemfile", "serde", "serde_json", "sha2", "smallvec", "sqlformat", "thiserror", - "tokio", - "tokio-stream", "tracing", "url", - "webpki-roots", ] [[package]] @@ -1652,7 +1647,6 @@ dependencies = [ "sqlx-sqlite", "syn 1.0.109", "tempfile", - "tokio", "url", ] @@ -1895,17 +1889,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.8" @@ -2208,15 +2191,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-roots" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" -dependencies = [ - "rustls-webpki", -] - [[package]] name = "whoami" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index ddabb38..6a615fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ rustls-pemfile = "1.0.2" quick-xml = { version = "0.30.0", features = ["async-tokio"] } derive_more = "0.99.17" uuid = { version = "1.3.0", features = ["v4"] } -sqlx = { version = "0.7.0-alpha.2", features = ["sqlite", "runtime-tokio-rustls", "migrate"] } +sqlx = { version = "0.7.0-alpha.2", features = ["sqlite", "migrate"] } [dev-dependencies] assert_matches = "1.5.0" diff --git a/deny.toml b/deny.toml index 19a24a9..f7ace18 100644 --- a/deny.toml +++ b/deny.toml @@ -17,14 +17,18 @@ allow = [ "MIT", "Apache-2.0", "ISC", - "MPL-2.0", "BSD-3-Clause", ] exceptions = [ { allow = ["Unicode-DFS-2016"], name = "unicode-ident" }, { allow = ["OpenSSL"], name = "ring" }, ] -deny = ["GPL-2.0", "GPL-3.0", "AGPL-3.0"] +deny = [ + "GPL-2.0", + "GPL-3.0", + "AGPL-3.0", + "MPL-2.0" # it is used by webpki-roots; we do not hardcode root certs in the binary +] copyleft = "deny" confidence-threshold = 0.93 private = { ignore = true } From 3de7a131f05c2b148f78d7f8568ea4c2a3172084 Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Fri, 15 Sep 2023 17:03:06 +0200 Subject: [PATCH 02/17] add dockerfile --- .dockerignore | 6 ++++++ .gitignore | 1 + Cargo.lock | 2 +- Cargo.toml | 2 +- dist/alpine3.18.Dockerfile | 11 +++++++++++ src/main.rs | 5 ++++- 6 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 dist/alpine3.18.Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3feddeb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +* +!/src/ +!/migrations/ +!Cargo.lock +!Cargo.toml +!rust-toolchain diff --git a/.gitignore b/.gitignore index e2c5d98..ce1cc97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /db.sqlite +.idea/ diff --git a/Cargo.lock b/Cargo.lock index 94d6a3e..78099b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -750,7 +750,7 @@ dependencies = [ [[package]] name = "lavina" -version = "0.1.0" +version = "0.0.0" dependencies = [ "anyhow", "assert_matches", diff --git a/Cargo.toml b/Cargo.toml index 6a615fb..04e15f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lavina" -version = "0.1.0" +version = "0.0.0" edition = "2021" publish = false diff --git a/dist/alpine3.18.Dockerfile b/dist/alpine3.18.Dockerfile new file mode 100644 index 0000000..3a4a5d4 --- /dev/null +++ b/dist/alpine3.18.Dockerfile @@ -0,0 +1,11 @@ +FROM rust:1.72.0-alpine3.18@sha256:2f5592c561cef195c9fa4462633a674458dc375fc0ba4b80e7efe4c3c8e68403 as bld + +RUN apk add --no-cache musl-dev +COPY . . +RUN cargo build --release + +FROM alpine:3.18@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a + +COPY --from=bld target/release/lavina /usr/bin/lavina +VOLUME ["/etc/lavina/", "/var/lib/lavina/"] +ENTRYPOINT ["lavina"] diff --git a/src/main.rs b/src/main.rs index a623441..cfc3955 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,10 @@ struct ServerConfig { } fn load_config() -> Result { - let raw_config = Figment::new().merge(Toml::file("config.toml")); + // TODO get config path as a cmd line arg + let raw_config = Figment::new() + .merge(Toml::file("config.toml")) + .merge(Toml::file("/etc/lavina/config.toml")); let config: ServerConfig = raw_config.extract()?; Ok(config) } From 298245f3f53f0e4c784b41f67a883e277b0f5380 Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Fri, 15 Sep 2023 17:12:12 +0200 Subject: [PATCH 03/17] bump version --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 78099b5..d4b2512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -750,7 +750,7 @@ dependencies = [ [[package]] name = "lavina" -version = "0.0.0" +version = "0.0.1-dev" dependencies = [ "anyhow", "assert_matches", diff --git a/Cargo.toml b/Cargo.toml index 04e15f8..7c72368 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lavina" -version = "0.0.0" +version = "0.0.1-dev" edition = "2021" publish = false From 87d73af8113508117e88396befa5a7bc0a91f70c Mon Sep 17 00:00:00 2001 From: JustTestingV Date: Fri, 15 Sep 2023 16:33:25 +0000 Subject: [PATCH 04/17] [irc] used nonempty prefixed nicks in 353 reply (#9) Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/9 Co-authored-by: JustTestingV Co-committed-by: JustTestingV --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/core/player.rs | 2 +- src/projections/irc/mod.rs | 17 +++++++---------- src/protos/irc/mod.rs | 1 + src/protos/irc/server.rs | 10 ++++++++-- src/protos/irc/user.rs | 32 ++++++++++++++++++++++++++++++++ src/util/table.rs | 2 +- 8 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 src/protos/irc/user.rs diff --git a/Cargo.lock b/Cargo.lock index d4b2512..ad25c33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -761,6 +761,7 @@ dependencies = [ "hyper 1.0.0-rc.3", "lazy_static", "nom", + "nonempty", "prometheus", "quick-xml", "regex", @@ -887,6 +888,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeaf4ad7403de93e699c191202f017118df734d3850b01e13a3a8b2e6953d3c9" + [[package]] name = "nu-ansi-term" version = "0.46.0" diff --git a/Cargo.toml b/Cargo.toml index 7c72368..7b855e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ prometheus = { version = "0.13.3", default-features = false } regex = "1.7.1" lazy_static = "1.4.0" nom = "7.1.3" +nonempty = "0.8.1" tokio-rustls = "0.24.1" rustls-pemfile = "1.0.2" quick-xml = { version = "0.30.0", features = ["async-tokio"] } diff --git a/src/core/player.rs b/src/core/player.rs index 3457a88..1533941 100644 --- a/src/core/player.rs +++ b/src/core/player.rs @@ -302,7 +302,7 @@ impl Player { player_id, connections: AnonTable::new(), my_rooms: HashMap::new(), - banned_from: HashSet::from([RoomId::from("empty").unwrap()]), + banned_from: HashSet::from([RoomId::from("Empty").unwrap()]), rx, handle, rooms, diff --git a/src/projections/irc/mod.rs b/src/projections/irc/mod.rs index fc490fd..43e580b 100644 --- a/src/projections/irc/mod.rs +++ b/src/projections/irc/mod.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use std::net::SocketAddr; use futures_util::future::join_all; +use nonempty::nonempty; +use nonempty::NonEmpty; use prometheus::{IntCounter, IntGauge, Registry as MetricsRegistry}; use serde::Deserialize; use tokio::io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter}; @@ -16,6 +18,7 @@ use crate::prelude::*; use crate::protos::irc::client::{client_message, ClientMessage}; use crate::protos::irc::server::{AwayStatus, ServerMessage, ServerMessageBody}; use crate::protos::irc::{Chan, Recipient}; +use crate::protos::irc::user::PrefixedNick; use crate::util::Terminator; #[cfg(test)] @@ -666,22 +669,16 @@ async fn produce_on_join_cmd_messages( } .write_async(writer) .await?; - let mut members: String = if let Some(head) = room_info.members.first() { - head.as_inner().clone() - } else { - user.nickname.clone() - }.as_ref().into(); - for i in &room_info.members[1..] { - members.push(' '); - members.push_str(i.as_inner()); - } + let prefixed_members: Vec = room_info.members.iter().map(|member| PrefixedNick::from_player_id(member.clone())).collect(); + let non_empty_members: NonEmpty = NonEmpty::from_vec(prefixed_members).unwrap_or(nonempty![PrefixedNick::from_str(user.nickname.clone())]); + ServerMessage { tags: vec![], sender: Some(config.server_name.clone()), body: ServerMessageBody::N353NamesReply { client: user.nickname.clone(), chan: chan.clone(), - members: members.into(), + members: non_empty_members.into(), }, } .write_async(writer) diff --git a/src/protos/irc/mod.rs b/src/protos/irc/mod.rs index 4d65efd..0997a79 100644 --- a/src/protos/irc/mod.rs +++ b/src/protos/irc/mod.rs @@ -1,6 +1,7 @@ //! Client-to-Server IRC protocol. pub mod client; pub mod server; +pub mod user; use std::io::Result; use crate::prelude::Str; diff --git a/src/protos/irc/server.rs b/src/protos/irc/server.rs index b2efcd4..194c756 100644 --- a/src/protos/irc/server.rs +++ b/src/protos/irc/server.rs @@ -1,7 +1,9 @@ +use nonempty::NonEmpty; use tokio::io::AsyncWrite; use tokio::io::AsyncWriteExt; use super::*; +use crate::protos::irc::user::PrefixedNick; /// Server-to-client message. #[derive(Clone, Debug, PartialEq, Eq)] @@ -121,7 +123,7 @@ pub enum ServerMessageBody { N353NamesReply { client: Str, chan: Chan, - members: Str, // TODO make this a non-empty list with prefixes + members: NonEmpty, }, N366NamesReplyEnd { client: Str, @@ -284,7 +286,11 @@ impl ServerMessageBody { writer.write_all(b" = ").await?; chan.write_async(writer).await?; writer.write_all(b" :").await?; - writer.write_all(members.as_bytes()).await?; + for member in members { + writer.write_all(member.prefix.to_string().as_bytes()).await?; + writer.write_all(member.nick.as_bytes()).await?; + writer.write_all(b" ").await?; + } } ServerMessageBody::N366NamesReplyEnd { client, chan } => { writer.write_all(b"366 ").await?; diff --git a/src/protos/irc/user.rs b/src/protos/irc/user.rs new file mode 100644 index 0000000..e426812 --- /dev/null +++ b/src/protos/irc/user.rs @@ -0,0 +1,32 @@ +use super::*; +use std::fmt; +use crate::core::player::PlayerId; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Prefix { + Empty, +} + +impl fmt::Display for Prefix { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Prefix::Empty => write!(f, ""), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PrefixedNick { + pub prefix: Prefix, + pub nick: Str, +} + +impl PrefixedNick { + pub fn from_str(nick: Str) -> PrefixedNick { + PrefixedNick { prefix: Prefix::Empty, nick } + } + + pub fn from_player_id(id: PlayerId) -> PrefixedNick { + PrefixedNick { prefix: Prefix::Empty, nick: id.into_inner() } + } +} \ No newline at end of file diff --git a/src/util/table.rs b/src/util/table.rs index 2d93866..92d2ad7 100644 --- a/src/util/table.rs +++ b/src/util/table.rs @@ -21,7 +21,7 @@ impl AnonTable { pub fn insert(&mut self, value: V) -> Key { let id = self.next; self.next += 1; - self.inner.insert(id, value); // should be always empty + self.inner.insert(id, value); // should be always Empty Key(id) } From ad49703714eea8c9e9abd0c806feb140c42c9a50 Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Fri, 22 Sep 2023 13:20:47 +0000 Subject: [PATCH 05/17] split proto irc defs into its own crate (#13) Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/13 --- Cargo.lock | 13 ++++++++ Cargo.toml | 32 +++++++++++++++---- crates/proto-irc/Cargo.toml | 14 ++++++++ .../irc => crates/proto-irc/src}/client.rs | 0 .../irc/mod.rs => crates/proto-irc/src/lib.rs | 7 ++-- crates/proto-irc/src/prelude.rs | 3 ++ .../irc => crates/proto-irc/src}/server.rs | 8 +++-- crates/proto-irc/src/testkit.rs | 16 ++++++++++ .../irc => crates/proto-irc/src}/user.rs | 12 +++---- src/projections/irc/mod.rs | 11 ++++--- src/protos/mod.rs | 1 - 11 files changed, 92 insertions(+), 25 deletions(-) create mode 100644 crates/proto-irc/Cargo.toml rename {src/protos/irc => crates/proto-irc/src}/client.rs (100%) rename src/protos/irc/mod.rs => crates/proto-irc/src/lib.rs (98%) create mode 100644 crates/proto-irc/src/prelude.rs rename {src/protos/irc => crates/proto-irc/src}/server.rs (98%) create mode 100644 crates/proto-irc/src/testkit.rs rename {src/protos/irc => crates/proto-irc/src}/user.rs (67%) diff --git a/Cargo.lock b/Cargo.lock index ad25c33..4b7207c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -763,6 +763,7 @@ dependencies = [ "nom", "nonempty", "prometheus", + "proto-irc", "quick-xml", "regex", "reqwest", @@ -1151,6 +1152,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "proto-irc" +version = "0.0.1-dev" +dependencies = [ + "anyhow", + "assert_matches", + "futures-util", + "nom", + "nonempty", + "tokio", +] + [[package]] name = "quick-xml" version = "0.30.0" diff --git a/Cargo.toml b/Cargo.toml index 7b855e3..7f6593c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,33 +1,51 @@ +[workspace] +members = [ + ".", + "crates/proto-irc" +] + +[workspace.package] +version = "0.0.1-dev" + +[workspace.dependencies] +nom = "7.1.3" +assert_matches = "1.5.0" +tokio = { version = "1.24.1", features = ["full"] } # async runtime +futures-util = "0.3.25" +anyhow = "1.0.68" # error utils +nonempty = "0.8.1" + [package] name = "lavina" -version = "0.0.1-dev" +version.workspace = true edition = "2021" publish = false [dependencies] -anyhow = "1.0.68" # error utils +anyhow.workspace = true figment = { version = "0.10.8", features = ["env", "toml"] } # configuration files hyper = { version = "1.0.0-rc.3,<1.0.0-rc.4", features = ["server", "http1"] } # http server http-body-util = "0.1.0-rc.3" serde = { version = "1.0.152", features = ["rc", "serde_derive"] } serde_json = "1.0.93" -tokio = { version = "1.24.1", features = ["full"] } # async runtime +tokio.workspace = true tracing = "0.1.37" # logging & tracing api tracing-subscriber = "0.3.16" -futures-util = "0.3.25" +futures-util.workspace = true prometheus = { version = "0.13.3", default-features = false } regex = "1.7.1" lazy_static = "1.4.0" -nom = "7.1.3" -nonempty = "0.8.1" +nom.workspace = true +nonempty.workspace = true tokio-rustls = "0.24.1" rustls-pemfile = "1.0.2" quick-xml = { version = "0.30.0", features = ["async-tokio"] } derive_more = "0.99.17" uuid = { version = "1.3.0", features = ["v4"] } sqlx = { version = "0.7.0-alpha.2", features = ["sqlite", "migrate"] } +proto-irc = { path = "crates/proto-irc" } [dev-dependencies] -assert_matches = "1.5.0" +assert_matches.workspace = true regex = "1.7.1" reqwest = { version = "0.11", default-features = false } diff --git a/crates/proto-irc/Cargo.toml b/crates/proto-irc/Cargo.toml new file mode 100644 index 0000000..e2225a7 --- /dev/null +++ b/crates/proto-irc/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "proto-irc" +edition = "2021" +version.workspace = true + +[dependencies] +nom.workspace = true +nonempty.workspace = true +tokio.workspace = true +futures-util.workspace = true +anyhow.workspace = true + +[dev-dependencies] +assert_matches.workspace = true diff --git a/src/protos/irc/client.rs b/crates/proto-irc/src/client.rs similarity index 100% rename from src/protos/irc/client.rs rename to crates/proto-irc/src/client.rs diff --git a/src/protos/irc/mod.rs b/crates/proto-irc/src/lib.rs similarity index 98% rename from src/protos/irc/mod.rs rename to crates/proto-irc/src/lib.rs index 0997a79..cd1f64f 100644 --- a/src/protos/irc/mod.rs +++ b/crates/proto-irc/src/lib.rs @@ -1,10 +1,13 @@ //! Client-to-Server IRC protocol. pub mod client; +mod prelude; pub mod server; +#[cfg(test)] +mod testkit; pub mod user; -use std::io::Result; use crate::prelude::Str; +use std::io::Result; use nom::{ branch::alt, @@ -101,7 +104,7 @@ mod test { use assert_matches::*; use super::*; - use crate::util::testkit::*; + use crate::testkit::*; #[test] fn test_chan_global() { diff --git a/crates/proto-irc/src/prelude.rs b/crates/proto-irc/src/prelude.rs new file mode 100644 index 0000000..04c4d62 --- /dev/null +++ b/crates/proto-irc/src/prelude.rs @@ -0,0 +1,3 @@ +use std::sync::Arc; + +pub type Str = Arc; diff --git a/src/protos/irc/server.rs b/crates/proto-irc/src/server.rs similarity index 98% rename from src/protos/irc/server.rs rename to crates/proto-irc/src/server.rs index 194c756..1bead65 100644 --- a/src/protos/irc/server.rs +++ b/crates/proto-irc/src/server.rs @@ -3,7 +3,7 @@ use tokio::io::AsyncWrite; use tokio::io::AsyncWriteExt; use super::*; -use crate::protos::irc::user::PrefixedNick; +use crate::user::PrefixedNick; /// Server-to-client message. #[derive(Clone, Debug, PartialEq, Eq)] @@ -287,7 +287,9 @@ impl ServerMessageBody { chan.write_async(writer).await?; writer.write_all(b" :").await?; for member in members { - writer.write_all(member.prefix.to_string().as_bytes()).await?; + writer + .write_all(member.prefix.to_string().as_bytes()) + .await?; writer.write_all(member.nick.as_bytes()).await?; writer.write_all(b" ").await?; } @@ -387,7 +389,7 @@ mod test { use assert_matches::*; use super::*; - use crate::util::testkit::*; + use crate::testkit::*; #[test] fn test_server_message_notice() { diff --git a/crates/proto-irc/src/testkit.rs b/crates/proto-irc/src/testkit.rs new file mode 100644 index 0000000..57949d0 --- /dev/null +++ b/crates/proto-irc/src/testkit.rs @@ -0,0 +1,16 @@ +use std::future::Future; +use std::task::{Context, Poll}; + +use futures_util::task::noop_waker_ref; +use tokio::pin; + +pub fn sync_future(future: impl Future) -> anyhow::Result { + let waker = noop_waker_ref(); + let mut context = Context::from_waker(waker); + pin!(future); + if let Poll::Ready(a) = future.poll(&mut context) { + Ok(a) + } else { + Err(anyhow::Error::msg("Future has suspended")) + } +} diff --git a/src/protos/irc/user.rs b/crates/proto-irc/src/user.rs similarity index 67% rename from src/protos/irc/user.rs rename to crates/proto-irc/src/user.rs index e426812..3998fc2 100644 --- a/src/protos/irc/user.rs +++ b/crates/proto-irc/src/user.rs @@ -1,6 +1,5 @@ use super::*; use std::fmt; -use crate::core::player::PlayerId; #[derive(Clone, Debug, PartialEq, Eq)] pub enum Prefix { @@ -23,10 +22,9 @@ pub struct PrefixedNick { impl PrefixedNick { pub fn from_str(nick: Str) -> PrefixedNick { - PrefixedNick { prefix: Prefix::Empty, nick } + PrefixedNick { + prefix: Prefix::Empty, + nick, + } } - - pub fn from_player_id(id: PlayerId) -> PrefixedNick { - PrefixedNick { prefix: Prefix::Empty, nick: id.into_inner() } - } -} \ No newline at end of file +} diff --git a/src/projections/irc/mod.rs b/src/projections/irc/mod.rs index 43e580b..96dc877 100644 --- a/src/projections/irc/mod.rs +++ b/src/projections/irc/mod.rs @@ -5,6 +5,7 @@ use futures_util::future::join_all; use nonempty::nonempty; use nonempty::NonEmpty; use prometheus::{IntCounter, IntGauge, Registry as MetricsRegistry}; + use serde::Deserialize; use tokio::io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter}; use tokio::net::tcp::{ReadHalf, WriteHalf}; @@ -15,10 +16,10 @@ use crate::core::player::*; use crate::core::repo::Storage; use crate::core::room::{RoomId, RoomInfo, RoomRegistry}; use crate::prelude::*; -use crate::protos::irc::client::{client_message, ClientMessage}; -use crate::protos::irc::server::{AwayStatus, ServerMessage, ServerMessageBody}; -use crate::protos::irc::{Chan, Recipient}; -use crate::protos::irc::user::PrefixedNick; +use proto_irc::client::{client_message, ClientMessage}; +use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody}; +use proto_irc::{Chan, Recipient}; +use proto_irc::user::{Prefix, PrefixedNick}; use crate::util::Terminator; #[cfg(test)] @@ -669,7 +670,7 @@ async fn produce_on_join_cmd_messages( } .write_async(writer) .await?; - let prefixed_members: Vec = room_info.members.iter().map(|member| PrefixedNick::from_player_id(member.clone())).collect(); + let prefixed_members: Vec = room_info.members.iter().map(|member| PrefixedNick::from_str(member.clone().into_inner())).collect(); let non_empty_members: NonEmpty = NonEmpty::from_vec(prefixed_members).unwrap_or(nonempty![PrefixedNick::from_str(user.nickname.clone())]); ServerMessage { diff --git a/src/protos/mod.rs b/src/protos/mod.rs index 25f58e2..c8c39c3 100644 --- a/src/protos/mod.rs +++ b/src/protos/mod.rs @@ -1,3 +1,2 @@ //! Definitions of wire protocols to be used in implementations of projections. -pub mod irc; pub mod xmpp; From 3d59f6aae55a7d78f4ab04a5cc6fc7c21c08c9ac Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Fri, 22 Sep 2023 17:24:36 +0200 Subject: [PATCH 06/17] read config path from cli args --- Cargo.lock | 107 ++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + crates/proto-irc/Cargo.toml | 1 + dist/alpine3.18.Dockerfile | 2 +- src/main.rs | 14 +++-- 5 files changed, 121 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b7207c..cd416ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,54 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -161,6 +209,52 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "4.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "const-oid" version = "0.9.5" @@ -754,6 +848,7 @@ version = "0.0.1-dev" dependencies = [ "anyhow", "assert_matches", + "clap", "derive_more", "figment", "futures-util", @@ -1783,6 +1878,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + [[package]] name = "subtle" version = "2.5.0" @@ -2093,6 +2194,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.4.1" diff --git a/Cargo.toml b/Cargo.toml index 7f6593c..f2d6e9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ tokio = { version = "1.24.1", features = ["full"] } # async runtime futures-util = "0.3.25" anyhow = "1.0.68" # error utils nonempty = "0.8.1" +clap = { version = "4.4.4", features = ["derive"] } [package] name = "lavina" @@ -44,6 +45,7 @@ derive_more = "0.99.17" uuid = { version = "1.3.0", features = ["v4"] } sqlx = { version = "0.7.0-alpha.2", features = ["sqlite", "migrate"] } proto-irc = { path = "crates/proto-irc" } +clap.workspace = true [dev-dependencies] assert_matches.workspace = true diff --git a/crates/proto-irc/Cargo.toml b/crates/proto-irc/Cargo.toml index e2225a7..4a5286e 100644 --- a/crates/proto-irc/Cargo.toml +++ b/crates/proto-irc/Cargo.toml @@ -2,6 +2,7 @@ name = "proto-irc" edition = "2021" version.workspace = true +publish = false [dependencies] nom.workspace = true diff --git a/dist/alpine3.18.Dockerfile b/dist/alpine3.18.Dockerfile index 3a4a5d4..facdfb0 100644 --- a/dist/alpine3.18.Dockerfile +++ b/dist/alpine3.18.Dockerfile @@ -8,4 +8,4 @@ FROM alpine:3.18@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4 COPY --from=bld target/release/lavina /usr/bin/lavina VOLUME ["/etc/lavina/", "/var/lib/lavina/"] -ENTRYPOINT ["lavina"] +ENTRYPOINT ["lavina", "--config", "/etc/lavina/config.toml"] diff --git a/src/main.rs b/src/main.rs index cfc3955..b6f19da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,9 @@ mod protos; mod util; use std::future::Future; +use std::path::Path; +use clap::Parser; use figment::providers::Format; use figment::{providers::Toml, Figment}; use prometheus::Registry as MetricsRegistry; @@ -31,11 +33,15 @@ struct ServerConfig { storage: core::repo::StorageConfig, } +#[derive(Parser)] +struct CliArgs { + #[arg(long)] + config: Box, +} + fn load_config() -> Result { - // TODO get config path as a cmd line arg - let raw_config = Figment::new() - .merge(Toml::file("config.toml")) - .merge(Toml::file("/etc/lavina/config.toml")); + let args = CliArgs::parse(); + let raw_config = Figment::from(Toml::file(args.config)); let config: ServerConfig = raw_config.extract()?; Ok(config) } From df6cdd4861747f46092df376a8509af8ba122149 Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Sat, 23 Sep 2023 01:12:03 +0200 Subject: [PATCH 07/17] add a bit more logging --- src/projections/irc/mod.rs | 6 +++++- src/projections/xmpp/mod.rs | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/projections/irc/mod.rs b/src/projections/irc/mod.rs index 96dc877..a98394b 100644 --- a/src/projections/irc/mod.rs +++ b/src/projections/irc/mod.rs @@ -52,6 +52,7 @@ async fn handle_socket( termination: Deferred<()>, // TODO use it to stop the connection gracefully mut storage: Storage, ) -> Result<()> { + log::info!("Received an IRC connection from {socket_addr}"); let (reader, writer) = stream.split(); let mut reader: BufReader = BufReader::new(reader); let mut writer = BufWriter::new(writer); @@ -74,10 +75,13 @@ async fn handle_socket( match registered_user { Ok(user) => { + log::debug!("User registered"); handle_registered_socket(config, players, rooms, &mut reader, &mut writer, user) .await?; } - Err(_) => {} + Err(_) => { + log::debug!("Registration failed"); + } } stream.shutdown().await?; diff --git a/src/projections/xmpp/mod.rs b/src/projections/xmpp/mod.rs index a02ceca..fb62e82 100644 --- a/src/projections/xmpp/mod.rs +++ b/src/projections/xmpp/mod.rs @@ -149,7 +149,7 @@ async fn handle_socket( rooms: RoomRegistry, termination: Deferred<()>, // TODO use it to stop the connection gracefully ) -> Result<()> { - log::debug!("Received an XMPP connection from {socket_addr}"); + log::info!("Received an XMPP connection from {socket_addr}"); let mut reader_buf = vec![]; let (reader, writer) = stream.split(); let mut buf_reader = BufReader::new(reader); @@ -172,6 +172,7 @@ async fn handle_socket( let mut xml_writer = Writer::new(b); let authenticated = socket_auth(&mut xml_reader, &mut xml_writer, &mut reader_buf).await?; + log::debug!("User authenticated"); let mut connection = players .connect_to_player(authenticated.player_id.clone()) .await; @@ -314,7 +315,6 @@ async fn socket_final( match parser.consume(ns, &event) { Continuation::Final(res) => { let res = res?; - dbg!(&res); let stop = handle_packet(&mut events, res, authenticated, user_handle, rooms).await?; for i in &events { xml_writer.write_event_async(i).await?; From 58f6a5d90ab08b2610a227f78564e565558b0c4c Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Sun, 24 Sep 2023 22:59:34 +0200 Subject: [PATCH 08/17] add management API endpoints --- Cargo.lock | 8 ++ Cargo.toml | 7 +- crates/mgmt-api/Cargo.toml | 8 ++ crates/mgmt-api/src/lib.rs | 29 +++++++ rustfmt.toml | 1 + src/core/repo/mod.rs | 46 ++++++++++- src/main.rs | 2 +- src/util/http.rs | 10 --- src/util/mod.rs | 1 - src/util/telemetry.rs | 151 +++++++++++++++++++++++++++++++------ 10 files changed, 223 insertions(+), 40 deletions(-) create mode 100644 crates/mgmt-api/Cargo.toml create mode 100644 crates/mgmt-api/src/lib.rs create mode 100644 rustfmt.toml delete mode 100644 src/util/http.rs diff --git a/Cargo.lock b/Cargo.lock index cd416ce..80a40aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -855,6 +855,7 @@ dependencies = [ "http-body-util", "hyper 1.0.0-rc.3", "lazy_static", + "mgmt-api", "nom", "nonempty", "prometheus", @@ -942,6 +943,13 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "mgmt-api" +version = "0.0.1-dev" +dependencies = [ + "serde", +] + [[package]] name = "mime" version = "0.3.17" diff --git a/Cargo.toml b/Cargo.toml index f2d6e9e..f8f0d55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [workspace] members = [ ".", - "crates/proto-irc" + "crates/proto-irc", + "crates/mgmt-api", ] [workspace.package] @@ -15,6 +16,7 @@ futures-util = "0.3.25" anyhow = "1.0.68" # error utils nonempty = "0.8.1" clap = { version = "4.4.4", features = ["derive"] } +serde = { version = "1.0.152", features = ["rc", "serde_derive"] } [package] name = "lavina" @@ -27,7 +29,7 @@ anyhow.workspace = true figment = { version = "0.10.8", features = ["env", "toml"] } # configuration files hyper = { version = "1.0.0-rc.3,<1.0.0-rc.4", features = ["server", "http1"] } # http server http-body-util = "0.1.0-rc.3" -serde = { version = "1.0.152", features = ["rc", "serde_derive"] } +serde.workspace = true serde_json = "1.0.93" tokio.workspace = true tracing = "0.1.37" # logging & tracing api @@ -45,6 +47,7 @@ derive_more = "0.99.17" uuid = { version = "1.3.0", features = ["v4"] } sqlx = { version = "0.7.0-alpha.2", features = ["sqlite", "migrate"] } proto-irc = { path = "crates/proto-irc" } +mgmt-api = { path = "crates/mgmt-api" } clap.workspace = true [dev-dependencies] diff --git a/crates/mgmt-api/Cargo.toml b/crates/mgmt-api/Cargo.toml new file mode 100644 index 0000000..030f3cf --- /dev/null +++ b/crates/mgmt-api/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "mgmt-api" +edition = "2021" +version.workspace = true +publish = false + +[dependencies] +serde.workspace = true diff --git a/crates/mgmt-api/src/lib.rs b/crates/mgmt-api/src/lib.rs new file mode 100644 index 0000000..cfe5b69 --- /dev/null +++ b/crates/mgmt-api/src/lib.rs @@ -0,0 +1,29 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct ErrorResponse<'a> { + pub code: &'a str, + pub message: &'a str, +} + +#[derive(Serialize, Deserialize)] +pub struct CreatePlayerRequest<'a> { + pub name: &'a str, +} + +#[derive(Serialize, Deserialize)] +pub struct ChangePasswordRequest<'a> { + pub player_name: &'a str, + pub password: &'a str, +} + +pub mod paths { + pub const CREATE_PLAYER: &'static str = "/mgmt/create_player"; + pub const SET_PASSWORD: &'static str = "/mgmt/set_password"; +} + +pub mod errors { + pub const INVALID_PATH: &'static str = "invalid_path"; + pub const MALFORMED_REQUEST: &'static str = "malformed_request"; + pub const PLAYER_NOT_FOUND: &'static str = "player_not_found"; +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..866c756 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 120 \ No newline at end of file diff --git a/src/core/repo/mod.rs b/src/core/repo/mod.rs index 10b601a..57371ed 100644 --- a/src/core/repo/mod.rs +++ b/src/core/repo/mod.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use serde::Deserialize; use sqlx::sqlite::SqliteConnectOptions; -use sqlx::{ConnectOptions, Connection, FromRow, SqliteConnection}; +use sqlx::{ConnectOptions, Connection, FromRow, Sqlite, SqliteConnection, Transaction}; use tokio::sync::Mutex; use crate::prelude::*; @@ -102,6 +102,50 @@ impl Storage { res.close().await?; Ok(()) } + + pub async fn create_user(&mut self, name: &str) -> Result<()> { + let query = sqlx::query( + "insert into users(name) + values (?);", + ) + .bind(name); + let mut executor = self.conn.lock().await; + query.execute(&mut *executor).await?; + + Ok(()) + } + + pub async fn set_password<'a>(&'a mut self, name: &'a str, pwd: &'a str) -> Result> { + async fn inner(txn: &mut Transaction<'_, Sqlite>, name: &str, pwd: &str) -> Result> { + let id: Option<(u32,)> = sqlx::query_as("select * from users where name = ? limit 1;") + .bind(name) + .fetch_optional(&mut **txn) + .await?; + let Some((id,)) = id else { + return Ok(None); + }; + sqlx::query("insert or replace into challenges_plain_password(user_id, password) values (?, ?);") + .bind(id) + .bind(pwd) + .execute(&mut **txn) + .await?; + Ok(Some(())) + } + + let mut executor = self.conn.lock().await; + let mut tx = executor.begin().await?; + let res = inner(&mut tx, name, pwd).await; + match res { + Ok(e) => { + tx.commit().await?; + Ok(e) + } + Err(e) => { + tx.rollback().await?; + Err(e) + } + } + } } #[derive(FromRow)] diff --git a/src/main.rs b/src/main.rs index b6f19da..d10c9ee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,7 +65,7 @@ async fn main() -> Result<()> { let rooms = RoomRegistry::new(&mut metrics, storage.clone())?; let mut players = PlayerRegistry::empty(rooms.clone(), &mut metrics)?; let telemetry_terminator = - util::telemetry::launch(telemetry_config, metrics.clone(), rooms.clone()).await?; + util::telemetry::launch(telemetry_config, metrics.clone(), rooms.clone(), storage.clone()).await?; let irc = projections::irc::launch(irc_config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await?; let xmpp = projections::xmpp::launch(xmpp_config, players.clone(), rooms.clone(), metrics.clone()).await?; tracing::info!("Started"); diff --git a/src/util/http.rs b/src/util/http.rs deleted file mode 100644 index 1f79e32..0000000 --- a/src/util/http.rs +++ /dev/null @@ -1,10 +0,0 @@ -use std::convert::Infallible; - -use http_body_util::Full; -use hyper::{body::Bytes, Response, StatusCode}; - -pub fn not_found() -> std::result::Result>, Infallible> { - let mut response = Response::new(Full::new(Bytes::from("404"))); - *response.status_mut() = StatusCode::NOT_FOUND; - Ok(response) -} diff --git a/src/util/mod.rs b/src/util/mod.rs index 0593dbd..2fde69e 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -pub mod http; pub mod table; pub mod telemetry; #[cfg(test)] diff --git a/src/util/telemetry.rs b/src/util/telemetry.rs index 26387dd..3bb5ec9 100644 --- a/src/util/telemetry.rs +++ b/src/util/telemetry.rs @@ -6,18 +6,18 @@ use http_body_util::{BodyExt, Full}; use hyper::body::Bytes; use hyper::server::conn::http1; use hyper::service::service_fn; -use hyper::{Method, Request, Response}; +use hyper::{Method, Request, Response, StatusCode}; use prometheus::{Encoder, Registry as MetricsRegistry, TextEncoder}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; +use crate::core::repo::Storage; use crate::core::room::RoomRegistry; use crate::prelude::*; - -use crate::util::http::*; use crate::util::Terminator; -type BoxBody = http_body_util::combinators::BoxBody; +use mgmt_api::*; + type HttpResult = std::result::Result; #[derive(Deserialize, Debug)] @@ -29,11 +29,12 @@ pub async fn launch( config: ServerConfig, metrics: MetricsRegistry, rooms: RoomRegistry, + storage: Storage, ) -> Result { log::info!("Starting the telemetry service"); let listener = TcpListener::bind(config.listen_on).await?; log::debug!("Listener started"); - let terminator = Terminator::spawn(|rx| main_loop(listener, metrics, rooms, rx.map(|_| ()))); + let terminator = Terminator::spawn(|rx| main_loop(listener, metrics, rooms, storage, rx.map(|_| ()))); Ok(terminator) } @@ -41,6 +42,7 @@ async fn main_loop( listener: TcpListener, metrics: MetricsRegistry, rooms: RoomRegistry, + storage: Storage, termination: impl Future, ) -> Result<()> { pin!(termination); @@ -52,10 +54,12 @@ async fn main_loop( let (stream, _) = result?; let metrics = metrics.clone(); let rooms = rooms.clone(); + let storage = storage.clone(); tokio::task::spawn(async move { let registry = metrics.clone(); let rooms = rooms.clone(); - let server = http1::Builder::new().serve_connection(stream, service_fn(move |r| route(registry.clone(), rooms.clone(), r))); + let storage = storage.clone(); + let server = http1::Builder::new().serve_connection(stream, service_fn(move |r| route(registry.clone(), rooms.clone(), storage.clone(), r))); if let Err(err) = server.await { tracing::error!("Error serving connection: {:?}", err); } @@ -70,27 +74,124 @@ async fn main_loop( async fn route( registry: MetricsRegistry, rooms: RoomRegistry, + storage: Storage, request: Request, -) -> std::result::Result, Infallible> { - match (request.method(), request.uri().path()) { - (&Method::GET, "/metrics") => Ok(endpoint_metrics(registry)?.map(BodyExt::boxed)), - (&Method::GET, "/rooms") => Ok(endpoint_rooms(rooms).await?.map(BodyExt::boxed)), - _ => Ok(not_found()?.map(BodyExt::boxed)), +) -> HttpResult>> { + let res = match (request.method(), request.uri().path()) { + (&Method::GET, "/metrics") => endpoint_metrics(registry), + (&Method::GET, "/rooms") => endpoint_rooms(rooms).await, + (&Method::POST, paths::CREATE_PLAYER) => endpoint_create_player(request, storage).await.or5xx(), + (&Method::POST, paths::SET_PASSWORD) => endpoint_set_password(request, storage).await.or5xx(), + _ => not_found(), + }; + Ok(res) +} + +fn endpoint_metrics(registry: MetricsRegistry) -> Response> { + let mf = registry.gather(); + let mut buffer = vec![]; + TextEncoder.encode(&mf, &mut buffer).expect("write to vec cannot fail"); + Response::new(Full::new(Bytes::from(buffer))) +} + +async fn endpoint_rooms(rooms: RoomRegistry) -> Response> { + // TODO introduce management API types independent from core-domain types + // TODO remove `Serialize` implementations from all core-domain types + let room_list = rooms.get_all_rooms().await.to_body(); + Response::new(room_list) +} + +async fn endpoint_create_player( + request: Request, + mut storage: Storage, +) -> Result>> { + let str = request.collect().await?.to_bytes(); + let Ok(res) = serde_json::from_slice::(&str[..]) else { + let payload = ErrorResponse { + code: errors::MALFORMED_REQUEST, + message: "The request payload contains incorrect JSON value", + } + .to_body(); + let mut response = Response::new(payload); + *response.status_mut() = StatusCode::BAD_REQUEST; + return Ok(response); + }; + storage.create_user(&res.name).await?; + log::info!("Player {} created", res.name); + let mut response = Response::new(Full::::default()); + *response.status_mut() = StatusCode::CREATED; + Ok(response) +} + +async fn endpoint_set_password( + request: Request, + mut storage: Storage, +) -> Result>> { + let str = request.collect().await?.to_bytes(); + let Ok(res) = serde_json::from_slice::(&str[..]) else { + let payload = ErrorResponse { + code: errors::MALFORMED_REQUEST, + message: "The request payload contains incorrect JSON value", + } + .to_body(); + let mut response = Response::new(payload); + *response.status_mut() = StatusCode::BAD_REQUEST; + return Ok(response); + }; + let Some(_) = storage.set_password(&res.player_name, &res.password).await? else { + let payload = ErrorResponse { + code: errors::PLAYER_NOT_FOUND, + message: "No such player exists", + } + .to_body(); + let mut response = Response::new(payload); + *response.status_mut() = StatusCode::UNPROCESSABLE_ENTITY; + return Ok(response); + }; + log::info!("Password changed for player {}", res.player_name); + let mut response = Response::new(Full::::default()); + *response.status_mut() = StatusCode::NO_CONTENT; + Ok(response) +} + +pub fn not_found() -> Response> { + let payload = ErrorResponse { + code: errors::INVALID_PATH, + message: "The path does not exist", + } + .to_body(); + + let mut response = Response::new(payload); + *response.status_mut() = StatusCode::NOT_FOUND; + response +} + +trait Or5xx { + fn or5xx(self) -> Response>; +} +impl Or5xx for Result>> { + fn or5xx(self) -> Response> { + match self { + Ok(e) => e, + Err(e) => { + let mut response = Response::new(Full::new(e.to_string().into())); + *response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR; + response + } + } } } -fn endpoint_metrics(registry: MetricsRegistry) -> HttpResult>> { - let mf = registry.gather(); - let mut buffer = vec![]; - TextEncoder - .encode(&mf, &mut buffer) - .expect("write to vec cannot fail"); - Ok(Response::new(Full::new(Bytes::from(buffer)))) +trait ToBody { + fn to_body(&self) -> Full; } - -async fn endpoint_rooms(rooms: RoomRegistry) -> HttpResult>> { - let room_list = rooms.get_all_rooms().await; - let mut buffer = vec![]; - serde_json::to_writer(&mut buffer, &room_list).expect("unexpected fail when writing to vec"); - Ok(Response::new(Full::new(Bytes::from(buffer)))) +impl ToBody for T +where + T: Serialize, +{ + fn to_body(&self) -> Full { + let mut buffer = vec![]; + serde_json::to_writer(&mut buffer, self).expect("unexpected fail when writing to vec"); + Full::new(Bytes::from(buffer)) + } } From 563811cbca4e81a118bef392dbbf500376b8249b Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Sat, 30 Sep 2023 15:38:51 +0000 Subject: [PATCH 09/17] pull-request workflow (#15) --- .gitea/workflows/test-pr.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .gitea/workflows/test-pr.yml diff --git a/.gitea/workflows/test-pr.yml b/.gitea/workflows/test-pr.yml new file mode 100644 index 0000000..18dca5b --- /dev/null +++ b/.gitea/workflows/test-pr.yml @@ -0,0 +1,19 @@ +name: check-and-test +on: [push, pull_request] +jobs: + check-and-test: + runs-on: ubuntu-latest + steps: + - name: git checkout repository + uses: actions/checkout@v4 + - name: setup rust + uses: https://github.com/actions-rs/toolchain@v1 + - name: cargo check + uses: https://github.com/actions-rs/cargo@v1 + with: + command: check + - name: test + uses: https://github.com/actions-rs/cargo@v1 + with: + command: test + args: --workspace -- --skip projections::irc From 444b608e9638a0dc37b4bf280737edbeed620230 Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Sat, 30 Sep 2023 15:43:46 +0000 Subject: [PATCH 10/17] split proto xmpp defs into its own crate (#14) Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/14 --- Cargo.lock | 15 +++++++- Cargo.toml | 13 ++++--- crates/proto-xmpp/Cargo.toml | 12 ++++++ .../xmpp => crates/proto-xmpp/src}/bind.rs | 24 ++++++------ .../xmpp => crates/proto-xmpp/src}/client.rs | 13 ++++--- .../xmpp => crates/proto-xmpp/src}/disco.rs | 4 +- .../mod.rs => crates/proto-xmpp/src/lib.rs | 11 +++++- .../xmpp => crates/proto-xmpp/src}/muc/mod.rs | 20 +++++----- crates/proto-xmpp/src/prelude.rs | 3 ++ .../xmpp => crates/proto-xmpp/src}/roster.rs | 4 +- .../xmpp => crates/proto-xmpp/src}/sasl.rs | 12 +++--- .../xmpp => crates/proto-xmpp/src}/session.rs | 8 ++-- .../proto-xmpp/src}/stanzaerror.rs | 0 .../xmpp => crates/proto-xmpp/src}/stream.rs | 6 +-- .../xmpp => crates/proto-xmpp/src}/tls.rs | 5 ++- .../proto-xmpp/src}/xml/ignore.rs | 0 .../proto-xmpp/src/xml/mod.rs | 8 ++-- src/main.rs | 1 - src/projections/xmpp/mod.rs | 37 +++++++++---------- src/projections/xmpp/proto.rs | 12 +++--- src/protos/mod.rs | 2 - src/util/mod.rs | 1 - 22 files changed, 125 insertions(+), 86 deletions(-) create mode 100644 crates/proto-xmpp/Cargo.toml rename {src/protos/xmpp => crates/proto-xmpp/src}/bind.rs (89%) rename {src/protos/xmpp => crates/proto-xmpp/src}/client.rs (99%) rename {src/protos/xmpp => crates/proto-xmpp/src}/disco.rs (99%) rename src/protos/xmpp/mod.rs => crates/proto-xmpp/src/lib.rs (78%) rename {src/protos/xmpp => crates/proto-xmpp/src}/muc/mod.rs (86%) create mode 100644 crates/proto-xmpp/src/prelude.rs rename {src/protos/xmpp => crates/proto-xmpp/src}/roster.rs (96%) rename {src/protos/xmpp => crates/proto-xmpp/src}/sasl.rs (83%) rename {src/protos/xmpp => crates/proto-xmpp/src}/session.rs (86%) rename {src/protos/xmpp => crates/proto-xmpp/src}/stanzaerror.rs (100%) rename {src/protos/xmpp => crates/proto-xmpp/src}/stream.rs (98%) rename {src/protos/xmpp => crates/proto-xmpp/src}/tls.rs (91%) rename {src/util => crates/proto-xmpp/src}/xml/ignore.rs (100%) rename src/util/xml.rs => crates/proto-xmpp/src/xml/mod.rs (96%) delete mode 100644 src/protos/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 80a40aa..2469e90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -854,12 +854,11 @@ dependencies = [ "futures-util", "http-body-util", "hyper 1.0.0-rc.3", - "lazy_static", "mgmt-api", - "nom", "nonempty", "prometheus", "proto-irc", + "proto-xmpp", "quick-xml", "regex", "reqwest", @@ -1267,6 +1266,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "proto-xmpp" +version = "0.0.1-dev" +dependencies = [ + "anyhow", + "derive_more", + "lazy_static", + "quick-xml", + "regex", + "tokio", +] + [[package]] name = "quick-xml" version = "0.30.0" diff --git a/Cargo.toml b/Cargo.toml index f8f0d55..4c90804 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ ".", "crates/proto-irc", + "crates/proto-xmpp", "crates/mgmt-api", ] @@ -15,6 +16,10 @@ tokio = { version = "1.24.1", features = ["full"] } # async runtime futures-util = "0.3.25" anyhow = "1.0.68" # error utils nonempty = "0.8.1" +quick-xml = { version = "0.30.0", features = ["async-tokio"] } +lazy_static = "1.4.0" +regex = "1.7.1" +derive_more = "0.99.17" clap = { version = "4.4.4", features = ["derive"] } serde = { version = "1.0.152", features = ["rc", "serde_derive"] } @@ -36,17 +41,15 @@ tracing = "0.1.37" # logging & tracing api tracing-subscriber = "0.3.16" futures-util.workspace = true prometheus = { version = "0.13.3", default-features = false } -regex = "1.7.1" -lazy_static = "1.4.0" -nom.workspace = true nonempty.workspace = true tokio-rustls = "0.24.1" rustls-pemfile = "1.0.2" -quick-xml = { version = "0.30.0", features = ["async-tokio"] } -derive_more = "0.99.17" +quick-xml.workspace = true +derive_more.workspace = true uuid = { version = "1.3.0", features = ["v4"] } sqlx = { version = "0.7.0-alpha.2", features = ["sqlite", "migrate"] } proto-irc = { path = "crates/proto-irc" } +proto-xmpp = { path = "crates/proto-xmpp" } mgmt-api = { path = "crates/mgmt-api" } clap.workspace = true diff --git a/crates/proto-xmpp/Cargo.toml b/crates/proto-xmpp/Cargo.toml new file mode 100644 index 0000000..d8b28f9 --- /dev/null +++ b/crates/proto-xmpp/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "proto-xmpp" +edition = "2021" +version.workspace = true + +[dependencies] +quick-xml.workspace = true +lazy_static.workspace = true +regex.workspace = true +anyhow.workspace = true +tokio.workspace = true +derive_more.workspace = true diff --git a/src/protos/xmpp/bind.rs b/crates/proto-xmpp/src/bind.rs similarity index 89% rename from src/protos/xmpp/bind.rs rename to crates/proto-xmpp/src/bind.rs index 35d0dfc..e6ef5d9 100644 --- a/src/protos/xmpp/bind.rs +++ b/crates/proto-xmpp/src/bind.rs @@ -1,11 +1,11 @@ use std::fmt::Display; -use nom::AsBytes; +use anyhow::{anyhow, Result}; use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use quick_xml::name::{Namespace, ResolveResult}; use crate::prelude::*; -use crate::util::xml::*; +use crate::xml::*; pub const XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-bind"; @@ -49,7 +49,7 @@ impl Jid { } let m = RE .captures(i) - .ok_or(ffail!("Incorrectly format jid: {i}"))?; + .ok_or(anyhow!("Incorrectly format jid: {i}"))?; let name = m.get(2).map(|name| Name(name.as_str().into())); let server = m.get(3).unwrap(); @@ -117,19 +117,19 @@ impl Parser for BindRequestParser { match self.0 { Initial => { let Event::Start(bytes) = event else { - return Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))); + return Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))); }; if bytes.name().0 != BindRequest::NAME.as_bytes() { - return Continuation::Final(Err(ffail!( + return Continuation::Final(Err(anyhow!( "Unexpected XML tag: {:?}", bytes.name() ))); } let ResolveResult::Bound(Namespace(ns)) = namespace else { - return Continuation::Final(Err(ffail!("No namespace provided"))); + return Continuation::Final(Err(anyhow!("No namespace provided"))); }; if ns != XMLNS.as_bytes() { - return Continuation::Final(Err(ffail!("Incorrect namespace"))); + return Continuation::Final(Err(anyhow!("Incorrect namespace"))); } Continuation::Continue(BindRequestParser(InBind(None))) } @@ -139,17 +139,17 @@ impl Parser for BindRequestParser { } Event::End(bytes) => { let Some(resource) = resource else { - return Continuation::Final(Err(ffail!("No resource was provided"))); + return Continuation::Final(Err(anyhow!("No resource was provided"))); }; Continuation::Final(Ok(BindRequest(Resource(resource.into())))) } - _ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))), + _ => Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))), }, InBindResourceInitial => { let Event::Text(text) = event else { - return Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))); + return Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))); }; - let resource = match std::str::from_utf8(text.as_bytes()) { + let resource = match std::str::from_utf8(&*text) { Ok(e) => e.to_string(), Err(err) => return Continuation::Final(Err(err.into())), }; @@ -157,7 +157,7 @@ impl Parser for BindRequestParser { } InBindResourceEnd(resource) => { let Event::End(bytes) = event else { - return Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))); + return Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))); }; Continuation::Continue(BindRequestParser(InBind(Some(resource)))) } diff --git a/src/protos/xmpp/client.rs b/crates/proto-xmpp/src/client.rs similarity index 99% rename from src/protos/xmpp/client.rs rename to crates/proto-xmpp/src/client.rs index 606e03c..2a6697f 100644 --- a/src/protos/xmpp/client.rs +++ b/crates/proto-xmpp/src/client.rs @@ -3,8 +3,11 @@ use quick_xml::events::attributes::Attribute; use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; use quick_xml::name::{QName, ResolveResult}; +use anyhow::{Result, anyhow as ffail}; + + use crate::prelude::*; -use crate::util::xml::*; +use crate::xml::*; use super::bind::Jid; @@ -308,9 +311,9 @@ impl Parser for IqParser { } IqParserInner::Final(state) => { if let Event::End(ref bytes) = event { - let id = fail_fast!(state.id.ok_or_else(|| fail("No id provided"))); - let r#type = fail_fast!(state.r#type.ok_or_else(|| fail("No type provided"))); - let body = fail_fast!(state.body.ok_or_else(|| fail("No body provided"))); + let id = fail_fast!(state.id.ok_or_else(|| ffail!("No id provided"))); + let r#type = fail_fast!(state.r#type.ok_or_else(|| ffail!("No type provided"))); + let body = fail_fast!(state.body.ok_or_else(|| ffail!("No body provided"))); Continuation::Final(Ok(Iq { from: state.from, id, @@ -579,7 +582,7 @@ impl ToXml for Presence { #[cfg(test)] mod tests { - use crate::protos::xmpp::bind::{BindRequest, Name, Resource, Server}; + use crate::bind::{BindRequest, Name, Resource, Server}; use super::*; use quick_xml::NsReader; diff --git a/src/protos/xmpp/disco.rs b/crates/proto-xmpp/src/disco.rs similarity index 99% rename from src/protos/xmpp/disco.rs rename to crates/proto-xmpp/src/disco.rs index a15e610..973242d 100644 --- a/src/protos/xmpp/disco.rs +++ b/crates/proto-xmpp/src/disco.rs @@ -2,8 +2,8 @@ use quick_xml::events::attributes::Attribute; use quick_xml::events::{BytesEnd, BytesStart, Event}; use quick_xml::name::{QName, ResolveResult}; -use crate::prelude::*; -use crate::util::xml::*; +use anyhow::{Result, anyhow as ffail}; +use crate::xml::*; use super::bind::Jid; diff --git a/src/protos/xmpp/mod.rs b/crates/proto-xmpp/src/lib.rs similarity index 78% rename from src/protos/xmpp/mod.rs rename to crates/proto-xmpp/src/lib.rs index 0f99af7..ee85319 100644 --- a/src/protos/xmpp/mod.rs +++ b/crates/proto-xmpp/src/lib.rs @@ -1,3 +1,10 @@ +#![feature( + generators, + generator_trait, + type_alias_impl_trait, + impl_trait_in_assoc_type +)] + pub mod bind; pub mod client; pub mod disco; @@ -8,6 +15,8 @@ pub mod session; pub mod stanzaerror; pub mod stream; pub mod tls; +mod prelude; +pub mod xml; // Implemented as a macro instead of a fn due to borrowck limitations macro_rules! skip_text { @@ -25,4 +34,4 @@ macro_rules! skip_text { }; } -pub(super) use skip_text; +pub(crate) use skip_text; diff --git a/src/protos/xmpp/muc/mod.rs b/crates/proto-xmpp/src/muc/mod.rs similarity index 86% rename from src/protos/xmpp/muc/mod.rs rename to crates/proto-xmpp/src/muc/mod.rs index 8822fe3..250d50d 100644 --- a/src/protos/xmpp/muc/mod.rs +++ b/crates/proto-xmpp/src/muc/mod.rs @@ -1,8 +1,8 @@ use quick_xml::events::Event; use quick_xml::name::ResolveResult; -use crate::prelude::*; -use crate::util::xml::*; +use anyhow::{anyhow, Result}; +use crate::xml::*; pub const XMLNS: &'static str = "http://jabber.org/protocol/muc"; @@ -22,7 +22,7 @@ impl FromXml for History { let (bytes, end) = match event { Event::Start(bytes) => (bytes, false), Event::Empty(bytes) => (bytes, true), - _ => return Err(ffail!("Unexpected XML event: {event:?}")), + _ => return Err(anyhow!("Unexpected XML event: {event:?}")), }; for attr in bytes.attributes() { let attr = attr?; @@ -51,7 +51,7 @@ impl FromXml for History { let (namespace, event) = yield; let Event::End(bytes) = event else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); }; Ok(history) } @@ -73,15 +73,15 @@ impl FromXml for Password { fn parse() -> Self::P { |(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result { let Event::Start(bytes) = event else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); }; let (namespace, event) = yield; let Event::Text(bytes) = event else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); }; let s = std::str::from_utf8(bytes)?.to_string(); let Event::End(bytes) = event else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); }; Ok(Password(s)) } @@ -109,7 +109,7 @@ impl FromXml for X { let (bytes, end) = match event { Event::Start(bytes) => (bytes, false), Event::Empty(bytes) => (bytes, true), - _ => return Err(ffail!("Unexpected XML event: {event:?}")), + _ => return Err(anyhow!("Unexpected XML event: {event:?}")), }; if end { return Ok(res); @@ -121,7 +121,7 @@ impl FromXml for X { Event::Start(bytes) => bytes, Event::Empty(bytes) => bytes, Event::End(_) => break, - _ => return Err(ffail!("Unexpected XML event: {event:?}")), + _ => return Err(anyhow!("Unexpected XML event: {event:?}")), }; if bytes.name().0 == Password::NAME.as_bytes() { let password = delegate_parsing!(Password, namespace, event)?; @@ -130,7 +130,7 @@ impl FromXml for X { let history = delegate_parsing!(History, namespace, event)?; res.history = Some(history); } else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); } } diff --git a/crates/proto-xmpp/src/prelude.rs b/crates/proto-xmpp/src/prelude.rs new file mode 100644 index 0000000..04c4d62 --- /dev/null +++ b/crates/proto-xmpp/src/prelude.rs @@ -0,0 +1,3 @@ +use std::sync::Arc; + +pub type Str = Arc; diff --git a/src/protos/xmpp/roster.rs b/crates/proto-xmpp/src/roster.rs similarity index 96% rename from src/protos/xmpp/roster.rs rename to crates/proto-xmpp/src/roster.rs index 4990bc1..e153843 100644 --- a/src/protos/xmpp/roster.rs +++ b/crates/proto-xmpp/src/roster.rs @@ -1,7 +1,7 @@ use quick_xml::events::{BytesStart, Event}; -use crate::prelude::*; -use crate::util::xml::*; +use crate::xml::*; +use anyhow::{anyhow as ffail, Result}; pub const XMLNS: &'static str = "jabber:iq:roster"; diff --git a/src/protos/xmpp/sasl.rs b/crates/proto-xmpp/src/sasl.rs similarity index 83% rename from src/protos/xmpp/sasl.rs rename to crates/proto-xmpp/src/sasl.rs index e78f605..7068e7a 100644 --- a/src/protos/xmpp/sasl.rs +++ b/crates/proto-xmpp/src/sasl.rs @@ -7,7 +7,7 @@ use quick_xml::{ use tokio::io::{AsyncBufRead, AsyncWrite}; use super::skip_text; -use crate::prelude::*; +use anyhow::{anyhow, Result}; pub enum Mechanism { Plain, @@ -22,7 +22,7 @@ impl Mechanism { pub fn from_str(input: &[u8]) -> Result { match input { b"PLAIN" => Ok(Mechanism::Plain), - _ => Err(fail(format!("unknown auth mechanism: {input:?}").as_str())), + _ => Err(anyhow!("unknown auth mechanism: {input:?}")), } } } @@ -48,20 +48,20 @@ impl Auth { if let Some(mechanism) = mechanism { Mechanism::from_str(mechanism.borrow())? } else { - return Err(fail("expected mechanism attribute in ")); + return Err(anyhow!("expected mechanism attribute in ")); } } else { - return Err(fail("expected start of ")); + return Err(anyhow!("expected start of ")); }; let body = if let Event::Text(text) = reader.read_event_into_async(buf).await? { text.into_inner().into_owned() } else { - return Err(fail("expected text body in ")); + return Err(anyhow!("expected text body in ")); }; if let Event::End(_) = reader.read_event_into_async(buf).await? { //TODO } else { - return Err(fail("expected end of ")); + return Err(anyhow!("expected end of ")); }; Ok(Auth { mechanism, body }) diff --git a/src/protos/xmpp/session.rs b/crates/proto-xmpp/src/session.rs similarity index 86% rename from src/protos/xmpp/session.rs rename to crates/proto-xmpp/src/session.rs index 9f0104b..ba1ab43 100644 --- a/src/protos/xmpp/session.rs +++ b/crates/proto-xmpp/src/session.rs @@ -1,7 +1,7 @@ use quick_xml::events::{BytesStart, Event}; -use crate::prelude::*; -use crate::util::xml::*; +use crate::xml::*; +use anyhow::{anyhow, Result}; pub const XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-session"; @@ -29,11 +29,11 @@ impl Parser for SessionParser { Continuation::Continue(SessionParser(SessionParserInner::InSession)) } Event::Empty(_) => Continuation::Final(Ok(Session)), - _ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))), + _ => Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))), }, SessionParserInner::InSession => match event { Event::End(_) => Continuation::Final(Ok(Session)), - _ => Continuation::Final(Err(ffail!("Unexpected XML event: {event:?}"))), + _ => Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))), }, } } diff --git a/src/protos/xmpp/stanzaerror.rs b/crates/proto-xmpp/src/stanzaerror.rs similarity index 100% rename from src/protos/xmpp/stanzaerror.rs rename to crates/proto-xmpp/src/stanzaerror.rs diff --git a/src/protos/xmpp/stream.rs b/crates/proto-xmpp/src/stream.rs similarity index 98% rename from src/protos/xmpp/stream.rs rename to crates/proto-xmpp/src/stream.rs index 1f133a3..4d11039 100644 --- a/src/protos/xmpp/stream.rs +++ b/crates/proto-xmpp/src/stream.rs @@ -5,8 +5,9 @@ use quick_xml::{NsReader, Writer}; use tokio::io::{AsyncBufRead, AsyncWrite}; use super::skip_text; -use crate::prelude::*; -use crate::util::xml::ToXml; + +use anyhow::{anyhow, Result}; +use crate::xml::ToXml; pub static XMLNS: &'static str = "http://etherx.jabber.org/streams"; pub static PREFIX: &'static str = "stream"; @@ -63,7 +64,6 @@ impl ClientStreamStart { version: version.unwrap(), }) } else { - log::error!("WAT: {incoming:?}"); Err(panic!()) } } diff --git a/src/protos/xmpp/tls.rs b/crates/proto-xmpp/src/tls.rs similarity index 91% rename from src/protos/xmpp/tls.rs rename to crates/proto-xmpp/src/tls.rs index 6fdf4ad..a5a9e0d 100644 --- a/src/protos/xmpp/tls.rs +++ b/crates/proto-xmpp/src/tls.rs @@ -5,7 +5,8 @@ use quick_xml::{NsReader, Writer}; use tokio::io::{AsyncBufRead, AsyncWrite}; use super::skip_text; -use crate::prelude::*; + +use anyhow::{anyhow, Result}; pub static XMLNS: &'static str = "urn:ietf:params:xml:ns:xmpp-tls"; @@ -21,7 +22,7 @@ impl StartTLS { return Ok(StartTLS); } } - Err(ffail!("XML tag starttls expected, received: {incoming:?}")) + Err(anyhow!("XML tag starttls expected, received: {incoming:?}")) } } diff --git a/src/util/xml/ignore.rs b/crates/proto-xmpp/src/xml/ignore.rs similarity index 100% rename from src/util/xml/ignore.rs rename to crates/proto-xmpp/src/xml/ignore.rs diff --git a/src/util/xml.rs b/crates/proto-xmpp/src/xml/mod.rs similarity index 96% rename from src/util/xml.rs rename to crates/proto-xmpp/src/xml/mod.rs index 9ad847e..771bef4 100644 --- a/src/util/xml.rs +++ b/crates/proto-xmpp/src/xml/mod.rs @@ -4,7 +4,7 @@ use std::pin::Pin; use quick_xml::events::Event; use quick_xml::name::ResolveResult; -use crate::prelude::Result; +use anyhow::{anyhow, Result}; mod ignore; pub use ignore::Ignore; @@ -72,6 +72,7 @@ macro_rules! fail_fast { }; } +#[macro_export] macro_rules! delegate_parsing { ($parser: ty, $namespace: expr, $event: expr) => {{ let mut parser = <$parser as FromXml>::parse().consume($namespace, $event); @@ -88,6 +89,7 @@ macro_rules! delegate_parsing { }}; } +#[macro_export] macro_rules! match_parser { ($name: expr, $ns: expr, $event: expr; $subtype: ty, $fin: block) => { if $name.0 == <$subtype as FromXmlTag>::NAME.as_bytes() && $ns == ResolveResult::Bound(Namespace(<$subtype as FromXmlTag>::NS.as_bytes())) { @@ -105,6 +107,6 @@ macro_rules! match_parser { }; } -pub(crate) use delegate_parsing; +pub use delegate_parsing; pub(crate) use fail_fast; -pub(crate) use match_parser; +pub use match_parser; diff --git a/src/main.rs b/src/main.rs index d10c9ee..ddaab27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,6 @@ mod core; mod prelude; mod projections; -mod protos; mod util; use std::future::Future; diff --git a/src/projections/xmpp/mod.rs b/src/projections/xmpp/mod.rs index fb62e82..db19455 100644 --- a/src/projections/xmpp/mod.rs +++ b/src/projections/xmpp/mod.rs @@ -22,14 +22,13 @@ use tokio_rustls::TlsAcceptor; use crate::core::player::{PlayerConnection, PlayerId, PlayerRegistry}; use crate::core::room::{RoomId, RoomRegistry}; use crate::prelude::*; -use crate::protos::xmpp; -use crate::protos::xmpp::bind::{BindResponse, Jid, Name, Resource, Server}; -use crate::protos::xmpp::client::{Iq, Message, MessageType, Presence}; -use crate::protos::xmpp::disco::*; -use crate::protos::xmpp::roster::RosterQuery; -use crate::protos::xmpp::session::Session; -use crate::protos::xmpp::stream::*; -use crate::util::xml::{Continuation, FromXml, Parser, ToXml}; +use proto_xmpp::bind::{BindResponse, Jid, Name, Resource, Server}; +use proto_xmpp::client::{Iq, Message, MessageType, Presence}; +use proto_xmpp::disco::*; +use proto_xmpp::roster::RosterQuery; +use proto_xmpp::session::Session; +use proto_xmpp::stream::*; +use proto_xmpp::xml::{Continuation, FromXml, Parser, ToXml}; use crate::util::Terminator; use self::proto::{ClientPacket, IqClientBody}; @@ -197,7 +196,7 @@ async fn socket_force_tls( writer: &mut (impl AsyncWrite + Unpin), reader_buf: &mut Vec, ) -> Result<()> { - use crate::protos::xmpp::tls::*; + use proto_xmpp::tls::*; let xml_reader = &mut NsReader::from_reader(reader); let xml_writer = &mut Writer::new(writer); read_xml_header(xml_reader, reader_buf).await?; @@ -254,8 +253,8 @@ async fn socket_auth( .await?; xml_writer.get_mut().flush().await?; - let _ = xmpp::sasl::Auth::parse(xml_reader, reader_buf).await?; - xmpp::sasl::Success.write_xml(xml_writer).await?; + let _ = proto_xmpp::sasl::Auth::parse(xml_reader, reader_buf).await?; + proto_xmpp::sasl::Success.write_xml(xml_writer).await?; let name: Str = "darova".into(); Ok(Authenticated { @@ -346,7 +345,7 @@ async fn socket_final( resource: Some(Resource(author_id.into_inner().into())), }), id: None, - r#type: xmpp::client::MessageType::Groupchat, + r#type: proto_xmpp::client::MessageType::Groupchat, lang: None, subject: None, body: body.into(), @@ -414,7 +413,7 @@ async fn handle_packet( resource: Some(user.xmpp_muc_name.clone()), }), id: m.id, - r#type: xmpp::client::MessageType::Groupchat, + r#type: proto_xmpp::client::MessageType::Groupchat, lang: None, subject: None, body: m.body.clone(), @@ -485,7 +484,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: None, id: iq.id, to: None, - r#type: xmpp::client::IqType::Result, + r#type: proto_xmpp::client::IqType::Result, body: BindResponse(Jid { name: Some(Name("darova".into())), server: Server("localhost".into()), @@ -499,7 +498,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: None, id: iq.id, to: None, - r#type: xmpp::client::IqType::Result, + r#type: proto_xmpp::client::IqType::Result, body: Session, }; req.serialize(output); @@ -509,7 +508,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: None, id: iq.id, to: None, - r#type: xmpp::client::IqType::Result, + r#type: proto_xmpp::client::IqType::Result, body: RosterQuery, }; req.serialize(output); @@ -520,7 +519,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: iq.to, id: iq.id, to: None, - r#type: xmpp::client::IqType::Result, + r#type: proto_xmpp::client::IqType::Result, body: response, }; req.serialize(output); @@ -531,7 +530,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: iq.to, id: iq.id, to: None, - r#type: xmpp::client::IqType::Result, + r#type: proto_xmpp::client::IqType::Result, body: response, }; req.serialize(output); @@ -541,7 +540,7 @@ async fn handle_iq(output: &mut Vec>, iq: Iq, rooms from: None, id: iq.id, to: None, - r#type: xmpp::client::IqType::Error, + r#type: proto_xmpp::client::IqType::Error, body: (), }; req.serialize(output); diff --git a/src/projections/xmpp/proto.rs b/src/projections/xmpp/proto.rs index 4667dd1..5fd54d1 100644 --- a/src/projections/xmpp/proto.rs +++ b/src/projections/xmpp/proto.rs @@ -2,12 +2,12 @@ use derive_more::From; use quick_xml::events::Event; use quick_xml::name::{Namespace, ResolveResult}; -use crate::protos::xmpp::bind::BindRequest; -use crate::protos::xmpp::client::{Iq, Message, Presence}; -use crate::protos::xmpp::disco::{InfoQuery, ItemQuery}; -use crate::protos::xmpp::roster::RosterQuery; -use crate::protos::xmpp::session::Session; -use crate::util::xml::*; +use proto_xmpp::bind::BindRequest; +use proto_xmpp::client::{Iq, Message, Presence}; +use proto_xmpp::disco::{InfoQuery, ItemQuery}; +use proto_xmpp::roster::RosterQuery; +use proto_xmpp::session::Session; +use proto_xmpp::xml::*; use crate::prelude::*; diff --git a/src/protos/mod.rs b/src/protos/mod.rs deleted file mode 100644 index c8c39c3..0000000 --- a/src/protos/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Definitions of wire protocols to be used in implementations of projections. -pub mod xmpp; diff --git a/src/util/mod.rs b/src/util/mod.rs index 2fde69e..fb2f952 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -4,7 +4,6 @@ pub mod table; pub mod telemetry; #[cfg(test)] pub mod testkit; -pub mod xml; pub struct Terminator { signal: Promise<()>, From 1b3551f108f29d97b942be48f8f0703573540d7c Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Sat, 30 Sep 2023 16:02:18 +0000 Subject: [PATCH 11/17] xmpp: rewrite bind request parser as a coroutine (#12) Reviewed-on: https://git.vilunov.me/lavina/lavina/pulls/12 --- crates/proto-xmpp/src/bind.rs | 109 ++++++++++++---------------------- 1 file changed, 38 insertions(+), 71 deletions(-) diff --git a/crates/proto-xmpp/src/bind.rs b/crates/proto-xmpp/src/bind.rs index e6ef5d9..8d9a4cf 100644 --- a/crates/proto-xmpp/src/bind.rs +++ b/crates/proto-xmpp/src/bind.rs @@ -78,89 +78,56 @@ impl Jid { #[derive(PartialEq, Eq, Debug)] pub struct BindRequest(pub Resource); -pub struct BindRequestParser(BindRequestParserInner); - -enum BindRequestParserInner { - Initial, - /// Consumed start and expects - InBind(Option), - /// Consumed start - InBindResourceInitial, - /// Consumer start and inner text - InBindResourceEnd(String), -} - impl FromXmlTag for BindRequest { const NS: &'static str = XMLNS; const NAME: &'static str = "bind"; } impl FromXml for BindRequest { - type P = BindRequestParser; + type P = impl Parser>; fn parse() -> Self::P { - BindRequestParser(BindRequestParserInner::Initial) - } -} - -// TODO rewrite as a generator -impl Parser for BindRequestParser { - type Output = Result; - - fn consume<'a>( - self: Self, - namespace: ResolveResult, - event: &Event<'a>, - ) -> Continuation { - // TODO validate tag names and namespaces - use BindRequestParserInner::*; - match self.0 { - Initial => { - let Event::Start(bytes) = event else { - return Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))); - }; - if bytes.name().0 != BindRequest::NAME.as_bytes() { - return Continuation::Final(Err(anyhow!( - "Unexpected XML tag: {:?}", - bytes.name() - ))); - } - let ResolveResult::Bound(Namespace(ns)) = namespace else { - return Continuation::Final(Err(anyhow!("No namespace provided"))); - }; - if ns != XMLNS.as_bytes() { - return Continuation::Final(Err(anyhow!("Incorrect namespace"))); - } - Continuation::Continue(BindRequestParser(InBind(None))) + |(namespace, event): (ResolveResult<'static>, &'static Event<'static>)| -> Result { + let mut resource: Option = None; + let Event::Start(bytes) = event else { + return Err(anyhow!("Unexpected XML event: {event:?}")); + }; + if bytes.name().0 != BindRequest::NAME.as_bytes() { + return Err(anyhow!("Unexpected XML tag: {:?}", bytes.name())); } - InBind(resource) => match event { - Event::Start(bytes) => { - Continuation::Continue(BindRequestParser(InBindResourceInitial)) - } - Event::End(bytes) => { - let Some(resource) = resource else { - return Continuation::Final(Err(anyhow!("No resource was provided"))); + let ResolveResult::Bound(Namespace(ns)) = namespace else { + return Err(anyhow!("No namespace provided")); + }; + if ns != XMLNS.as_bytes() { + return Err(anyhow!("Incorrect namespace")); + } + loop { + let (namespace, event) = yield; + match event { + Event::Start(bytes) if bytes.name().0 == b"resource" => { + let (namespace, event) = yield; + let Event::Text(text) = event else { + return Err(anyhow!("Unexpected XML event: {event:?}")); }; - Continuation::Final(Ok(BindRequest(Resource(resource.into())))) + resource = Some(std::str::from_utf8(&*text)?.into()); + let (namespace, event) = yield; + let Event::End(bytes) = event else { + return Err(anyhow!("Unexpected XML event: {event:?}")); + }; + if bytes.name().0 != b"resource" { + return Err(anyhow!("Unexpected XML tag: {:?}", bytes.name())); + } + } + Event::End(bytes) if bytes.name().0 == BindRequest::NAME.as_bytes() => { + break; + } + _ => return Err(anyhow!("Unexpected XML event: {event:?}")), } - _ => Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))), - }, - InBindResourceInitial => { - let Event::Text(text) = event else { - return Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))); - }; - let resource = match std::str::from_utf8(&*text) { - Ok(e) => e.to_string(), - Err(err) => return Continuation::Final(Err(err.into())), - }; - Continuation::Continue(BindRequestParser(InBindResourceEnd(resource))) - } - InBindResourceEnd(resource) => { - let Event::End(bytes) = event else { - return Continuation::Final(Err(anyhow!("Unexpected XML event: {event:?}"))); - }; - Continuation::Continue(BindRequestParser(InBind(Some(resource)))) } + let Some(resource) = resource else { + return Err(anyhow!("No resource was provided")); + }; + Ok(BindRequest(Resource(resource))) } } } From a1db17c77993699ae84c952519231dfe151813aa Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Sat, 30 Sep 2023 23:12:11 +0000 Subject: [PATCH 12/17] split core into a separate crate (#17) --- Cargo.lock | 14 ++++- Cargo.toml | 9 ++- crates/lavina-core/Cargo.toml | 12 ++++ .../lavina-core/migrations}/0_first.sql | 0 .../mod.rs => crates/lavina-core/src/lib.rs | 3 + .../core => crates/lavina-core/src}/player.rs | 58 +++++-------------- crates/lavina-core/src/prelude.rs | 17 ++++++ .../lavina-core/src}/repo/mod.rs | 4 +- {src/core => crates/lavina-core/src}/room.rs | 29 ++++------ {src/util => crates/lavina-core/src}/table.rs | 0 src/main.rs | 9 ++- src/projections/irc/mod.rs | 6 +- src/projections/xmpp/mod.rs | 6 +- src/util/mod.rs | 1 - src/util/telemetry.rs | 4 +- 15 files changed, 89 insertions(+), 83 deletions(-) create mode 100644 crates/lavina-core/Cargo.toml rename {migrations => crates/lavina-core/migrations}/0_first.sql (100%) rename src/core/mod.rs => crates/lavina-core/src/lib.rs (81%) rename {src/core => crates/lavina-core/src}/player.rs (91%) create mode 100644 crates/lavina-core/src/prelude.rs rename {src/core => crates/lavina-core/src}/repo/mod.rs (97%) rename {src/core => crates/lavina-core/src}/room.rs (90%) rename {src/util => crates/lavina-core/src}/table.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 2469e90..015cb46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -854,6 +854,7 @@ dependencies = [ "futures-util", "http-body-util", "hyper 1.0.0-rc.3", + "lavina-core", "mgmt-api", "nonempty", "prometheus", @@ -865,7 +866,6 @@ dependencies = [ "rustls-pemfile", "serde", "serde_json", - "sqlx", "tokio", "tokio-rustls", "tracing", @@ -873,6 +873,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "lavina-core" +version = "0.0.1-dev" +dependencies = [ + "anyhow", + "prometheus", + "serde", + "sqlx", + "tokio", + "tracing", +] + [[package]] name = "lazy_static" version = "1.4.0" diff --git a/Cargo.toml b/Cargo.toml index 4c90804..2e469b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ ".", + "crates/lavina-core", "crates/proto-irc", "crates/proto-xmpp", "crates/mgmt-api", @@ -22,6 +23,8 @@ regex = "1.7.1" derive_more = "0.99.17" clap = { version = "4.4.4", features = ["derive"] } serde = { version = "1.0.152", features = ["rc", "serde_derive"] } +tracing = "0.1.37" # logging & tracing api +prometheus = { version = "0.13.3", default-features = false } [package] name = "lavina" @@ -37,17 +40,17 @@ http-body-util = "0.1.0-rc.3" serde.workspace = true serde_json = "1.0.93" tokio.workspace = true -tracing = "0.1.37" # logging & tracing api +tracing.workspace = true tracing-subscriber = "0.3.16" futures-util.workspace = true -prometheus = { version = "0.13.3", default-features = false } +prometheus.workspace = true nonempty.workspace = true tokio-rustls = "0.24.1" rustls-pemfile = "1.0.2" quick-xml.workspace = true derive_more.workspace = true uuid = { version = "1.3.0", features = ["v4"] } -sqlx = { version = "0.7.0-alpha.2", features = ["sqlite", "migrate"] } +lavina-core = { path = "crates/lavina-core" } proto-irc = { path = "crates/proto-irc" } proto-xmpp = { path = "crates/proto-xmpp" } mgmt-api = { path = "crates/mgmt-api" } diff --git a/crates/lavina-core/Cargo.toml b/crates/lavina-core/Cargo.toml new file mode 100644 index 0000000..727835c --- /dev/null +++ b/crates/lavina-core/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "lavina-core" +edition = "2021" +version.workspace = true + +[dependencies] +anyhow.workspace = true +sqlx = { version = "0.7.0-alpha.2", features = ["sqlite", "migrate"] } +serde.workspace = true +tokio.workspace = true +tracing.workspace = true +prometheus.workspace = true diff --git a/migrations/0_first.sql b/crates/lavina-core/migrations/0_first.sql similarity index 100% rename from migrations/0_first.sql rename to crates/lavina-core/migrations/0_first.sql diff --git a/src/core/mod.rs b/crates/lavina-core/src/lib.rs similarity index 81% rename from src/core/mod.rs rename to crates/lavina-core/src/lib.rs index 8ee26f7..c1cef53 100644 --- a/src/core/mod.rs +++ b/crates/lavina-core/src/lib.rs @@ -2,3 +2,6 @@ pub mod player; pub mod repo; pub mod room; + +mod prelude; +mod table; diff --git a/src/core/player.rs b/crates/lavina-core/src/player.rs similarity index 91% rename from src/core/player.rs rename to crates/lavina-core/src/player.rs index 1533941..763a82a 100644 --- a/src/core/player.rs +++ b/crates/lavina-core/src/player.rs @@ -19,11 +19,9 @@ use tokio::{ task::JoinHandle, }; -use crate::{ - core::room::{RoomHandle, RoomId, RoomInfo, RoomRegistry}, - prelude::*, - util::table::{AnonTable, Key as AnonKey}, -}; +use crate::prelude::*; +use crate::room::{RoomHandle, RoomId, RoomInfo, RoomRegistry}; +use crate::table::{AnonTable, Key as AnonKey}; /// Opaque player identifier. Cannot contain spaces, must be shorter than 32. #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] @@ -63,9 +61,7 @@ impl PlayerConnection { } pub async fn join_room(&mut self, room_id: RoomId) -> Result { - self.player_handle - .join_room(room_id, self.connection_id.clone()) - .await + self.player_handle.join_room(room_id, self.connection_id.clone()).await } pub async fn change_topic(&mut self, room_id: RoomId, new_topic: Str) -> Result<()> { @@ -100,9 +96,7 @@ impl PlayerConnection { pub async fn get_rooms(&self) -> Result> { let (promise, deferred) = oneshot(); - self.player_handle - .send(PlayerCommand::GetRooms(promise)) - .await; + self.player_handle.send(PlayerCommand::GetRooms(promise)).await; Ok(deferred.await?) } } @@ -126,27 +120,14 @@ impl PlayerHandle { } } - pub async fn send_message( - &self, - room_id: RoomId, - connection_id: ConnectionId, - body: Str, - ) -> Result<()> { + pub async fn send_message(&self, room_id: RoomId, connection_id: ConnectionId, body: Str) -> Result<()> { let (promise, deferred) = oneshot(); - let cmd = Cmd::SendMessage { - room_id, - body, - promise, - }; + let cmd = Cmd::SendMessage { room_id, body, promise }; let _ = self.tx.send(PlayerCommand::Cmd(cmd, connection_id)).await; Ok(deferred.await?) } - pub async fn join_room( - &self, - room_id: RoomId, - connection_id: ConnectionId, - ) -> Result { + pub async fn join_room(&self, room_id: RoomId, connection_id: ConnectionId) -> Result { let (promise, deferred) = oneshot(); let cmd = Cmd::JoinRoom { room_id, promise }; let _ = self.tx.send(PlayerCommand::Cmd(cmd, connection_id)).await; @@ -230,12 +211,8 @@ pub enum Updates { #[derive(Clone)] pub struct PlayerRegistry(Arc>); impl PlayerRegistry { - pub fn empty( - room_registry: RoomRegistry, - metrics: &mut MetricsRegistry, - ) -> Result { - let metric_active_players = - IntGauge::new("chat_players_active", "Number of alive player actors")?; + pub fn empty(room_registry: RoomRegistry, metrics: &mut MetricsRegistry) -> Result { + let metric_active_players = IntGauge::new("chat_players_active", "Number of alive player actors")?; metrics.register(Box::new(metric_active_players.clone()))?; let inner = PlayerRegistryInner { room_registry, @@ -375,8 +352,7 @@ impl Player { return; } }; - room.subscribe(self.player_id.clone(), self.handle.clone()) - .await; + room.subscribe(self.player_id.clone(), self.handle.clone()).await; self.my_rooms.insert(room_id.clone(), room.clone()); let room_info = room.get_room_info().await; let _ = promise.send(JoinResult::Success(room_info)); @@ -399,15 +375,10 @@ impl Player { }; self.broadcast_update(update, connection_id).await; } - Cmd::SendMessage { - room_id, - body, - promise, - } => { + Cmd::SendMessage { room_id, body, promise } => { let room = self.rooms.get_room(&room_id).await; if let Some(room) = room { - room.send_message(self.player_id.clone(), body.clone()) - .await; + room.send_message(self.player_id.clone(), body.clone()).await; } else { tracing::info!("no room found"); } @@ -426,8 +397,7 @@ impl Player { } => { let room = self.rooms.get_room(&room_id).await; if let Some(mut room) = room { - room.set_topic(self.player_id.clone(), new_topic.clone()) - .await; + room.set_topic(self.player_id.clone(), new_topic.clone()).await; } else { tracing::info!("no room found"); } diff --git a/crates/lavina-core/src/prelude.rs b/crates/lavina-core/src/prelude.rs new file mode 100644 index 0000000..b405477 --- /dev/null +++ b/crates/lavina-core/src/prelude.rs @@ -0,0 +1,17 @@ +pub use std::future::Future; + +pub use tokio::pin; +pub use tokio::select; +pub use tokio::sync::oneshot::{channel as oneshot, Receiver as Deferred, Sender as Promise}; +pub use tokio::task::JoinHandle; + +pub mod log { + pub use tracing::{debug, error, info, warn}; +} + +pub type Result = std::result::Result; +pub type Str = std::sync::Arc; + +pub fn fail(msg: &str) -> anyhow::Error { + anyhow::Error::msg(msg.to_owned()) +} diff --git a/src/core/repo/mod.rs b/crates/lavina-core/src/repo/mod.rs similarity index 97% rename from src/core/repo/mod.rs rename to crates/lavina-core/src/repo/mod.rs index 57371ed..d24e8b5 100644 --- a/src/core/repo/mod.rs +++ b/crates/lavina-core/src/repo/mod.rs @@ -93,10 +93,10 @@ impl Storage { Ok(()) } - pub async fn close(mut self) -> Result<()> { + pub async fn close(self) -> Result<()> { let res = match Arc::try_unwrap(self.conn) { Ok(e) => e, - Err(e) => return Err(fail("failed to acquire DB ownership on shutdown")), + Err(_) => return Err(fail("failed to acquire DB ownership on shutdown")), }; let res = res.into_inner(); res.close().await?; diff --git a/src/core/room.rs b/crates/lavina-core/src/room.rs similarity index 90% rename from src/core/room.rs rename to crates/lavina-core/src/room.rs index 47826bb..5c6dbd9 100644 --- a/src/core/room.rs +++ b/crates/lavina-core/src/room.rs @@ -1,17 +1,13 @@ //! Domain of rooms — chats with multiple participants. -use std::{ - collections::HashMap, - hash::Hash, - sync::Arc, -}; +use std::{collections::HashMap, hash::Hash, sync::Arc}; use prometheus::{IntGauge, Registry as MetricRegistry}; use serde::Serialize; use tokio::sync::RwLock as AsyncRwLock; -use crate::core::player::{PlayerHandle, PlayerId, Updates}; -use crate::core::repo::Storage; +use crate::player::{PlayerHandle, PlayerId, Updates}; use crate::prelude::*; +use crate::repo::Storage; /// Opaque room id #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] @@ -20,9 +16,7 @@ impl RoomId { pub fn from(str: impl Into) -> Result { let bytes = str.into(); if bytes.len() > 32 { - return Err(anyhow::Error::msg( - "Room name cannot be longer than 32 symbols", - )); + return Err(anyhow::Error::msg("Room name cannot be longer than 32 symbols")); } if bytes.contains(' ') { return Err(anyhow::Error::msg("Room name cannot contain spaces")); @@ -42,8 +36,7 @@ impl RoomId { pub struct RoomRegistry(Arc>); impl RoomRegistry { pub fn new(metrics: &mut MetricRegistry, storage: Storage) -> Result { - let metric_active_rooms = - IntGauge::new("chat_rooms_active", "Number of alive room actors")?; + let metric_active_rooms = IntGauge::new("chat_rooms_active", "Number of alive room actors")?; metrics.register(Box::new(metric_active_rooms.clone()))?; let inner = RoomRegistryInner { rooms: HashMap::new(), @@ -140,7 +133,7 @@ impl RoomHandle { pub async fn send_message(&self, player_id: PlayerId, body: Str) { let mut lock = self.0.write().await; - let res = lock.send_message(player_id, body).await; + let res = lock.send_message(player_id, body).await; if let Err(err) = res { log::warn!("Failed to send message: {err:?}"); } @@ -150,11 +143,7 @@ impl RoomHandle { let lock = self.0.read().await; RoomInfo { id: lock.room_id.clone(), - members: lock - .subscriptions - .keys() - .map(|x| x.clone()) - .collect::>(), + members: lock.subscriptions.keys().map(|x| x.clone()).collect::>(), topic: lock.topic.clone(), } } @@ -191,7 +180,9 @@ impl Room { async fn send_message(&mut self, author_id: PlayerId, body: Str) -> Result<()> { tracing::info!("Adding a message to room"); - self.storage.insert_message(self.storage_id, self.message_count, &body).await?; + self.storage + .insert_message(self.storage_id, self.message_count, &body) + .await?; self.message_count += 1; let update = Updates::NewMessage { room_id: self.room_id.clone(), diff --git a/src/util/table.rs b/crates/lavina-core/src/table.rs similarity index 100% rename from src/util/table.rs rename to crates/lavina-core/src/table.rs diff --git a/src/main.rs b/src/main.rs index ddaab27..c23a7c0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ impl_trait_in_assoc_type )] -mod core; mod prelude; mod projections; mod util; @@ -19,9 +18,9 @@ use figment::{providers::Toml, Figment}; use prometheus::Registry as MetricsRegistry; use serde::Deserialize; -use crate::core::player::PlayerRegistry; -use crate::core::repo::Storage; -use crate::core::room::RoomRegistry; +use lavina_core::player::PlayerRegistry; +use lavina_core::repo::Storage; +use lavina_core::room::RoomRegistry; use crate::prelude::*; #[derive(Deserialize, Debug)] @@ -29,7 +28,7 @@ struct ServerConfig { telemetry: util::telemetry::ServerConfig, irc: projections::irc::ServerConfig, xmpp: projections::xmpp::ServerConfig, - storage: core::repo::StorageConfig, + storage: lavina_core::repo::StorageConfig, } #[derive(Parser)] diff --git a/src/projections/irc/mod.rs b/src/projections/irc/mod.rs index a98394b..1890890 100644 --- a/src/projections/irc/mod.rs +++ b/src/projections/irc/mod.rs @@ -12,9 +12,9 @@ use tokio::net::tcp::{ReadHalf, WriteHalf}; use tokio::net::{TcpListener, TcpStream}; use tokio::sync::mpsc::channel; -use crate::core::player::*; -use crate::core::repo::Storage; -use crate::core::room::{RoomId, RoomInfo, RoomRegistry}; +use lavina_core::player::*; +use lavina_core::repo::Storage; +use lavina_core::room::{RoomId, RoomInfo, RoomRegistry}; use crate::prelude::*; use proto_irc::client::{client_message, ClientMessage}; use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody}; diff --git a/src/projections/xmpp/mod.rs b/src/projections/xmpp/mod.rs index db19455..5b9e035 100644 --- a/src/projections/xmpp/mod.rs +++ b/src/projections/xmpp/mod.rs @@ -19,8 +19,8 @@ use tokio::sync::mpsc::channel; use tokio_rustls::rustls::{Certificate, PrivateKey}; use tokio_rustls::TlsAcceptor; -use crate::core::player::{PlayerConnection, PlayerId, PlayerRegistry}; -use crate::core::room::{RoomId, RoomRegistry}; +use lavina_core::player::{PlayerConnection, PlayerId, PlayerRegistry}; +use lavina_core::room::{RoomId, RoomRegistry}; use crate::prelude::*; use proto_xmpp::bind::{BindResponse, Jid, Name, Resource, Server}; use proto_xmpp::client::{Iq, Message, MessageType, Presence}; @@ -332,7 +332,7 @@ async fn socket_final( update = user_handle.receiver.recv() => { if let Some(update) = update { match update { - crate::core::player::Updates::NewMessage { room_id, author_id, body } => { + lavina_core::player::Updates::NewMessage { room_id, author_id, body } => { Message { to: Some(Jid { name: Some(authenticated.xmpp_name.clone()), diff --git a/src/util/mod.rs b/src/util/mod.rs index fb2f952..b3d7d82 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,6 +1,5 @@ use crate::prelude::*; -pub mod table; pub mod telemetry; #[cfg(test)] pub mod testkit; diff --git a/src/util/telemetry.rs b/src/util/telemetry.rs index 3bb5ec9..92006ad 100644 --- a/src/util/telemetry.rs +++ b/src/util/telemetry.rs @@ -11,8 +11,8 @@ use prometheus::{Encoder, Registry as MetricsRegistry, TextEncoder}; use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; -use crate::core::repo::Storage; -use crate::core::room::RoomRegistry; +use lavina_core::repo::Storage; +use lavina_core::room::RoomRegistry; use crate::prelude::*; use crate::util::Terminator; From 854a244dbcaf72e302ae033e00174a76a0b9f3b1 Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Sat, 30 Sep 2023 23:34:35 +0000 Subject: [PATCH 13/17] split irc proejction into a separate crate (#18) --- .gitea/workflows/test-pr.yml | 1 - Cargo.lock | 17 +- Cargo.toml | 6 +- crates/lavina-core/src/lib.rs | 3 +- crates/lavina-core/src/terminator.rs | 28 +++ crates/projection-irc/Cargo.toml | 15 ++ .../projection-irc/src/lib.rs | 70 +++----- src/main.rs | 7 +- src/prelude.rs | 25 --- src/projections/irc/test.rs | 166 ------------------ src/projections/mod.rs | 1 - src/projections/xmpp/mod.rs | 4 +- src/projections/xmpp/proto.rs | 12 +- src/util/mod.rs | 29 --- src/util/telemetry.rs | 4 +- src/util/testkit.rs | 2 +- 16 files changed, 104 insertions(+), 286 deletions(-) create mode 100644 crates/lavina-core/src/terminator.rs create mode 100644 crates/projection-irc/Cargo.toml rename src/projections/irc/mod.rs => crates/projection-irc/src/lib.rs (94%) delete mode 100644 src/prelude.rs delete mode 100644 src/projections/irc/test.rs diff --git a/.gitea/workflows/test-pr.yml b/.gitea/workflows/test-pr.yml index 18dca5b..3fabe6a 100644 --- a/.gitea/workflows/test-pr.yml +++ b/.gitea/workflows/test-pr.yml @@ -16,4 +16,3 @@ jobs: uses: https://github.com/actions-rs/cargo@v1 with: command: test - args: --workspace -- --skip projections::irc diff --git a/Cargo.lock b/Cargo.lock index 015cb46..6459827 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -857,8 +857,8 @@ dependencies = [ "lavina-core", "mgmt-api", "nonempty", + "projection-irc", "prometheus", - "proto-irc", "proto-xmpp", "quick-xml", "regex", @@ -1252,6 +1252,21 @@ dependencies = [ "yansi", ] +[[package]] +name = "projection-irc" +version = "0.0.1-dev" +dependencies = [ + "anyhow", + "futures-util", + "lavina-core", + "nonempty", + "prometheus", + "proto-irc", + "serde", + "tokio", + "tracing", +] + [[package]] name = "prometheus" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 2e469b1..1bf61d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ ".", "crates/lavina-core", "crates/proto-irc", + "crates/projection-irc", "crates/proto-xmpp", "crates/mgmt-api", ] @@ -25,6 +26,7 @@ clap = { version = "4.4.4", features = ["derive"] } serde = { version = "1.0.152", features = ["rc", "serde_derive"] } tracing = "0.1.37" # logging & tracing api prometheus = { version = "0.13.3", default-features = false } +lavina-core = { path = "crates/lavina-core" } [package] name = "lavina" @@ -50,8 +52,8 @@ rustls-pemfile = "1.0.2" quick-xml.workspace = true derive_more.workspace = true uuid = { version = "1.3.0", features = ["v4"] } -lavina-core = { path = "crates/lavina-core" } -proto-irc = { path = "crates/proto-irc" } +lavina-core.workspace = true +projection-irc = { path = "crates/projection-irc" } proto-xmpp = { path = "crates/proto-xmpp" } mgmt-api = { path = "crates/mgmt-api" } clap.workspace = true diff --git a/crates/lavina-core/src/lib.rs b/crates/lavina-core/src/lib.rs index c1cef53..401e49e 100644 --- a/crates/lavina-core/src/lib.rs +++ b/crates/lavina-core/src/lib.rs @@ -1,7 +1,8 @@ //! Domain definitions and implementation of common chat logic. pub mod player; +pub mod prelude; pub mod repo; pub mod room; +pub mod terminator; -mod prelude; mod table; diff --git a/crates/lavina-core/src/terminator.rs b/crates/lavina-core/src/terminator.rs new file mode 100644 index 0000000..8abbf58 --- /dev/null +++ b/crates/lavina-core/src/terminator.rs @@ -0,0 +1,28 @@ +use crate::prelude::*; + +pub struct Terminator { + signal: Promise<()>, + completion: JoinHandle>, +} +impl Terminator { + pub async fn terminate(self) -> Result<()> { + match self.signal.send(()) { + Ok(()) => {} + Err(_) => log::warn!("Termination channel is dropped"), + } + self.completion.await??; + Ok(()) + } + + /// Used to spawn managed tasks with support for graceful shutdown + pub fn spawn(launcher: Fun) -> Terminator + where + Fun: FnOnce(Deferred<()>) -> Fut, + Fut: Future> + Send + 'static, + { + let (signal, rx) = oneshot(); + let future = launcher(rx); + let completion = tokio::task::spawn(future); + Terminator { signal, completion } + } +} diff --git a/crates/projection-irc/Cargo.toml b/crates/projection-irc/Cargo.toml new file mode 100644 index 0000000..c607327 --- /dev/null +++ b/crates/projection-irc/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "projection-irc" +edition = "2021" +version.workspace = true + +[dependencies] +lavina-core.workspace = true +tracing.workspace = true +anyhow.workspace = true +nonempty.workspace = true +serde.workspace = true +tokio.workspace = true +prometheus.workspace = true +proto-irc = { path = "../proto-irc" } +futures-util.workspace = true diff --git a/src/projections/irc/mod.rs b/crates/projection-irc/src/lib.rs similarity index 94% rename from src/projections/irc/mod.rs rename to crates/projection-irc/src/lib.rs index 1890890..42a4126 100644 --- a/src/projections/irc/mod.rs +++ b/crates/projection-irc/src/lib.rs @@ -1,11 +1,11 @@ use std::collections::HashMap; use std::net::SocketAddr; +use anyhow::{anyhow, Result}; use futures_util::future::join_all; use nonempty::nonempty; use nonempty::NonEmpty; use prometheus::{IntCounter, IntGauge, Registry as MetricsRegistry}; - use serde::Deserialize; use tokio::io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter}; use tokio::net::tcp::{ReadHalf, WriteHalf}; @@ -13,17 +13,14 @@ use tokio::net::{TcpListener, TcpStream}; use tokio::sync::mpsc::channel; use lavina_core::player::*; +use lavina_core::prelude::*; use lavina_core::repo::Storage; use lavina_core::room::{RoomId, RoomInfo, RoomRegistry}; -use crate::prelude::*; +use lavina_core::terminator::Terminator; use proto_irc::client::{client_message, ClientMessage}; use proto_irc::server::{AwayStatus, ServerMessage, ServerMessageBody}; +use proto_irc::user::PrefixedNick; use proto_irc::{Chan, Recipient}; -use proto_irc::user::{Prefix, PrefixedNick}; -use crate::util::Terminator; - -#[cfg(test)] -mod test; #[derive(Deserialize, Debug, Clone)] pub struct ServerConfig { @@ -70,14 +67,12 @@ async fn handle_socket( .await?; writer.flush().await?; - let registered_user: Result = - handle_registration(&mut reader, &mut writer, &mut storage).await; + let registered_user: Result = handle_registration(&mut reader, &mut writer, &mut storage).await; match registered_user { Ok(user) => { log::debug!("User registered"); - handle_registered_socket(config, players, rooms, &mut reader, &mut writer, user) - .await?; + handle_registered_socket(config, players, rooms, &mut reader, &mut writer, user).await?; } Err(_) => { log::debug!("Registration failed"); @@ -159,23 +154,22 @@ async fn handle_registration<'a>( buffer.clear(); }?; - let stored_user = storage.retrieve_user_by_name(&*user.nickname).await?; let stored_user = match stored_user { Some(u) => u, None => { log::info!("User '{}' not found", user.nickname); - return Err(fail("no user found")); + return Err(anyhow!("no user found")); } }; if stored_user.password.is_none() { log::info!("Password not defined for user '{}'", user.nickname); - return Err(fail("password is not defined")); + return Err(anyhow!("password is not defined")); } if stored_user.password.as_deref() != pass.as_deref() { log::info!("Incorrect password supplied for user '{}'", user.nickname); - return Err(fail("passwords do not match")); + return Err(anyhow!("passwords do not match")); } // TODO properly implement session temination @@ -250,14 +244,7 @@ async fn handle_registered_socket<'a>( let rooms_list = connection.get_rooms().await?; for room in &rooms_list { - produce_on_join_cmd_messages( - &config, - &user, - &Chan::Global(room.id.as_inner().clone()), - room, - writer, - ) - .await?; + produce_on_join_cmd_messages(&config, &user, &Chan::Global(room.id.as_inner().clone()), room, writer).await?; } writer.flush().await?; @@ -314,10 +301,7 @@ async fn handle_update( ) -> Result<()> { log::debug!("Sending irc message to player {player_id:?} on update {update:?}"); match update { - Updates::RoomJoined { - new_member_id, - room_id, - } => { + Updates::RoomJoined { new_member_id, room_id } => { if player_id == &new_member_id { if let Some(room) = rooms.get_room(&room_id).await { let room_info = room.get_room_info().await; @@ -438,16 +422,14 @@ async fn handle_incoming_message( Recipient::Chan(Chan::Global(chan)) => { let room_id = RoomId::from(chan)?; user_handle.send_message(room_id, body).await?; - }, + } _ => log::warn!("Unsupported target type"), }, ClientMessage::Topic { chan, topic } => { match chan { Chan::Global(chan) => { let room_id = RoomId::from(chan)?; - user_handle - .change_topic(room_id.clone(), topic.clone()) - .await?; + user_handle.change_topic(room_id.clone(), topic.clone()).await?; ServerMessage { tags: vec![], sender: Some(config.server_name.clone()), @@ -569,11 +551,7 @@ async fn handle_incoming_message( Ok(HandleResult::Continue) } -fn user_to_who_msg( - config: &ServerConfig, - requestor: &RegisteredUser, - target_user_nickname: &Str, -) -> ServerMessageBody { +fn user_to_who_msg(config: &ServerConfig, requestor: &RegisteredUser, target_user_nickname: &Str) -> ServerMessageBody { // Username is equal to nickname let username = format!("~{target_user_nickname}").into(); @@ -674,8 +652,13 @@ async fn produce_on_join_cmd_messages( } .write_async(writer) .await?; - let prefixed_members: Vec = room_info.members.iter().map(|member| PrefixedNick::from_str(member.clone().into_inner())).collect(); - let non_empty_members: NonEmpty = NonEmpty::from_vec(prefixed_members).unwrap_or(nonempty![PrefixedNick::from_str(user.nickname.clone())]); + let prefixed_members: Vec = room_info + .members + .iter() + .map(|member| PrefixedNick::from_str(member.clone().into_inner())) + .collect(); + let non_empty_members: NonEmpty = + NonEmpty::from_vec(prefixed_members).unwrap_or(nonempty![PrefixedNick::from_str(user.nickname.clone())]); ServerMessage { tags: vec![], @@ -710,12 +693,8 @@ pub async fn launch( ) -> Result { log::info!("Starting IRC projection"); let (stopped_tx, mut stopped_rx) = channel(32); - let current_connections = - IntGauge::new("irc_current_connections", "Open and alive TCP connections")?; - let total_connections = IntCounter::new( - "irc_total_connections", - "Total number of opened connections", - )?; + let current_connections = IntGauge::new("irc_current_connections", "Open and alive TCP connections")?; + let total_connections = IntCounter::new("irc_total_connections", "Total number of opened connections")?; metrics.register(Box::new(current_connections.clone()))?; metrics.register(Box::new(total_connections.clone()))?; @@ -780,7 +759,8 @@ pub async fn launch( log::warn!("IRC connection to {socket_addr} finished with error: {err}") } } - })).await; + })) + .await; log::info!("Stopped IRC projection"); Ok(()) }); diff --git a/src/main.rs b/src/main.rs index c23a7c0..61ff315 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ impl_trait_in_assoc_type )] -mod prelude; mod projections; mod util; @@ -19,14 +18,14 @@ use prometheus::Registry as MetricsRegistry; use serde::Deserialize; use lavina_core::player::PlayerRegistry; +use lavina_core::prelude::*; use lavina_core::repo::Storage; use lavina_core::room::RoomRegistry; -use crate::prelude::*; #[derive(Deserialize, Debug)] struct ServerConfig { telemetry: util::telemetry::ServerConfig, - irc: projections::irc::ServerConfig, + irc: projection_irc::ServerConfig, xmpp: projections::xmpp::ServerConfig, storage: lavina_core::repo::StorageConfig, } @@ -64,7 +63,7 @@ async fn main() -> Result<()> { let mut players = PlayerRegistry::empty(rooms.clone(), &mut metrics)?; let telemetry_terminator = util::telemetry::launch(telemetry_config, metrics.clone(), rooms.clone(), storage.clone()).await?; - let irc = projections::irc::launch(irc_config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await?; + let irc = projection_irc::launch(irc_config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await?; let xmpp = projections::xmpp::launch(xmpp_config, players.clone(), rooms.clone(), metrics.clone()).await?; tracing::info!("Started"); diff --git a/src/prelude.rs b/src/prelude.rs deleted file mode 100644 index 9488bc2..0000000 --- a/src/prelude.rs +++ /dev/null @@ -1,25 +0,0 @@ -pub use std::future::Future; - -pub use tokio::pin; -pub use tokio::select; -pub use tokio::sync::oneshot::{channel as oneshot, Receiver as Deferred, Sender as Promise}; -pub use tokio::task::JoinHandle; - -pub mod log { - pub use tracing::{debug, error, info, warn}; -} - -pub type Result = std::result::Result; -pub type Str = std::sync::Arc; - -pub fn fail(msg: &str) -> anyhow::Error { - anyhow::Error::msg(msg.to_owned()) -} - -macro_rules! ffail { - ($($arg:tt)*) => { - fail(&format!($($arg)*)) - }; -} - -pub(crate) use ffail; diff --git a/src/projections/irc/test.rs b/src/projections/irc/test.rs deleted file mode 100644 index c3b238f..0000000 --- a/src/projections/irc/test.rs +++ /dev/null @@ -1,166 +0,0 @@ -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::net::tcp::{ReadHalf, WriteHalf}; -use tokio::net::TcpStream; - -use crate::prelude::*; - -struct TestScope<'a> { - reader: BufReader>, - writer: WriteHalf<'a>, - buffer: Vec, -} -impl<'a> TestScope<'a> { - async fn send(&mut self, str: &(impl AsRef + ?Sized)) -> Result<()> { - self.writer.write_all(str.as_ref().as_bytes()).await?; - self.writer.flush().await?; - Ok(()) - } - - async fn expect(&mut self, str: &(impl AsRef + ?Sized)) -> Result<()> { - let len = self.reader.read_until(b'\n', &mut self.buffer).await?; - assert_eq!(std::str::from_utf8(&self.buffer[0..len])?, str.as_ref()); - self.buffer.clear(); - Ok(()) - } - - async fn assert(&mut self, f: impl FnOnce(&str) -> Result<()>) -> Result<()> { - let len = self.reader.read_until(b'\n', &mut self.buffer).await?; - let res = f(std::str::from_utf8(&self.buffer[0..len])?); - self.buffer.clear(); - res - } -} - -async fn init_client(stream: &mut TcpStream) -> Result { - let (reader, writer) = stream.split(); - let reader = BufReader::new(reader); - let buffer = vec![]; - Ok(TestScope { - reader, - writer, - buffer, - }) -} - -macro_rules! send { - ($scope: expr, $($arg:tt),*) => {{ - $scope.send(&format!($($arg,)*)).await?; - }}; -} - -macro_rules! expect { - ($scope: expr, $($arg:tt),*) => {{ - $scope.expect(&format!($($arg,)*)).await?; - }}; -} - -const SERVER_HOSTNAME: &'static str = "irc.localhost"; - -#[rustfmt::skip] -async fn registration(scope: &mut TestScope<'_>, nickname: &str) -> Result<()> { - expect!(scope, ":irc.localhost NOTICE * :Welcome to my server!\r\n"); - send!(scope, "NICK {nickname}\r\n"); - send!(scope, "USER UserName 0 * :Real Name\r\n"); - expect!(scope, ":irc.localhost 001 {nickname} :Welcome to Kek Server\r\n"); - expect!(scope, ":irc.localhost 002 {nickname} :Welcome to Kek Server\r\n"); - expect!(scope, ":irc.localhost 003 {nickname} :Welcome to Kek Server\r\n"); - expect!(scope, ":irc.localhost 004 {nickname} irc.localhost kek-0.1.alpha.3 r CFILPQbcefgijklmnopqrstvz\r\n"); - expect!(scope, ":irc.localhost 005 {nickname} CHANTYPES=# :are supported by this server\r\n"); - Ok(()) -} - -#[rustfmt::skip] -async fn join(scope: &mut TestScope<'_>, nickname: &str, chan: &str) -> Result<()> { - send!(scope, "JOIN #{chan}\r\n"); - expect!(scope, ":{nickname} JOIN #{chan}\r\n"); - expect!(scope, ":irc.localhost 332 {nickname} #{chan} :New room\r\n"); - expect!(scope, ":irc.localhost 353 {nickname} = #{chan} :{nickname}\r\n"); - expect!(scope, ":irc.localhost 366 {nickname} #{chan} :End of /NAMES list\r\n"); - Ok(()) -} - -#[tokio::test] -async fn test_registration() -> Result<()> { - let mut stream = TcpStream::connect("127.0.0.1:6667").await?; - let mut scope = init_client(&mut stream).await?; - - registration(&mut scope, "NickName1").await?; - join(&mut scope, "NickName1", "chan1").await?; - Ok(()) -} - -#[rustfmt::skip] -#[tokio::test] -async fn test_two_connections_one_player() -> Result<()> { - let mut stream1 = TcpStream::connect("127.0.0.1:6667").await?; - let mut scope1 = init_client(&mut stream1).await?; - let mut stream2 = TcpStream::connect("127.0.0.1:6667").await?; - let mut scope2 = init_client(&mut stream2).await?; - - let nickname = "NickName2"; - let chan = "chan2"; - - registration(&mut scope1, nickname).await?; - registration(&mut scope2, nickname).await?; - join(&mut scope1, nickname, chan).await?; - - // force join on second connection when the other one joins a room - expect!(scope2, ":{nickname} JOIN #{chan}\r\n"); - expect!(scope2, ":{SERVER_HOSTNAME} 332 {nickname} #{chan} :New room\r\n"); - expect!(scope2, ":{SERVER_HOSTNAME} 353 {nickname} = #{chan} :{nickname}\r\n"); - expect!(scope2, ":{SERVER_HOSTNAME} 366 {nickname} #{chan} :End of /NAMES list\r\n"); - - // force send PRIVMSG to other connections - send!(scope1, "PRIVMSG #{chan} :Chmoki vsem v etam chati!\r\n"); - expect!(scope2, ":{nickname} PRIVMSG #{chan} :Chmoki vsem v etam chati!\r\n"); - send!(scope2, "PRIVMSG #{chan} :I tebe privetiki\r\n"); - expect!(scope1, ":{nickname} PRIVMSG #{chan} :I tebe privetiki\r\n"); - - let mut stream3 = TcpStream::connect("127.0.0.1:6667").await?; - let mut scope3 = init_client(&mut stream3).await?; - registration(&mut scope3, nickname).await?; - - // force join on registration - expect!(scope3, ":{nickname} JOIN #{chan}\r\n"); - expect!(scope3, ":{SERVER_HOSTNAME} 332 {nickname} #{chan} :New room\r\n"); - expect!(scope3, ":{SERVER_HOSTNAME} 353 {nickname} = #{chan} :{nickname}\r\n"); - expect!(scope3, ":{SERVER_HOSTNAME} 366 {nickname} #{chan} :End of /NAMES list\r\n"); - - Ok(()) -} - -#[rustfmt::skip] -#[tokio::test] -async fn test_two_players() -> Result<()> { - let mut stream1 = TcpStream::connect("127.0.0.1:6667").await?; - let mut scope1 = init_client(&mut stream1).await?; - let mut stream2 = TcpStream::connect("127.0.0.1:6667").await?; - let mut scope2 = init_client(&mut stream2).await?; - - let nickname1 = "NickName3"; - let nickname2 = "NickName4"; - - let chan = "chan3"; - - registration(&mut scope1, "NickName3").await?; - registration(&mut scope2, "NickName4").await?; - join(&mut scope1, nickname1, "chan3").await?; - send!(scope2, "JOIN #{chan}\r\n"); - expect!(scope2, ":{nickname2} JOIN #{chan}\r\n"); - expect!(scope2, ":irc.localhost 332 {nickname2} #{chan} :New room\r\n"); - scope2.assert(|line| { - if line == format!(":irc.localhost 353 {nickname2} = #{chan} :{nickname1} {nickname2}\r\n") { return Ok(()) } - if line == format!(":irc.localhost 353 {nickname2} = #{chan} :{nickname2} {nickname1}\r\n") { return Ok(()) } - panic!("incorrect chan member list received: {line}"); - }).await?; - expect!(scope2, ":irc.localhost 366 {nickname2} #{chan} :End of /NAMES list\r\n"); - - expect!(scope1, ":{nickname2} JOIN #{chan}\r\n"); - - send!(scope1, "PRIVMSG #chan3 :Chmoki vsem v etam chati!\r\n"); - expect!(scope2, ":{nickname1} PRIVMSG #chan3 :Chmoki vsem v etam chati!\r\n"); - send!(scope2, "PRIVMSG #chan3 :I tebe privetiki\r\n"); - expect!(scope1, ":{nickname2} PRIVMSG #chan3 :I tebe privetiki\r\n"); - - Ok(()) -} diff --git a/src/projections/mod.rs b/src/projections/mod.rs index 333c971..3d73ce5 100644 --- a/src/projections/mod.rs +++ b/src/projections/mod.rs @@ -1,3 +1,2 @@ //! Protocol projections — implementations of public APIs. -pub mod irc; pub mod xmpp; diff --git a/src/projections/xmpp/mod.rs b/src/projections/xmpp/mod.rs index 5b9e035..530385c 100644 --- a/src/projections/xmpp/mod.rs +++ b/src/projections/xmpp/mod.rs @@ -20,8 +20,9 @@ use tokio_rustls::rustls::{Certificate, PrivateKey}; use tokio_rustls::TlsAcceptor; use lavina_core::player::{PlayerConnection, PlayerId, PlayerRegistry}; +use lavina_core::prelude::*; use lavina_core::room::{RoomId, RoomRegistry}; -use crate::prelude::*; +use lavina_core::terminator::Terminator; use proto_xmpp::bind::{BindResponse, Jid, Name, Resource, Server}; use proto_xmpp::client::{Iq, Message, MessageType, Presence}; use proto_xmpp::disco::*; @@ -29,7 +30,6 @@ use proto_xmpp::roster::RosterQuery; use proto_xmpp::session::Session; use proto_xmpp::stream::*; use proto_xmpp::xml::{Continuation, FromXml, Parser, ToXml}; -use crate::util::Terminator; use self::proto::{ClientPacket, IqClientBody}; diff --git a/src/projections/xmpp/proto.rs b/src/projections/xmpp/proto.rs index 5fd54d1..f9bed8c 100644 --- a/src/projections/xmpp/proto.rs +++ b/src/projections/xmpp/proto.rs @@ -1,7 +1,9 @@ +use anyhow::anyhow; use derive_more::From; use quick_xml::events::Event; use quick_xml::name::{Namespace, ResolveResult}; +use lavina_core::prelude::*; use proto_xmpp::bind::BindRequest; use proto_xmpp::client::{Iq, Message, Presence}; use proto_xmpp::disco::{InfoQuery, ItemQuery}; @@ -9,8 +11,6 @@ use proto_xmpp::roster::RosterQuery; use proto_xmpp::session::Session; use proto_xmpp::xml::*; -use crate::prelude::*; - #[derive(PartialEq, Eq, Debug, From)] pub enum IqClientBody { Bind(BindRequest), @@ -29,7 +29,7 @@ impl FromXml for IqClientBody { let bytes = match event { Event::Start(bytes) => bytes, Event::Empty(bytes) => bytes, - _ => return Err(ffail!("Unexpected XML event: {event:?}")), + _ => return Err(anyhow!("Unexpected XML event: {event:?}")), }; let name = bytes.name(); match_parser!(name, namespace, event; @@ -67,7 +67,7 @@ impl FromXml for ClientPacket { Presence::, Message, { - Err(ffail!( + Err(anyhow!( "Unexpected XML event of name {:?} in namespace {:?}", name, namespace @@ -80,11 +80,11 @@ impl FromXml for ClientPacket { if name.local_name().as_ref() == b"stream" { return Ok(ClientPacket::StreamEnd); } else { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); } } _ => { - return Err(ffail!("Unexpected XML event: {event:?}")); + return Err(anyhow!("Unexpected XML event: {event:?}")); } } } diff --git a/src/util/mod.rs b/src/util/mod.rs index b3d7d82..90e7c7c 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,32 +1,3 @@ -use crate::prelude::*; - pub mod telemetry; #[cfg(test)] pub mod testkit; - -pub struct Terminator { - signal: Promise<()>, - completion: JoinHandle>, -} -impl Terminator { - pub async fn terminate(self) -> Result<()> { - match self.signal.send(()) { - Ok(()) => {} - Err(_) => log::warn!("Termination channel is dropped"), - } - self.completion.await??; - Ok(()) - } - - /// Used to spawn managed tasks with support for graceful shutdown - pub fn spawn(launcher: Fun) -> Terminator - where - Fun: FnOnce(Deferred<()>) -> Fut, - Fut: Future> + Send + 'static, - { - let (signal, rx) = oneshot(); - let future = launcher(rx); - let completion = tokio::task::spawn(future); - Terminator { signal, completion } - } -} diff --git a/src/util/telemetry.rs b/src/util/telemetry.rs index 92006ad..f238978 100644 --- a/src/util/telemetry.rs +++ b/src/util/telemetry.rs @@ -11,10 +11,10 @@ use prometheus::{Encoder, Registry as MetricsRegistry, TextEncoder}; use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; +use lavina_core::prelude::*; use lavina_core::repo::Storage; use lavina_core::room::RoomRegistry; -use crate::prelude::*; -use crate::util::Terminator; +use lavina_core::terminator::Terminator; use mgmt_api::*; diff --git a/src/util/testkit.rs b/src/util/testkit.rs index d568bea..59277e0 100644 --- a/src/util/testkit.rs +++ b/src/util/testkit.rs @@ -2,7 +2,7 @@ use std::task::{Context, Poll}; use futures_util::task::noop_waker_ref; -use crate::prelude::*; +use lavina_core::prelude::*; pub fn sync_future(future: impl Future) -> Result { let waker = noop_waker_ref(); From dc0a101fe645e1b3d858283415d2c5906b8b126e Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Sun, 1 Oct 2023 01:47:18 +0200 Subject: [PATCH 14/17] split xmpp projection into a separate crate --- Cargo.lock | 25 ++++++++--- Cargo.toml | 6 +-- crates/projection-irc/Cargo.toml | 5 ++- crates/projection-xmpp/Cargo.toml | 20 +++++++++ .../projection-xmpp/src/lib.rs | 44 +++++++------------ .../projection-xmpp/src}/proto.rs | 0 src/main.rs | 12 +---- src/projections/mod.rs | 2 - 8 files changed, 63 insertions(+), 51 deletions(-) create mode 100644 crates/projection-xmpp/Cargo.toml rename src/projections/xmpp/mod.rs => crates/projection-xmpp/src/lib.rs (94%) rename {src/projections/xmpp => crates/projection-xmpp/src}/proto.rs (100%) delete mode 100644 src/projections/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 6459827..895cc5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -858,19 +858,15 @@ dependencies = [ "mgmt-api", "nonempty", "projection-irc", + "projection-xmpp", "prometheus", - "proto-xmpp", - "quick-xml", "regex", "reqwest", - "rustls-pemfile", "serde", "serde_json", "tokio", - "tokio-rustls", "tracing", "tracing-subscriber", - "uuid", ] [[package]] @@ -1267,6 +1263,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "projection-xmpp" +version = "0.0.1-dev" +dependencies = [ + "anyhow", + "derive_more", + "futures-util", + "lavina-core", + "prometheus", + "proto-xmpp", + "quick-xml", + "rustls-pemfile", + "serde", + "tokio", + "tokio-rustls", + "tracing", + "uuid", +] + [[package]] name = "prometheus" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index 1bf61d3..12df833 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,14 +47,10 @@ tracing-subscriber = "0.3.16" futures-util.workspace = true prometheus.workspace = true nonempty.workspace = true -tokio-rustls = "0.24.1" -rustls-pemfile = "1.0.2" -quick-xml.workspace = true derive_more.workspace = true -uuid = { version = "1.3.0", features = ["v4"] } lavina-core.workspace = true projection-irc = { path = "crates/projection-irc" } -proto-xmpp = { path = "crates/proto-xmpp" } +projection-xmpp = { path = "crates/projection-xmpp" } mgmt-api = { path = "crates/mgmt-api" } clap.workspace = true diff --git a/crates/projection-irc/Cargo.toml b/crates/projection-irc/Cargo.toml index c607327..e8166f0 100644 --- a/crates/projection-irc/Cargo.toml +++ b/crates/projection-irc/Cargo.toml @@ -7,9 +7,10 @@ version.workspace = true lavina-core.workspace = true tracing.workspace = true anyhow.workspace = true -nonempty.workspace = true serde.workspace = true tokio.workspace = true prometheus.workspace = true -proto-irc = { path = "../proto-irc" } futures-util.workspace = true + +nonempty.workspace = true +proto-irc = { path = "../proto-irc" } diff --git a/crates/projection-xmpp/Cargo.toml b/crates/projection-xmpp/Cargo.toml new file mode 100644 index 0000000..c69cb12 --- /dev/null +++ b/crates/projection-xmpp/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "projection-xmpp" +edition = "2021" +version.workspace = true + +[dependencies] +lavina-core.workspace = true +tracing.workspace = true +anyhow.workspace = true +serde.workspace = true +tokio.workspace = true +prometheus.workspace = true +futures-util.workspace = true + +quick-xml.workspace = true +proto-xmpp = { path = "../proto-xmpp" } +uuid = { version = "1.3.0", features = ["v4"] } +tokio-rustls = "0.24.1" +rustls-pemfile = "1.0.2" +derive_more.workspace = true diff --git a/src/projections/xmpp/mod.rs b/crates/projection-xmpp/src/lib.rs similarity index 94% rename from src/projections/xmpp/mod.rs rename to crates/projection-xmpp/src/lib.rs index 530385c..d55a09f 100644 --- a/src/projections/xmpp/mod.rs +++ b/crates/projection-xmpp/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(generators, generator_trait, type_alias_impl_trait, impl_trait_in_assoc_type)] + mod proto; use std::collections::HashMap; @@ -7,6 +9,7 @@ use std::net::SocketAddr; use std::path::PathBuf; use std::sync::Arc; +use anyhow::anyhow; use futures_util::future::join_all; use prometheus::Registry as MetricsRegistry; use quick_xml::events::{BytesDecl, Event}; @@ -117,21 +120,15 @@ pub async fn launch( } } log::info!("Stopping XMPP projection"); - join_all( - actors - .into_iter() - .map(|(socket_addr, terminator)| async move { - log::debug!("Stopping XMPP connection at {socket_addr}"); - match terminator.terminate().await { - Ok(_) => log::debug!("Stopped XMPP connection at {socket_addr}"), - Err(err) => { - log::warn!( - "XMPP connection to {socket_addr} finished with error: {err}" - ) - } - } - }), - ) + join_all(actors.into_iter().map(|(socket_addr, terminator)| async move { + log::debug!("Stopping XMPP connection at {socket_addr}"); + match terminator.terminate().await { + Ok(_) => log::debug!("Stopped XMPP connection at {socket_addr}"), + Err(err) => { + log::warn!("XMPP connection to {socket_addr} finished with error: {err}") + } + } + })) .await; log::info!("Stopped XMPP projection"); Ok(()) @@ -172,9 +169,7 @@ async fn handle_socket( let authenticated = socket_auth(&mut xml_reader, &mut xml_writer, &mut reader_buf).await?; log::debug!("User authenticated"); - let mut connection = players - .connect_to_player(authenticated.player_id.clone()) - .await; + let mut connection = players.connect_to_player(authenticated.player_id.clone()).await; socket_final( &mut xml_reader, &mut xml_writer, @@ -396,10 +391,7 @@ async fn handle_packet( { if server.0.as_ref() == "rooms.localhost" && m.r#type == MessageType::Groupchat { user_handle - .send_message( - RoomId::from(name.0.clone())?, - m.body.clone().into(), - ) + .send_message(RoomId::from(name.0.clone())?, m.body.clone().into()) .await?; Message { to: Some(Jid { @@ -448,9 +440,7 @@ async fn handle_packet( resource: Some(resource), }) = p.to { - let a = user_handle - .join_room(RoomId::from(name.0.clone())?) - .await?; + let a = user_handle.join_room(RoomId::from(name.0.clone())?).await?; Presence::<()> { to: Some(Jid { name: Some(user.xmpp_name.clone()), @@ -633,13 +623,13 @@ async fn read_xml_header( if &*encoding == b"UTF-8" { Ok(()) } else { - Err(fail(format!("Unsupported encoding: {encoding:?}").as_str())) + Err(anyhow!("Unsupported encoding: {encoding:?}")) } } else { // Err(fail("No XML encoding provided")) Ok(()) } } else { - Err(fail("Expected XML header")) + Err(anyhow!("Expected XML header")) } } diff --git a/src/projections/xmpp/proto.rs b/crates/projection-xmpp/src/proto.rs similarity index 100% rename from src/projections/xmpp/proto.rs rename to crates/projection-xmpp/src/proto.rs diff --git a/src/main.rs b/src/main.rs index 61ff315..a7eab22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,3 @@ -#![feature( - generators, - generator_trait, - type_alias_impl_trait, - impl_trait_in_assoc_type -)] - -mod projections; mod util; use std::future::Future; @@ -26,7 +18,7 @@ use lavina_core::room::RoomRegistry; struct ServerConfig { telemetry: util::telemetry::ServerConfig, irc: projection_irc::ServerConfig, - xmpp: projections::xmpp::ServerConfig, + xmpp: projection_xmpp::ServerConfig, storage: lavina_core::repo::StorageConfig, } @@ -64,7 +56,7 @@ async fn main() -> Result<()> { let telemetry_terminator = util::telemetry::launch(telemetry_config, metrics.clone(), rooms.clone(), storage.clone()).await?; let irc = projection_irc::launch(irc_config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await?; - let xmpp = projections::xmpp::launch(xmpp_config, players.clone(), rooms.clone(), metrics.clone()).await?; + let xmpp = projection_xmpp::launch(xmpp_config, players.clone(), rooms.clone(), metrics.clone()).await?; tracing::info!("Started"); sleep.await; diff --git a/src/projections/mod.rs b/src/projections/mod.rs deleted file mode 100644 index 3d73ce5..0000000 --- a/src/projections/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -//! Protocol projections — implementations of public APIs. -pub mod xmpp; From 2f034284cfeaeb3c04affc9d3794edcc8e0958d2 Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Sun, 1 Oct 2023 01:50:04 +0200 Subject: [PATCH 15/17] rename telemetry mod to http --- src/{util/telemetry.rs => http.rs} | 4 ++-- src/main.rs | 18 ++++++++++++------ src/util/mod.rs | 3 --- src/util/testkit.rs | 16 ---------------- 4 files changed, 14 insertions(+), 27 deletions(-) rename src/{util/telemetry.rs => http.rs} (98%) delete mode 100644 src/util/mod.rs delete mode 100644 src/util/testkit.rs diff --git a/src/util/telemetry.rs b/src/http.rs similarity index 98% rename from src/util/telemetry.rs rename to src/http.rs index f238978..d860c10 100644 --- a/src/util/telemetry.rs +++ b/src/http.rs @@ -31,7 +31,7 @@ pub async fn launch( rooms: RoomRegistry, storage: Storage, ) -> Result { - log::info!("Starting the telemetry service"); + log::info!("Starting the http service"); let listener = TcpListener::bind(config.listen_on).await?; log::debug!("Listener started"); let terminator = Terminator::spawn(|rx| main_loop(listener, metrics, rooms, storage, rx.map(|_| ()))); @@ -67,7 +67,7 @@ async fn main_loop( }, } } - log::info!("Terminating the telemetry service"); + log::info!("Terminating the http service"); Ok(()) } diff --git a/src/main.rs b/src/main.rs index a7eab22..74e40ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -mod util; +mod http; use std::future::Future; use std::path::Path; @@ -16,7 +16,7 @@ use lavina_core::room::RoomRegistry; #[derive(Deserialize, Debug)] struct ServerConfig { - telemetry: util::telemetry::ServerConfig, + telemetry: http::ServerConfig, irc: projection_irc::ServerConfig, xmpp: projection_xmpp::ServerConfig, storage: lavina_core::repo::StorageConfig, @@ -25,7 +25,7 @@ struct ServerConfig { #[derive(Parser)] struct CliArgs { #[arg(long)] - config: Box, + config: Box, } fn load_config() -> Result { @@ -53,9 +53,15 @@ async fn main() -> Result<()> { let storage = Storage::open(storage_config).await?; let rooms = RoomRegistry::new(&mut metrics, storage.clone())?; let mut players = PlayerRegistry::empty(rooms.clone(), &mut metrics)?; - let telemetry_terminator = - util::telemetry::launch(telemetry_config, metrics.clone(), rooms.clone(), storage.clone()).await?; - let irc = projection_irc::launch(irc_config, players.clone(), rooms.clone(), metrics.clone(), storage.clone()).await?; + let telemetry_terminator = http::launch(telemetry_config, metrics.clone(), rooms.clone(), storage.clone()).await?; + let irc = projection_irc::launch( + irc_config, + players.clone(), + rooms.clone(), + metrics.clone(), + storage.clone(), + ) + .await?; let xmpp = projection_xmpp::launch(xmpp_config, players.clone(), rooms.clone(), metrics.clone()).await?; tracing::info!("Started"); diff --git a/src/util/mod.rs b/src/util/mod.rs deleted file mode 100644 index 90e7c7c..0000000 --- a/src/util/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod telemetry; -#[cfg(test)] -pub mod testkit; diff --git a/src/util/testkit.rs b/src/util/testkit.rs deleted file mode 100644 index 59277e0..0000000 --- a/src/util/testkit.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std::task::{Context, Poll}; - -use futures_util::task::noop_waker_ref; - -use lavina_core::prelude::*; - -pub fn sync_future(future: impl Future) -> Result { - let waker = noop_waker_ref(); - let mut context = Context::from_waker(waker); - pin!(future); - if let Poll::Ready(a) = future.poll(&mut context) { - Ok(a) - } else { - Err(anyhow::Error::msg("Future has suspended")) - } -} From 47195f5eee7c7a49bcb4ebfe5f1b13d4e3e65d49 Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Sun, 1 Oct 2023 18:15:37 +0200 Subject: [PATCH 16/17] add crates/README.md --- crates/README.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 crates/README.md diff --git a/crates/README.md b/crates/README.md new file mode 100644 index 0000000..04909f2 --- /dev/null +++ b/crates/README.md @@ -0,0 +1,30 @@ +## Dependency diagram of the project + +```mermaid +graph TD; + lavina-->mgmt-api; + lavina-->projection-irc; + lavina-->projection-xmpp; + lavina-->lavina-core; + + projection-irc-->proto-irc; + projection-irc-->lavina-core; + + projection-xmpp-->proto-xmpp; + projection-xmpp-->lavina-core; + + sim-irc-->proto-irc; + sim-irc-->mgmt-api; + + sim-xmpp-->proto-xmpp; + sim-xmpp-->mgmt-api; + + workspace-->lavina; + workspace-->sim-irc; + workspace-->sim-xmpp; +``` + +A few rules: +- Only projections should be direct deps of `lavina`, there is no need to depend on `proto-*` crates. +- On the other hand, projections should not be dependencies of `sim-*` crates. +- `lavina-core` does not depend on protocol-specific crates. From 8047a97baa912efec2c8e4b961a7164f3a10587f Mon Sep 17 00:00:00 2001 From: Nikita Vilunov Date: Sun, 1 Oct 2023 18:16:11 +0200 Subject: [PATCH 17/17] remove /test/ --- test/.gitignore | 1 - test/init_state.sql | 6 ------ test/run.sh | 5 ----- 3 files changed, 12 deletions(-) delete mode 100644 test/.gitignore delete mode 100644 test/init_state.sql delete mode 100755 test/run.sh diff --git a/test/.gitignore b/test/.gitignore deleted file mode 100644 index 397b4a7..0000000 --- a/test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.log diff --git a/test/init_state.sql b/test/init_state.sql deleted file mode 100644 index 8555332..0000000 --- a/test/init_state.sql +++ /dev/null @@ -1,6 +0,0 @@ -insert into users(name) -values ('kek'), ('shrek') -returning id; - -insert into challenges_plain_password(user_id, password) -values (1, 'parolchik1'), (2, 'qwerty123'); diff --git a/test/run.sh b/test/run.sh deleted file mode 100755 index f1162e2..0000000 --- a/test/run.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -cargo build -./target/debug/lavina 1>test/lavina.stdout.log 2>test/lavina.stderr.log & -cargo test -kill %1