From 8e6f26aa578b76a99cd13a10f80bfa88c0c43973 Mon Sep 17 00:00:00 2001 From: hank Date: Fri, 2 Aug 2024 11:40:24 +0800 Subject: [PATCH] socket.io basics --- .github/workflows/rust.yml | 6 +- Cargo.lock | 149 +++++++++++++++++++ api/Cargo.toml | 4 + api/src/config.rs | 4 +- api/src/handler/message_handler.rs | 1 + api/src/handler/mod.rs | 1 + api/src/lib.rs | 14 +- api/src/{middleware.rs => middleware/mod.rs} | 1 + api/src/middleware/socket_io.rs | 0 api/src/models/message.rs | 39 +++++ api/src/{router.rs => router/mod.rs} | 5 +- api/src/router/socket_chat.rs | 68 +++++++++ api/src/state.rs | 1 + 13 files changed, 282 insertions(+), 11 deletions(-) create mode 100644 api/src/handler/message_handler.rs rename api/src/{middleware.rs => middleware/mod.rs} (51%) create mode 100644 api/src/middleware/socket_io.rs rename api/src/{router.rs => router/mod.rs} (87%) create mode 100644 api/src/router/socket_chat.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 6ab00d1..1ae274c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -57,13 +57,15 @@ jobs: }' > ~/.config/realm/realm.lua - name: Display realm.lua for verification - run: cat ~/.config/realm/realm.lua + run: | + cat ~/.config/realm/realm.lua + echo $USER - name: Postgres setup run: | sudo apt-get update sudo apt-get install -y postgresql-client - PGPASSWORD=0528 psql -h localhost -U hank -d test_db -c "SELECT 1" + PGPASSWORD=0528 psql -h localhost -U hank -d hank -c "SELECT 1" - name: Build run: cargo build --verbose diff --git a/Cargo.lock b/Cargo.lock index 5b48266..e4309e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -598,6 +598,9 @@ name = "bytes" version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" +dependencies = [ + "serde", +] [[package]] name = "cc" @@ -855,6 +858,12 @@ dependencies = [ "cipher", ] +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + [[package]] name = "der" version = "0.7.9" @@ -924,6 +933,32 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "engineioxide" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b9cfc311d0ac3237b8177d2ee8962aa5bb4cfa22faf284356f2ebaf9d698f0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "pin-project-lite", + "rand", + "serde", + "serde_json", + "smallvec", + "thiserror", + "tokio", + "tokio-tungstenite", + "tower", +] + [[package]] name = "entity" version = "0.1.0" @@ -1254,6 +1289,19 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1792,6 +1840,21 @@ dependencies = [ "value-bag", ] +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lua-src" version = "547.0.0" @@ -1820,6 +1883,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "md-5" version = "0.10.6" @@ -2615,8 +2684,11 @@ dependencies = [ "sea-orm-migration", "serde", "serde_json", + "socketioxide", "sonyflake", "tokio", + "tower", + "tower-http", "tracing", "tracing-subscriber", ] @@ -2945,6 +3017,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -3128,6 +3206,7 @@ dependencies = [ "tokio", "tokio-rustls", "tokio-util", + "tower", "tracing", "url", "zstd", @@ -3185,6 +3264,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3561,6 +3646,30 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socketioxide" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23f50a295325631d230022f1562fde3d1351edf4d8eac73265f657cc762f655c" +dependencies = [ + "bytes", + "engineioxide", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "pin-project-lite", + "serde", + "serde_json", + "state", + "thiserror", + "tokio", + "tower", +] + [[package]] name = "sonyflake" version = "0.2.0" @@ -3814,6 +3923,15 @@ dependencies = [ "uuid", ] +[[package]] +name = "state" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" +dependencies = [ + "loom", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -4124,6 +4242,24 @@ dependencies = [ "pin-project", "pin-project-lite", "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", "tower-layer", "tower-service", ] @@ -4226,8 +4362,12 @@ checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" dependencies = [ "byteorder", "bytes", + "data-encoding", + "http", + "httparse", "log", "rand", + "sha1", "thiserror", "utf-8", ] @@ -4582,6 +4722,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-core" version = "0.52.0" diff --git a/api/Cargo.toml b/api/Cargo.toml index 477ec81..5c120c6 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -16,6 +16,9 @@ chrono = { version = "0.4", features = ["serde"] } sonyflake = { version = "0.2.0" } argon2 = "0.5" color-eyre = "0.6" +tower-http = { version = "0.5.2", features = ["cors"] } +tower = "0.4" +socketioxide = {version = "0.14", features = ["state"]} entity = { path = "../entity" } migration = { path = "../migration" } @@ -30,6 +33,7 @@ features = [ "basic-auth", "compression", "affix", + "tower-compat", ] [dependencies.sea-orm] diff --git a/api/src/config.rs b/api/src/config.rs index 51381a5..6d60cd1 100644 --- a/api/src/config.rs +++ b/api/src/config.rs @@ -45,8 +45,8 @@ pub async fn get_config() -> Result { let config: Table = luai.load(&config_content).eval()?; let config = serde_json::to_string(&config)?; - let config: Config = serde_json::from_str(&config)?; - if let Some(url) = env::var("DATABASE_URL").ok() { + let mut config: Config = serde_json::from_str(&config)?; + if let Ok(url) = env::var("DATABASE_URL") { info!("overriding database url with DATABASE_URL"); config.db_url = url; } diff --git a/api/src/handler/message_handler.rs b/api/src/handler/message_handler.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/api/src/handler/message_handler.rs @@ -0,0 +1 @@ + diff --git a/api/src/handler/mod.rs b/api/src/handler/mod.rs index 5af582f..ae54efd 100644 --- a/api/src/handler/mod.rs +++ b/api/src/handler/mod.rs @@ -1,2 +1,3 @@ pub mod user_handler; pub mod server_handler; +pub mod message_handler; diff --git a/api/src/lib.rs b/api/src/lib.rs index 86c1472..58ce66c 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -8,7 +8,6 @@ pub mod router; pub mod state; pub mod utils; -use crate::router::make_route; use color_eyre::Result; use salvo::prelude::*; use tokio::signal; @@ -18,11 +17,12 @@ use tracing_subscriber::EnvFilter; #[tokio::main] pub async fn main() -> Result<()> { let filter = EnvFilter::from_default_env(); - tracing_subscriber::fmt().with_env_filter(filter).with_test_writer().init(); + tracing_subscriber::fmt() + .with_env_filter(filter) + .with_test_writer() + .init(); color_eyre::install()?; - - let config = config::get_config().await.unwrap_or_else(|_| { info!("failed to read config file, using default instead"); config::Config::default() @@ -32,7 +32,9 @@ pub async fn main() -> Result<()> { .bind() .await; - let router = Router::with_path("api").push(make_route(&config).await); + let router = Router::new() + .push(Router::with_path("api").push(router::make_router(&config).await)) + .push(crate::router::socket_chat::make_router()); // TODO: http3 let server = Server::new(acceptor); @@ -59,7 +61,7 @@ mod tests { #[tokio::test] async fn test_basic_auth() { let test_config = Config::default(); - let service = Service::new(super::make_route(&test_config).await); + let service = Service::new(super::make_router(&test_config).await); let url = format!("http://{}:{}/", test_config.host, test_config.port); diff --git a/api/src/middleware.rs b/api/src/middleware/mod.rs similarity index 51% rename from api/src/middleware.rs rename to api/src/middleware/mod.rs index 175f81f..9e9b8ee 100644 --- a/api/src/middleware.rs +++ b/api/src/middleware/mod.rs @@ -1 +1,2 @@ pub mod basic_auth; +pub mod socket_io; diff --git a/api/src/middleware/socket_io.rs b/api/src/middleware/socket_io.rs new file mode 100644 index 0000000..e69de29 diff --git a/api/src/models/message.rs b/api/src/models/message.rs index e69de29..00547e7 100644 --- a/api/src/models/message.rs +++ b/api/src/models/message.rs @@ -0,0 +1,39 @@ +use std::collections::{HashMap, VecDeque}; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; +use std::sync::Arc; + +use chrono::{DateTime, Utc}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Message { + pub text: String, + pub user: String, + pub date: DateTime +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Messages { + pub messages: Vec, +} + +pub type RoomStore = HashMap>; + +#[derive(Clone, Default)] +pub struct MessageStore { + pub messages: Arc>, +} + +impl MessageStore { + pub async fn insert(&self, room: &str, message: Message) { + let mut binding = self.messages.write().await; + let messages = binding.entry(room.to_owned()).or_default(); + messages.push_front(message); + messages.truncate(20); + } + + pub async fn get(&self, room: &str) -> Vec { + let messages = self.messages.read().await.get(room).cloned(); + messages.unwrap_or_default().into_iter().rev().collect() + } +} diff --git a/api/src/router.rs b/api/src/router/mod.rs similarity index 87% rename from api/src/router.rs rename to api/src/router/mod.rs index e359857..2b7fa9c 100644 --- a/api/src/router.rs +++ b/api/src/router/mod.rs @@ -1,3 +1,5 @@ +pub mod socket_chat; + use crate::config::Config; use crate::db; use crate::middleware::basic_auth::Validator; @@ -18,7 +20,7 @@ async fn hello_admin() -> Result { } // Main Router -pub async fn make_route(config: &Config) -> Router { +pub async fn make_router(config: &Config) -> Router { let auth_handler = BasicAuth::new(Validator); let db = db::init(config).await.unwrap(); @@ -29,6 +31,7 @@ pub async fn make_route(config: &Config) -> Router { let router = Router::new() .hoop(affix::inject(state)) .push(Router::with_path("hello").get(hello)) + .push(socket_chat::make_router()) .push( Router::with_hoop(auth_handler) .path("hello_admin") diff --git a/api/src/router/socket_chat.rs b/api/src/router/socket_chat.rs new file mode 100644 index 0000000..9433d52 --- /dev/null +++ b/api/src/router/socket_chat.rs @@ -0,0 +1,68 @@ +use chrono::{DateTime, Utc}; +use salvo::prelude::*; +use serde::{Deserialize, Serialize}; +use socketioxide::{ + extract::{Data, SocketRef, State}, + SocketIo, +}; +use tower::ServiceBuilder; +use tower_http::cors::CorsLayer; +use tracing::info; + +use crate::models::message::{Message, MessageStore, Messages}; + +#[handler] +async fn hello() -> &'static str { + "Hello World" +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MessageIn { + room: String, + text: String, +} + +fn on_connect(socket: SocketRef) { + info!("Socket.IO connected: {:?} {:?}", socket.ns(), socket.id); + + socket.on( + "join", + |socket: SocketRef, Data::(room), store: State| async move { + info!("Received join: {:?}", room); + let _ = socket.leave_all(); + let _ = socket.join(room.clone()); + let messages = store.get(&room).await; + let _ = socket.emit("messages", Messages { messages }); + }, + ); + + socket.on( + "message", + |socket: SocketRef, Data::(data), store: State| async move { + info!("Received message: {:?}", data); + + let response = Message { + text: data.text, + user: format!("anon-{}", socket.id), + date: Utc::now(), + }; + + store.insert(&data.room, response.clone()).await; + + let _ = socket.within(data.room).emit("message", response); + }, + ); +} + +pub fn make_router() -> Router { + let messages = crate::models::message::MessageStore::default(); + let (layer, io) = SocketIo::builder().with_state(messages).build_layer(); + let layer = ServiceBuilder::new() + .layer(CorsLayer::permissive()) + .layer(layer); + + io.ns("/", on_connect); + io.ns("/custom", on_connect); + let layer = layer.compat(); + Router::with_path("socket.io").hoop(layer).goal(hello) +} diff --git a/api/src/state.rs b/api/src/state.rs index a0f0ad5..af8ed37 100644 --- a/api/src/state.rs +++ b/api/src/state.rs @@ -10,3 +10,4 @@ impl AppState { Self { conn } } } +