From c6e4dda41fb37b1407c733a00194949ebcdada6d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 9 Jul 2024 16:29:39 +0200 Subject: [PATCH 001/162] poem gateway experimentation --- src/gateway/mod.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 9 ++++---- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index e69de29..dde9e26 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -0,0 +1,54 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use futures::{SinkExt, StreamExt}; +use log::{debug, info}; +use poem::listener::TcpListener; +use poem::web::websocket::{Message, WebSocket}; +use poem::web::Data; +use poem::{get, handler, EndpointExt, IntoResponse, Route, Server}; + +use crate::errors::Error; + +#[handler] +fn ws(ws: WebSocket, sender: Data<&tokio::sync::broadcast::Sender>) -> impl IntoResponse { + let sender = sender.clone(); + let mut receiver = sender.subscribe(); + ws.on_upgrade(move |socket| async move { + let (mut sink, mut stream) = socket.split(); + + tokio::spawn(async move { + while let Some(Ok(msg)) = stream.next().await { + if let Message::Text(text) = msg { + if sender.send(Message::text(text)).is_err() { + break; + } + } + } + }); + + tokio::spawn(async move { + while let Ok(msg) = receiver.recv().await { + if sink.send(msg).await.is_err() { + break; + } + } + }); + }) +} + +pub async fn start_gateway() -> Result<(), Error> { + info!(target: "symfonia::gateway", "Starting gateway server"); + let ws_app = Route::new().at( + "/", + get(ws.data(tokio::sync::mpsc::channel::(32).0)), + ); + let bind = std::env::var("API_BIND").unwrap_or_else(|_| String::from("localhost:3001")); + debug!(target: "symfonia::gateway", "Binding to {}", bind); + Server::new(TcpListener::bind("0.0.0.0:3000")) + .run(ws_app) + .await?; + debug!(target: "symfonia::gateway", "Gateway server started"); + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 3da7acd..9478575 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,20 @@ use clap::Parser; +use log::LevelFilter; use log4rs::{ append::{ console::{ConsoleAppender, Target}, rolling_file::{ policy::compound::{ - CompoundPolicy, roll::delete::DeleteRoller, trigger::size::SizeTrigger, + roll::delete::DeleteRoller, trigger::size::SizeTrigger, CompoundPolicy, }, RollingFileAppender, }, }, config::{Appender, Logger, Root}, - Config, encode::pattern::PatternEncoder, filter::Filter, + Config, }; -use log::LevelFilter; mod api; mod cdn; @@ -124,7 +124,7 @@ async fn main() { .logger( Logger::builder() .appender("gateway") - .build("symfonia::gateway", LevelFilter::Info), + .build("symfonia::gateway", LevelFilter::Debug), ) .build(Root::builder().appender("stdout").build({ let mode = std::env::var("MODE").unwrap_or("DEBUG".to_string()); @@ -141,6 +141,7 @@ async fn main() { log::info!(target: "symfonia", "Starting up Symfonia"); + gateway::start_gateway().await.unwrap(); // TODO: This should be near api::start... log::info!(target: "symfonia::db", "Establishing database connection"); let db = database::establish_connection() .await From 76e3e303480cec20d1f45b9c99b112719307644a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 9 Jul 2024 22:42:17 +0200 Subject: [PATCH 002/162] create `start_server`, moving server starting logic to main.rs from api module --- src/main.rs | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9478575..cf03cfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,12 @@ use log4rs::{ filter::Filter, Config, }; +use poem::listener::TcpListener; +use poem::middleware::{NormalizePath, TrailingSlash}; +use poem::web::Json; +use poem::{EndpointExt, IntoResponse, Route, Server}; +use serde_json::json; +use sqlx::MySqlPool; mod api; mod cdn; @@ -23,6 +29,8 @@ mod errors; mod gateway; mod util; +pub type PathRouteTuple = (String, Route); + #[derive(Debug)] struct LogFilter; @@ -141,7 +149,6 @@ async fn main() { log::info!(target: "symfonia", "Starting up Symfonia"); - gateway::start_gateway().await.unwrap(); // TODO: This should be near api::start... log::info!(target: "symfonia::db", "Establishing database connection"); let db = database::establish_connection() .await @@ -181,6 +188,50 @@ async fn main() { .await .expect("Failed to seed config"); } + let bind = std::env::var("API_BIND").unwrap_or_else(|_| String::from("localhost:3001")); + let api_route = api::setup_api(); + let gateway_route = gateway::setup_gateway(); + start_server(vec![api_route, gateway_route], &bind, db) + .await + .expect("Failed to start server") +} + +async fn start_server( + routes: Vec<(impl AsRef, Route)>, + addr: &impl ToString, + db: MySqlPool, +) -> Result<(), crate::errors::Error> { + let mut app_routes = Route::new(); + let config = crate::database::entities::Config::init(&db).await?; + if config.sentry.enabled { + let _guard = sentry::init(( + "https://241c6fb08adb469da1bb82522b25c99f@sentry.quartzinc.space/3", + sentry::ClientOptions { + release: sentry::release_name!(), + traces_sample_rate: config.sentry.trace_sample_rate as f32, + ..Default::default() + }, + )); + } + for (path, route) in routes.into_iter() { + app_routes = app_routes.nest(path, route); + } + let app = app_routes + .data(db) + .data(config) + .with(NormalizePath::new(TrailingSlash::Trim)) + .catch_all_error(custom_error); + log::info!(target: "symfonia::api", "Starting HTTP Server"); + Server::new(TcpListener::bind(addr.to_string())) + .run(app) + .await?; + Ok(()) +} - api::start_api(db).await.unwrap(); +async fn custom_error(err: poem::Error) -> impl IntoResponse { + Json(json! ({ + "success": false, + "message": err.to_string(), + })) + .with_status(err.status()) } From f46abc2b1512bf0cf783b9f05cd149878f9e99b7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 9 Jul 2024 22:42:28 +0200 Subject: [PATCH 003/162] Move poem::Server starting logic --- src/api/mod.rs | 59 ++++++-------------------------------------------- 1 file changed, 7 insertions(+), 52 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 138049b..0bc9494 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,41 +1,15 @@ -use poem::{ - EndpointExt, - IntoResponse, - listener::TcpListener, - middleware::{NormalizePath, TrailingSlash}, Route, Server, web::Json, -}; -use serde_json::json; -use sqlx::MySqlPool; +use poem::{EndpointExt, Route}; -use crate::{ - api::{ - middleware::{ - authentication::AuthenticationMiddleware, current_user::CurrentUserMiddleware, - }, - routes::{auth, channels, guilds, users}, - }, - database::entities::Config, - errors::Error, +use crate::api::{ + middleware::{authentication::AuthenticationMiddleware, current_user::CurrentUserMiddleware}, + routes::{auth, channels, guilds, users}, }; +use crate::PathRouteTuple; mod middleware; mod routes; -pub async fn start_api(db: MySqlPool) -> Result<(), Error> { - log::info!(target: "symfonia::api::cfg", "Loading configuration"); - let config = Config::init(&db).await?; - - if config.sentry.enabled { - let _guard = sentry::init(( - "https://241c6fb08adb469da1bb82522b25c99f@sentry.quartzinc.space/3", - sentry::ClientOptions { - release: sentry::release_name!(), - traces_sample_rate: config.sentry.trace_sample_rate as f32, - ..Default::default() - }, - )); - } - +pub fn setup_api() -> PathRouteTuple { let routes = Route::new() .nest("/auth", auth::setup_routes()) .nest( @@ -65,24 +39,5 @@ pub async fn start_api(db: MySqlPool) -> Result<(), Error> { .nest("/policies", routes::policies::setup_routes()) .nest("/-", routes::health::setup_routes()); - let v9_api = Route::new() - .nest("/api/v9", routes) - .data(db) - .data(config) - .with(NormalizePath::new(TrailingSlash::Trim)) - .catch_all_error(custom_error); - - let bind = std::env::var("API_BIND").unwrap_or_else(|_| String::from("localhost:3001")); - - log::info!(target: "symfonia::api", "Starting HTTP Server"); - Server::new(TcpListener::bind(bind)).run(v9_api).await?; - Ok(()) -} - -async fn custom_error(err: poem::Error) -> impl IntoResponse { - Json(json! ({ - "success": false, - "message": err.to_string(), - })) - .with_status(err.status()) + ("/api/v9".to_string(), routes) } From 62ab1f3ec330b98967a3ba98749087a28b1fe4b9 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 9 Jul 2024 22:42:49 +0200 Subject: [PATCH 004/162] Change start_gateway() to setup_gateway() --- src/gateway/mod.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index dde9e26..cf0c2bf 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -10,6 +10,7 @@ use poem::web::Data; use poem::{get, handler, EndpointExt, IntoResponse, Route, Server}; use crate::errors::Error; +use crate::PathRouteTuple; #[handler] fn ws(ws: WebSocket, sender: Data<&tokio::sync::broadcast::Sender>) -> impl IntoResponse { @@ -38,17 +39,10 @@ fn ws(ws: WebSocket, sender: Data<&tokio::sync::broadcast::Sender>) -> }) } -pub async fn start_gateway() -> Result<(), Error> { - info!(target: "symfonia::gateway", "Starting gateway server"); - let ws_app = Route::new().at( +pub fn setup_gateway() -> PathRouteTuple { + let ws_route = Route::new().at( "/", get(ws.data(tokio::sync::mpsc::channel::(32).0)), ); - let bind = std::env::var("API_BIND").unwrap_or_else(|_| String::from("localhost:3001")); - debug!(target: "symfonia::gateway", "Binding to {}", bind); - Server::new(TcpListener::bind("0.0.0.0:3000")) - .run(ws_app) - .await?; - debug!(target: "symfonia::gateway", "Gateway server started"); - Ok(()) + ("/".to_string(), ws_route) } From cef98f42fa00a726abd2a0a4db76fb318e93b534 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 10 Jul 2024 17:00:32 +0200 Subject: [PATCH 005/162] Revert "Change start_gateway() to setup_gateway()" This reverts commit 62ab1f3ec330b98967a3ba98749087a28b1fe4b9. --- src/gateway/mod.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index cf0c2bf..dde9e26 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -10,7 +10,6 @@ use poem::web::Data; use poem::{get, handler, EndpointExt, IntoResponse, Route, Server}; use crate::errors::Error; -use crate::PathRouteTuple; #[handler] fn ws(ws: WebSocket, sender: Data<&tokio::sync::broadcast::Sender>) -> impl IntoResponse { @@ -39,10 +38,17 @@ fn ws(ws: WebSocket, sender: Data<&tokio::sync::broadcast::Sender>) -> }) } -pub fn setup_gateway() -> PathRouteTuple { - let ws_route = Route::new().at( +pub async fn start_gateway() -> Result<(), Error> { + info!(target: "symfonia::gateway", "Starting gateway server"); + let ws_app = Route::new().at( "/", get(ws.data(tokio::sync::mpsc::channel::(32).0)), ); - ("/".to_string(), ws_route) + let bind = std::env::var("API_BIND").unwrap_or_else(|_| String::from("localhost:3001")); + debug!(target: "symfonia::gateway", "Binding to {}", bind); + Server::new(TcpListener::bind("0.0.0.0:3000")) + .run(ws_app) + .await?; + debug!(target: "symfonia::gateway", "Gateway server started"); + Ok(()) } From ce38cad6b632abb93125710d75820bdca74c881c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 10 Jul 2024 17:00:36 +0200 Subject: [PATCH 006/162] Revert "Move poem::Server starting logic" This reverts commit f46abc2b1512bf0cf783b9f05cd149878f9e99b7. --- src/api/mod.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 0bc9494..138049b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,15 +1,41 @@ -use poem::{EndpointExt, Route}; +use poem::{ + EndpointExt, + IntoResponse, + listener::TcpListener, + middleware::{NormalizePath, TrailingSlash}, Route, Server, web::Json, +}; +use serde_json::json; +use sqlx::MySqlPool; -use crate::api::{ - middleware::{authentication::AuthenticationMiddleware, current_user::CurrentUserMiddleware}, - routes::{auth, channels, guilds, users}, +use crate::{ + api::{ + middleware::{ + authentication::AuthenticationMiddleware, current_user::CurrentUserMiddleware, + }, + routes::{auth, channels, guilds, users}, + }, + database::entities::Config, + errors::Error, }; -use crate::PathRouteTuple; mod middleware; mod routes; -pub fn setup_api() -> PathRouteTuple { +pub async fn start_api(db: MySqlPool) -> Result<(), Error> { + log::info!(target: "symfonia::api::cfg", "Loading configuration"); + let config = Config::init(&db).await?; + + if config.sentry.enabled { + let _guard = sentry::init(( + "https://241c6fb08adb469da1bb82522b25c99f@sentry.quartzinc.space/3", + sentry::ClientOptions { + release: sentry::release_name!(), + traces_sample_rate: config.sentry.trace_sample_rate as f32, + ..Default::default() + }, + )); + } + let routes = Route::new() .nest("/auth", auth::setup_routes()) .nest( @@ -39,5 +65,24 @@ pub fn setup_api() -> PathRouteTuple { .nest("/policies", routes::policies::setup_routes()) .nest("/-", routes::health::setup_routes()); - ("/api/v9".to_string(), routes) + let v9_api = Route::new() + .nest("/api/v9", routes) + .data(db) + .data(config) + .with(NormalizePath::new(TrailingSlash::Trim)) + .catch_all_error(custom_error); + + let bind = std::env::var("API_BIND").unwrap_or_else(|_| String::from("localhost:3001")); + + log::info!(target: "symfonia::api", "Starting HTTP Server"); + Server::new(TcpListener::bind(bind)).run(v9_api).await?; + Ok(()) +} + +async fn custom_error(err: poem::Error) -> impl IntoResponse { + Json(json! ({ + "success": false, + "message": err.to_string(), + })) + .with_status(err.status()) } From 9129141209348a6b3d48cf6c5cf7655a893aa63a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 10 Jul 2024 17:00:48 +0200 Subject: [PATCH 007/162] Revert "create `start_server`, moving server starting logic to main.rs from api module" This reverts commit 76e3e303480cec20d1f45b9c99b112719307644a. --- src/main.rs | 55 ++--------------------------------------------------- 1 file changed, 2 insertions(+), 53 deletions(-) diff --git a/src/main.rs b/src/main.rs index cf03cfc..9478575 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,12 +15,6 @@ use log4rs::{ filter::Filter, Config, }; -use poem::listener::TcpListener; -use poem::middleware::{NormalizePath, TrailingSlash}; -use poem::web::Json; -use poem::{EndpointExt, IntoResponse, Route, Server}; -use serde_json::json; -use sqlx::MySqlPool; mod api; mod cdn; @@ -29,8 +23,6 @@ mod errors; mod gateway; mod util; -pub type PathRouteTuple = (String, Route); - #[derive(Debug)] struct LogFilter; @@ -149,6 +141,7 @@ async fn main() { log::info!(target: "symfonia", "Starting up Symfonia"); + gateway::start_gateway().await.unwrap(); // TODO: This should be near api::start... log::info!(target: "symfonia::db", "Establishing database connection"); let db = database::establish_connection() .await @@ -188,50 +181,6 @@ async fn main() { .await .expect("Failed to seed config"); } - let bind = std::env::var("API_BIND").unwrap_or_else(|_| String::from("localhost:3001")); - let api_route = api::setup_api(); - let gateway_route = gateway::setup_gateway(); - start_server(vec![api_route, gateway_route], &bind, db) - .await - .expect("Failed to start server") -} - -async fn start_server( - routes: Vec<(impl AsRef, Route)>, - addr: &impl ToString, - db: MySqlPool, -) -> Result<(), crate::errors::Error> { - let mut app_routes = Route::new(); - let config = crate::database::entities::Config::init(&db).await?; - if config.sentry.enabled { - let _guard = sentry::init(( - "https://241c6fb08adb469da1bb82522b25c99f@sentry.quartzinc.space/3", - sentry::ClientOptions { - release: sentry::release_name!(), - traces_sample_rate: config.sentry.trace_sample_rate as f32, - ..Default::default() - }, - )); - } - for (path, route) in routes.into_iter() { - app_routes = app_routes.nest(path, route); - } - let app = app_routes - .data(db) - .data(config) - .with(NormalizePath::new(TrailingSlash::Trim)) - .catch_all_error(custom_error); - log::info!(target: "symfonia::api", "Starting HTTP Server"); - Server::new(TcpListener::bind(addr.to_string())) - .run(app) - .await?; - Ok(()) -} -async fn custom_error(err: poem::Error) -> impl IntoResponse { - Json(json! ({ - "success": false, - "message": err.to_string(), - })) - .with_status(err.status()) + api::start_api(db).await.unwrap(); } From 5aea0534c9530541c437e58e84e694201c6b46f8 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 12 Jul 2024 23:00:28 +0200 Subject: [PATCH 008/162] Add `tokio-tungstenite` dependency, remove `openssl` where possible in favor of `rustls` --- Cargo.toml | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac88f53..b8d498a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,23 +28,35 @@ log4rs = { version = "1.3.0", features = [ num-bigint = "0.4.5" num-traits = "0.2.19" openssl = "0.10.64" -poem = { version = "3.0.1", features = ["websocket"] } +poem = "3.0.1" utoipa = { version = "5.0.0-alpha.0", features = [] } rand = "0.8.5" regex = "1.10.4" -reqwest = "0.12.4" +reqwest = { version = "0.12.5", default-features = false, features = [ + "http2", + "macos-system-configuration", + "charset", + "rustls-tls-webpki-roots", +] } serde = { version = "1.0.203", features = ["derive"] } serde_json = { version = "1.0.117", features = ["raw_value"] } sqlx = { version = "0.7.4", features = [ "json", "chrono", "ipnetwork", - "runtime-tokio-native-tls", + "runtime-tokio-rustls", "any", ] } thiserror = "1.0.61" tokio = { version = "1.38.0", features = ["full"] } -sentry = "0.33.0" +sentry = { version = "0.34.0", default-features = false, features = [ + "backtrace", + "contexts", + "debug-images", + "panic", + "reqwest", + "rustls", +] } clap = { version = "4.5.4", features = ["derive"] } chorus = { git = "http://github.com/polyphony-chat/chorus", rev = "d591616", features = [ @@ -54,3 +66,6 @@ serde_path_to_error = "0.1.16" percent-encoding = "2.3.1" hex = "0.4.3" itertools = "0.13.0" +tokio-tungstenite = { version = "0.23.1", features = [ + "rustls-tls-webpki-roots", +] } From 43ca8e2223caadb0fc56b170d027f8ecc9c5b844 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 12 Jul 2024 23:00:52 +0200 Subject: [PATCH 009/162] Revert poem gateway implementation changes --- src/gateway/mod.rs | 47 ++-------------------------------------------- 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index dde9e26..a729002 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -1,54 +1,11 @@ +use log::info; + // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. - -use futures::{SinkExt, StreamExt}; -use log::{debug, info}; -use poem::listener::TcpListener; -use poem::web::websocket::{Message, WebSocket}; -use poem::web::Data; -use poem::{get, handler, EndpointExt, IntoResponse, Route, Server}; - use crate::errors::Error; -#[handler] -fn ws(ws: WebSocket, sender: Data<&tokio::sync::broadcast::Sender>) -> impl IntoResponse { - let sender = sender.clone(); - let mut receiver = sender.subscribe(); - ws.on_upgrade(move |socket| async move { - let (mut sink, mut stream) = socket.split(); - - tokio::spawn(async move { - while let Some(Ok(msg)) = stream.next().await { - if let Message::Text(text) = msg { - if sender.send(Message::text(text)).is_err() { - break; - } - } - } - }); - - tokio::spawn(async move { - while let Ok(msg) = receiver.recv().await { - if sink.send(msg).await.is_err() { - break; - } - } - }); - }) -} - pub async fn start_gateway() -> Result<(), Error> { info!(target: "symfonia::gateway", "Starting gateway server"); - let ws_app = Route::new().at( - "/", - get(ws.data(tokio::sync::mpsc::channel::(32).0)), - ); - let bind = std::env::var("API_BIND").unwrap_or_else(|_| String::from("localhost:3001")); - debug!(target: "symfonia::gateway", "Binding to {}", bind); - Server::new(TcpListener::bind("0.0.0.0:3000")) - .run(ws_app) - .await?; - debug!(target: "symfonia::gateway", "Gateway server started"); Ok(()) } From 972d26e64eec97c835e161b8736dbac3ea793205 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 12 Jul 2024 23:01:31 +0200 Subject: [PATCH 010/162] Revert "poem gateway experimentation" This reverts commit c6e4dda41fb37b1407c733a00194949ebcdada6d. --- src/main.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 9478575..3da7acd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,20 @@ use clap::Parser; -use log::LevelFilter; use log4rs::{ append::{ console::{ConsoleAppender, Target}, rolling_file::{ policy::compound::{ - roll::delete::DeleteRoller, trigger::size::SizeTrigger, CompoundPolicy, + CompoundPolicy, roll::delete::DeleteRoller, trigger::size::SizeTrigger, }, RollingFileAppender, }, }, config::{Appender, Logger, Root}, + Config, encode::pattern::PatternEncoder, filter::Filter, - Config, }; +use log::LevelFilter; mod api; mod cdn; @@ -124,7 +124,7 @@ async fn main() { .logger( Logger::builder() .appender("gateway") - .build("symfonia::gateway", LevelFilter::Debug), + .build("symfonia::gateway", LevelFilter::Info), ) .build(Root::builder().appender("stdout").build({ let mode = std::env::var("MODE").unwrap_or("DEBUG".to_string()); @@ -141,7 +141,6 @@ async fn main() { log::info!(target: "symfonia", "Starting up Symfonia"); - gateway::start_gateway().await.unwrap(); // TODO: This should be near api::start... log::info!(target: "symfonia::db", "Establishing database connection"); let db = database::establish_connection() .await From e41a6d561315f154be12803b71dfaf29b7b53ea4 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 14 Jul 2024 17:39:17 +0200 Subject: [PATCH 011/162] Add pubserve dependency --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index b8d498a..1cf206a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,3 +69,4 @@ itertools = "0.13.0" tokio-tungstenite = { version = "0.23.1", features = [ "rustls-tls-webpki-roots", ] } +pubserve = { version = "1.1.0", features = ["async", "send"] } From 7939f47325c06d407de5cd065e6178bf1145c838 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 14 Jul 2024 17:39:52 +0200 Subject: [PATCH 012/162] Add HashMap of Emitters --- src/main.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3da7acd..72b33bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,26 @@ +use std::collections::HashMap; +use std::rc::Rc; + use clap::Parser; + +use gateway::{EventEmitter, Events}; +use log::LevelFilter; use log4rs::{ append::{ console::{ConsoleAppender, Target}, rolling_file::{ policy::compound::{ - CompoundPolicy, roll::delete::DeleteRoller, trigger::size::SizeTrigger, + roll::delete::DeleteRoller, trigger::size::SizeTrigger, CompoundPolicy, }, RollingFileAppender, }, }, config::{Appender, Logger, Root}, - Config, encode::pattern::PatternEncoder, filter::Filter, + Config, }; -use log::LevelFilter; +use pubserve::Publisher; mod api; mod cdn; @@ -181,5 +187,6 @@ async fn main() { .expect("Failed to seed config"); } + let mut emitters: HashMap>> = HashMap::new(); api::start_api(db).await.unwrap(); } From ea64e171a1665353060185dc026bdd3f94880ce7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 14 Jul 2024 17:40:26 +0200 Subject: [PATCH 013/162] Make Channels have Event Emitters --- src/api/routes/guilds/id/channels.rs | 12 +++++++++--- src/database/entities/channel.rs | 21 ++++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/api/routes/guilds/id/channels.rs b/src/api/routes/guilds/id/channels.rs index 8d46ef3..ef8f6bc 100644 --- a/src/api/routes/guilds/id/channels.rs +++ b/src/api/routes/guilds/id/channels.rs @@ -1,10 +1,10 @@ use chorus::types::{ - ChannelModifySchema, ChannelType, jwt::Claims, ModifyChannelPositionsSchema, Snowflake, + jwt::Claims, ChannelModifySchema, ChannelType, ModifyChannelPositionsSchema, Snowflake, }; use poem::{ handler, - IntoResponse, - Response, web::{Data, Json, Path}, + web::{Data, Json, Path}, + IntoResponse, Response, }; use reqwest::StatusCode; use sqlx::MySqlPool; @@ -139,6 +139,7 @@ mod tests { position: Some(0), ..Default::default() }, + ..Default::default() }, Channel { inner: chorus::types::Channel { @@ -146,6 +147,7 @@ mod tests { position: Some(1), ..Default::default() }, + ..Default::default() }, Channel { inner: chorus::types::Channel { @@ -153,6 +155,7 @@ mod tests { position: Some(2), ..Default::default() }, + ..Default::default() }, Channel { inner: chorus::types::Channel { @@ -160,6 +163,7 @@ mod tests { position: Some(3), ..Default::default() }, + ..Default::default() }, Channel { inner: chorus::types::Channel { @@ -167,6 +171,7 @@ mod tests { position: Some(4), ..Default::default() }, + ..Default::default() }, Channel { inner: chorus::types::Channel { @@ -174,6 +179,7 @@ mod tests { position: Some(5), ..Default::default() }, + ..Default::default() }, ]; diff --git a/src/database/entities/channel.rs b/src/database/entities/channel.rs index b6c0c8f..777d4ef 100644 --- a/src/database/entities/channel.rs +++ b/src/database/entities/channel.rs @@ -1,25 +1,35 @@ use std::ops::{Deref, DerefMut}; use chorus::types::{ - ChannelMessagesAnchor, ChannelModifySchema, ChannelType, CreateChannelInviteSchema, InviteType, - MessageSendSchema, PermissionOverwrite, Snowflake, + ChannelDelete, ChannelMessagesAnchor, ChannelModifySchema, ChannelType, ChannelUpdate, + CreateChannelInviteSchema, InviteType, MessageSendSchema, PermissionOverwrite, Snowflake, }; use itertools::Itertools; +use pubserve::Publisher; use serde::{Deserialize, Serialize}; -use sqlx::{MySqlPool, types::Json}; +use sqlx::{types::Json, MySqlPool}; use crate::{ database::entities::{ - GuildMember, invite::Invite, message::Message, read_state::ReadState, recipient::Recipient, + invite::Invite, message::Message, read_state::ReadState, recipient::Recipient, GuildMember, User, Webhook, }, errors::{ChannelError, Error, GuildError, UserError}, }; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, sqlx::FromRow, Default)] pub struct Channel { #[sqlx(flatten)] pub(crate) inner: chorus::types::Channel, + #[sqlx(skip)] + #[serde(skip)] + pub publisher: ChannelEventPublisher, +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct ChannelEventPublisher { + pub update: Publisher, + pub delete: Publisher, } impl Deref for Channel { @@ -87,6 +97,7 @@ impl Channel { guild_id, ..Default::default() }, + ..Default::default() }; sqlx::query("INSERT INTO channels (id, type, name, nsfw, guild_id, parent_id, flags, permission_overwrites, default_thread_rate_limit_per_user, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW())") From de2a31b6c222b8b1d36cc8b293acf97db96556e8 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 14 Jul 2024 17:40:48 +0200 Subject: [PATCH 014/162] Have user store list of subscribed_events --- src/database/entities/user.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index dae2ed1..d3a2452 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use sqlx::{FromRow, MySqlPool, Row}; +use crate::gateway::EventEmitter; use crate::{ database::entities::{Config, Guild, GuildMember, UserSettings}, errors::{Error, GuildError}, @@ -28,6 +29,9 @@ pub struct User { #[sqlx(skip)] pub settings: UserSettings, pub extended_settings: sqlx::types::Json, + #[sqlx(rename = "subscribedEvents")] + /// A list of [EventEmitter]s that the server has determined the user should be subscribed to. + pub subscribed_events: sqlx::types::Json>, } impl Deref for User { From 9343c2e9b154b2aba0125c0a4686f72f9c965ef6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 14 Jul 2024 17:41:03 +0200 Subject: [PATCH 015/162] Add some Gateway events types --- src/gateway/events.rs | 22 +++++++++ src/gateway/mod.rs | 16 +++++++ src/gateway/types.rs | 103 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 src/gateway/events.rs create mode 100644 src/gateway/types.rs diff --git a/src/gateway/events.rs b/src/gateway/events.rs new file mode 100644 index 0000000..984560d --- /dev/null +++ b/src/gateway/events.rs @@ -0,0 +1,22 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use chorus::types::{ + ChannelCreate, ChannelDelete, ChannelUpdate, GuildCreate, GuildDelete, GuildUpdate, + RelationshipAdd, RelationshipRemove, UserUpdate, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub enum Events { + ChannelCreate(ChannelCreate), + ChannelUpdate(ChannelUpdate), + ChannelDelete(ChannelDelete), + GuildCreate(GuildCreate), + GuildUpdate(GuildUpdate), + GuildDelete(GuildDelete), + RelationshipAdd(RelationshipAdd), + RelationshipRemove(RelationshipRemove), + UserUpdate(UserUpdate), +} diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index a729002..854fd6b 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -1,3 +1,12 @@ +mod events; +mod types; + +use chorus::types::Snowflake; +use serde::{Deserialize, Serialize}; + +pub use events::*; +pub use types::*; + use log::info; // This Source Code Form is subject to the terms of the Mozilla Public @@ -9,3 +18,10 @@ pub async fn start_gateway() -> Result<(), Error> { info!(target: "symfonia::gateway", "Starting gateway server"); Ok(()) } + +#[derive(Hash, PartialEq, Eq, Debug, Clone, Copy, Deserialize, Serialize)] +/// Identifies a unique event emitter with an event type and a snowflake ID. +pub struct EventEmitter { + pub event_type: EventType, + pub id: Snowflake, +} diff --git a/src/gateway/types.rs b/src/gateway/types.rs new file mode 100644 index 0000000..c44b3a4 --- /dev/null +++ b/src/gateway/types.rs @@ -0,0 +1,103 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use serde::{Deserialize, Serialize}; + +#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug, Deserialize, Serialize)] +pub enum EventType { + Ready, + ReadySupplemental, + Resumed, + AuthSessionChange, + AuthenticatorCreate, + AuthenticatorUpdate, + AuthenticatorDelete, + ApplicationCommandPermissionsUpdate, + AutoModerationRuleCreate, + AutoModerationRuleUpdate, + AutoModerationRuleDelete, + AutoModerationActionExecution, + AutoModerationMentionRaidDetection, + CallCreate, + CallUpdate, + CallDelete, + ChannelCreate, + ChannelUpdate, + ChannelDelete, + ChannelStatuses, + VoiceChannelStatusUpdate, + ChannelPinsUpdate, + ChannelRecipientAdd, + ChannelRecipientRemove, + ThreadCreate, + ThreadUpdate, + ThreadDelete, + ThreadListSync, + ThreadMemberUpdate, + ThreadMembersUpdate, + FriendSuggestionCreate, + FriendSuggestionDelete, + GuildCreate, + GuildUpdate, + GuildDelete, + GuildAuditLogEntryCreate, + GuildBanAdd, + GuildBanRemove, + GuildEmojisUpdate, + GuildStickersUpdate, + GuildJoinRequestCreate, + GuildJoinRequestUpdate, + GuildJoinRequestDelete, + GuildMemberAdd, + GuildMemberRemove, + GuildMemberUpdate, + GuildMembersChunk, + GuildRoleCreate, + GuildRoleUpdate, + GuildRoleDelete, + GuildScheduledEventCreate, + GuildScheduledEventUpdate, + GuildScheduledEventDelete, + GuildScheduledEventUserAdd, + GuildScheduledEventUserRemove, + GuildSoundboardSoundCreate, + GuildSoundboardSoundUpdate, + GuildSoundboardSoundDelete, + SoundboardSounds, + GuildIntegrationsUpdate, + IntegrationCreate, + IntegrationUpdate, + IntegrationDelete, + InteractionCreate, + InviteCreate, + InviteDelete, + MessageCreate, + MessageUpdate, + MessageDelete, + MessageDeleteBulk, + MessagePollVoteAdd, + MessagePollVoteRemove, + MessageReactionAdd, + MessageReactionAddMany, + MessageReactionRemove, + MessageReactionRemoveAll, + MessageReactionRemoveEmoji, + RecentMentionDelete, + LastMessages, + PresenceUpdate, + RelationshipAdd, + RelationshipUpdate, + RelationshipRemove, + StageInstanceCreate, + StageInstanceUpdate, + StageInstanceDelete, + TypingStart, + UserUpdate, + UserNoteUpdate, + UserRequiredActionUpdate, + VoiceStateUpdate, + VoiceServerUpdate, + VoiceChannelEffectSend, + WebhooksUpdate, +} From 94c9d1d11e640e4a6e8fc3385f8d58a66063904b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 14 Jul 2024 21:31:04 +0200 Subject: [PATCH 016/162] Move HashMap of Emitters to API, where it belongs --- src/api/mod.rs | 17 ++++++++++++++--- src/database/entities/channel.rs | 13 ++++++++++--- src/main.rs | 6 ------ 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 138049b..b1f342b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,12 +1,17 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + use poem::{ - EndpointExt, - IntoResponse, listener::TcpListener, - middleware::{NormalizePath, TrailingSlash}, Route, Server, web::Json, + middleware::{NormalizePath, TrailingSlash}, + web::Json, + EndpointExt, IntoResponse, Route, Server, }; +use pubserve::Publisher; use serde_json::json; use sqlx::MySqlPool; +use crate::gateway::{EventEmitter, Events}; use crate::{ api::{ middleware::{ @@ -23,6 +28,11 @@ mod routes; pub async fn start_api(db: MySqlPool) -> Result<(), Error> { log::info!(target: "symfonia::api::cfg", "Loading configuration"); + + // To avoid having to load all entities from disk every time we want to subscribe a newly + // connected user to their events, we store the emitters in a HashMap. + let emitters: HashMap>>> = HashMap::new(); + let config = Config::init(&db).await?; if config.sentry.enabled { @@ -69,6 +79,7 @@ pub async fn start_api(db: MySqlPool) -> Result<(), Error> { .nest("/api/v9", routes) .data(db) .data(config) + .data(emitters) .with(NormalizePath::new(TrailingSlash::Trim)) .catch_all_error(custom_error); diff --git a/src/database/entities/channel.rs b/src/database/entities/channel.rs index 777d4ef..0937a30 100644 --- a/src/database/entities/channel.rs +++ b/src/database/entities/channel.rs @@ -1,4 +1,5 @@ use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, Mutex}; use chorus::types::{ ChannelDelete, ChannelMessagesAnchor, ChannelModifySchema, ChannelType, ChannelUpdate, @@ -26,10 +27,16 @@ pub struct Channel { pub publisher: ChannelEventPublisher, } -#[derive(Debug, Default, Clone, PartialEq)] +#[derive(Debug, Default, Clone)] pub struct ChannelEventPublisher { - pub update: Publisher, - pub delete: Publisher, + pub update: Arc>>, + pub delete: Arc>>, +} + +impl PartialEq for ChannelEventPublisher { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.delete, &other.delete) && Arc::ptr_eq(&self.update, &other.update) + } } impl Deref for Channel { diff --git a/src/main.rs b/src/main.rs index 72b33bd..5ea6f28 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,5 @@ -use std::collections::HashMap; -use std::rc::Rc; - use clap::Parser; -use gateway::{EventEmitter, Events}; use log::LevelFilter; use log4rs::{ append::{ @@ -20,7 +16,6 @@ use log4rs::{ filter::Filter, Config, }; -use pubserve::Publisher; mod api; mod cdn; @@ -187,6 +182,5 @@ async fn main() { .expect("Failed to seed config"); } - let mut emitters: HashMap>> = HashMap::new(); api::start_api(db).await.unwrap(); } From 2d80b69a17460366ef294935ec20e0e699e23544 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 21 Jul 2024 16:53:11 +0200 Subject: [PATCH 017/162] Update Events Types --- src/gateway/events.rs | 22 -------- src/gateway/types.rs | 127 +++++++++++++++++++++++++----------------- 2 files changed, 76 insertions(+), 73 deletions(-) delete mode 100644 src/gateway/events.rs diff --git a/src/gateway/events.rs b/src/gateway/events.rs deleted file mode 100644 index 984560d..0000000 --- a/src/gateway/events.rs +++ /dev/null @@ -1,22 +0,0 @@ -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at https://mozilla.org/MPL/2.0/. - -use chorus::types::{ - ChannelCreate, ChannelDelete, ChannelUpdate, GuildCreate, GuildDelete, GuildUpdate, - RelationshipAdd, RelationshipRemove, UserUpdate, -}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Deserialize, Serialize)] -pub enum Events { - ChannelCreate(ChannelCreate), - ChannelUpdate(ChannelUpdate), - ChannelDelete(ChannelDelete), - GuildCreate(GuildCreate), - GuildUpdate(GuildUpdate), - GuildDelete(GuildDelete), - RelationshipAdd(RelationshipAdd), - RelationshipRemove(RelationshipRemove), - UserUpdate(UserUpdate), -} diff --git a/src/gateway/types.rs b/src/gateway/types.rs index c44b3a4..5e647bb 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -2,53 +2,46 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use serde::{Deserialize, Serialize}; +use ::serde::{Deserialize, Serialize}; +use chorus::types::*; -#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug, Deserialize, Serialize)] +#[derive( + Debug, + ::serde::Deserialize, + ::serde::Serialize, + Clone, + PartialEq, + PartialOrd, + Eq, + Ord, + Copy, + Hash, +)] +/// Enum representing all possible* event types that can be received from or sent to the gateway. +/// +/// TODO: This is only temporary. Replace with this enum from chorus, when it is ready. pub enum EventType { + Hello, Ready, - ReadySupplemental, Resumed, - AuthSessionChange, - AuthenticatorCreate, - AuthenticatorUpdate, - AuthenticatorDelete, - ApplicationCommandPermissionsUpdate, - AutoModerationRuleCreate, - AutoModerationRuleUpdate, - AutoModerationRuleDelete, - AutoModerationActionExecution, - AutoModerationMentionRaidDetection, - CallCreate, - CallUpdate, - CallDelete, + InvalidSession, ChannelCreate, ChannelUpdate, ChannelDelete, - ChannelStatuses, - VoiceChannelStatusUpdate, ChannelPinsUpdate, - ChannelRecipientAdd, - ChannelRecipientRemove, ThreadCreate, ThreadUpdate, ThreadDelete, ThreadListSync, ThreadMemberUpdate, ThreadMembersUpdate, - FriendSuggestionCreate, - FriendSuggestionDelete, GuildCreate, GuildUpdate, GuildDelete, - GuildAuditLogEntryCreate, GuildBanAdd, GuildBanRemove, GuildEmojisUpdate, - GuildStickersUpdate, - GuildJoinRequestCreate, - GuildJoinRequestUpdate, - GuildJoinRequestDelete, + GuildIntegrationsUpdate, GuildMemberAdd, GuildMemberRemove, GuildMemberUpdate, @@ -56,16 +49,6 @@ pub enum EventType { GuildRoleCreate, GuildRoleUpdate, GuildRoleDelete, - GuildScheduledEventCreate, - GuildScheduledEventUpdate, - GuildScheduledEventDelete, - GuildScheduledEventUserAdd, - GuildScheduledEventUserRemove, - GuildSoundboardSoundCreate, - GuildSoundboardSoundUpdate, - GuildSoundboardSoundDelete, - SoundboardSounds, - GuildIntegrationsUpdate, IntegrationCreate, IntegrationUpdate, IntegrationDelete, @@ -76,28 +59,70 @@ pub enum EventType { MessageUpdate, MessageDelete, MessageDeleteBulk, - MessagePollVoteAdd, - MessagePollVoteRemove, MessageReactionAdd, - MessageReactionAddMany, MessageReactionRemove, MessageReactionRemoveAll, MessageReactionRemoveEmoji, - RecentMentionDelete, - LastMessages, PresenceUpdate, - RelationshipAdd, - RelationshipUpdate, - RelationshipRemove, - StageInstanceCreate, - StageInstanceUpdate, - StageInstanceDelete, TypingStart, UserUpdate, - UserNoteUpdate, - UserRequiredActionUpdate, VoiceStateUpdate, VoiceServerUpdate, - VoiceChannelEffectSend, WebhooksUpdate, + StageInstanceCreate, + StageInstanceUpdate, + StageInstanceDelete, + RequestMembers, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +/// Enum representing all possible* events that can be received from or sent to the gateway. +/// +/// TODO: This is only temporary. Replace with this enum from chorus, when it is ready. +#[serde(rename_all = "PascalCase")] +pub enum Event { + Hello(GatewayHello), + Ready(GatewayReady), + Resumed(GatewayResume), + InvalidSession(GatewayInvalidSession), + ChannelCreate(ChannelCreate), + ChannelUpdate(ChannelUpdate), + ChannelDelete(ChannelDelete), + ThreadCreate(ThreadCreate), + ThreadUpdate(ThreadUpdate), + ThreadDelete(ThreadDelete), + ThreadListSync(ThreadListSync), + ThreadMemberUpdate(ThreadMemberUpdate), + ThreadMembersUpdate(ThreadMembersUpdate), + GuildCreate(GuildCreate), + GuildUpdate(GuildUpdate), + GuildDelete(GuildDelete), + GuildBanAdd(GuildBanAdd), + GuildBanRemove(GuildBanRemove), + GuildEmojisUpdate(GuildEmojisUpdate), + GuildIntegrationsUpdate(GuildIntegrationsUpdate), + GuildMemberAdd(GuildMemberAdd), + GuildMemberRemove(GuildMemberRemove), + GuildMemberUpdate(GuildMemberUpdate), + GuildMembersChunk(GuildMembersChunk), + InteractionCreate(InteractionCreate), + InviteCreate(InviteCreate), + InviteDelete(InviteDelete), + MessageCreate(MessageCreate), + MessageUpdate(MessageUpdate), + MessageDelete(MessageDelete), + MessageDeleteBulk(MessageDeleteBulk), + MessageReactionAdd(MessageReactionAdd), + MessageReactionRemove(MessageReactionRemove), + MessageReactionRemoveAll(MessageReactionRemoveAll), + MessageReactionRemoveEmoji(MessageReactionRemoveEmoji), + PresenceUpdate(PresenceUpdate), + TypingStart(TypingStartEvent), + UserUpdate(UserUpdate), + VoiceStateUpdate(VoiceStateUpdate), + VoiceServerUpdate(VoiceServerUpdate), + WebhooksUpdate(WebhooksUpdate), + StageInstanceCreate(StageInstanceCreate), + StageInstanceUpdate(StageInstanceUpdate), + StageInstanceDelete(StageInstanceDelete), } From b13906ffaa29d4d5b17544c0598f1a0a6483564f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 21 Jul 2024 16:53:30 +0200 Subject: [PATCH 018/162] Remove EventEmitter --- src/api/mod.rs | 8 +------- src/database/entities/user.rs | 4 ---- src/gateway/mod.rs | 18 ++++-------------- 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index b1f342b..d7541ad 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,17 +1,12 @@ -use std::collections::HashMap; -use std::sync::{Arc, Mutex}; - use poem::{ listener::TcpListener, middleware::{NormalizePath, TrailingSlash}, web::Json, EndpointExt, IntoResponse, Route, Server, }; -use pubserve::Publisher; use serde_json::json; use sqlx::MySqlPool; -use crate::gateway::{EventEmitter, Events}; use crate::{ api::{ middleware::{ @@ -31,7 +26,6 @@ pub async fn start_api(db: MySqlPool) -> Result<(), Error> { // To avoid having to load all entities from disk every time we want to subscribe a newly // connected user to their events, we store the emitters in a HashMap. - let emitters: HashMap>>> = HashMap::new(); let config = Config::init(&db).await?; @@ -75,11 +69,11 @@ pub async fn start_api(db: MySqlPool) -> Result<(), Error> { .nest("/policies", routes::policies::setup_routes()) .nest("/-", routes::health::setup_routes()); + // TODO: Add emitters here let v9_api = Route::new() .nest("/api/v9", routes) .data(db) .data(config) - .data(emitters) .with(NormalizePath::new(TrailingSlash::Trim)) .catch_all_error(custom_error); diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index d3a2452..dae2ed1 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize}; use serde_json::{Map, Value}; use sqlx::{FromRow, MySqlPool, Row}; -use crate::gateway::EventEmitter; use crate::{ database::entities::{Config, Guild, GuildMember, UserSettings}, errors::{Error, GuildError}, @@ -29,9 +28,6 @@ pub struct User { #[sqlx(skip)] pub settings: UserSettings, pub extended_settings: sqlx::types::Json, - #[sqlx(rename = "subscribedEvents")] - /// A list of [EventEmitter]s that the server has determined the user should be subscribed to. - pub subscribed_events: sqlx::types::Json>, } impl Deref for User { diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 854fd6b..292bffa 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -1,27 +1,17 @@ -mod events; mod types; -use chorus::types::Snowflake; -use serde::{Deserialize, Serialize}; - -pub use events::*; -pub use types::*; - use log::info; +use pubserve::Publisher; +pub use types::*; // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::errors::Error; +pub type EventPublisher = Publisher; + pub async fn start_gateway() -> Result<(), Error> { info!(target: "symfonia::gateway", "Starting gateway server"); Ok(()) } - -#[derive(Hash, PartialEq, Eq, Debug, Clone, Copy, Deserialize, Serialize)] -/// Identifies a unique event emitter with an event type and a snowflake ID. -pub struct EventEmitter { - pub event_type: EventType, - pub id: Snowflake, -} From 88a78ab000d6f587b95f2c490b206693609894b6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 21 Jul 2024 16:53:53 +0200 Subject: [PATCH 019/162] Add publisher to application --- src/database/entities/application.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/database/entities/application.rs b/src/database/entities/application.rs index 0e05cc7..9bd2f2d 100644 --- a/src/database/entities/application.rs +++ b/src/database/entities/application.rs @@ -1,12 +1,13 @@ use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, RwLock}; -use bitflags::Flags; use chorus::types::{ApplicationFlags, Snowflake}; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, MySqlPool}; +use sqlx::MySqlPool; +use crate::gateway::Event; use crate::{ - database::entities::{Config, user::User}, + database::entities::{user::User, Config}, errors::Error, }; @@ -17,6 +18,9 @@ pub struct Application { pub owner_id: Snowflake, pub bot_user_id: Option, pub team_id: Option, + #[sqlx(skip)] + #[serde(skip)] + pub publisher: Arc>>, } impl Deref for Application { @@ -62,6 +66,7 @@ impl Application { owner_id: owner_id.to_owned(), bot_user_id, team_id: None, + publisher: Arc::new(RwLock::new(pubserve::Publisher::new())), }; let _res = sqlx::query("INSERT INTO applications (id, name, summary, hook, bot_public, verify_key, owner_id, flags, integration_public, discoverability_state, discovery_eligibility_flags) VALUES (?, ?, ?, true, true, ?, ?, ?, true, 1, 2240)") From 68bedff42ca8672b6167605ea2a8a3da32bca97b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 21 Jul 2024 22:28:36 +0200 Subject: [PATCH 020/162] Typedefs and dependency injection into api::start_api/gateway::start_gateway --- src/main.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5ea6f28..385e5be 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,10 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use chorus::types::Snowflake; use clap::Parser; +use gateway::Event; use log::LevelFilter; use log4rs::{ append::{ @@ -16,6 +21,7 @@ use log4rs::{ filter::Filter, Config, }; +use parking_lot::RwLock; mod api; mod cdn; @@ -24,6 +30,10 @@ mod errors; mod gateway; mod util; +pub type SharedPublisher = Arc>>; +pub type PublisherMap = HashMap; +pub type SharedPublisherMap = Arc>; + #[derive(Debug)] struct LogFilter; @@ -181,6 +191,11 @@ async fn main() { .await .expect("Failed to seed config"); } - - api::start_api(db).await.unwrap(); + let shared_publisher_map = Arc::new(RwLock::new(HashMap::new())); + api::start_api(db.clone(), shared_publisher_map.clone()) + .await + .unwrap(); + gateway::start_gateway(db.clone(), shared_publisher_map.clone()) + .await + .unwrap(); } From 4fda8f84e93ce153ec54717b49f444263c0835eb Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 21 Jul 2024 22:28:55 +0200 Subject: [PATCH 021/162] Add publisher_map to start_api data --- src/api/mod.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index d7541ad..ed3eac6 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -7,6 +7,7 @@ use poem::{ use serde_json::json; use sqlx::MySqlPool; +use crate::SharedPublisherMap; use crate::{ api::{ middleware::{ @@ -21,12 +22,9 @@ use crate::{ mod middleware; mod routes; -pub async fn start_api(db: MySqlPool) -> Result<(), Error> { +pub async fn start_api(db: MySqlPool, publisher_map: SharedPublisherMap) -> Result<(), Error> { log::info!(target: "symfonia::api::cfg", "Loading configuration"); - // To avoid having to load all entities from disk every time we want to subscribe a newly - // connected user to their events, we store the emitters in a HashMap. - let config = Config::init(&db).await?; if config.sentry.enabled { @@ -69,11 +67,11 @@ pub async fn start_api(db: MySqlPool) -> Result<(), Error> { .nest("/policies", routes::policies::setup_routes()) .nest("/-", routes::health::setup_routes()); - // TODO: Add emitters here let v9_api = Route::new() .nest("/api/v9", routes) .data(db) .data(config) + .data(publisher_map) .with(NormalizePath::new(TrailingSlash::Trim)) .catch_all_error(custom_error); From 586836020100d331ac72beea7dc14cd29879a886 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 21 Jul 2024 22:29:03 +0200 Subject: [PATCH 022/162] Add parking_lot --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 1cf206a..124fda0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,3 +70,4 @@ tokio-tungstenite = { version = "0.23.1", features = [ "rustls-tls-webpki-roots", ] } pubserve = { version = "1.1.0", features = ["async", "send"] } +parking_lot = { version = "0.12.3", features = ["deadlock_detection"] } From 51eac9e9056c4b43745467c6d9870405df4d46c4 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 21 Jul 2024 22:29:25 +0200 Subject: [PATCH 023/162] Use parking_lot::RwLock instead of std::sync::RwLock --- src/database/entities/application.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/database/entities/application.rs b/src/database/entities/application.rs index 9bd2f2d..8febf2c 100644 --- a/src/database/entities/application.rs +++ b/src/database/entities/application.rs @@ -1,7 +1,8 @@ use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use chorus::types::{ApplicationFlags, Snowflake}; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use sqlx::MySqlPool; From ac8fe255d8f4ab494dd4866a51b2eded00e4c30c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 21 Jul 2024 22:29:37 +0200 Subject: [PATCH 024/162] Remove unused imports --- src/database/entities/guild.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/database/entities/guild.rs b/src/database/entities/guild.rs index 1ebe129..dd02fa3 100644 --- a/src/database/entities/guild.rs +++ b/src/database/entities/guild.rs @@ -1,11 +1,8 @@ -use std::{ - ops::{Deref, DerefMut}, - sync::{Arc, RwLock}, -}; +use std::ops::{Deref, DerefMut}; use chorus::types::{ - ChannelType, NSFWLevel, PermissionFlags, PremiumTier, - PublicUser, Snowflake, SystemChannelFlags, types::guild_configuration::GuildFeaturesList, WelcomeScreenObject, + types::guild_configuration::GuildFeaturesList, ChannelType, NSFWLevel, PermissionFlags, + PremiumTier, PublicUser, Snowflake, SystemChannelFlags, WelcomeScreenObject, }; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, MySqlPool, QueryBuilder, Row}; From 4f10db765b4d1cf6f29abb6212e6c2d2c6566e91 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 21 Jul 2024 22:29:54 +0200 Subject: [PATCH 025/162] inject MySqlPool, SharedPublisherMap into start_gateway() --- src/gateway/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 292bffa..cc7d288 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -1,17 +1,17 @@ mod types; use log::info; -use pubserve::Publisher; +use sqlx::MySqlPool; pub use types::*; // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::errors::Error; +use crate::SharedPublisherMap; -pub type EventPublisher = Publisher; - -pub async fn start_gateway() -> Result<(), Error> { +pub async fn start_gateway(db: MySqlPool, publisher_map: SharedPublisherMap) -> Result<(), Error> { info!(target: "symfonia::gateway", "Starting gateway server"); + // `publishers` will live for the lifetime of the gateway server, in the main gateway thread Ok(()) } From 8864c2e618d17bb84e083a0fa3fa2b533599e7b7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 23 Jul 2024 11:07:14 +0200 Subject: [PATCH 026/162] Rename SharedPublisher to SharedEventPublisher --- src/api/mod.rs | 4 ++-- src/main.rs | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index ed3eac6..ad85afa 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -7,7 +7,7 @@ use poem::{ use serde_json::json; use sqlx::MySqlPool; -use crate::SharedPublisherMap; +use crate::SharedEventPublisherMap; use crate::{ api::{ middleware::{ @@ -22,7 +22,7 @@ use crate::{ mod middleware; mod routes; -pub async fn start_api(db: MySqlPool, publisher_map: SharedPublisherMap) -> Result<(), Error> { +pub async fn start_api(db: MySqlPool, publisher_map: SharedEventPublisherMap) -> Result<(), Error> { log::info!(target: "symfonia::api::cfg", "Loading configuration"); let config = Config::init(&db).await?; diff --git a/src/main.rs b/src/main.rs index 385e5be..16dce4d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ use log4rs::{ Config, }; use parking_lot::RwLock; +use pubserve::Publisher; mod api; mod cdn; @@ -30,9 +31,9 @@ mod errors; mod gateway; mod util; -pub type SharedPublisher = Arc>>; -pub type PublisherMap = HashMap; -pub type SharedPublisherMap = Arc>; +pub type SharedEventPublisher = Arc>>; +pub type EventPublisherMap = HashMap; +pub type SharedEventPublisherMap = Arc>; #[derive(Debug)] struct LogFilter; From cbe11b2379f22a8321b316c117b79d2258f01ff3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 23 Jul 2024 11:07:37 +0200 Subject: [PATCH 027/162] Add SharedEventPublisher to relevant entities --- src/database/entities/application.rs | 5 +++-- src/database/entities/channel.rs | 19 +++---------------- src/database/entities/guild.rs | 5 +++++ src/database/entities/mod.rs | 2 ++ src/database/entities/role.rs | 5 +++++ src/database/entities/user.rs | 5 +++++ src/gateway/mod.rs | 7 +++++-- 7 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/database/entities/application.rs b/src/database/entities/application.rs index 8febf2c..591e972 100644 --- a/src/database/entities/application.rs +++ b/src/database/entities/application.rs @@ -1,3 +1,5 @@ +use super::*; + use std::ops::{Deref, DerefMut}; use std::sync::Arc; @@ -6,7 +8,6 @@ use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use sqlx::MySqlPool; -use crate::gateway::Event; use crate::{ database::entities::{user::User, Config}, errors::Error, @@ -21,7 +22,7 @@ pub struct Application { pub team_id: Option, #[sqlx(skip)] #[serde(skip)] - pub publisher: Arc>>, + pub publisher: SharedEventPublisher, } impl Deref for Application { diff --git a/src/database/entities/channel.rs b/src/database/entities/channel.rs index 0937a30..f01ada1 100644 --- a/src/database/entities/channel.rs +++ b/src/database/entities/channel.rs @@ -1,6 +1,4 @@ -use std::ops::{Deref, DerefMut}; -use std::sync::{Arc, Mutex}; - +use super::*; use chorus::types::{ ChannelDelete, ChannelMessagesAnchor, ChannelModifySchema, ChannelType, ChannelUpdate, CreateChannelInviteSchema, InviteType, MessageSendSchema, PermissionOverwrite, Snowflake, @@ -9,6 +7,7 @@ use itertools::Itertools; use pubserve::Publisher; use serde::{Deserialize, Serialize}; use sqlx::{types::Json, MySqlPool}; +use std::ops::{Deref, DerefMut}; use crate::{ database::entities::{ @@ -24,19 +23,7 @@ pub struct Channel { pub(crate) inner: chorus::types::Channel, #[sqlx(skip)] #[serde(skip)] - pub publisher: ChannelEventPublisher, -} - -#[derive(Debug, Default, Clone)] -pub struct ChannelEventPublisher { - pub update: Arc>>, - pub delete: Arc>>, -} - -impl PartialEq for ChannelEventPublisher { - fn eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.delete, &other.delete) && Arc::ptr_eq(&self.update, &other.update) - } + pub publisher: SharedEventPublisher, } impl Deref for Channel { diff --git a/src/database/entities/guild.rs b/src/database/entities/guild.rs index dd02fa3..978905c 100644 --- a/src/database/entities/guild.rs +++ b/src/database/entities/guild.rs @@ -1,3 +1,5 @@ +use super::*; + use std::ops::{Deref, DerefMut}; use chorus::types::{ @@ -28,6 +30,9 @@ pub struct Guild { pub parent: Option, pub template_id: Option, pub nsfw: bool, + #[sqlx(skip)] + #[serde(skip)] + pub publisher: SharedEventPublisher, } impl Deref for Guild { diff --git a/src/database/entities/mod.rs b/src/database/entities/mod.rs index fdb7c32..896fc70 100644 --- a/src/database/entities/mod.rs +++ b/src/database/entities/mod.rs @@ -16,6 +16,8 @@ pub use user_settings::*; pub use voice_state::*; pub use webhook::*; +use crate::SharedEventPublisher; + mod application; mod attachment; mod audit_log; diff --git a/src/database/entities/role.rs b/src/database/entities/role.rs index 183defc..9de6b27 100644 --- a/src/database/entities/role.rs +++ b/src/database/entities/role.rs @@ -1,3 +1,5 @@ +use super::*; + use std::ops::{Deref, DerefMut}; use chorus::types::{PermissionFlags, Snowflake}; @@ -11,6 +13,9 @@ pub struct Role { #[sqlx(flatten)] inner: chorus::types::RoleObject, pub guild_id: Snowflake, + #[sqlx(skip)] + #[serde(skip)] + pub publisher: SharedEventPublisher, } impl Deref for Role { diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index dae2ed1..a20cf23 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -1,3 +1,5 @@ +use super::*; + use std::{ default::Default, ops::{Deref, DerefMut}, @@ -28,6 +30,9 @@ pub struct User { #[sqlx(skip)] pub settings: UserSettings, pub extended_settings: sqlx::types::Json, + #[sqlx(skip)] + #[serde(skip)] + pub publisher: SharedEventPublisher, } impl Deref for User { diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index cc7d288..f7f1656 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -8,9 +8,12 @@ pub use types::*; // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::errors::Error; -use crate::SharedPublisherMap; +use crate::SharedEventPublisherMap; -pub async fn start_gateway(db: MySqlPool, publisher_map: SharedPublisherMap) -> Result<(), Error> { +pub async fn start_gateway( + db: MySqlPool, + publisher_map: SharedEventPublisherMap, +) -> Result<(), Error> { info!(target: "symfonia::gateway", "Starting gateway server"); // `publishers` will live for the lifetime of the gateway server, in the main gateway thread Ok(()) From c683faf967f2d7bae2507ad4b01e61f945abe073 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 23 Jul 2024 17:29:32 +0200 Subject: [PATCH 028/162] Fix: PartialEq impl for database types with publishers --- src/database/entities/channel.rs | 10 ++++++++-- src/database/entities/role.rs | 18 ++++++++++++------ src/main.rs | 6 ++++++ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/database/entities/channel.rs b/src/database/entities/channel.rs index f01ada1..6fd0237 100644 --- a/src/database/entities/channel.rs +++ b/src/database/entities/channel.rs @@ -4,11 +4,11 @@ use chorus::types::{ CreateChannelInviteSchema, InviteType, MessageSendSchema, PermissionOverwrite, Snowflake, }; use itertools::Itertools; -use pubserve::Publisher; use serde::{Deserialize, Serialize}; use sqlx::{types::Json, MySqlPool}; use std::ops::{Deref, DerefMut}; +use crate::eq_shared_event_publisher; use crate::{ database::entities::{ invite::Invite, message::Message, read_state::ReadState, recipient::Recipient, GuildMember, @@ -17,7 +17,7 @@ use crate::{ errors::{ChannelError, Error, GuildError, UserError}, }; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, sqlx::FromRow, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow, Default)] pub struct Channel { #[sqlx(flatten)] pub(crate) inner: chorus::types::Channel, @@ -26,6 +26,12 @@ pub struct Channel { pub publisher: SharedEventPublisher, } +impl PartialEq for Channel { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner && eq_shared_event_publisher(&self.publisher, &other.publisher) + } +} + impl Deref for Channel { type Target = chorus::types::Channel; fn deref(&self) -> &Self::Target { diff --git a/src/database/entities/role.rs b/src/database/entities/role.rs index 9de6b27..383fd2a 100644 --- a/src/database/entities/role.rs +++ b/src/database/entities/role.rs @@ -6,9 +6,10 @@ use chorus::types::{PermissionFlags, Snowflake}; use serde::{Deserialize, Serialize}; use sqlx::{MySqlPool, Row}; +use crate::eq_shared_event_publisher; use crate::errors::Error; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct Role { #[sqlx(flatten)] inner: chorus::types::RoleObject, @@ -18,6 +19,14 @@ pub struct Role { pub publisher: SharedEventPublisher, } +impl PartialEq for Role { + fn eq(&self, other: &Self) -> bool { + self.inner == other.inner + && self.guild_id == other.guild_id + && eq_shared_event_publisher(&self.publisher, &other.publisher) + } +} + impl Deref for Role { type Target = chorus::types::RoleObject; @@ -53,11 +62,7 @@ impl Role { ) -> Result { let role = Self { inner: chorus::types::RoleObject { - id: if let Some(sf) = id { - sf - } else { - Snowflake::default() - }, + id: id.unwrap_or_default(), color, hoist, managed, @@ -70,6 +75,7 @@ impl Role { ..Default::default() }, guild_id: guild_id.to_owned(), + publisher: SharedEventPublisher::default(), }; sqlx::query("INSERT INTO roles (id, guild_id, name, color, hoist, managed, mentionable, permissions, position, icon, unicode_emoji) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") diff --git a/src/main.rs b/src/main.rs index 16dce4d..188ddb4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,12 @@ pub type SharedEventPublisher = Arc>>; pub type EventPublisherMap = HashMap; pub type SharedEventPublisherMap = Arc>; +pub fn eq_shared_event_publisher(a: &SharedEventPublisher, b: &SharedEventPublisher) -> bool { + let a = a.read(); + let b = b.read(); + *a == *b +} + #[derive(Debug)] struct LogFilter; From 7722b8088d9bacad38cf0ce44208f246f0a967de Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 23 Jul 2024 17:56:59 +0200 Subject: [PATCH 029/162] Add Guild Publisher to SharedPublisherMap on Guild Creation --- src/api/routes/guilds/id/roles/mod.rs | 5 ++++- src/api/routes/guilds/mod.rs | 11 +++++++---- src/api/routes/guilds/templates.rs | 17 +++++++++++++---- src/database/entities/guild.rs | 18 +++++++++++++++++- src/database/entities/role.rs | 7 +++++-- 5 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/api/routes/guilds/id/roles/mod.rs b/src/api/routes/guilds/id/roles/mod.rs index fb8f24d..08c6905 100644 --- a/src/api/routes/guilds/id/roles/mod.rs +++ b/src/api/routes/guilds/id/roles/mod.rs @@ -1,11 +1,12 @@ use chorus::types::{PermissionFlags, RoleCreateModifySchema, RolePositionUpdateSchema, Snowflake}; use poem::{ handler, - IntoResponse, web::{Data, Json, Path}, + IntoResponse, }; use sqlx::MySqlPool; +use crate::SharedEventPublisherMap; use crate::{ database::entities::{Config, Guild, Role, User}, errors::{Error, GuildError}, @@ -36,6 +37,7 @@ pub async fn get_roles( #[handler] pub async fn create_role( Data(db): Data<&MySqlPool>, + Data(publisher_map): Data<&SharedEventPublisherMap>, Data(authed_user): Data<&User>, Data(config): Data<&Config>, Path(guild_id): Path, @@ -59,6 +61,7 @@ pub async fn create_role( let role = Role::create( db, + publisher_map.clone(), None, guild.id, &name, diff --git a/src/api/routes/guilds/mod.rs b/src/api/routes/guilds/mod.rs index 29c2ceb..4e22c55 100644 --- a/src/api/routes/guilds/mod.rs +++ b/src/api/routes/guilds/mod.rs @@ -1,11 +1,12 @@ -use chorus::types::{GuildCreateSchema, jwt::Claims}; +use chorus::types::{jwt::Claims, GuildCreateSchema}; use poem::{ - get, handler, IntoResponse, patch, post, - put, - Route, web::{Data, Json}, + get, handler, patch, post, put, + web::{Data, Json}, + IntoResponse, Route, }; use sqlx::MySqlPool; +use crate::SharedEventPublisherMap; use crate::{ database::entities::{Config, Guild, User}, errors::{Error, UserError}, @@ -134,6 +135,7 @@ pub fn setup_routes() -> Route { #[handler] pub async fn create_guild( Data(db): Data<&MySqlPool>, + Data(publisher_map): Data<&SharedEventPublisherMap>, Data(cfg): Data<&Config>, Data(claims): Data<&Claims>, Json(payload): Json, @@ -151,6 +153,7 @@ pub async fn create_guild( let guild = Guild::create( db, + publisher_map.clone(), cfg, &guild_name, payload.icon, diff --git a/src/api/routes/guilds/templates.rs b/src/api/routes/guilds/templates.rs index 6989911..34147d7 100644 --- a/src/api/routes/guilds/templates.rs +++ b/src/api/routes/guilds/templates.rs @@ -1,13 +1,14 @@ -use chorus::types::{GuildTemplateCreateSchema, jwt::Claims, Snowflake}; +use chorus::types::{jwt::Claims, GuildTemplateCreateSchema, Snowflake}; use poem::{ handler, - IntoResponse, web::{Data, Json, Path}, + IntoResponse, }; use reqwest::StatusCode; use serde_json::json; use sqlx::MySqlPool; +use crate::SharedEventPublisherMap; use crate::{ database::entities::{Config, Guild, GuildTemplate, User}, errors::{Error, GuildError}, @@ -79,6 +80,7 @@ pub async fn get_template( #[handler] pub async fn create_guild_from_template( Data(db): Data<&MySqlPool>, + Data(publisher_map): Data<&SharedEventPublisherMap>, Data(authed_user): Data<&User>, Data(config): Data<&Config>, Path(code): Path, @@ -112,8 +114,15 @@ pub async fn create_guild_from_template( .await? .ok_or(Error::Guild(GuildError::TemplateNotFound))?; - let guild = - Guild::create_from_template(db, config, authed_user.id, &template, &payload.name).await?; + let guild = Guild::create_from_template( + db, + config, + publisher_map.clone(), + authed_user.id, + &template, + &payload.name, + ) + .await?; guild.add_member(db, authed_user.id).await?; diff --git a/src/database/entities/guild.rs b/src/database/entities/guild.rs index 978905c..f55a34c 100644 --- a/src/database/entities/guild.rs +++ b/src/database/entities/guild.rs @@ -9,6 +9,7 @@ use chorus::types::{ use serde::{Deserialize, Serialize}; use sqlx::{FromRow, MySqlPool, QueryBuilder, Row}; +use crate::SharedEventPublisherMap; use crate::{ database::{ entities::{ @@ -51,6 +52,7 @@ impl DerefMut for Guild { impl Guild { pub async fn create( db: &MySqlPool, + shared_event_publisher_map: SharedEventPublisherMap, cfg: &Config, name: &str, icon: Option, @@ -87,6 +89,9 @@ impl Guild { }, ..Default::default() }; + shared_event_publisher_map + .write() + .insert(guild.id, guild.publisher.clone()); sqlx::query("INSERT INTO guilds (id, afk_timeout, default_message_notifications, explicit_content_filter, features, icon, max_members, max_presences, max_video_channel_users, name, owner_id, region, system_channel_flags, preferred_locale, welcome_screen, large, premium_tier, unavailable, widget_enabled, nsfw) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,?,0,0,?)") .bind(guild.id) @@ -112,6 +117,7 @@ impl Guild { let everyone = Role::create( db, + shared_event_publisher_map, Some(guild.id), guild.id, "@everyone", @@ -181,6 +187,7 @@ impl Guild { pub async fn create_from_template( db: &MySqlPool, cfg: &Config, + shared_event_publisher_map: SharedEventPublisherMap, owner_id: Snowflake, template: &GuildTemplate, name: &str, @@ -189,7 +196,16 @@ impl Guild { return Err(Error::Guild(GuildError::NoSourceGuild)); }; - Self::create(db, cfg, name, None, owner_id, &g.channels).await + Self::create( + db, + shared_event_publisher_map, + cfg, + name, + None, + owner_id, + &g.channels, + ) + .await } pub async fn get_by_id(db: &MySqlPool, id: Snowflake) -> Result, Error> { diff --git a/src/database/entities/role.rs b/src/database/entities/role.rs index 383fd2a..62fda0e 100644 --- a/src/database/entities/role.rs +++ b/src/database/entities/role.rs @@ -6,8 +6,8 @@ use chorus::types::{PermissionFlags, Snowflake}; use serde::{Deserialize, Serialize}; use sqlx::{MySqlPool, Row}; -use crate::eq_shared_event_publisher; use crate::errors::Error; +use crate::{eq_shared_event_publisher, SharedEventPublisherMap}; #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct Role { @@ -48,6 +48,7 @@ impl Role { pub async fn create( db: &MySqlPool, + shared_event_publisher_map: SharedEventPublisherMap, id: Option, guild_id: Snowflake, name: &str, @@ -77,7 +78,9 @@ impl Role { guild_id: guild_id.to_owned(), publisher: SharedEventPublisher::default(), }; - + shared_event_publisher_map + .write() + .insert(role.id, role.publisher.clone()); sqlx::query("INSERT INTO roles (id, guild_id, name, color, hoist, managed, mentionable, permissions, position, icon, unicode_emoji) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)") .bind(role.id) .bind(role.guild_id) From aaa87ce3ea657762a764ab84720a0ca2b0bedff6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 23 Jul 2024 22:12:03 +0200 Subject: [PATCH 030/162] Spawn tokio tasks for each "subserver" --- src/main.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index 188ddb4..ee7b813 100644 --- a/src/main.rs +++ b/src/main.rs @@ -198,11 +198,18 @@ async fn main() { .await .expect("Failed to seed config"); } + let shared_publisher_map = Arc::new(RwLock::new(HashMap::new())); - api::start_api(db.clone(), shared_publisher_map.clone()) - .await - .unwrap(); - gateway::start_gateway(db.clone(), shared_publisher_map.clone()) - .await - .unwrap(); + let mut tasks = [ + tokio::spawn(api::start_api(db.clone(), shared_publisher_map.clone())), + tokio::spawn(gateway::start_gateway( + db.clone(), + shared_publisher_map.clone(), + )), + ]; + for task in tasks.iter_mut() { + task.await + .expect("Failed to start server") + .expect("Failed to start server"); + } } From 9ec03cd71e2b9916fb606edd685edc676baeb40e Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 24 Jul 2024 17:05:39 +0200 Subject: [PATCH 031/162] add subscribed events to user --- src/database/entities/user.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index a20cf23..8b3a765 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -33,6 +33,7 @@ pub struct User { #[sqlx(skip)] #[serde(skip)] pub publisher: SharedEventPublisher, + pub subscribed_events: Vec, } impl Deref for User { From d9b07624b12e3e574c009246236b94124c00347b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 24 Jul 2024 19:03:32 +0200 Subject: [PATCH 032/162] Fix broken compile, use import FromRow instead of fully qualified pathname --- src/database/entities/user.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index 8b3a765..c5dbb07 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -16,7 +16,7 @@ use crate::{ errors::{Error, GuildError}, }; -#[derive(Debug, Clone, Default, Serialize, Deserialize, sqlx::FromRow)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, FromRow)] pub struct User { #[sqlx(flatten)] #[serde(flatten)] @@ -33,7 +33,7 @@ pub struct User { #[sqlx(skip)] #[serde(skip)] pub publisher: SharedEventPublisher, - pub subscribed_events: Vec, + pub subscribed_events: sqlx::types::Json>, } impl Deref for User { From 51bbdac79ed1e5c7c0b75b45f4d719d4c2dd0339 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 24 Jul 2024 22:15:14 +0200 Subject: [PATCH 033/162] Rename subscribed_events to relevant_events --- src/database/entities/user.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index c5dbb07..3207af7 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -33,7 +33,7 @@ pub struct User { #[sqlx(skip)] #[serde(skip)] pub publisher: SharedEventPublisher, - pub subscribed_events: sqlx::types::Json>, + pub relevant_events: sqlx::types::Json>, } impl Deref for User { From eba6931bbd927a534679bacc45a5264636cb2d01 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 24 Jul 2024 22:15:34 +0200 Subject: [PATCH 034/162] Write notes, start constructing initial Gateway architecture --- src/gateway/mod.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index f7f1656..9e76230 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -1,7 +1,14 @@ mod types; +use std::collections::BTreeMap; +use std::sync::{Arc, Mutex, Weak}; + +use chorus::types::{GatewayIdentifyPayload, Snowflake}; use log::info; +use pubserve::Subscriber; use sqlx::MySqlPool; +use tokio::net::{TcpListener, TcpStream}; + pub use types::*; // This Source Code Form is subject to the terms of the Mozilla Public @@ -10,11 +17,73 @@ pub use types::*; use crate::errors::Error; use crate::SharedEventPublisherMap; +/* NOTES (bitfl0wer) [These will be removed] +The gateway is supposed to be highly concurrent. It will be handling a lot of connections at once. +Thus, it makes sense to have each user connection be handled in a separate task. + +It is important to make a distinction between the user and the client. A user can potentially +be connected with many devices at once. They are still just one user. Respecting this fact +will likely save a lot of computational power. + +Handling a connection involves the following steps: + +1. Accepting the connection +2. Sending a hello event back +3. Receiving a Heartbeat event +4. Returning a Heartbeat ACK event +5. Receiving an Identify payload <- "GatewayUser" and/or "GatewayClient" are instantiated here. +6. Responding with a Ready event + +Handling disconnects and session resumes is for later, I think. +*/ + +/// A single identifiable User connected to the Gateway - possibly using many clients at the same +/// time. +struct GatewayUser { + /// Sessions a User is connected with. + clients: Vec, + /// The Snowflake ID of the User. + id: Snowflake, + /// A collection of [Subscribers](Subscriber) to [Event] [Publishers](pubserve::Publisher). + /// + /// A GatewayUser may have many [GatewayClients](GatewayClient), but he only gets subscribed to + /// all relevant [Publishers](pubserve::Publisher) *once* to save resources. + subscriptions: Vec>>, +} + +/// A concrete session, that a [GatewayUser] is connected to the Gateway with. +struct GatewayClient { + /// A [Weak] reference to the [GatewayUser] this client belongs to. + parent: Weak, + /// The [TcpStream] for this WebSocket session + connection: TcpStream, + /// [GatewayIdentifyPayload] the client has sent when connecting (or re-connecting) with this client. + identify: GatewayIdentifyPayload, +} + +struct NewConnection { + user: Option, + client: GatewayClient, +} + pub async fn start_gateway( db: MySqlPool, publisher_map: SharedEventPublisherMap, ) -> Result<(), Error> { info!(target: "symfonia::gateway", "Starting gateway server"); - // `publishers` will live for the lifetime of the gateway server, in the main gateway thread + let bind = std::env::var("GATEWAY_BIND").unwrap_or_else(|_| String::from("localhost:3003")); + let try_socket = TcpListener::bind(&bind).await; + let listener = try_socket.expect("Failed to bind to address"); + + let mut gateway_users: Arc>> = + Arc::new(Mutex::new(BTreeMap::new())); + + while let Ok((stream, _)) = listener.accept().await { + tokio::task::spawn(establish_connection(stream)); + } + Ok(()) +} + +async fn establish_connection(stream: TcpStream) -> Result { Ok(()) } From 144d24ade7a01ce998ddabe9a5f3b0d4c9834285 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 25 Jul 2024 11:14:06 +0200 Subject: [PATCH 035/162] Add fn checked_add_new_connection, update other structs --- src/gateway/mod.rs | 71 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 9e76230..8b70aa5 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -41,28 +41,28 @@ Handling disconnects and session resumes is for later, I think. /// time. struct GatewayUser { /// Sessions a User is connected with. - clients: Vec, + pub clients: Vec, /// The Snowflake ID of the User. - id: Snowflake, + pub id: Snowflake, /// A collection of [Subscribers](Subscriber) to [Event] [Publishers](pubserve::Publisher). /// /// A GatewayUser may have many [GatewayClients](GatewayClient), but he only gets subscribed to /// all relevant [Publishers](pubserve::Publisher) *once* to save resources. - subscriptions: Vec>>, + pub subscriptions: Vec>>, } /// A concrete session, that a [GatewayUser] is connected to the Gateway with. struct GatewayClient { /// A [Weak] reference to the [GatewayUser] this client belongs to. - parent: Weak, + pub parent: Weak>, /// The [TcpStream] for this WebSocket session - connection: TcpStream, + pub connection: TcpStream, /// [GatewayIdentifyPayload] the client has sent when connecting (or re-connecting) with this client. - identify: GatewayIdentifyPayload, + pub identify: GatewayIdentifyPayload, } struct NewConnection { - user: Option, + user: Arc>, client: GatewayClient, } @@ -75,15 +75,64 @@ pub async fn start_gateway( let try_socket = TcpListener::bind(&bind).await; let listener = try_socket.expect("Failed to bind to address"); - let mut gateway_users: Arc>> = + let gateway_users: Arc>>>> = Arc::new(Mutex::new(BTreeMap::new())); - while let Ok((stream, _)) = listener.accept().await { - tokio::task::spawn(establish_connection(stream)); + match tokio::task::spawn(establish_connection(stream)) + .await + .expect("The establish_connection task died") // QUESTION(bitfl0wer) Is it ok to .expect() here? + { + Ok(new_connection) => checked_add_new_connection(gateway_users.clone(), new_connection), + Err(_) => todo!(), + } } Ok(()) } +/// Handle the Gateway connection initalization process of a client connecting to the Gateway up +/// until the point where we receive the identify payload, at which point a [NewConnection] will +/// be returned. +/// +/// If successful, returns a [NewConnection] with a new [Arc>] and a +/// [GatewayClient], whose `.parent` field contains a [Weak] reference to the new [GatewayUser]. async fn establish_connection(stream: TcpStream) -> Result { - Ok(()) + todo!() +} + +/// Adds the contents of a [NewConnection] struct to a `gateway_users` map in a "checked" manner. +/// +/// If the `NewConnection` contains a [GatewayUser] which is already in `gateway_users`, then +/// change the `parent` of the `NewConnection` [GatewayClient] to the User +/// from our `gateway_users` and push the client to the `clients` field of the User in our +/// `gateway_users``. +/// +/// Else, add the [new GatewayUser] and the new [GatewayClient] into `gateway_users` as-is. +fn checked_add_new_connection( + gateway_users: Arc>>>>, + new_connection: NewConnection, +) { + // Make `new_connection` mutable + let mut new_connection = new_connection; + // To avoid having to get the lock a lot of times, lock once here and hold this lock for most + // of the way through this method + let new_connection_user = new_connection.user.lock().unwrap(); + let mut locked_map = gateway_users.lock().unwrap(); + // If our map contains the user from `new_connection` already, modify the `parent` of the `client` + // of `new_connection` to point to the user already in our map, then insert that `client` into + // the `clients` field of our existing user. + if locked_map.contains_key(&new_connection_user.id) { + let existing_user = locked_map.get(&new_connection_user.id).unwrap(); + new_connection.client.parent = Arc::downgrade(existing_user); + existing_user + .lock() + .unwrap() + .clients + .push(new_connection.client); + } else { + // We cannot do `locked_map.insert(id, new_connection.user)` if new_connection is still + // locked. Just bind the id we need to a new variable, then drop the lock. + let id = new_connection_user.id; + drop(new_connection_user); + locked_map.insert(id, new_connection.user); + } } From 1fd125872d1f690bf60de10134048f7dff28d0d0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 25 Jul 2024 13:21:35 +0200 Subject: [PATCH 036/162] Add tokio-rustls feature because I am unsure whether its enabled :3 --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 124fda0..1e22de2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ hex = "0.4.3" itertools = "0.13.0" tokio-tungstenite = { version = "0.23.1", features = [ "rustls-tls-webpki-roots", + "tokio-rustls", ] } pubserve = { version = "1.1.0", features = ["async", "send"] } parking_lot = { version = "0.12.3", features = ["deadlock_detection"] } From fafa01f98f51dd4d8c25268fb9272b17d46f247b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 25 Jul 2024 13:21:43 +0200 Subject: [PATCH 037/162] [from] tokio_tungstenite::tungstenite::Error --- src/errors.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index aa22632..cf876a4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -46,6 +46,9 @@ pub enum Error { #[error(transparent)] Reqwest(#[from] reqwest::Error), + + #[error(transparent)] + Tungstenite(#[from] tokio_tungstenite::tungstenite::Error), } #[derive(Debug, thiserror::Error)] @@ -208,6 +211,7 @@ impl ResponseError for Error { Error::Rand(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::Utf8(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::Reqwest(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::Tungstenite(_) => StatusCode::INTERNAL_SERVER_ERROR, } } From d40f65f6153ccdfb2c9ad3fdcaf642fa8c54a4f0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 25 Jul 2024 13:23:21 +0200 Subject: [PATCH 038/162] Change GatewayClient.connection to tuple of SplitSink, SplitStream, as this fits better --- src/gateway/mod.rs | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 8b70aa5..64bb37f 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -4,11 +4,15 @@ use std::collections::BTreeMap; use std::sync::{Arc, Mutex, Weak}; use chorus::types::{GatewayIdentifyPayload, Snowflake}; +use futures::stream::{SplitSink, SplitStream}; +use futures::StreamExt; use log::info; use pubserve::Subscriber; use sqlx::MySqlPool; use tokio::net::{TcpListener, TcpStream}; +use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::{accept_async, WebSocketStream}; pub use types::*; // This Source Code Form is subject to the terms of the Mozilla Public @@ -55,8 +59,11 @@ struct GatewayUser { struct GatewayClient { /// A [Weak] reference to the [GatewayUser] this client belongs to. pub parent: Weak>, - /// The [TcpStream] for this WebSocket session - pub connection: TcpStream, + /// The [SplitSink] and [SplitStream] for this clients' WebSocket session + pub connection: ( + SplitSink, Message>, + SplitStream>, + ), /// [GatewayIdentifyPayload] the client has sent when connecting (or re-connecting) with this client. pub identify: GatewayIdentifyPayload, } @@ -71,6 +78,7 @@ pub async fn start_gateway( publisher_map: SharedEventPublisherMap, ) -> Result<(), Error> { info!(target: "symfonia::gateway", "Starting gateway server"); + let bind = std::env::var("GATEWAY_BIND").unwrap_or_else(|_| String::from("localhost:3003")); let try_socket = TcpListener::bind(&bind).await; let listener = try_socket.expect("Failed to bind to address"); @@ -78,10 +86,11 @@ pub async fn start_gateway( let gateway_users: Arc>>>> = Arc::new(Mutex::new(BTreeMap::new())); while let Ok((stream, _)) = listener.accept().await { - match tokio::task::spawn(establish_connection(stream)) - .await - .expect("The establish_connection task died") // QUESTION(bitfl0wer) Is it ok to .expect() here? - { + let connection_result = match tokio::task::spawn(establish_connection(stream)).await { + Ok(result) => result, + Err(_) => continue, + }; + match connection_result { Ok(new_connection) => checked_add_new_connection(gateway_users.clone(), new_connection), Err(_) => todo!(), } @@ -96,7 +105,23 @@ pub async fn start_gateway( /// If successful, returns a [NewConnection] with a new [Arc>] and a /// [GatewayClient], whose `.parent` field contains a [Weak] reference to the new [GatewayUser]. async fn establish_connection(stream: TcpStream) -> Result { - todo!() + let ws_stream = accept_async(stream).await?; + let (write, read) = ws_stream.split(); + + // TODO: Everything below this is just an example and absolutely unfinished. + let user = Arc::new(Mutex::new(GatewayUser { + clients: Vec::new(), + id: Snowflake(1), + subscriptions: Vec::new(), + })); + Ok(NewConnection { + user: user.clone(), + client: GatewayClient { + parent: Arc::downgrade(&user), + connection: (write, read), + identify: GatewayIdentifyPayload::common(), + }, + }) } /// Adds the contents of a [NewConnection] struct to a `gateway_users` map in a "checked" manner. From 7e3972789b473b0ba54e78b5ca2191e9af3a9788 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 25 Jul 2024 13:26:14 +0200 Subject: [PATCH 039/162] Update notes --- src/gateway/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 64bb37f..13e52de 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -38,7 +38,11 @@ Handling a connection involves the following steps: 5. Receiving an Identify payload <- "GatewayUser" and/or "GatewayClient" are instantiated here. 6. Responding with a Ready event -Handling disconnects and session resumes is for later, I think. +Handling disconnects and session resumes is for later and not considered at this exact moment. + +From there on, run a task that takes ownership of the GatewayClient struct. This task will be what +is sending the events that the (to be implemented) Subscribers receive from the Publishers that the +GatewayUser is subscribed to */ /// A single identifiable User connected to the Gateway - possibly using many clients at the same From 1835855ff362d985f1a3cd7dbfec7ef57a1ec46b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 25 Jul 2024 22:52:23 +0200 Subject: [PATCH 040/162] Generalize Dockerfile: Remove --target triple --- Dockerfile | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index fdf36c0..19400c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,6 @@ FROM rust:1-bookworm AS chef RUN cargo install cargo-chef -RUN rustup target add x86_64-unknown-linux-gnu && \ - update-ca-certificates +RUN update-ca-certificates WORKDIR /app FROM chef AS planner @@ -10,9 +9,9 @@ RUN cargo chef prepare --recipe-path recipe.json FROM chef AS builder COPY --from=planner /app/recipe.json recipe.json -RUN cargo chef cook --release --target x86_64-unknown-linux-gnu --recipe-path recipe.json +RUN cargo chef cook --release --recipe-path recipe.json COPY . . -RUN cargo build --target x86_64-unknown-linux-gnu --release +RUN cargo build --release FROM debian:latest AS runtime @@ -35,7 +34,7 @@ RUN adduser \ --uid 10001 \ "symfonia" -COPY --from=builder --chown=symfonia:symfonia /app/target/x86_64-unknown-linux-gnu/release/symfonia /app/symfonia +COPY --from=builder --chown=symfonia:symfonia /app/target/release/symfonia /app/symfonia USER symfonia:symfonia WORKDIR /app/ From 7ede4a438ab6337174769223089a7ea3dcdf5f0a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 25 Jul 2024 22:53:18 +0200 Subject: [PATCH 041/162] Add Gateway Error type --- src/errors.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index cf876a4..c2cced2 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -49,6 +49,15 @@ pub enum Error { #[error(transparent)] Tungstenite(#[from] tokio_tungstenite::tungstenite::Error), + + #[error(transparent)] + Gateway(#[from] GatewayError), +} + +#[derive(Debug, thiserror::Error)] +pub enum GatewayError { + #[error("UNEXPECTED_MESSAGE")] + UnexpectedMessage, } #[derive(Debug, thiserror::Error)] @@ -212,6 +221,9 @@ impl ResponseError for Error { Error::Utf8(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::Reqwest(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::Tungstenite(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::Gateway(err) => match err { + GatewayError::UnexpectedMessage => StatusCode::BAD_REQUEST, + }, } } From 304e06729926fb980a011a3e2c52dc3868675ca3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 25 Jul 2024 22:53:39 +0200 Subject: [PATCH 042/162] establish_connection(): Wait for heartbeat --- src/gateway/mod.rs | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 13e52de..5cec2d1 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -3,11 +3,12 @@ mod types; use std::collections::BTreeMap; use std::sync::{Arc, Mutex, Weak}; -use chorus::types::{GatewayIdentifyPayload, Snowflake}; +use chorus::types::{GatewayHeartbeat, GatewayIdentifyPayload, Snowflake}; use futures::stream::{SplitSink, SplitStream}; use futures::StreamExt; use log::info; use pubserve::Subscriber; +use serde_json::from_str; use sqlx::MySqlPool; use tokio::net::{TcpListener, TcpStream}; @@ -18,7 +19,7 @@ pub use types::*; // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use crate::errors::Error; +use crate::errors::{Error, GatewayError}; use crate::SharedEventPublisherMap; /* NOTES (bitfl0wer) [These will be removed] @@ -96,7 +97,7 @@ pub async fn start_gateway( }; match connection_result { Ok(new_connection) => checked_add_new_connection(gateway_users.clone(), new_connection), - Err(_) => todo!(), + Err(_) => continue, } } Ok(()) @@ -110,22 +111,21 @@ pub async fn start_gateway( /// [GatewayClient], whose `.parent` field contains a [Weak] reference to the new [GatewayUser]. async fn establish_connection(stream: TcpStream) -> Result { let ws_stream = accept_async(stream).await?; - let (write, read) = ws_stream.split(); - - // TODO: Everything below this is just an example and absolutely unfinished. - let user = Arc::new(Mutex::new(GatewayUser { - clients: Vec::new(), - id: Snowflake(1), - subscriptions: Vec::new(), - })); - Ok(NewConnection { - user: user.clone(), - client: GatewayClient { - parent: Arc::downgrade(&user), - connection: (write, read), - identify: GatewayIdentifyPayload::common(), - }, - }) + let (write, mut read) = ws_stream.split(); + if let Some(maybe_heartbeat) = read.next().await { + let maybe_heartbeat = maybe_heartbeat?; + let maybe_heartbeat_text = match maybe_heartbeat.is_text() { + true => maybe_heartbeat.to_text()?, + false => return Err(GatewayError::UnexpectedMessage.into()), + }; + let heartbeat = match from_str::(maybe_heartbeat_text) { + Ok(msg) => msg, + Err(_) => return Err(GatewayError::UnexpectedMessage.into()), + }; + // TODO(bitfl0wer) Do something with the heartbeat, respond to the heartbeat, wait for + // identify payload, construct [NewConnection] + } + todo!() } /// Adds the contents of a [NewConnection] struct to a `gateway_users` map in a "checked" manner. From 6a7cf78494b411b67e4ee70579c94bf2b65ea9c5 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 26 Jul 2024 23:13:45 +0200 Subject: [PATCH 043/162] Send GatewayHello to peer on connect --- src/gateway/mod.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 5cec2d1..f07d190 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -3,12 +3,12 @@ mod types; use std::collections::BTreeMap; use std::sync::{Arc, Mutex, Weak}; -use chorus::types::{GatewayHeartbeat, GatewayIdentifyPayload, Snowflake}; +use chorus::types::{GatewayHeartbeat, GatewayHello, GatewayIdentifyPayload, Snowflake}; use futures::stream::{SplitSink, SplitStream}; -use futures::StreamExt; +use futures::{SinkExt, StreamExt}; use log::info; use pubserve::Subscriber; -use serde_json::from_str; +use serde_json::{from_str, json}; use sqlx::MySqlPool; use tokio::net::{TcpListener, TcpStream}; @@ -111,8 +111,11 @@ pub async fn start_gateway( /// [GatewayClient], whose `.parent` field contains a [Weak] reference to the new [GatewayUser]. async fn establish_connection(stream: TcpStream) -> Result { let ws_stream = accept_async(stream).await?; - let (write, mut read) = ws_stream.split(); - if let Some(maybe_heartbeat) = read.next().await { + let (mut sender, mut receiver) = ws_stream.split(); + sender + .send(Message::Text(json!(GatewayHello::default()).to_string())) + .await?; + if let Some(maybe_heartbeat) = receiver.next().await { let maybe_heartbeat = maybe_heartbeat?; let maybe_heartbeat_text = match maybe_heartbeat.is_text() { true => maybe_heartbeat.to_text()?, From 3dc94ecd511eef624474308cbff8bdf7a6458b84 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 27 Jul 2024 14:34:30 +0200 Subject: [PATCH 044/162] Add logs if connection fails --- src/gateway/mod.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 72060b0..520676d 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -99,11 +99,17 @@ pub async fn start_gateway( while let Ok((stream, _)) = listener.accept().await { let connection_result = match tokio::task::spawn(establish_connection(stream)).await { Ok(result) => result, - Err(_) => continue, + Err(e) => { + log::warn!(target: "symfonia::db::establish_connection", "User gateway task died. Is the host healthy?: {e}"); + continue; + } }; match connection_result { Ok(new_connection) => checked_add_new_connection(gateway_users.clone(), new_connection), - Err(_) => continue, + Err(e) => { + log::debug!(target: "symfonia::db::establish_connection", "User gateway connection could not be established: {e}"); + continue; + } } } Ok(()) From 84c42006c4fd00819b8828a327840d374b3e9a04 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 27 Jul 2024 23:49:22 +0200 Subject: [PATCH 045/162] Use latest chorus:dev --- Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7cbac10..735998e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,8 +58,7 @@ sentry = { version = "0.34.0", default-features = false, features = [ "rustls", ] } clap = { version = "4.5.4", features = ["derive"] } - -chorus = { git = "http://github.com/polyphony-chat/chorus", rev = "537b025", features = [ +chorus = { git = "http://github.com/polyphony-chat/chorus", branch = "dev", features = [ "backend", ], default-features = false } # git = "ssh://git@github.com/Quat3rnion/chorus" # path = "../chorus" git = "ssh://git@github.com/polyphony-chat/chorus" serde_path_to_error = "0.1.16" From 9ac63f8f3dbadedc55c9faf7e029e55c2ec80497 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 27 Jul 2024 23:49:36 +0200 Subject: [PATCH 046/162] New GatewayError variant: Timeout --- src/errors.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index 631f154..785fecb 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -64,6 +64,8 @@ pub enum Error { pub enum GatewayError { #[error("UNEXPECTED_MESSAGE")] UnexpectedMessage, + #[error("TIMEOUT")] + Timeout, } #[derive(Debug, thiserror::Error)] From 476096d4bcdf2045d7ad78a7d46696f4969447e6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 27 Jul 2024 23:49:52 +0200 Subject: [PATCH 047/162] Work on establish_connection --- src/gateway/mod.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 520676d..2c683c8 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -8,8 +8,11 @@ mod types; use std::collections::BTreeMap; use std::sync::{Arc, Mutex, Weak}; +use std::time::SystemTime; -use chorus::types::{GatewayHeartbeat, GatewayHello, GatewayIdentifyPayload, Snowflake}; +use chorus::types::{ + GatewayHeartbeat, GatewayHeartbeatAck, GatewayHello, GatewayIdentifyPayload, Snowflake, +}; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use log::info; @@ -56,7 +59,7 @@ GatewayUser is subscribed to /// time. struct GatewayUser { /// Sessions a User is connected with. - pub clients: Vec, + pub clients: Vec>>, /// The Snowflake ID of the User. pub id: Snowflake, /// A collection of [Subscribers](Subscriber) to [Event] [Publishers](pubserve::Publisher). @@ -75,8 +78,8 @@ struct GatewayClient { SplitSink, Message>, SplitStream>, ), - /// [GatewayIdentifyPayload] the client has sent when connecting (or re-connecting) with this client. - pub identify: GatewayIdentifyPayload, + pub session_id: String, + pub last_sequence: u64, } struct NewConnection { @@ -127,6 +130,11 @@ async fn establish_connection(stream: TcpStream) -> Result sender .send(Message::Text(json!(GatewayHello::default()).to_string())) .await?; + let next = match receiver.next().await { + Some(next) => next, + None => return Err(GatewayError::Timeout.into()), + }?; + // TODO: Check whether `next` is a Resume message or a Heartbeat if let Some(maybe_heartbeat) = receiver.next().await { let maybe_heartbeat = maybe_heartbeat?; let maybe_heartbeat_text = match maybe_heartbeat.is_text() { @@ -137,12 +145,24 @@ async fn establish_connection(stream: TcpStream) -> Result Ok(msg) => msg, Err(_) => return Err(GatewayError::UnexpectedMessage.into()), }; + let last_heartbeat = LastHeartbeat { + timestamp: SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Please check the system time of the host machine") + .as_secs(), + heartbeat, + }; + sender.send(Message::Text(GatewayHeartbeatAck { op: todo!() })) // TODO(bitfl0wer) Do something with the heartbeat, respond to the heartbeat, wait for // identify payload, construct [NewConnection] } todo!() } +async fn resume_connection() { + todo!() +} + /// Adds the contents of a [NewConnection] struct to a `gateway_users` map in a "checked" manner. /// /// If the `NewConnection` contains a [GatewayUser] which is already in `gateway_users`, then @@ -171,7 +191,7 @@ fn checked_add_new_connection( .lock() .unwrap() .clients - .push(new_connection.client); + .push(Arc::new(Mutex::new(new_connection.client))); } else { // We cannot do `locked_map.insert(id, new_connection.user)` if new_connection is still // locked. Just bind the id we need to a new variable, then drop the lock. From 0a12267f0a40f2c14f985c910f0927b9a3ffd9a0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 28 Jul 2024 12:23:46 +0200 Subject: [PATCH 048/162] Define WebSocketSend and WebSocketReceive types as ergonomic shorthands --- src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main.rs b/src/main.rs index 1ef8fa5..6d46f2e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,6 +40,12 @@ mod util; pub type SharedEventPublisher = Arc>>; pub type EventPublisherMap = HashMap; pub type SharedEventPublisherMap = Arc>; +pub type WebSocketSend = + futures::stream::SplitStream>; +pub type WebSocketReceive = futures::stream::SplitSink< + tokio_tungstenite::WebSocketStream, + tokio_tungstenite::tungstenite::Message, +>; pub fn eq_shared_event_publisher(a: &SharedEventPublisher, b: &SharedEventPublisher) -> bool { let a = a.read(); From cf0b14586f970d80dcc05388dfa9009380feb2ea Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 28 Jul 2024 12:24:04 +0200 Subject: [PATCH 049/162] Restructure establish_connection to make space for the resume procedure --- src/gateway/mod.rs | 40 ++++++++++++---------------------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 2c683c8..883707f 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -11,7 +11,8 @@ use std::sync::{Arc, Mutex, Weak}; use std::time::SystemTime; use chorus::types::{ - GatewayHeartbeat, GatewayHeartbeatAck, GatewayHello, GatewayIdentifyPayload, Snowflake, + GatewayHeartbeat, GatewayHeartbeatAck, GatewayHello, GatewayIdentifyPayload, GatewayResume, + Snowflake, }; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; @@ -29,7 +30,7 @@ pub use types::*; // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::errors::{Error, GatewayError}; -use crate::SharedEventPublisherMap; +use crate::{SharedEventPublisherMap, WebSocketReceive, WebSocketSend}; /* NOTES (bitfl0wer) [These will be removed] The gateway is supposed to be highly concurrent. It will be handling a lot of connections at once. @@ -74,10 +75,7 @@ struct GatewayClient { /// A [Weak] reference to the [GatewayUser] this client belongs to. pub parent: Weak>, /// The [SplitSink] and [SplitStream] for this clients' WebSocket session - pub connection: ( - SplitSink, Message>, - SplitStream>, - ), + pub connection: (WebSocketReceive, WebSocketSend), pub session_id: String, pub last_sequence: u64, } @@ -130,32 +128,18 @@ async fn establish_connection(stream: TcpStream) -> Result sender .send(Message::Text(json!(GatewayHello::default()).to_string())) .await?; - let next = match receiver.next().await { + let message = match receiver.next().await { Some(next) => next, None => return Err(GatewayError::Timeout.into()), }?; - // TODO: Check whether `next` is a Resume message or a Heartbeat - if let Some(maybe_heartbeat) = receiver.next().await { - let maybe_heartbeat = maybe_heartbeat?; - let maybe_heartbeat_text = match maybe_heartbeat.is_text() { - true => maybe_heartbeat.to_text()?, - false => return Err(GatewayError::UnexpectedMessage.into()), - }; - let heartbeat = match from_str::(maybe_heartbeat_text) { - Ok(msg) => msg, - Err(_) => return Err(GatewayError::UnexpectedMessage.into()), - }; - let last_heartbeat = LastHeartbeat { - timestamp: SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .expect("Please check the system time of the host machine") - .as_secs(), - heartbeat, - }; - sender.send(Message::Text(GatewayHeartbeatAck { op: todo!() })) - // TODO(bitfl0wer) Do something with the heartbeat, respond to the heartbeat, wait for - // identify payload, construct [NewConnection] + if let Ok(resume_message) = from_str::(&message.to_string()) { + // Resume procedure + } else if let Ok(heartbeat_message) = from_str::(&message.to_string()) { + // Continue setting up fresh/new Gateway connection + } else { + return Err(GatewayError::UnexpectedMessage.into()); } + todo!() } From 666fd8d175155c0a9bbe7d20830cd24880314032 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 28 Jul 2024 18:47:59 +0200 Subject: [PATCH 050/162] oopsie i mixed up receive and send --- src/main.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6d46f2e..369c5e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -40,9 +40,9 @@ mod util; pub type SharedEventPublisher = Arc>>; pub type EventPublisherMap = HashMap; pub type SharedEventPublisherMap = Arc>; -pub type WebSocketSend = +pub type WebSocketReceive = futures::stream::SplitStream>; -pub type WebSocketReceive = futures::stream::SplitSink< +pub type WebSocketSend = futures::stream::SplitSink< tokio_tungstenite::WebSocketStream, tokio_tungstenite::tungstenite::Message, >; @@ -154,7 +154,7 @@ async fn main() { .logger( Logger::builder() .appender("gateway") - .build("symfonia::gateway", LevelFilter::Info), + .build("symfonia::gateway", LevelFilter::Trace), ) .build(Root::builder().appender("stdout").build({ let mode = std::env::var("MODE").unwrap_or("DEBUG".to_string()); From 7ab83d1f615cfd429eaa06144175f80803c3fcc3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 28 Jul 2024 18:48:10 +0200 Subject: [PATCH 051/162] add new error to ResponseError impl --- src/errors.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index 785fecb..aafb162 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -230,7 +230,9 @@ impl ResponseError for Error { Error::Reqwest(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::Tungstenite(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::Gateway(err) => match err { + // TODO: Check if the associated statuscodes are okay GatewayError::UnexpectedMessage => StatusCode::BAD_REQUEST, + GatewayError::Timeout => StatusCode::BAD_REQUEST, }, } } From c5663629e4de6ad3912becf52312e5665e789118 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 28 Jul 2024 18:48:53 +0200 Subject: [PATCH 052/162] Handle removing resumeable sessions after 90s of being disconnected --- src/gateway/mod.rs | 95 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 883707f..77ae2cf 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -4,11 +4,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +static RESUME_RECONNECT_WINDOW_SECONDS: u8 = 90; + mod types; use std::collections::BTreeMap; use std::sync::{Arc, Mutex, Weak}; -use std::time::SystemTime; +use std::thread::sleep; +use std::time::{Duration, SystemTime}; use chorus::types::{ GatewayHeartbeat, GatewayHeartbeatAck, GatewayHello, GatewayIdentifyPayload, GatewayResume, @@ -75,11 +78,41 @@ struct GatewayClient { /// A [Weak] reference to the [GatewayUser] this client belongs to. pub parent: Weak>, /// The [SplitSink] and [SplitStream] for this clients' WebSocket session - pub connection: (WebSocketReceive, WebSocketSend), + pub connection: Connection, pub session_id: String, pub last_sequence: u64, } +struct Connection { + pub sender: WebSocketSend, + pub receiver: WebSocketReceive, +} + +struct DisconnectInfo { + session_id: String, + disconnected_at: u64, + with_opcode: u16, +} + +impl + From<( + SplitSink, Message>, + SplitStream>, + )> for Connection +{ + fn from( + value: ( + SplitSink, Message>, + SplitStream>, + ), + ) -> Self { + Self { + sender: value.0, + receiver: value.1, + } + } +} + struct NewConnection { user: Arc>, client: GatewayClient, @@ -97,18 +130,21 @@ pub async fn start_gateway( let gateway_users: Arc>>>> = Arc::new(Mutex::new(BTreeMap::new())); + let resumeable_clients: Arc>> = + Arc::new(Mutex::new(BTreeMap::new())); + tokio::task::spawn(async { purge_expired_disconnects(resumeable_clients) }); while let Ok((stream, _)) = listener.accept().await { let connection_result = match tokio::task::spawn(establish_connection(stream)).await { Ok(result) => result, Err(e) => { - log::warn!(target: "symfonia::db::establish_connection", "User gateway task died. Is the host healthy?: {e}"); + log::warn!(target: "symfonia::gateway::establish_connection", "User gateway task died. Is the host healthy?: {e}"); continue; } }; match connection_result { Ok(new_connection) => checked_add_new_connection(gateway_users.clone(), new_connection), Err(e) => { - log::debug!(target: "symfonia::db::establish_connection", "User gateway connection could not be established: {e}"); + log::debug!(target: "symfonia::gateway::establish_connection", "User gateway connection could not be established: {e}"); continue; } } @@ -116,26 +152,56 @@ pub async fn start_gateway( Ok(()) } -/// Handle the Gateway connection initalization process of a client connecting to the Gateway up -/// until the point where we receive the identify payload, at which point a [NewConnection] will -/// be returned. +/// A disconnected, resumable session can only be resumed within 90 seconds after a disconnect occurs. +/// Sessions that can be resumed are stored in a `Map`. The purpose of this method is to periodically +/// throw out expired sessions from that map. +fn purge_expired_disconnects(resumeable_clients: Arc>>) { + loop { + sleep(Duration::from_secs(5)); + log::trace!(target: "symfonia::gateway::purge_expired_disconnects", "Removing stale disconnected sessions from list of resumeable sessions"); + let current_unix_timestamp = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("Check the clock/time settings on the host machine") + .as_secs(); + let mut to_remove = Vec::new(); + let mut lock = resumeable_clients.lock().unwrap(); + for (disconnected_session_id, disconnected_session_info) in lock.iter() { + if current_unix_timestamp - disconnected_session_info.disconnected_at + > RESUME_RECONNECT_WINDOW_SECONDS as u64 + { + to_remove.push(disconnected_session_id.clone()); + } + } + let len = to_remove.len(); + for session_id in to_remove.iter() { + lock.remove(session_id); + } + drop(lock); + log::trace!(target: "symfonia::gateway::purge_expired_disconnects", "Removed {} stale sessions", len); + } +} + +/// `establish_connection` is the entrypoint method that gets called when a client tries to connect +/// to the WebSocket server. /// /// If successful, returns a [NewConnection] with a new [Arc>] and a /// [GatewayClient], whose `.parent` field contains a [Weak] reference to the new [GatewayUser]. async fn establish_connection(stream: TcpStream) -> Result { let ws_stream = accept_async(stream).await?; - let (mut sender, mut receiver) = ws_stream.split(); - sender + let mut connection: Connection = ws_stream.split().into(); + connection + .sender .send(Message::Text(json!(GatewayHello::default()).to_string())) .await?; - let message = match receiver.next().await { + let message = match connection.receiver.next().await { Some(next) => next, None => return Err(GatewayError::Timeout.into()), }?; if let Ok(resume_message) = from_str::(&message.to_string()) { - // Resume procedure + log::debug!(target: "symfonia::gateway::establish_connection", "[{}] Received GatewayResume. Trying to resume gateway connection", &resume_message.session_id); + return resume_connection(connection, resume_message).await; } else if let Ok(heartbeat_message) = from_str::(&message.to_string()) { - // Continue setting up fresh/new Gateway connection + log::debug!(target: "symfonia::gateway::establish_connection", "Received GatewayHeartbeat. Continuing to build fresh gateway connection"); } else { return Err(GatewayError::UnexpectedMessage.into()); } @@ -143,7 +209,10 @@ async fn establish_connection(stream: TcpStream) -> Result todo!() } -async fn resume_connection() { +async fn resume_connection( + connection: Connection, + resume_message: GatewayResume, +) -> Result { todo!() } From bda19248b8f5de92f73143f0ffd1a58806e9e520 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 29 Jul 2024 10:54:06 +0200 Subject: [PATCH 053/162] add MODE to compose example --- compose-example.env | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compose-example.env b/compose-example.env index c91cf35..dd26618 100644 --- a/compose-example.env +++ b/compose-example.env @@ -1,3 +1,4 @@ MARIADB_USER=symfonia MARIADB_PASSWORD=symfonia -MARIADB_DATABASE=symfonia \ No newline at end of file +MARIADB_DATABASE=symfonia +MODE=debug \ No newline at end of file From 1209337e6a15caa8c571c89f4f59de741c41b1bf Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 29 Jul 2024 10:54:28 +0200 Subject: [PATCH 054/162] Change docstring for purge_expired_disconnects --- src/gateway/mod.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 77ae2cf..5fe39e1 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -11,12 +11,9 @@ mod types; use std::collections::BTreeMap; use std::sync::{Arc, Mutex, Weak}; use std::thread::sleep; -use std::time::{Duration, SystemTime}; +use std::time::Duration; -use chorus::types::{ - GatewayHeartbeat, GatewayHeartbeatAck, GatewayHello, GatewayIdentifyPayload, GatewayResume, - Snowflake, -}; +use chorus::types::{GatewayHeartbeat, GatewayHello, GatewayResume, Snowflake}; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use log::info; @@ -152,9 +149,9 @@ pub async fn start_gateway( Ok(()) } -/// A disconnected, resumable session can only be resumed within 90 seconds after a disconnect occurs. -/// Sessions that can be resumed are stored in a `Map`. The purpose of this method is to periodically -/// throw out expired sessions from that map. +/// A disconnected, resumable session can only be resumed within `RESUME_RECONNECT_WINDOW_SECONDS` +/// seconds after a disconnect occurs. Sessions that can be resumed are stored in a `Map`. The +/// purpose of this method is to periodically throw out expired sessions from that map. fn purge_expired_disconnects(resumeable_clients: Arc>>) { loop { sleep(Duration::from_secs(5)); From a920cb3129a0ff93866ce18a69cfa5b576fae5a0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 1 Aug 2024 19:41:08 +0200 Subject: [PATCH 055/162] Receive identify --- src/gateway/mod.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 77ae2cf..dd3e2ce 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -137,7 +137,7 @@ pub async fn start_gateway( let connection_result = match tokio::task::spawn(establish_connection(stream)).await { Ok(result) => result, Err(e) => { - log::warn!(target: "symfonia::gateway::establish_connection", "User gateway task died. Is the host healthy?: {e}"); + log::debug!(target: "symfonia::gateway::establish_connection", "User gateway task died: {e}"); continue; } }; @@ -193,15 +193,26 @@ async fn establish_connection(stream: TcpStream) -> Result .sender .send(Message::Text(json!(GatewayHello::default()).to_string())) .await?; - let message = match connection.receiver.next().await { + let raw_message = match connection.receiver.next().await { Some(next) => next, None => return Err(GatewayError::Timeout.into()), }?; - if let Ok(resume_message) = from_str::(&message.to_string()) { + if let Ok(resume_message) = from_str::(&raw_message.to_string()) { log::debug!(target: "symfonia::gateway::establish_connection", "[{}] Received GatewayResume. Trying to resume gateway connection", &resume_message.session_id); return resume_connection(connection, resume_message).await; - } else if let Ok(heartbeat_message) = from_str::(&message.to_string()) { + } else if let Ok(heartbeat_message) = from_str::(&raw_message.to_string()) { log::debug!(target: "symfonia::gateway::establish_connection", "Received GatewayHeartbeat. Continuing to build fresh gateway connection"); + let raw_identify = match connection.receiver.next().await { + Some(next) => next, + None => return Err(GatewayError::Timeout.into()), + }?; + let identify = match from_str::(&raw_identify.to_string()) { + Ok(identify) => identify, + Err(e) => { + log::debug!(target: "symfonia::gateway::establish_connection", "Expected GatewayIdentifyPayload, received wrong data: {e}"); + return Err(GatewayError::UnexpectedMessage.into()); + } + }; } else { return Err(GatewayError::UnexpectedMessage.into()); } From 55e852e163f41ed689e1966a0069d2e1d0f3f532 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 1 Aug 2024 20:28:26 +0200 Subject: [PATCH 056/162] Unindent code --- src/gateway/mod.rs | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index dd3e2ce..c4460c2 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -200,22 +200,21 @@ async fn establish_connection(stream: TcpStream) -> Result if let Ok(resume_message) = from_str::(&raw_message.to_string()) { log::debug!(target: "symfonia::gateway::establish_connection", "[{}] Received GatewayResume. Trying to resume gateway connection", &resume_message.session_id); return resume_connection(connection, resume_message).await; - } else if let Ok(heartbeat_message) = from_str::(&raw_message.to_string()) { - log::debug!(target: "symfonia::gateway::establish_connection", "Received GatewayHeartbeat. Continuing to build fresh gateway connection"); - let raw_identify = match connection.receiver.next().await { - Some(next) => next, - None => return Err(GatewayError::Timeout.into()), - }?; - let identify = match from_str::(&raw_identify.to_string()) { - Ok(identify) => identify, - Err(e) => { - log::debug!(target: "symfonia::gateway::establish_connection", "Expected GatewayIdentifyPayload, received wrong data: {e}"); - return Err(GatewayError::UnexpectedMessage.into()); - } - }; - } else { - return Err(GatewayError::UnexpectedMessage.into()); } + let heartbeat_message = from_str::(&raw_message.to_string()) + .map_err(|_| GatewayError::UnexpectedMessage)?; + log::debug!(target: "symfonia::gateway::establish_connection", "Received GatewayHeartbeat. Continuing to build fresh gateway connection"); + let raw_identify = match connection.receiver.next().await { + Some(next) => next, + None => return Err(GatewayError::Timeout.into()), + }?; + let identify = match from_str::(&raw_identify.to_string()) { + Ok(identify) => identify, + Err(e) => { + log::debug!(target: "symfonia::gateway::establish_connection", "Expected GatewayIdentifyPayload, received wrong data: {e}"); + return Err(GatewayError::UnexpectedMessage.into()); + } + }; todo!() } From 6b541598aee0d59ff19fe4642c06ab492f82888f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 22 Aug 2024 21:20:49 +0200 Subject: [PATCH 057/162] Fix gateway/mod.rs --- src/gateway/mod.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 10c3d4a..63b7f78 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -13,13 +13,15 @@ use std::sync::{Arc, Mutex, Weak}; use std::thread::sleep; use std::time::Duration; -use chorus::types::{GatewayHeartbeat, GatewayHello, GatewayResume, Snowflake}; +use chorus::types::{ + GatewayHeartbeat, GatewayHello, GatewayIdentifyPayload, GatewayResume, Snowflake, +}; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use log::info; use pubserve::Subscriber; use serde_json::{from_str, json}; -use sqlx::MySqlPool; +use sqlx::PgPool; use tokio::net::{TcpListener, TcpStream}; use tokio_tungstenite::tungstenite::Message; @@ -116,7 +118,7 @@ struct NewConnection { } pub async fn start_gateway( - db: MySqlPool, + db: PgPool, publisher_map: SharedEventPublisherMap, ) -> Result<(), Error> { info!(target: "symfonia::gateway", "Starting gateway server"); From 62911221154f56ca5ac2127e406a28b57f1c4c35 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 22 Aug 2024 22:02:15 +0200 Subject: [PATCH 058/162] add todo list for me --- src/gateway/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 63b7f78..ebb1b01 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -214,6 +214,10 @@ async fn establish_connection(stream: TcpStream) -> Result return Err(GatewayError::UnexpectedMessage.into()); } }; + // TODO(bitfl0wer): + // - extract user id and token claims from identify + // - use jwt decode to verify claims of user id and token validity + // - if valid, create new gatewayclient todo!() } From 3b219d30fb9503ec7062c3a8336450c394102275 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 23 Aug 2024 14:03:04 +0200 Subject: [PATCH 059/162] Implement establishing connection --- src/gateway/mod.rs | 70 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index ebb1b01..b9e8228 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -28,10 +28,12 @@ use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::{accept_async, WebSocketStream}; pub use types::*; +use crate::database::entities::Config; // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. use crate::errors::{Error, GatewayError}; +use crate::util::token::check_token; use crate::{SharedEventPublisherMap, WebSocketReceive, WebSocketSend}; /* NOTES (bitfl0wer) [These will be removed] @@ -117,9 +119,13 @@ struct NewConnection { client: GatewayClient, } +type ResumableClientsStore = Arc>>; +type GatewayUsersStore = Arc>>>>; + pub async fn start_gateway( db: PgPool, publisher_map: SharedEventPublisherMap, + config: Config, ) -> Result<(), Error> { info!(target: "symfonia::gateway", "Starting gateway server"); @@ -127,13 +133,17 @@ pub async fn start_gateway( let try_socket = TcpListener::bind(&bind).await; let listener = try_socket.expect("Failed to bind to address"); - let gateway_users: Arc>>>> = - Arc::new(Mutex::new(BTreeMap::new())); - let resumeable_clients: Arc>> = - Arc::new(Mutex::new(BTreeMap::new())); + let gateway_users: GatewayUsersStore = Arc::new(Mutex::new(BTreeMap::new())); + let resumeable_clients: ResumableClientsStore = Arc::new(Mutex::new(BTreeMap::new())); tokio::task::spawn(async { purge_expired_disconnects(resumeable_clients) }); while let Ok((stream, _)) = listener.accept().await { - let connection_result = match tokio::task::spawn(establish_connection(stream)).await { + let connection_result = match tokio::task::spawn(establish_connection( + stream, + db.clone(), + gateway_users.clone(), + )) + .await + { Ok(result) => result, Err(e) => { log::debug!(target: "symfonia::gateway::establish_connection", "User gateway task died: {e}"); @@ -154,7 +164,7 @@ pub async fn start_gateway( /// A disconnected, resumable session can only be resumed within `RESUME_RECONNECT_WINDOW_SECONDS` /// seconds after a disconnect occurs. Sessions that can be resumed are stored in a `Map`. The /// purpose of this method is to periodically throw out expired sessions from that map. -fn purge_expired_disconnects(resumeable_clients: Arc>>) { +fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { loop { sleep(Duration::from_secs(5)); log::trace!(target: "symfonia::gateway::purge_expired_disconnects", "Removing stale disconnected sessions from list of resumeable sessions"); @@ -185,7 +195,12 @@ fn purge_expired_disconnects(resumeable_clients: Arc>] and a /// [GatewayClient], whose `.parent` field contains a [Weak] reference to the new [GatewayUser]. -async fn establish_connection(stream: TcpStream) -> Result { +async fn establish_connection( + stream: TcpStream, + db: PgPool, + gateway_users_store: GatewayUsersStore, + config: Config, +) -> Result { let ws_stream = accept_async(stream).await?; let mut connection: Connection = ws_stream.split().into(); connection @@ -200,7 +215,7 @@ async fn establish_connection(stream: TcpStream) -> Result log::debug!(target: "symfonia::gateway::establish_connection", "[{}] Received GatewayResume. Trying to resume gateway connection", &resume_message.session_id); return resume_connection(connection, resume_message).await; } - let heartbeat_message = from_str::(&raw_message.to_string()) + let _heartbeat_message = from_str::(&raw_message.to_string()) // TODO: Implement hearbeating and sequence handling .map_err(|_| GatewayError::UnexpectedMessage)?; log::debug!(target: "symfonia::gateway::establish_connection", "Received GatewayHeartbeat. Continuing to build fresh gateway connection"); let raw_identify = match connection.receiver.next().await { @@ -214,19 +229,46 @@ async fn establish_connection(stream: TcpStream) -> Result return Err(GatewayError::UnexpectedMessage.into()); } }; - // TODO(bitfl0wer): - // - extract user id and token claims from identify - // - use jwt decode to verify claims of user id and token validity - // - if valid, create new gatewayclient - todo!() + // Retrieve the token without the "Bearer " prefix + let token = identify.token.trim_start_matches("Bearer "); + // Check the token and retrieve the valid claims + let claims = check_token(&db, token, &config.security.jwt_secret).await?; + let id = claims.id; + if let Some(user) = gateway_users_store.lock().unwrap().get(&id) { + log::debug!(target: "symfonia::gateway::establish_connection", "User with ID {id} already connected. Adding new client to existing user"); + let client = GatewayClient { + parent: Arc::downgrade(user), + connection, + session_id: identify.token, + last_sequence: 0, + }; + Ok(NewConnection { + user: user.clone(), + client, + }) + } else { + log::debug!(target: "symfonia::gateway::establish_connection", "User with ID {id} not connected yet. Creating new user and client"); + let user = Arc::new(Mutex::new(GatewayUser { + clients: Vec::new(), + id, + subscriptions: Vec::new(), // TODO: Subscribe to relevant publishers + })); + let client = GatewayClient { + parent: Arc::downgrade(&user), + connection, + session_id: identify.token, + last_sequence: 0, + }; + Ok(NewConnection { user, client }) + } } async fn resume_connection( connection: Connection, resume_message: GatewayResume, ) -> Result { - todo!() + todo!() // TODO Implement resuming connections } /// Adds the contents of a [NewConnection] struct to a `gateway_users` map in a "checked" manner. From ce49da272c1249ce4b21289e1bea8c9c48d7dae8 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 23 Aug 2024 14:09:23 +0200 Subject: [PATCH 060/162] Init config once in main.rs instead of in each module --- src/api/mod.rs | 8 +++++--- src/gateway/mod.rs | 1 + src/main.rs | 11 ++++++++++- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 092a24b..1cfb20e 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -28,11 +28,13 @@ use crate::{ mod middleware; mod routes; -pub async fn start_api(db: PgPool, publisher_map: SharedEventPublisherMap) -> Result<(), Error> { +pub async fn start_api( + db: PgPool, + publisher_map: SharedEventPublisherMap, + config: Config, +) -> Result<(), Error> { log::info!(target: "symfonia::api::cfg", "Loading configuration"); - let config = Config::init(&db).await?; - if config.sentry.enabled { let _guard = sentry::init(( "https://241c6fb08adb469da1bb82522b25c99f@sentry.quartzinc.space/3", diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index b9e8228..c093227 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -141,6 +141,7 @@ pub async fn start_gateway( stream, db.clone(), gateway_users.clone(), + config.clone(), )) .await { diff --git a/src/main.rs b/src/main.rs index c122414..c61c68e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -211,12 +211,21 @@ async fn main() { .expect("Failed to seed config"); } + let symfonia_config = crate::database::entities::Config::init(&db) + .await + .unwrap_or_default(); + let shared_publisher_map = Arc::new(RwLock::new(HashMap::new())); let mut tasks = [ - tokio::spawn(api::start_api(db.clone(), shared_publisher_map.clone())), + tokio::spawn(api::start_api( + db.clone(), + shared_publisher_map.clone(), + symfonia_config.clone(), + )), tokio::spawn(gateway::start_gateway( db.clone(), shared_publisher_map.clone(), + symfonia_config.clone(), )), ]; for task in tasks.iter_mut() { From aecfa7d7f43991c59f5a34e7d627fe040e45c7b7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 24 Aug 2024 18:31:41 +0200 Subject: [PATCH 061/162] log host:port of gateway and api server --- src/api/mod.rs | 12 +++++++++++- src/gateway/mod.rs | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 1cfb20e..ee82b9a 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -84,9 +84,19 @@ pub async fn start_api( .catch_all_error(custom_error); let bind = std::env::var("API_BIND").unwrap_or_else(|_| String::from("localhost:3001")); + let bind_clone = bind.clone(); log::info!(target: "symfonia::api", "Starting HTTP Server"); - Server::new(TcpListener::bind(bind)).run(v9_api).await?; + + tokio::task::spawn(async move { + Server::new(TcpListener::bind(bind_clone)) + .run(v9_api) + .await + .expect("Failed to start HTTP server"); + log::info!(target: "symfonia::api", "HTTP Server stopped"); + }); + + log::info!(target: "symfonia::api", "HTTP Server listening on {bind}"); Ok(()) } diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index c093227..6b26eeb 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -133,6 +133,8 @@ pub async fn start_gateway( let try_socket = TcpListener::bind(&bind).await; let listener = try_socket.expect("Failed to bind to address"); + info!(target: "symfonia::gateway", "Gateway server listening on port {bind}"); + let gateway_users: GatewayUsersStore = Arc::new(Mutex::new(BTreeMap::new())); let resumeable_clients: ResumableClientsStore = Arc::new(Mutex::new(BTreeMap::new())); tokio::task::spawn(async { purge_expired_disconnects(resumeable_clients) }); @@ -236,6 +238,7 @@ async fn establish_connection( // Check the token and retrieve the valid claims let claims = check_token(&db, token, &config.security.jwt_secret).await?; let id = claims.id; + // Check if the user is already connected. If so, add the new client to the existing user if let Some(user) = gateway_users_store.lock().unwrap().get(&id) { log::debug!(target: "symfonia::gateway::establish_connection", "User with ID {id} already connected. Adding new client to existing user"); let client = GatewayClient { From d3f8c44e2df322420e7f437db1503e93f5ad34ba Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 24 Aug 2024 18:59:11 +0200 Subject: [PATCH 062/162] Fix some pgsql queries --- src/database/entities/user.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index 668de39..4c78ad7 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -78,7 +78,7 @@ impl User { let user = Self { inner: chorus::types::User { username: username.to_string(), - discriminator: "0001".to_string(), + discriminator: "0000".to_string(), email: email.clone(), premium: cfg.defaults.user.premium.into(), premium_type: Some(cfg.defaults.user.premium_type.into()), @@ -92,19 +92,19 @@ impl User { }), fingerprints: fingerprint.unwrap_or_default(), rights: cfg.register.default_rights, - settings_index: user_settings.index.clone().into(), + settings_index: user_settings.index.clone(), extended_settings: sqlx::types::Json(Value::Object(Map::default())), settings: user_settings.clone(), ..Default::default() }; - sqlx::query("INSERT INTO users (id, username, email, data, fingerprints, discriminator, desktop, mobile, premium, premium_type, bot, bio, system, nsfw_allowed, mfa_enabled, created_at, verified, disabled, deleted, flags, public_flags, purchased_flags, premium_usage_flags, rights, extended_settings, settingsIndex) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, 0, ?, '', 0, ?, 0, NOW(), 0, 0, 0, ?, 0, 0, 0, ?, '{}', ?)") + sqlx::query("INSERT INTO users (id, username, email, data, fingerprints, discriminator, desktop, mobile, premium, premium_type, bot, bio, system, nsfw_allowed, mfa_enabled, created_at, verified, disabled, deleted, flags, public_flags, purchased_flags, premium_usage_flags, rights, extended_settings, settingsIndex) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 0, 0, $9, '', 0, $10, 0, NOW(), 0, 0, 0, $11, 0, 0, 0, $12, '{}', $13)") .bind(user.id) .bind(username) .bind(email) .bind(&user.data) .bind(&user.fingerprints) - .bind("0001") + .bind("0000") .bind(true) .bind(false) .bind(bot) @@ -124,7 +124,7 @@ impl User { } pub async fn get_by_id(db: &PgPool, id: Snowflake) -> Result, Error> { - sqlx::query_as("SELECT * FROM users WHERE id = ?") + sqlx::query_as("SELECT * FROM users WHERE id = $1") .bind(id) .fetch_optional(db) .await @@ -145,10 +145,10 @@ impl User { separated.push_unseparated(") "); if let Some(after) = after { - separated.push_unseparated("AND id > ? "); + separated.push_unseparated("AND id > $1 "); separated.push_bind_unseparated(after); } - separated.push_unseparated("LIMIT ?"); + separated.push_unseparated("LIMIT $2"); separated.push_bind_unseparated(limit); let query = query_builder.build(); @@ -168,7 +168,7 @@ impl User { user: &str, discrim: &str, ) -> Result, Error> { - sqlx::query_as("SELECT * FROM users WHERE username = ? AND discriminator = ?") + sqlx::query_as("SELECT * FROM users WHERE username = $1 AND discriminator = $2") .bind(user) .bind(discrim) .fetch_optional(db) @@ -181,7 +181,7 @@ impl User { email: &str, phone: &str, ) -> Result, Error> { - sqlx::query_as("SELECT * FROM users WHERE email = ? OR phone = ? LIMIT 1") + sqlx::query_as("SELECT * FROM users WHERE email = $1 OR phone = $2 LIMIT 1") .bind(email) .bind(phone) .fetch_optional(db) From 124e797e9d577776593092eb875d04f8b1236e77 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 24 Aug 2024 19:24:32 +0200 Subject: [PATCH 063/162] TEMPORARY allow(unused) --- src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main.rs b/src/main.rs index c61c68e..0496d01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,8 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +#![allow(unused)] // TODO: Remove, I just want to clean up my build output + use std::collections::HashMap; use std::sync::Arc; From 9115905d3f54eb3e2662bd66f00059376d4b2247 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 24 Aug 2024 19:25:20 +0200 Subject: [PATCH 064/162] Change some column types to match their actual/expected types --- migrations/20231007011757_users.sql | 3 +-- migrations/20231007011857_audit_logs.sql | 17 ++++++++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/migrations/20231007011757_users.sql b/migrations/20231007011757_users.sql index f89821a..ff122e2 100644 --- a/migrations/20231007011757_users.sql +++ b/migrations/20231007011757_users.sql @@ -1,7 +1,6 @@ create table if not exists users ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, username varchar(255) not null, discriminator varchar(255) not null, avatar varchar(255) null, diff --git a/migrations/20231007011857_audit_logs.sql b/migrations/20231007011857_audit_logs.sql index 07202a4..2fbcf12 100644 --- a/migrations/20231007011857_audit_logs.sql +++ b/migrations/20231007011857_audit_logs.sql @@ -1,14 +1,13 @@ create table if not exists audit_logs ( - id varchar(255) not null - primary key, - user_id varchar(255) null, - guild_id varchar(255) not null, - action_type int not null, - options text null, - changes text not null, - reason varchar(255) null, - target_id varchar(255) null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), + guild_id varchar(255) not null, + action_type int not null, + options text null, + changes text not null, + reason varchar(255) null, + target_id numeric(20, 0) null constraint chk_target_id_range check (target_id >= 0 AND target_id <= 18446744073709551615), constraint FK_3cd01cd3ae7aab010310d96ac8e foreign key (target_id) references users (id), constraint FK_bd2726fd31b35443f2245b93ba0 From d3073af7a055f3bdae0df136dcc57b087ef007e1 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 24 Aug 2024 22:43:47 +0200 Subject: [PATCH 065/162] Change varchar(255) to numeric(20, 0) where applicable Where varchar(255) was used to represent an unsigned 64-bit integer, it was replaced by a constrained numeric(20, 0) type. --- migrations/20231007011602_categories.sql | 5 +- migrations/20231007011617_client_release.sql | 5 +- migrations/20231007011705_embed_cache.sql | 5 +- migrations/20231007011719_rate_limits.sql | 7 ++- .../20231007011737_security_settings.sql | 7 ++- migrations/20231007011751_sticker_packs.sql | 9 ++-- migrations/20231007011857_audit_logs.sql | 2 +- migrations/20231007011910_backup_codes.sql | 5 +- migrations/20231007011933_channels.sql | 11 ++--- .../20231007011946_connected_accounts.sql | 2 +- migrations/20231007011956_guilds.sql | 19 ++++---- migrations/20231007012027_bans.sql | 11 ++--- migrations/20231007012108_emojis.sql | 21 ++++----- migrations/20231007012121_invites.sql | 11 ++--- migrations/20231007012133_members.sql | 6 +-- migrations/20231007012149_notes.sql | 7 ++- migrations/20231007012204_read_states.sql | 10 ++-- migrations/20231007012219_recipients.sql | 9 ++-- migrations/20231007012235_relationships.sql | 9 ++-- migrations/20231007012255_roles.sql | 13 +++-- migrations/20231007012306_member_roles.sql | 4 +- migrations/20231007012332_security_keys.sql | 9 ++-- migrations/20231007012343_sessions.sql | 9 ++-- migrations/20231007012359_stickers.sql | 11 ++--- migrations/20231007012439_teams.sql | 7 ++- migrations/20231007012456_applications.sql | 13 +++-- migrations/20231007012505_team_members.sql | 9 ++-- migrations/20231007012517_templates.sql | 9 ++-- migrations/20231007012555_voice_states.sql | 13 +++-- migrations/20231007012609_webhooks.sql | 15 +++--- migrations/20231007012620_messages.sql | 47 +++++++++---------- migrations/20231007012640_attachments.sql | 13 +++-- ...0231007012656_message_channel_mentions.sql | 6 +-- .../20231007012735_message_role_mentions.sql | 6 +-- .../20231007012751_message_stickers.sql | 6 +-- .../20231007012803_message_user_mentions.sql | 6 +-- .../20240605191045_guild_scheduled_events.sql | 35 +++++++------- migrations/20240605192122_stage_instances.sql | 17 ++++--- ...40607211359_read_states_add_message_fk.sql | 2 +- .../20240619142920_channel_followers.sql | 5 +- 40 files changed, 193 insertions(+), 223 deletions(-) diff --git a/migrations/20231007011602_categories.sql b/migrations/20231007011602_categories.sql index f5e3988..2edf4b6 100644 --- a/migrations/20231007011602_categories.sql +++ b/migrations/20231007011602_categories.sql @@ -1,8 +1,7 @@ create table if not exists categories ( - id int not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, name varchar(255) null, localizations text not null, is_primary smallint null -); \ No newline at end of file +); diff --git a/migrations/20231007011617_client_release.sql b/migrations/20231007011617_client_release.sql index 5cbe38b..76adae7 100644 --- a/migrations/20231007011617_client_release.sql +++ b/migrations/20231007011617_client_release.sql @@ -1,11 +1,10 @@ create table if not exists client_release ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, name varchar(255) not null, pub_date timestamp not null, url varchar(255) not null, platform varchar(255) not null, enabled smallint not null, notes varchar(255) null -); \ No newline at end of file +); diff --git a/migrations/20231007011705_embed_cache.sql b/migrations/20231007011705_embed_cache.sql index 5677863..cffcb6a 100644 --- a/migrations/20231007011705_embed_cache.sql +++ b/migrations/20231007011705_embed_cache.sql @@ -1,7 +1,6 @@ create table if not exists embed_cache ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, url varchar(255) not null, embed text not null -); \ No newline at end of file +); diff --git a/migrations/20231007011719_rate_limits.sql b/migrations/20231007011719_rate_limits.sql index 9e7b068..917be48 100644 --- a/migrations/20231007011719_rate_limits.sql +++ b/migrations/20231007011719_rate_limits.sql @@ -1,9 +1,8 @@ create table if not exists rate_limits ( - id varchar(255) not null - primary key, - executor_id varchar(255) not null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + executor_id numeric(20, 0) not null constraint chk_executor_id_range check (executor_id >= 0 AND executor_id <= 18446744073709551615), hits int not null, blocked smallint not null, expires_at timestamp not null -); \ No newline at end of file +); diff --git a/migrations/20231007011737_security_settings.sql b/migrations/20231007011737_security_settings.sql index eec883a..638a064 100644 --- a/migrations/20231007011737_security_settings.sql +++ b/migrations/20231007011737_security_settings.sql @@ -1,9 +1,8 @@ create table if not exists security_settings ( - id varchar(255) not null - primary key, - guild_id varchar(255) null, - channel_id varchar(255) null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + guild_id numeric(20, 0) null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), + channel_id numeric(20, 0) null constraint chk_channel_id_range check (channel_id >= 0 AND channel_id <= 18446744073709551615), encryption_permission_mask int not null, allowed_algorithms text not null, current_algorithm varchar(255) not null, diff --git a/migrations/20231007011751_sticker_packs.sql b/migrations/20231007011751_sticker_packs.sql index 89dc41e..6f29e9f 100644 --- a/migrations/20231007011751_sticker_packs.sql +++ b/migrations/20231007011751_sticker_packs.sql @@ -1,10 +1,9 @@ create table if not exists sticker_packs ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, name varchar(255) not null, description varchar(255) null, - banner_asset_id varchar(255) null, - cover_sticker_id varchar(255) null, - coverStickerId varchar(255) null + banner_asset_id numeric(20, 0) null constraint chk_banner_asset_id_range check (banner_asset_id >= 0 AND banner_asset_id <= 18446744073709551615), + cover_sticker_id numeric(20, 0) null constraint chk_cover_sticker_id_range check (cover_sticker_id >= 0 AND cover_sticker_id <= 18446744073709551615), + coverStickerId numeric(20, 0) null constraint chk_coverStickerId_range check (coverStickerId >= 0 AND coverStickerId <= 18446744073709551615) ); \ No newline at end of file diff --git a/migrations/20231007011857_audit_logs.sql b/migrations/20231007011857_audit_logs.sql index 2fbcf12..42a634f 100644 --- a/migrations/20231007011857_audit_logs.sql +++ b/migrations/20231007011857_audit_logs.sql @@ -2,7 +2,7 @@ create table if not exists audit_logs ( id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), - guild_id varchar(255) not null, + guild_id numeric(20, 0) not null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), action_type int not null, options text null, changes text not null, diff --git a/migrations/20231007011910_backup_codes.sql b/migrations/20231007011910_backup_codes.sql index 885c13b..5c100d1 100644 --- a/migrations/20231007011910_backup_codes.sql +++ b/migrations/20231007011910_backup_codes.sql @@ -1,11 +1,10 @@ create table if not exists backup_codes ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, code varchar(255) not null, consumed smallint not null, expired smallint not null, - user_id varchar(255) null, + user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), constraint FK_70066ea80d2f4b871beda32633b foreign key (user_id) references users (id) on delete cascade diff --git a/migrations/20231007011933_channels.sql b/migrations/20231007011933_channels.sql index 25eda10..19b3346 100644 --- a/migrations/20231007011933_channels.sql +++ b/migrations/20231007011933_channels.sql @@ -1,15 +1,14 @@ create table if not exists channels ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, created_at timestamp not null, name varchar(255) null, icon text null, type int not null, - last_message_id varchar(255) null, - guild_id varchar(255) null, - parent_id varchar(255) null, - owner_id varchar(255) null, + last_message_id numeric(20, 0) null constraint chk_last_message_id_range check (last_message_id >= 0 AND last_message_id <= 18446744073709551615), + guild_id numeric(20, 0) null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), + parent_id numeric(20, 0) null constraint chk_parent_id check (parent_id >= 0 AND parent_id <= 18446744073709551615), + owner_id numeric(20, 0) null constraint chk_owner_id_range check (owner_id >= 0 AND owner_id <= 18446744073709551615), last_pin_timestamp int null, default_auto_archive_duration int null, position int null, diff --git a/migrations/20231007011946_connected_accounts.sql b/migrations/20231007011946_connected_accounts.sql index 2f90a7b..8130d6b 100644 --- a/migrations/20231007011946_connected_accounts.sql +++ b/migrations/20231007011946_connected_accounts.sql @@ -3,7 +3,7 @@ create table if not exists connected_accounts id varchar(255) not null primary key, external_id varchar(255) not null, - user_id varchar(255) null, + user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), friend_sync smallint not null, name varchar(255) not null, revoked smallint not null, diff --git a/migrations/20231007011956_guilds.sql b/migrations/20231007011956_guilds.sql index e402044..84d6a61 100644 --- a/migrations/20231007011956_guilds.sql +++ b/migrations/20231007011956_guilds.sql @@ -1,8 +1,7 @@ create table if not exists guilds ( - id varchar(255) not null - primary key, - afk_channel_id varchar(255) null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + afk_channel_id numeric(20, 0) null constraint chk_afk_channel_range check (afk_channel_id >= 0 AND afk_channel_id <= 18446744073709551615), afk_timeout int null, banner varchar(255) null, default_message_notifications int null, @@ -18,27 +17,27 @@ create table if not exists guilds max_video_channel_users int null, member_count int null, presence_count int null, - template_id varchar(255) null, + template_id numeric(20, 0) null constraint chk_template_range check (template_id >= 0 AND template_id <= 18446744073709551615), mfa_level int null, name varchar(255) not null, - owner_id varchar(255) null, + owner_id numeric(20, 0) null constraint chk_owner_id_range check (owner_id >= 0 AND owner_id <= 18446744073709551615), preferred_locale varchar(255) null, premium_subscription_count int null, premium_tier int not null, - public_updates_channel_id varchar(255) null, - rules_channel_id varchar(255) null, + public_updates_channel_id numeric(20, 0) null constraint check_pub_upd_channel_id check (public_updates_channel_id >= 0 AND public_updates_channel_id <= 18446744073709551615), + rules_channel_id numeric(20, 0) null constraint chk_rules_channel_id check (rules_channel_id >= 0 AND rules_channel_id <= 18446744073709551615), region varchar(255) null, splash varchar(255) null, - system_channel_id varchar(255) null, + system_channel_id numeric(20, 0) null constraint chk_system_channel_id check (system_channel_id >= 0 AND system_channel_id <= 18446744073709551615), system_channel_flags int null, unavailable smallint not null, verification_level int null, welcome_screen text not null, - widget_channel_id varchar(255) null, + widget_channel_id numeric(20, 0) null constraint chk_widget_channel_id check (widget_channel_id >= 0 AND widget_channel_id <= 18446744073709551615), widget_enabled smallint not null, nsfw_level int null, nsfw smallint not null, - parent varchar(255) null, + parent numeric(20, 0) null constraint chk_parent_id check (parent >= 0 AND parent <= 18446744073709551615), premium_progress_bar_enabled smallint null, constraint FK_8d450b016dc8bec35f36729e4b0 foreign key (public_updates_channel_id) references channels (id), diff --git a/migrations/20231007012027_bans.sql b/migrations/20231007012027_bans.sql index 5b98857..63d6f0b 100644 --- a/migrations/20231007012027_bans.sql +++ b/migrations/20231007012027_bans.sql @@ -1,10 +1,9 @@ create table if not exists bans ( - id varchar(255) not null - primary key, - user_id varchar(255) null, - guild_id varchar(255) null, - executor_id varchar(255) null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), + guild_id numeric(20, 0) null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), + executor_id numeric(20, 0) null constraint chk_executor_id check (executor_id >= 0 AND executor_id <= 18446744073709551615), ip varchar(255) not null, reason varchar(255) null, constraint FK_07ad88c86d1f290d46748410d58 @@ -15,4 +14,4 @@ create table if not exists bans constraint FK_9d3ab7dd180ebdd245cdb66ecad foreign key (guild_id) references guilds (id) on delete cascade -); \ No newline at end of file +); \ No newline at end of file diff --git a/migrations/20231007012108_emojis.sql b/migrations/20231007012108_emojis.sql index f3ef9fc..5db1d59 100644 --- a/migrations/20231007012108_emojis.sql +++ b/migrations/20231007012108_emojis.sql @@ -1,16 +1,15 @@ create table if not exists emojis ( - id varchar(255) not null - primary key, - animated smallint not null, - available smallint not null, - guild_id varchar(255) not null, - user_id varchar(255) null, - managed smallint not null, - name varchar(255) not null, - require_colons smallint not null, - roles text not null, - groups text null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + animated smallint not null, + available smallint not null, + guild_id numeric(20, 0) not null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), + user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), + managed smallint not null, + name varchar(255) not null, + require_colons smallint not null, + roles text not null, + groups text null, constraint FK_4b988e0db89d94cebcf07f598cc foreign key (guild_id) references guilds (id) on delete cascade, diff --git a/migrations/20231007012121_invites.sql b/migrations/20231007012121_invites.sql index 04a9804..698097f 100644 --- a/migrations/20231007012121_invites.sql +++ b/migrations/20231007012121_invites.sql @@ -1,7 +1,6 @@ create table if not exists invites ( - code varchar(255) not null - primary key, + code varchar(255) not null primary key, type smallint not null, temporary smallint not null, uses int not null, @@ -9,10 +8,10 @@ create table if not exists invites max_age int not null, created_at timestamp not null, expires_at timestamp null, - guild_id varchar(255) null, - channel_id varchar(255) null, - inviter_id varchar(255) null, - target_user_id varchar(255) null, + guild_id numeric(20, 0) null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), + channel_id numeric(20, 0) null constraint chk_channel_id_range check (channel_id >= 0 AND channel_id <= 18446744073709551615), + inviter_id numeric(20, 0) null constraint chk_inviter_id check (inviter_id >= 0 AND inviter_id <= 18446744073709551615), + target_user_id numeric(20, 0) null constraint chk_target_user_id check (target_user_id >= 0 AND target_user_id <= 18446744073709551615), target_user_type int null, vanity_url smallint null, flags int not null, diff --git a/migrations/20231007012133_members.sql b/migrations/20231007012133_members.sql index cd4db6b..0d2ca7b 100644 --- a/migrations/20231007012133_members.sql +++ b/migrations/20231007012133_members.sql @@ -3,8 +3,8 @@ CREATE SEQUENCE members_index_seq; create table if not exists members ( index numeric(20, 0) not null default nextval('members_index_seq') constraint chk_index_range check (index >= 0 and index <= 18446744073709551615) primary key, - id varchar(255) not null, - guild_id varchar(255) not null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615), + guild_id numeric(20, 0) not null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), nick varchar(255) null, joined_at timestamp not null, premium_since bigint null, @@ -12,7 +12,7 @@ create table if not exists members mute smallint not null, pending smallint not null, settings text not null, - last_message_id varchar(255) null, + last_message_id numeric(20, 0) null constraint chk_last_message_id_range check (last_message_id >= 0 AND last_message_id <= 18446744073709551615), joined_by varchar(255) null, avatar varchar(255) null, banner varchar(255) null, diff --git a/migrations/20231007012149_notes.sql b/migrations/20231007012149_notes.sql index a072dcc..7bd2d9e 100644 --- a/migrations/20231007012149_notes.sql +++ b/migrations/20231007012149_notes.sql @@ -1,10 +1,9 @@ create table if not exists notes ( - id varchar(255) not null - primary key, + id varchar(255) not null primary key, content varchar(255) not null, - owner_id varchar(255) null, - target_id varchar(255) null, + owner_id numeric(20, 0) null constraint chk_owner_id_range check (owner_id >= 0 AND owner_id <= 18446744073709551615), + target_id numeric(20, 0) null constraint chk_target_id_range check (target_id >= 0 AND target_id <= 18446744073709551615), constraint IDX_74e6689b9568cc965b8bfc9150 unique (owner_id, target_id), constraint FK_23e08e5b4481711d573e1abecdc diff --git a/migrations/20231007012204_read_states.sql b/migrations/20231007012204_read_states.sql index 9d70207..d7a2b1a 100644 --- a/migrations/20231007012204_read_states.sql +++ b/migrations/20231007012204_read_states.sql @@ -1,10 +1,10 @@ create table if not exists read_states ( - channel_id varchar(255) not null, - user_id varchar(255) not null, - last_message_id varchar(255) null, + channel_id numeric(20, 0) not null constraint chk_channel_id_range check (channel_id >= 0 AND channel_id <= 18446744073709551615), + user_id numeric(20, 0) not null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), + last_message_id numeric(20, 0) null constraint chk_last_message_id_range check (last_message_id >= 0 AND last_message_id <= 18446744073709551615), public_ack varchar(255) null, - notifications_cursor varchar(255) null, + notifications_cursor numeric(20, 0) null constraint chk_notifications_cursor_range check (notifications_cursor >= 0 AND notifications_cursor <= 18446744073709551615), last_pin_timestamp timestamp null, mention_count int null, constraint read_states_channel_id_user_id_uindex @@ -15,4 +15,4 @@ create table if not exists read_states constraint read_states_channels_id_fk foreign key (channel_id) references channels (id) on delete cascade -); \ No newline at end of file +); diff --git a/migrations/20231007012219_recipients.sql b/migrations/20231007012219_recipients.sql index eed4d3e..62b1b4c 100644 --- a/migrations/20231007012219_recipients.sql +++ b/migrations/20231007012219_recipients.sql @@ -1,9 +1,8 @@ create table if not exists recipients ( - id varchar(255) not null - primary key, - channel_id varchar(255) not null, - user_id varchar(255) not null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + channel_id numeric(20, 0) not null constraint chk_channel_id_range check (channel_id >= 0 AND channel_id <= 18446744073709551615), + user_id numeric(20, 0) not null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), closed smallint default 0 not null, constraint FK_2f18ee1ba667f233ae86c0ea60e foreign key (channel_id) references channels (id) @@ -11,4 +10,4 @@ create table if not exists recipients constraint FK_6157e8b6ba4e6e3089616481fe2 foreign key (user_id) references users (id) on delete cascade -); \ No newline at end of file +); diff --git a/migrations/20231007012235_relationships.sql b/migrations/20231007012235_relationships.sql index 373bc9a..5a163a9 100644 --- a/migrations/20231007012235_relationships.sql +++ b/migrations/20231007012235_relationships.sql @@ -1,9 +1,8 @@ create table if not exists relationships ( - id varchar(255) not null - primary key, - from_id varchar(255) not null, - to_id varchar(255) not null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + from_id numeric(20, 0) not null constraint chk_from_id_range check (from_id >= 0 AND from_id <= 18446744073709551615), + to_id numeric(20, 0) not null constraint chk_to_id_range check (to_id >= 0 AND to_id <= 18446744073709551615), nickname varchar(255) null, type int not null, constraint IDX_a0b2ff0a598df0b0d055934a17 @@ -14,4 +13,4 @@ create table if not exists relationships constraint FK_9c7f6b98a9843b76dce1b0c878b foreign key (to_id) references users (id) on delete cascade -); \ No newline at end of file +); diff --git a/migrations/20231007012255_roles.sql b/migrations/20231007012255_roles.sql index 8d44d9e..ab0b219 100644 --- a/migrations/20231007012255_roles.sql +++ b/migrations/20231007012255_roles.sql @@ -1,12 +1,11 @@ create table if not exists roles ( - id varchar(255) not null - primary key, - guild_id varchar(255) not null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + guild_id numeric(20, 0) not null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), color int not null, - hoist smallint not null, - managed smallint not null, - mentionable smallint not null, + hoist smallint not null, + managed smallint not null, + mentionable smallint not null, name varchar(255) not null, permissions varchar(255) not null, position int not null, @@ -17,4 +16,4 @@ create table if not exists roles constraint FK_c32c1ab1c4dc7dcb0278c4b1b8b foreign key (guild_id) references guilds (id) on delete cascade -); \ No newline at end of file +); diff --git a/migrations/20231007012306_member_roles.sql b/migrations/20231007012306_member_roles.sql index 4d49416..d7841f9 100644 --- a/migrations/20231007012306_member_roles.sql +++ b/migrations/20231007012306_member_roles.sql @@ -3,7 +3,7 @@ CREATE SEQUENCE member_roles_index_seq; create table if not exists member_roles ( index numeric(20, 0) not null default nextval('member_roles_index_seq') constraint chk_index_range check (index >= 0 and index <= 18446744073709551615) unique, - role_id varchar(255) not null, + role_id numeric(20, 0) not null constraint chk_role_id_range check (role_id >= 0 AND role_id <= 18446744073709551615), primary key (index, role_id), constraint FK_5d7ddc8a5f9c167f548625e772e foreign key (index) references members (index) @@ -16,4 +16,4 @@ create table if not exists member_roles create index if not exists IDX_e9080e7a7997a0170026d5139c on member_roles (role_id); -ALTER SEQUENCE member_roles_index_seq OWNED BY member_roles.index; \ No newline at end of file +ALTER SEQUENCE member_roles_index_seq OWNED BY member_roles.index; diff --git a/migrations/20231007012332_security_keys.sql b/migrations/20231007012332_security_keys.sql index 4337a27..726796f 100644 --- a/migrations/20231007012332_security_keys.sql +++ b/migrations/20231007012332_security_keys.sql @@ -1,13 +1,12 @@ create table if not exists security_keys ( - id varchar(255) not null - primary key, - user_id varchar(255) null, - key_id varchar(255) not null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), + key_id numeric(20, 0) not null constraint chk_key_id_range check (key_id >= 0 AND key_id <= 18446744073709551615), public_key varchar(255) not null, counter int not null, name varchar(255) not null, constraint FK_24c97d0771cafedce6d7163eaad foreign key (user_id) references users (id) on delete cascade -); \ No newline at end of file +); diff --git a/migrations/20231007012343_sessions.sql b/migrations/20231007012343_sessions.sql index 8dcf765..b0262f3 100644 --- a/migrations/20231007012343_sessions.sql +++ b/migrations/20231007012343_sessions.sql @@ -1,13 +1,12 @@ create table if not exists sessions ( - id varchar(255) not null - primary key, - user_id varchar(255) null, - session_id varchar(255) not null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), + session_id numeric(20, 0) not null constraint chk_session_id_range check (session_id >= 0 AND session_id <= 18446744073709551615), activities text null, client_info text not null, status varchar(255) not null, constraint FK_085d540d9f418cfbdc7bd55bb19 foreign key (user_id) references users (id) on delete cascade -); \ No newline at end of file +); diff --git a/migrations/20231007012359_stickers.sql b/migrations/20231007012359_stickers.sql index 8cc2cfb..8774bff 100644 --- a/migrations/20231007012359_stickers.sql +++ b/migrations/20231007012359_stickers.sql @@ -1,14 +1,13 @@ create table if not exists stickers ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, name varchar(255) not null, description varchar(255) null, available smallint null, tags varchar(255) null, - pack_id varchar(255) null, - guild_id varchar(255) null, - user_id varchar(255) null, + pack_id numeric(20, 0) null constraint chk_pack_id_range check (pack_id >= 0 AND pack_id <= 18446744073709551615), + guild_id numeric(20, 0) null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), + user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), type int not null, format_type int not null, constraint FK_193d551d852aca5347ef5c9f205 @@ -24,4 +23,4 @@ create table if not exists stickers alter table sticker_packs add constraint FK_448fafba4355ee1c837bbc865f1 - foreign key (coverStickerId) references stickers (id); \ No newline at end of file + foreign key (coverStickerId) references stickers (id); diff --git a/migrations/20231007012439_teams.sql b/migrations/20231007012439_teams.sql index 980cc42..f2da761 100644 --- a/migrations/20231007012439_teams.sql +++ b/migrations/20231007012439_teams.sql @@ -1,10 +1,9 @@ create table if not exists teams ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, icon varchar(255) null, name varchar(255) not null, - owner_user_id varchar(255) null, + owner_user_id numeric(20, 0) null constraint chk_owner_user_id_range check (owner_user_id >= 0 AND owner_user_id <= 18446744073709551615), constraint FK_13f00abf7cb6096c43ecaf8c108 foreign key (owner_user_id) references users (id) -); \ No newline at end of file +); diff --git a/migrations/20231007012456_applications.sql b/migrations/20231007012456_applications.sql index 292c5c6..8206bbe 100644 --- a/migrations/20231007012456_applications.sql +++ b/migrations/20231007012456_applications.sql @@ -1,7 +1,6 @@ create table if not exists applications ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, name varchar(255) not null, icon varchar(255) null, description varchar(255) null, @@ -10,7 +9,7 @@ create table if not exists applications hook smallint not null, bot_public smallint not null, bot_require_code_grant smallint not null, - verify_key varchar(255) not null, + verify_key numeric(20, 0) not null constraint chk_verify_key_range check (verify_key >= 0 AND verify_key <= 18446744073709551615), flags int not null, redirect_uris text null, rpc_application_state int null, @@ -26,9 +25,9 @@ create table if not exists applications install_params text null, terms_of_service_url varchar(255) null, privacy_policy_url varchar(255) null, - owner_id varchar(255) null, - bot_user_id varchar(255) null, - team_id varchar(255) null, + owner_id numeric(20, 0) null constraint chk_owner_id_range check (owner_id >= 0 AND owner_id <= 18446744073709551615), + bot_user_id numeric(20, 0) null constraint chk_bot_user_id_range check (bot_user_id >= 0 AND bot_user_id <= 18446744073709551615), + team_id numeric(20, 0) null constraint chk_team_id_range check (team_id >= 0 AND team_id <= 18446744073709551615), constraint REL_2ce5a55796fe4c2f77ece57a64 unique (bot_user_id), constraint FK_2ce5a55796fe4c2f77ece57a647 @@ -40,4 +39,4 @@ create table if not exists applications constraint FK_e57508958bf92b9d9d25231b5e8 foreign key (owner_id) references users (id) on delete cascade -); \ No newline at end of file +); diff --git a/migrations/20231007012505_team_members.sql b/migrations/20231007012505_team_members.sql index 578808c..ce16ecf 100644 --- a/migrations/20231007012505_team_members.sql +++ b/migrations/20231007012505_team_members.sql @@ -1,15 +1,14 @@ create table if not exists team_members ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, membership_state int not null, permissions text not null, - team_id varchar(255) null, - user_id varchar(255) null, + team_id numeric(20, 0) null constraint chk_team_id_range check (team_id >= 0 AND team_id <= 18446744073709551615), + user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), constraint FK_c2bf4967c8c2a6b845dadfbf3d4 foreign key (user_id) references users (id) on delete cascade, constraint FK_fdad7d5768277e60c40e01cdcea foreign key (team_id) references teams (id) on delete cascade -); \ No newline at end of file +); diff --git a/migrations/20231007012517_templates.sql b/migrations/20231007012517_templates.sql index 5eeb787..9032c42 100644 --- a/migrations/20231007012517_templates.sql +++ b/migrations/20231007012517_templates.sql @@ -1,15 +1,14 @@ create table if not exists templates ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, code varchar(255) not null, name varchar(255) not null, description varchar(255) null, usage_count int null, - creator_id varchar(255) null, + creator_id numeric(20, 0) null constraint chk_creator_id_range check (creator_id >= 0 AND creator_id <= 18446744073709551615), created_at timestamp not null, updated_at timestamp not null, - source_guild_id varchar(255) null, + source_guild_id numeric(20, 0) null constraint chk_source_guild_id_range check (source_guild_id >= 0 AND source_guild_id <= 18446744073709551615), serialized_source_guild text not null, constraint IDX_be38737bf339baf63b1daeffb5 unique (code), @@ -22,4 +21,4 @@ create table if not exists templates alter table guilds add constraint FK_e2a2f873a64a5cf62526de42325 - foreign key (template_id) references templates (id); \ No newline at end of file + foreign key (template_id) references templates (id); diff --git a/migrations/20231007012555_voice_states.sql b/migrations/20231007012555_voice_states.sql index 4240e8b..6756d3c 100644 --- a/migrations/20231007012555_voice_states.sql +++ b/migrations/20231007012555_voice_states.sql @@ -1,11 +1,10 @@ create table if not exists voice_states ( - id varchar(255) not null - primary key, - guild_id varchar(255) null, - channel_id varchar(255) null, - user_id varchar(255) null, - session_id varchar(255) not null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + guild_id numeric(20, 0) null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), + channel_id numeric(20, 0) null constraint chk_channel_id_range check (channel_id >= 0 AND channel_id <= 18446744073709551615), + user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), + session_id numeric(20, 0) not null constraint chk_session_id_range check (session_id >= 0 AND session_id <= 18446744073709551615), token varchar(255) null, deaf smallint not null, mute smallint not null, @@ -24,4 +23,4 @@ create table if not exists voice_states constraint FK_9f8d389866b40b6657edd026dd4 foreign key (channel_id) references channels (id) on delete cascade -); \ No newline at end of file +); diff --git a/migrations/20231007012609_webhooks.sql b/migrations/20231007012609_webhooks.sql index 7f54c36..6026c35 100644 --- a/migrations/20231007012609_webhooks.sql +++ b/migrations/20231007012609_webhooks.sql @@ -1,16 +1,15 @@ create table if not exists webhooks ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, type int not null, name varchar(255) null, avatar varchar(255) null, token varchar(255) null, - guild_id varchar(255) null, - channel_id varchar(255) null, - application_id varchar(255) null, - user_id varchar(255) null, - source_guild_id varchar(255) null, + guild_id numeric(20, 0) null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), + channel_id numeric(20, 0) null constraint chk_channel_id_range check (channel_id >= 0 AND channel_id <= 18446744073709551615), + application_id numeric(20, 0) null constraint chk_application_id_range check (application_id >= 0 AND application_id <= 18446744073709551615), + user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), + source_guild_id numeric(20, 0) null constraint chk_source_guild_id_range check (source_guild_id >= 0 AND source_guild_id <= 18446744073709551615), constraint FK_0d523f6f997c86e052c49b1455f foreign key (user_id) references users (id) on delete cascade, @@ -26,4 +25,4 @@ create table if not exists webhooks constraint FK_df528cf77e82f8032230e7e37d8 foreign key (channel_id) references channels (id) on delete cascade -); \ No newline at end of file +); diff --git a/migrations/20231007012620_messages.sql b/migrations/20231007012620_messages.sql index 3b4ab63..fa276e6 100644 --- a/migrations/20231007012620_messages.sql +++ b/migrations/20231007012620_messages.sql @@ -1,29 +1,28 @@ create table if not exists messages ( - id varchar(255) not null - primary key, - channel_id varchar(255) null, - guild_id varchar(255) null, - author_id varchar(255) null, - member_id varchar(255) null, - webhook_id varchar(255) null, - application_id varchar(255) null, - content varchar(255) null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + channel_id numeric(20, 0) null constraint chk_channel_id_range check (channel_id >= 0 AND channel_id <= 18446744073709551615), + guild_id numeric(20, 0) null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), + author_id numeric(20, 0) null constraint chk_author_id_range check (author_id >= 0 AND author_id <= 18446744073709551615), + member_id numeric(20, 0) null constraint chk_member_id_range check (member_id >= 0 AND member_id <= 18446744073709551615), + webhook_id numeric(20, 0) null constraint chk_webhook_id_range check (webhook_id >= 0 AND webhook_id <= 18446744073709551615), + application_id numeric(20, 0) null constraint chk_application_id_range check (application_id >= 0 AND application_id <= 18446744073709551615), + content varchar(255) null, timestamp timestamp(6) default current_timestamp(6) not null, - edited_timestamp timestamp null, - tts smallint null, - mention_everyone smallint null, - embeds text not null, - reactions text not null, - nonce text null, - pinned smallint null, - type int not null, - activity text null, - flags int null, - message_reference text null, - interaction text null, - components text null, - message_reference_id varchar(255) null, + edited_timestamp timestamp null, + tts smallint null, + mention_everyone smallint null, + embeds text not null, + reactions text not null, + nonce text null, + pinned smallint null, + type int not null, + activity text null, + flags int null, + message_reference text null, + interaction text null, + components text null, + message_reference_id numeric(20, 0) null constraint chk_message_reference_id_range check (message_reference_id >= 0 AND message_reference_id <= 18446744073709551615), constraint IDX_3ed7a60fb7dbe04e1ba9332a8b unique (channel_id, id), constraint FK_05535bc695e9f7ee104616459d3 @@ -44,4 +43,4 @@ create table if not exists messages on delete cascade, constraint FK_f83c04bcf1df4e5c0e7a52ed348 foreign key (webhook_id) references webhooks (id) -); \ No newline at end of file +); diff --git a/migrations/20231007012640_attachments.sql b/migrations/20231007012640_attachments.sql index f95b8dd..bd77658 100644 --- a/migrations/20231007012640_attachments.sql +++ b/migrations/20231007012640_attachments.sql @@ -1,16 +1,15 @@ create table if not exists attachments ( - id varchar(255) not null - primary key, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, filename varchar(255) not null, - size int not null, + size int not null, url varchar(255) not null, proxy_url varchar(255) not null, - height int null, - width int null, + height int null, + width int null, content_type varchar(255) null, - message_id varchar(255) null, + message_id numeric(20, 0) null constraint chk_message_id_range check (message_id >= 0 AND message_id <= 18446744073709551615), constraint FK_623e10eec51ada466c5038979e3 foreign key (message_id) references messages (id) on delete cascade -); \ No newline at end of file +); diff --git a/migrations/20231007012656_message_channel_mentions.sql b/migrations/20231007012656_message_channel_mentions.sql index c83d5e5..105d7e8 100644 --- a/migrations/20231007012656_message_channel_mentions.sql +++ b/migrations/20231007012656_message_channel_mentions.sql @@ -1,7 +1,7 @@ create table if not exists message_channel_mentions ( - messagesId varchar(255) not null, - channelsId varchar(255) not null, + messagesId numeric(20, 0) not null constraint chk_messages_id_range check (messagesId >= 0 AND messagesId <= 18446744073709551615), + channelsId numeric(20, 0) not null constraint chk_channels_id_range check (channelsId >= 0 AND channelsId <= 18446744073709551615), primary key (messagesId, channelsId), constraint FK_2a27102ecd1d81b4582a4360921 foreign key (messagesId) references messages (id) @@ -15,4 +15,4 @@ create index if not exists IDX_2a27102ecd1d81b4582a436092 on message_channel_mentions (messagesId); create index if not exists IDX_bdb8c09e1464cabf62105bf4b9 - on message_channel_mentions (channelsId); \ No newline at end of file + on message_channel_mentions (channelsId); diff --git a/migrations/20231007012735_message_role_mentions.sql b/migrations/20231007012735_message_role_mentions.sql index 990ce9f..057d45e 100644 --- a/migrations/20231007012735_message_role_mentions.sql +++ b/migrations/20231007012735_message_role_mentions.sql @@ -1,7 +1,7 @@ create table if not exists message_role_mentions ( - messagesId varchar(255) not null, - rolesId varchar(255) not null, + messagesId numeric(20, 0) not null constraint chk_messages_id_range check (messagesId >= 0 AND messagesId <= 18446744073709551615), + rolesId numeric(20, 0) not null constraint chk_roles_id_range check (rolesId >= 0 AND rolesId <= 18446744073709551615), primary key (messagesId, rolesId), constraint FK_29d63eb1a458200851bc37d074b foreign key (rolesId) references roles (id) @@ -15,4 +15,4 @@ create index if not exists IDX_29d63eb1a458200851bc37d074 on message_role_mentions (rolesId); create index if not exists IDX_a8242cf535337a490b0feaea0b - on message_role_mentions (messagesId); \ No newline at end of file + on message_role_mentions (messagesId); diff --git a/migrations/20231007012751_message_stickers.sql b/migrations/20231007012751_message_stickers.sql index 4ff4f9c..9b4ee73 100644 --- a/migrations/20231007012751_message_stickers.sql +++ b/migrations/20231007012751_message_stickers.sql @@ -1,7 +1,7 @@ create table if not exists message_stickers ( - messagesId varchar(255) not null, - stickersId varchar(255) not null, + messagesId numeric(20, 0) not null constraint chk_messages_id_range check (messagesId >= 0 AND messagesId <= 18446744073709551615), + stickersId numeric(20, 0) not null constraint chk_stickers_id_range check (stickersId >= 0 AND stickersId <= 18446744073709551615), primary key (messagesId, stickersId), constraint FK_40bb6f23e7cc133292e92829d28 foreign key (messagesId) references messages (id) @@ -15,4 +15,4 @@ create index if not exists IDX_40bb6f23e7cc133292e92829d2 on message_stickers (messagesId); create index if not exists IDX_e22a70819d07659c7a71c112a1 - on message_stickers (stickersId); \ No newline at end of file + on message_stickers (stickersId); diff --git a/migrations/20231007012803_message_user_mentions.sql b/migrations/20231007012803_message_user_mentions.sql index 7ef9f41..99c7e78 100644 --- a/migrations/20231007012803_message_user_mentions.sql +++ b/migrations/20231007012803_message_user_mentions.sql @@ -1,7 +1,7 @@ create table if not exists message_user_mentions ( - messagesId varchar(255) not null, - usersId varchar(255) not null, + messagesId numeric(20, 0) not null constraint chk_messages_id_range check (messagesId >= 0 AND messagesId <= 18446744073709551615), + usersId numeric(20, 0) not null constraint chk_users_id_range check (usersId >= 0 AND usersId <= 18446744073709551615), primary key (messagesId, usersId), constraint FK_a343387fc560ef378760681c236 foreign key (messagesId) references messages (id) @@ -21,4 +21,4 @@ create index if not exists IDX_05535bc695e9f7ee104616459d on messages (author_id); create index if not exists IDX_86b9109b155eb70c0a2ca3b4b6 - on messages (channel_id); \ No newline at end of file + on messages (channel_id); diff --git a/migrations/20240605191045_guild_scheduled_events.sql b/migrations/20240605191045_guild_scheduled_events.sql index 1bfc038..4f06268 100644 --- a/migrations/20240605191045_guild_scheduled_events.sql +++ b/migrations/20240605191045_guild_scheduled_events.sql @@ -1,21 +1,20 @@ -create table guild_scheduled_events +create table if not exists guild_scheduled_events ( - id varchar(255) not null - primary key, - guild_id varchar(255) not null, - channel_id varchar(255) null, - creator_id varchar(255) null, - name varchar(100) null, - description text null, - scheduled_start_time timestamp default CURRENT_TIMESTAMP not null, - scheduled_end_time timestamp null, - privacy_level int not null, - status int not null, - entity_type int not null, - entity_id varchar(255) null, - location varchar(100) null, - user_count int default 0 not null, - image text null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + guild_id numeric(20, 0) not null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), + channel_id numeric(20, 0) null constraint chk_channel_id_range check (channel_id >= 0 AND channel_id <= 18446744073709551615), + creator_id numeric(20, 0) null constraint chk_creator_id_range check (creator_id >= 0 AND creator_id <= 18446744073709551615), + name varchar(100) null, + description text null, + scheduled_start_time timestamp default CURRENT_TIMESTAMP not null, + scheduled_end_time timestamp null, + privacy_level int not null, + status int not null, + entity_type int not null, + entity_id numeric(20, 0) null constraint chk_entity_id_range check (entity_id >= 0 AND entity_id <= 18446744073709551615), + location varchar(100) null, + user_count int default 0 not null, + image text null, constraint guild_scheduled_event_channels_id_fk foreign key (channel_id) references channels (id), constraint guild_scheduled_event_guilds_id_fk @@ -35,4 +34,4 @@ $$ LANGUAGE plpgsql; CREATE TRIGGER update_scheduled_start_time_trigger BEFORE UPDATE ON guild_scheduled_events FOR EACH ROW -EXECUTE FUNCTION update_scheduled_start_time(); \ No newline at end of file +EXECUTE FUNCTION update_scheduled_start_time(); diff --git a/migrations/20240605192122_stage_instances.sql b/migrations/20240605192122_stage_instances.sql index 77853fa..630acca 100644 --- a/migrations/20240605192122_stage_instances.sql +++ b/migrations/20240605192122_stage_instances.sql @@ -1,13 +1,13 @@ create table if not exists stage_instances ( - id int not null primary key, - guild_id varchar(255) not null, - channel_id varchar(255) not null, - topic varchar(120) not null, - privacy_level int not null, - invite_code varchar(16) null, - discoverable_disabled smallint not null default 0, - guild_scheduled_event_id varchar(255) null, + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + guild_id numeric(20, 0) not null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), + channel_id numeric(20, 0) not null constraint chk_channel_id_range check (channel_id >= 0 AND channel_id <= 18446744073709551615), + topic varchar(120) not null, + privacy_level int not null, + invite_code varchar(16) null, + discoverable_disabled smallint not null default 0, + guild_scheduled_event_id numeric(20, 0) null constraint chk_guild_scheduled_event_id_range check (guild_scheduled_event_id >= 0 AND guild_scheduled_event_id <= 18446744073709551615), constraint stage_instances_channels_id_fk foreign key (channel_id) references channels (id) on delete cascade, @@ -17,4 +17,3 @@ create table if not exists stage_instances constraint stage_instances_guild_scheduled_events_id_fk foreign key (guild_scheduled_event_id) references guild_scheduled_events (id) ); - diff --git a/migrations/20240607211359_read_states_add_message_fk.sql b/migrations/20240607211359_read_states_add_message_fk.sql index 660d3d8..ef5906c 100644 --- a/migrations/20240607211359_read_states_add_message_fk.sql +++ b/migrations/20240607211359_read_states_add_message_fk.sql @@ -5,4 +5,4 @@ alter table read_states alter table read_states add constraint read_states_users_id_fk_2 foreign key (notifications_cursor) references messages (id) - on delete cascade; \ No newline at end of file + on delete cascade; diff --git a/migrations/20240619142920_channel_followers.sql b/migrations/20240619142920_channel_followers.sql index 8e4defc..0a48f20 100644 --- a/migrations/20240619142920_channel_followers.sql +++ b/migrations/20240619142920_channel_followers.sql @@ -1,7 +1,7 @@ create table channel_followers ( - webhook_id varchar(255) not null, - channel_id varchar(255) not null, + webhook_id numeric(20, 0) not null constraint chk_webhook_id_range check (webhook_id >= 0 AND webhook_id <= 18446744073709551615), + channel_id numeric(20, 0) not null constraint chk_channel_id_range check (channel_id >= 0 AND channel_id <= 18446744073709551615), primary key (channel_id, webhook_id), constraint channel_followers_channels_id_fk foreign key (channel_id) references channels (id) @@ -10,4 +10,3 @@ create table channel_followers foreign key (webhook_id) references webhooks (id) on delete cascade ); - From f76f39c44978d7a1b71248c00b7ec01aa93719f5 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 00:03:43 +0200 Subject: [PATCH 066/162] Change smallints to be boolean where expected --- migrations/20231007011602_categories.sql | 2 +- migrations/20231007011617_client_release.sql | 2 +- migrations/20231007011719_rate_limits.sql | 10 +-- migrations/20231007011756_user_settings.sql | 38 +++++------ migrations/20231007011757_users.sql | 68 ++++++++----------- migrations/20231007011910_backup_codes.sql | 4 +- migrations/20231007011933_channels.sql | 2 +- .../20231007011946_connected_accounts.sql | 8 +-- migrations/20231007011956_guilds.sql | 10 +-- migrations/20231007012108_emojis.sql | 8 +-- migrations/20231007012121_invites.sql | 6 +- migrations/20231007012133_members.sql | 6 +- migrations/20231007012219_recipients.sql | 2 +- migrations/20231007012255_roles.sql | 6 +- migrations/20231007012359_stickers.sql | 2 +- migrations/20231007012456_applications.sql | 10 +-- migrations/20231007012555_voice_states.sql | 14 ++-- migrations/20231007012620_messages.sql | 6 +- migrations/20240605192122_stage_instances.sql | 2 +- 19 files changed, 98 insertions(+), 108 deletions(-) diff --git a/migrations/20231007011602_categories.sql b/migrations/20231007011602_categories.sql index 2edf4b6..41fd88e 100644 --- a/migrations/20231007011602_categories.sql +++ b/migrations/20231007011602_categories.sql @@ -3,5 +3,5 @@ create table if not exists categories id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, name varchar(255) null, localizations text not null, - is_primary smallint null + is_primary boolean null ); diff --git a/migrations/20231007011617_client_release.sql b/migrations/20231007011617_client_release.sql index 76adae7..7997d33 100644 --- a/migrations/20231007011617_client_release.sql +++ b/migrations/20231007011617_client_release.sql @@ -5,6 +5,6 @@ create table if not exists client_release pub_date timestamp not null, url varchar(255) not null, platform varchar(255) not null, - enabled smallint not null, + enabled boolean not null, notes varchar(255) null ); diff --git a/migrations/20231007011719_rate_limits.sql b/migrations/20231007011719_rate_limits.sql index 917be48..861df46 100644 --- a/migrations/20231007011719_rate_limits.sql +++ b/migrations/20231007011719_rate_limits.sql @@ -1,8 +1,8 @@ create table if not exists rate_limits ( - id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, - executor_id numeric(20, 0) not null constraint chk_executor_id_range check (executor_id >= 0 AND executor_id <= 18446744073709551615), - hits int not null, - blocked smallint not null, - expires_at timestamp not null + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + executor_id numeric(20, 0) not null constraint chk_executor_id_range check (executor_id >= 0 AND executor_id <= 18446744073709551615), + hits int not null, + blocked boolean not null, + expires_at timestamp not null ); diff --git a/migrations/20231007011756_user_settings.sql b/migrations/20231007011756_user_settings.sql index 0dfc85b..43e0bff 100644 --- a/migrations/20231007011756_user_settings.sql +++ b/migrations/20231007011756_user_settings.sql @@ -4,34 +4,34 @@ CREATE TABLE IF NOT EXISTS user_settings ( index numeric(20, 0) not null default nextval('user_settings_index_seq') constraint chk_index_range check (index >= 0 and index <= 18446744073709551615) primary key, afk_timeout int null, - allow_accessibility_detection smallint null, - animate_emoji smallint null, + allow_accessibility_detection boolean null, + animate_emoji boolean null, animate_stickers int null, - contact_sync_enabled smallint null, - convert_emoticons smallint null, + contact_sync_enabled boolean null, + convert_emoticons boolean null, custom_status text null, - default_guilds_restricted smallint null, - detect_platform_accounts smallint null, - developer_mode smallint null, - disable_games_tab smallint null, - enable_tts_command smallint null, + default_guilds_restricted boolean null, + detect_platform_accounts boolean null, + developer_mode boolean null, + disable_games_tab boolean null, + enable_tts_command boolean null, explicit_content_filter int null, friend_source_flags text null, - gateway_connected smallint null, - gif_auto_play smallint null, + gateway_connected boolean null, + gif_auto_play boolean null, guild_folders text null, guild_positions text null, - inline_attachment_media smallint null, - inline_embed_media smallint null, + inline_attachment_media boolean null, + inline_embed_media boolean null, locale varchar(255) null, - message_display_compact smallint null, - native_phone_integration_enabled smallint null, - render_embeds smallint null, - render_reactions smallint null, + message_display_compact boolean null, + native_phone_integration_enabled boolean null, + render_embeds boolean null, + render_reactions boolean null, restricted_guilds text null, - show_current_game smallint null, + show_current_game boolean null, status varchar(255) null, - stream_notifications_enabled smallint null, + stream_notifications_enabled boolean null, theme varchar(255) null, timezone_offset int null ); diff --git a/migrations/20231007011757_users.sql b/migrations/20231007011757_users.sql index ff122e2..5fdc21f 100644 --- a/migrations/20231007011757_users.sql +++ b/migrations/20231007011757_users.sql @@ -1,44 +1,34 @@ create table if not exists users ( - id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, - username varchar(255) not null, - discriminator varchar(255) not null, - avatar varchar(255) null, - accent_color int null, - banner varchar(255) null, - theme_colors text null, - pronouns varchar(255) null, - phone varchar(255) null, - desktop smallint not null, - mobile smallint not null, - premium smallint not null, - premium_type numeric(5, 0) not null constraint chk_smallint_unsigned check (premium_type >= 0 and premium_type <= 65535), - bot smallint not null, - bio varchar(255) not null, - system smallint not null, - nsfw_allowed smallint not null, - mfa_enabled smallint not null, - webauthn_enabled smallint default 0 not null, - totp_secret varchar(255) null, - totp_last_ticket varchar(255) null, - created_at timestamp not null, - premium_since timestamp null, - verified smallint not null, - disabled smallint not null, - deleted smallint not null, - email varchar(255) null, - flags numeric(20, 0) not null constraint chk_flags_range check (flags >= 0 AND flags <= 18446744073709551615), - public_flags numeric(10, 0) not null constraint chk_int_unsigned check (public_flags >= 0 and public_flags <= 4294967295), - purchased_flags int not null, - premium_usage_flags int not null, - rights bigint not null, - data text not null, - fingerprints text not null, - extended_settings text not null, - settingsIndex numeric(20, 0) null constraint chk_settingsIndex_range check (settingsIndex >= 0 AND settingsIndex <= 18446744073709551615), - constraint users_settingsIndex_uindex - unique (settingsIndex), + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + username varchar(255) not null, + discriminator varchar(255) default 0000 not null, + desktop boolean default false not null, + mobile boolean default false not null, + premium boolean not null, + premium_type numeric(5, 0) not null constraint chk_premium_type_unsigned check (premium_type >= 0 and premium_type <= 65535), + bot boolean default false not null, + bio varchar(255) not null, + system boolean default false not null, + nsfw_allowed boolean default false not null, + mfa_enabled boolean default false not null, + webauthn_enabled boolean default false not null, + created_at timestamp not null, + verified boolean default false not null, + disabled boolean default false not null, + deleted boolean default false not null, + flags numeric(20, 0) not null constraint chk_flags_range check (flags >= 0 AND flags <= 18446744073709551615), + public_flags numeric(10, 0) not null constraint chk_int_unsigned check (public_flags >= 0 and public_flags <= 4294967295), + purchased_flags int not null, + premium_usage_flags int not null, + rights bigint not null, + data json not null, + fingerprints text not null, + extended_settings text not null, + settings_index numeric(20, 0) not null constraint chk_index_range check (settings_index >= 0 and settings_index <= 18446744073709551615), + constraint users_settings_index_uindex + unique (settings_index), constraint users_user_settings_index_fk - foreign key (settingsIndex) references user_settings (index) + foreign key (settings_index) references user_settings (index) ); diff --git a/migrations/20231007011910_backup_codes.sql b/migrations/20231007011910_backup_codes.sql index 5c100d1..c02d9fa 100644 --- a/migrations/20231007011910_backup_codes.sql +++ b/migrations/20231007011910_backup_codes.sql @@ -2,8 +2,8 @@ create table if not exists backup_codes ( id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, code varchar(255) not null, - consumed smallint not null, - expired smallint not null, + consumed boolean not null, + expired boolean not null, user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), constraint FK_70066ea80d2f4b871beda32633b foreign key (user_id) references users (id) diff --git a/migrations/20231007011933_channels.sql b/migrations/20231007011933_channels.sql index 19b3346..1f9aeb3 100644 --- a/migrations/20231007011933_channels.sql +++ b/migrations/20231007011933_channels.sql @@ -16,7 +16,7 @@ create table if not exists channels video_quality_mode int null, bitrate int null, user_limit int null, - nsfw smallint not null, + nsfw boolean not null, rate_limit_per_user int null, topic varchar(255) null, retention_policy_id varchar(255) null, diff --git a/migrations/20231007011946_connected_accounts.sql b/migrations/20231007011946_connected_accounts.sql index 8130d6b..6fa08dd 100644 --- a/migrations/20231007011946_connected_accounts.sql +++ b/migrations/20231007011946_connected_accounts.sql @@ -4,17 +4,17 @@ create table if not exists connected_accounts primary key, external_id varchar(255) not null, user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), - friend_sync smallint not null, + friend_sync boolean not null, name varchar(255) not null, - revoked smallint not null, + revoked boolean not null, show_activity int not null, type varchar(255) not null, - verified smallint not null, + verified boolean not null, visibility int not null, integrations text not null, metadata text null, metadata_visibility int not null, - two_way_link smallint not null, + two_way_link boolean not null, token_data text null, constraint FK_f47244225a6a1eac04a3463dd90 foreign key (user_id) references users (id) diff --git a/migrations/20231007011956_guilds.sql b/migrations/20231007011956_guilds.sql index 84d6a61..cf320c4 100644 --- a/migrations/20231007011956_guilds.sql +++ b/migrations/20231007011956_guilds.sql @@ -11,7 +11,7 @@ create table if not exists guilds features text not null, primary_category_id varchar(255) null, icon varchar(255) null, - large smallint not null, + large boolean not null, max_members int null, max_presences int null, max_video_channel_users int null, @@ -30,15 +30,15 @@ create table if not exists guilds splash varchar(255) null, system_channel_id numeric(20, 0) null constraint chk_system_channel_id check (system_channel_id >= 0 AND system_channel_id <= 18446744073709551615), system_channel_flags int null, - unavailable smallint not null, + unavailable boolean not null, verification_level int null, welcome_screen text not null, widget_channel_id numeric(20, 0) null constraint chk_widget_channel_id check (widget_channel_id >= 0 AND widget_channel_id <= 18446744073709551615), - widget_enabled smallint not null, + widget_enabled boolean not null, nsfw_level int null, - nsfw smallint not null, + nsfw boolean not null, parent numeric(20, 0) null constraint chk_parent_id check (parent >= 0 AND parent <= 18446744073709551615), - premium_progress_bar_enabled smallint null, + premium_progress_bar_enabled boolean null, constraint FK_8d450b016dc8bec35f36729e4b0 foreign key (public_updates_channel_id) references channels (id), constraint FK_95828668aa333460582e0ca6396 diff --git a/migrations/20231007012108_emojis.sql b/migrations/20231007012108_emojis.sql index 5db1d59..5c8448a 100644 --- a/migrations/20231007012108_emojis.sql +++ b/migrations/20231007012108_emojis.sql @@ -1,13 +1,13 @@ create table if not exists emojis ( id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, - animated smallint not null, - available smallint not null, + animated boolean not null, + available boolean not null, guild_id numeric(20, 0) not null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), - managed smallint not null, + managed boolean not null, name varchar(255) not null, - require_colons smallint not null, + require_colons boolean not null, roles text not null, groups text null, constraint FK_4b988e0db89d94cebcf07f598cc diff --git a/migrations/20231007012121_invites.sql b/migrations/20231007012121_invites.sql index 698097f..e4595ca 100644 --- a/migrations/20231007012121_invites.sql +++ b/migrations/20231007012121_invites.sql @@ -1,8 +1,8 @@ create table if not exists invites ( code varchar(255) not null primary key, - type smallint not null, - temporary smallint not null, + type boolean not null, + temporary boolean not null, uses int not null, max_uses int not null, max_age int not null, @@ -13,7 +13,7 @@ create table if not exists invites inviter_id numeric(20, 0) null constraint chk_inviter_id check (inviter_id >= 0 AND inviter_id <= 18446744073709551615), target_user_id numeric(20, 0) null constraint chk_target_user_id check (target_user_id >= 0 AND target_user_id <= 18446744073709551615), target_user_type int null, - vanity_url smallint null, + vanity_url boolean null, flags int not null, constraint FK_11a0d394f8fc649c19ce5f16b59 foreign key (target_user_id) references users (id) diff --git a/migrations/20231007012133_members.sql b/migrations/20231007012133_members.sql index 0d2ca7b..514d151 100644 --- a/migrations/20231007012133_members.sql +++ b/migrations/20231007012133_members.sql @@ -8,9 +8,9 @@ create table if not exists members nick varchar(255) null, joined_at timestamp not null, premium_since bigint null, - deaf smallint not null, - mute smallint not null, - pending smallint not null, + deaf boolean not null, + mute boolean not null, + pending boolean not null, settings text not null, last_message_id numeric(20, 0) null constraint chk_last_message_id_range check (last_message_id >= 0 AND last_message_id <= 18446744073709551615), joined_by varchar(255) null, diff --git a/migrations/20231007012219_recipients.sql b/migrations/20231007012219_recipients.sql index 62b1b4c..c37e28f 100644 --- a/migrations/20231007012219_recipients.sql +++ b/migrations/20231007012219_recipients.sql @@ -3,7 +3,7 @@ create table if not exists recipients id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, channel_id numeric(20, 0) not null constraint chk_channel_id_range check (channel_id >= 0 AND channel_id <= 18446744073709551615), user_id numeric(20, 0) not null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), - closed smallint default 0 not null, + closed boolean default false not null, constraint FK_2f18ee1ba667f233ae86c0ea60e foreign key (channel_id) references channels (id) on delete cascade, diff --git a/migrations/20231007012255_roles.sql b/migrations/20231007012255_roles.sql index ab0b219..b2225fa 100644 --- a/migrations/20231007012255_roles.sql +++ b/migrations/20231007012255_roles.sql @@ -3,9 +3,9 @@ create table if not exists roles id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, guild_id numeric(20, 0) not null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), color int not null, - hoist smallint not null, - managed smallint not null, - mentionable smallint not null, + hoist boolean not null, + managed boolean not null, + mentionable boolean not null, name varchar(255) not null, permissions varchar(255) not null, position int not null, diff --git a/migrations/20231007012359_stickers.sql b/migrations/20231007012359_stickers.sql index 8774bff..e0ddc11 100644 --- a/migrations/20231007012359_stickers.sql +++ b/migrations/20231007012359_stickers.sql @@ -3,7 +3,7 @@ create table if not exists stickers id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, name varchar(255) not null, description varchar(255) null, - available smallint null, + available boolean null, tags varchar(255) null, pack_id numeric(20, 0) null constraint chk_pack_id_range check (pack_id >= 0 AND pack_id <= 18446744073709551615), guild_id numeric(20, 0) null constraint chk_guild_id_range check (guild_id >= 0 AND guild_id <= 18446744073709551615), diff --git a/migrations/20231007012456_applications.sql b/migrations/20231007012456_applications.sql index 8206bbe..34c452b 100644 --- a/migrations/20231007012456_applications.sql +++ b/migrations/20231007012456_applications.sql @@ -6,9 +6,9 @@ create table if not exists applications description varchar(255) null, summary varchar(255) null, type text null, - hook smallint not null, - bot_public smallint not null, - bot_require_code_grant smallint not null, + hook boolean not null, + bot_public boolean not null, + bot_require_code_grant boolean not null, verify_key numeric(20, 0) not null constraint chk_verify_key_range check (verify_key >= 0 AND verify_key <= 18446744073709551615), flags int not null, redirect_uris text null, @@ -16,8 +16,8 @@ create table if not exists applications store_application_state int null, verification_state int null, interactions_endpoint_url varchar(255) null, - integration_public smallint null, - integration_require_code_grant smallint null, + integration_public boolean null, + integration_require_code_grant boolean null, discoverability_state int null, discovery_eligibility_flags int null, tags text null, diff --git a/migrations/20231007012555_voice_states.sql b/migrations/20231007012555_voice_states.sql index 6756d3c..5b6a3a6 100644 --- a/migrations/20231007012555_voice_states.sql +++ b/migrations/20231007012555_voice_states.sql @@ -6,13 +6,13 @@ create table if not exists voice_states user_id numeric(20, 0) null constraint chk_user_id_range check (user_id >= 0 AND user_id <= 18446744073709551615), session_id numeric(20, 0) not null constraint chk_session_id_range check (session_id >= 0 AND session_id <= 18446744073709551615), token varchar(255) null, - deaf smallint not null, - mute smallint not null, - self_deaf smallint not null, - self_mute smallint not null, - self_stream smallint null, - self_video smallint not null, - suppress smallint not null, + deaf boolean not null, + mute boolean not null, + self_deaf boolean not null, + self_mute boolean not null, + self_stream boolean null, + self_video boolean not null, + suppress boolean not null, request_to_speak_timestamp timestamp null, constraint FK_03779ef216d4b0358470d9cb748 foreign key (guild_id) references guilds (id) diff --git a/migrations/20231007012620_messages.sql b/migrations/20231007012620_messages.sql index fa276e6..471284a 100644 --- a/migrations/20231007012620_messages.sql +++ b/migrations/20231007012620_messages.sql @@ -10,12 +10,12 @@ create table if not exists messages content varchar(255) null, timestamp timestamp(6) default current_timestamp(6) not null, edited_timestamp timestamp null, - tts smallint null, - mention_everyone smallint null, + tts boolean null, + mention_everyone boolean null, embeds text not null, reactions text not null, nonce text null, - pinned smallint null, + pinned boolean null, type int not null, activity text null, flags int null, diff --git a/migrations/20240605192122_stage_instances.sql b/migrations/20240605192122_stage_instances.sql index 630acca..549da35 100644 --- a/migrations/20240605192122_stage_instances.sql +++ b/migrations/20240605192122_stage_instances.sql @@ -6,7 +6,7 @@ create table if not exists stage_instances topic varchar(120) not null, privacy_level int not null, invite_code varchar(16) null, - discoverable_disabled smallint not null default 0, + discoverable_disabled boolean not null default false, guild_scheduled_event_id numeric(20, 0) null constraint chk_guild_scheduled_event_id_range check (guild_scheduled_event_id >= 0 AND guild_scheduled_event_id <= 18446744073709551615), constraint stage_instances_channels_id_fk foreign key (channel_id) references channels (id) From 3f52fb0eabdde776313b02bad83ec552926c2bdf Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 17:23:09 +0200 Subject: [PATCH 067/162] Restore accidentally deleted users table --- migrations/20231007011757_users.sql | 72 ++++++++++++++++------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/migrations/20231007011757_users.sql b/migrations/20231007011757_users.sql index 5fdc21f..8843ee6 100644 --- a/migrations/20231007011757_users.sql +++ b/migrations/20231007011757_users.sql @@ -1,34 +1,40 @@ -create table if not exists users -( - id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, - username varchar(255) not null, - discriminator varchar(255) default 0000 not null, - desktop boolean default false not null, - mobile boolean default false not null, - premium boolean not null, - premium_type numeric(5, 0) not null constraint chk_premium_type_unsigned check (premium_type >= 0 and premium_type <= 65535), - bot boolean default false not null, - bio varchar(255) not null, - system boolean default false not null, - nsfw_allowed boolean default false not null, - mfa_enabled boolean default false not null, - webauthn_enabled boolean default false not null, - created_at timestamp not null, - verified boolean default false not null, - disabled boolean default false not null, - deleted boolean default false not null, - flags numeric(20, 0) not null constraint chk_flags_range check (flags >= 0 AND flags <= 18446744073709551615), - public_flags numeric(10, 0) not null constraint chk_int_unsigned check (public_flags >= 0 and public_flags <= 4294967295), - purchased_flags int not null, - premium_usage_flags int not null, - rights bigint not null, - data json not null, - fingerprints text not null, - extended_settings text not null, - settings_index numeric(20, 0) not null constraint chk_index_range check (settings_index >= 0 and settings_index <= 18446744073709551615), - constraint users_settings_index_uindex - unique (settings_index), - constraint users_user_settings_index_fk - foreign key (settings_index) references user_settings (index) +create table if not exists users ( + id numeric(20, 0) not null constraint chk_id_range check (id >= 0 AND id <= 18446744073709551615) primary key, + username varchar(255) not null, + discriminator varchar(255) not null, + avatar varchar(255) null, + accent_color int null, + banner varchar(255) null, + theme_colors text null, + pronouns varchar(255) null, + phone varchar(255) null, + desktop boolean not null default false, + mobile boolean not null default false, + premium boolean not null, + premium_type numeric(5, 0) not null constraint chk_smallint_unsigned check (premium_type >= 0 and premium_type <= 65535), + bot boolean not null default false, + bio varchar(255) not null, + system boolean not null default false, + nsfw_allowed boolean not null default false, + mfa_enabled boolean not null default false, + webauthn_enabled boolean not null default false, + totp_secret varchar(255) null, + totp_last_ticket varchar(255) null, + created_at timestamp not null, + premium_since timestamp null, + verified boolean not null default false, + disabled boolean not null default false, + deleted boolean not null default false, + email varchar(255) null, + flags numeric(20, 0) not null constraint chk_flags_range check (flags >= 0 AND flags <= 18446744073709551615), + public_flags numeric(10, 0) not null constraint chk_int_unsigned check (public_flags >= 0 and public_flags <= 4294967295), + purchased_flags int not null, + premium_usage_flags int not null, + rights bigint not null, + data text not null, + fingerprints text not null, + extended_settings text not null, + settingsIndex numeric(20, 0) null constraint chk_settingsIndex_range check (settingsIndex >= 0 AND settingsIndex <= 18446744073709551615), + constraint users_settingsIndex_uindex unique (settingsIndex), + constraint users_user_settings_index_fk foreign key (settingsIndex) references user_settings (index) ); - From 029e0cbed2a919964600c175aa285145c2ab97c7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 18:48:07 +0200 Subject: [PATCH 068/162] Change some types, other minor changes --- migrations/20231007011757_users.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/migrations/20231007011757_users.sql b/migrations/20231007011757_users.sql index 8843ee6..4f6b07e 100644 --- a/migrations/20231007011757_users.sql +++ b/migrations/20231007011757_users.sql @@ -13,7 +13,7 @@ create table if not exists users ( premium boolean not null, premium_type numeric(5, 0) not null constraint chk_smallint_unsigned check (premium_type >= 0 and premium_type <= 65535), bot boolean not null default false, - bio varchar(255) not null, + bio varchar(255) not null default '', system boolean not null default false, nsfw_allowed boolean not null default false, mfa_enabled boolean not null default false, @@ -30,11 +30,11 @@ create table if not exists users ( public_flags numeric(10, 0) not null constraint chk_int_unsigned check (public_flags >= 0 and public_flags <= 4294967295), purchased_flags int not null, premium_usage_flags int not null, - rights bigint not null, - data text not null, + rights numeric(20, 0) not null constraint chk_rights_range check (rights >= 0 AND rights <= 18446744073709551615), + data json not null, fingerprints text not null, extended_settings text not null, - settingsIndex numeric(20, 0) null constraint chk_settingsIndex_range check (settingsIndex >= 0 AND settingsIndex <= 18446744073709551615), - constraint users_settingsIndex_uindex unique (settingsIndex), - constraint users_user_settings_index_fk foreign key (settingsIndex) references user_settings (index) + settings_index numeric(20, 0) null constraint chk_settings_index_range check (settings_index >= 0 AND settings_index <= 18446744073709551615), + constraint users_settings_index_uindex unique (settings_index), + constraint users_user_settings_index_fk foreign key (settings_index) references user_settings (index) ); From d2ee651cf55dc2c23eec14de25479b406b86aa40 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 18:48:13 +0200 Subject: [PATCH 069/162] bump sqlx --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e024954..eefca58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ reqwest = { version = "0.12.5", default-features = false, features = [ ] } serde = { version = "1.0.203", features = ["derive"] } serde_json = { version = "1.0.117", features = ["raw_value"] } -sqlx = { version = "0.8.0", features = [ +sqlx = { version = "0.8.1", features = [ "json", "chrono", "ipnetwork", From 9c77e27e7036bd0a2f767a77812fae0990428a2a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 18:49:04 +0200 Subject: [PATCH 070/162] Fix: Get last_insert_id on PostgreSQL --- src/database/entities/user.rs | 33 ++++++++++++++------------ src/database/entities/user_settings.rs | 16 ++++++------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index 4c78ad7..ec54156 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -6,6 +6,7 @@ use super::*; +use std::str::FromStr; use std::{ default::Default, ops::{Deref, DerefMut}, @@ -14,7 +15,7 @@ use std::{ use chorus::types::{PublicUser, Rights, Snowflake, UserData}; use chrono::{NaiveDate, Utc}; use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value}; +use serde_json::{from_str, Map, Value}; use sqlx::{FromRow, PgPool, Row}; use sqlx_pg_uint::{PgU32, PgU64}; @@ -57,6 +58,7 @@ impl DerefMut for User { } impl User { + #[allow(clippy::too_many_arguments)] pub async fn create( db: &PgPool, cfg: &Config, @@ -98,20 +100,21 @@ impl User { ..Default::default() }; - sqlx::query("INSERT INTO users (id, username, email, data, fingerprints, discriminator, desktop, mobile, premium, premium_type, bot, bio, system, nsfw_allowed, mfa_enabled, created_at, verified, disabled, deleted, flags, public_flags, purchased_flags, premium_usage_flags, rights, extended_settings, settingsIndex) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 0, 0, $9, '', 0, $10, 0, NOW(), 0, 0, 0, $11, 0, 0, 0, $12, '{}', $13)") - .bind(user.id) - .bind(username) - .bind(email) - .bind(&user.data) - .bind(&user.fingerprints) - .bind("0000") - .bind(true) - .bind(false) - .bind(bot) - .bind(false) // TODO: Base nsfw off date of birth - .bind(PgU32::from(0)) // TODO: flags - .bind(Rights::default()) - .bind(PgU64::from(user.settings.index.clone())) + let data: Value = from_str(&user.data.encode_to_string()?)?; + let rights = PgU64::from(Rights::default().bits()) + .as_big_decimal() + .to_owned(); + + sqlx::query!("INSERT INTO users (id, username, discriminator, email, data, fingerprints, premium, premium_type, created_at, flags, public_flags, purchased_flags, premium_usage_flags, rights, extended_settings, settings_index) VALUES ($1, $2, $3, $4, $5, $6, false, 0, $7, 0, 0, 0, 0, $8, '{}', $9)", + bigdecimal::BigDecimal::from(user.id.to_string().parse::().unwrap()), + username, + "0000", + email, + data, + &user.fingerprints, + Utc::now().naive_local(), + Some(rights), + user.settings_index.clone().as_big_decimal().to_owned()) .execute(db) .await?; diff --git a/src/database/entities/user_settings.rs b/src/database/entities/user_settings.rs index 97d9fce..95a1f0d 100644 --- a/src/database/entities/user_settings.rs +++ b/src/database/entities/user_settings.rs @@ -7,7 +7,7 @@ use std::ops::{Deref, DerefMut}; use serde::{Deserialize, Serialize}; -use sqlx::PgPool; +use sqlx::{FromRow, PgPool}; use sqlx_pg_uint::PgU64; use crate::errors::Error; @@ -50,15 +50,15 @@ impl UserSettings { index: PgU64::from(0), }; - let res = sqlx::query("INSERT INTO user_settings (locale) VALUES (?)") + let res = sqlx::query("INSERT INTO user_settings (locale) VALUES ($1) RETURNING index") .bind(locale) - .execute(db) + .fetch_one(db) .await?; - - // todo!("settings.index = res.last_insert_id(); Does not work!"); - // TODO(bitfl0wer) Why do we even need the index? :thinking: - // settings.index = res.last_insert_id(); // FIXME: Does not exist for Postgres - + log::trace!(target: "symfonia::db::entities::user_settings::create", "Creation query yielded {:?}", res); + let index = PgU64::from_row(&res); + log::trace!(target: "symfonia::db::entities::user_settings::create", "Index is {:?}", index); + let index = index?; + settings.index = index; Ok(settings) } From 5dff19b799f94952fffb5f75bbd49afeac1d01e2 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 18:52:02 +0200 Subject: [PATCH 071/162] Nit: Change stale session log to be minutely instead of every 5s --- src/gateway/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 6b26eeb..af2e605 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -168,6 +168,7 @@ pub async fn start_gateway( /// seconds after a disconnect occurs. Sessions that can be resumed are stored in a `Map`. The /// purpose of this method is to periodically throw out expired sessions from that map. fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { + let mut minutely_log_timer = 0; loop { sleep(Duration::from_secs(5)); log::trace!(target: "symfonia::gateway::purge_expired_disconnects", "Removing stale disconnected sessions from list of resumeable sessions"); @@ -189,7 +190,11 @@ fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { lock.remove(session_id); } drop(lock); - log::trace!(target: "symfonia::gateway::purge_expired_disconnects", "Removed {} stale sessions", len); + minutely_log_timer += 1; + if minutely_log_timer % 60 == 0 { + log::info!(target: "symfonia::gateway::purge_expired_disconnects", "Removed {} stale sessions", len); + minutely_log_timer = 0; + } } } From 35f065e933227adf68b31088b8c6ce41a175710b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 18:53:42 +0200 Subject: [PATCH 072/162] Fix: oopid --- src/gateway/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index af2e605..7e9b876 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -191,7 +191,7 @@ fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { } drop(lock); minutely_log_timer += 1; - if minutely_log_timer % 60 == 0 { + if minutely_log_timer == 12 { log::info!(target: "symfonia::gateway::purge_expired_disconnects", "Removed {} stale sessions", len); minutely_log_timer = 0; } From 7c9ed37e4451425eaef37dc4cb6017c8c1f483e3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 19:00:41 +0200 Subject: [PATCH 073/162] i am procrastinating with this --- src/gateway/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 7e9b876..bb7d864 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -169,6 +169,7 @@ pub async fn start_gateway( /// purpose of this method is to periodically throw out expired sessions from that map. fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { let mut minutely_log_timer = 0; + let mut removed_elements_last_minute: u128 = 0; loop { sleep(Duration::from_secs(5)); log::trace!(target: "symfonia::gateway::purge_expired_disconnects", "Removing stale disconnected sessions from list of resumeable sessions"); @@ -186,14 +187,18 @@ fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { } } let len = to_remove.len(); + removed_elements_last_minute = removed_elements_last_minute + .checked_add(len as u128) + .unwrap_or(u128::MAX); for session_id in to_remove.iter() { lock.remove(session_id); } drop(lock); minutely_log_timer += 1; if minutely_log_timer == 12 { - log::info!(target: "symfonia::gateway::purge_expired_disconnects", "Removed {} stale sessions", len); + log::debug!(target: "symfonia::gateway::purge_expired_disconnects", "Removed {} stale sessions in the last 60 seconds", removed_elements_last_minute); minutely_log_timer = 0; + removed_elements_last_minute = 0; } } } From 50dfcc34db3b90137cd3cb2525ca045d57a858a0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 20:55:34 +0200 Subject: [PATCH 074/162] Fix: Get index from user_settings --- src/database/entities/user_settings.rs | 28 ++++++++++++++++++-------- src/errors.rs | 4 ++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/database/entities/user_settings.rs b/src/database/entities/user_settings.rs index 95a1f0d..fd71c0f 100644 --- a/src/database/entities/user_settings.rs +++ b/src/database/entities/user_settings.rs @@ -6,12 +6,24 @@ use std::ops::{Deref, DerefMut}; +use bigdecimal::BigDecimal; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, PgPool}; use sqlx_pg_uint::PgU64; use crate::errors::Error; +#[derive(Debug, Clone)] +struct PgU64Mapper { + inner: BigDecimal, +} + +impl PgU64Mapper { + fn into_pg_u64(self) -> Result { + PgU64::try_from(self.inner) + } +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, sqlx::FromRow)] pub struct UserSettings { #[sqlx(flatten)] @@ -50,14 +62,14 @@ impl UserSettings { index: PgU64::from(0), }; - let res = sqlx::query("INSERT INTO user_settings (locale) VALUES ($1) RETURNING index") - .bind(locale) - .fetch_one(db) - .await?; - log::trace!(target: "symfonia::db::entities::user_settings::create", "Creation query yielded {:?}", res); - let index = PgU64::from_row(&res); - log::trace!(target: "symfonia::db::entities::user_settings::create", "Index is {:?}", index); - let index = index?; + let res = sqlx::query_as!( + PgU64Mapper, + "INSERT INTO user_settings (locale) VALUES ($1) RETURNING index as inner", + locale + ) + .fetch_one(db) + .await?; + let index = res.into_pg_u64()?; settings.index = index; Ok(settings) } diff --git a/src/errors.rs b/src/errors.rs index aafb162..09de8a8 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -58,6 +58,9 @@ pub enum Error { #[error(transparent)] Gateway(#[from] GatewayError), + + #[error(transparent)] + SqlxPgUint(#[from] sqlx_pg_uint::Error), } #[derive(Debug, thiserror::Error)] @@ -234,6 +237,7 @@ impl ResponseError for Error { GatewayError::UnexpectedMessage => StatusCode::BAD_REQUEST, GatewayError::Timeout => StatusCode::BAD_REQUEST, }, + Error::SqlxPgUint(_) => StatusCode::BAD_REQUEST, } } From 4d24c38157f4c7f2ee2833b3adebeb1df80d74b3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 22:00:47 +0200 Subject: [PATCH 075/162] make extended_settings be json type, add relevant_events column (default: empty array) --- migrations/20231007011757_users.sql | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/migrations/20231007011757_users.sql b/migrations/20231007011757_users.sql index 4f6b07e..c562729 100644 --- a/migrations/20231007011757_users.sql +++ b/migrations/20231007011757_users.sql @@ -33,8 +33,9 @@ create table if not exists users ( rights numeric(20, 0) not null constraint chk_rights_range check (rights >= 0 AND rights <= 18446744073709551615), data json not null, fingerprints text not null, - extended_settings text not null, - settings_index numeric(20, 0) null constraint chk_settings_index_range check (settings_index >= 0 AND settings_index <= 18446744073709551615), + extended_settings json not null, + settings_index numeric(20, 0) null constraint chk_settings_index_range check (settings_index >= 0 AND settings_index <= 18446744073709551615), + relevant_events json not null default '[]', constraint users_settings_index_uindex unique (settings_index), constraint users_user_settings_index_fk foreign key (settings_index) references user_settings (index) ); From 6b9b18279e64ff37bef71948023ebfdbd64b290c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 22:00:59 +0200 Subject: [PATCH 076/162] bump sqlx-pg-uint --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index eefca58..f3dbffc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,6 @@ tokio-tungstenite = { version = "0.23.1", features = [ pubserve = { version = "1.1.0", features = ["async", "send"] } parking_lot = { version = "0.12.3", features = ["deadlock_detection"] } -sqlx-pg-uint = { version = "0.4.1", features = ["serde"] } +sqlx-pg-uint = { version = "0.5.0", features = ["serde"] } [dev-dependencies] rusty-hook = "0.11.2" From e2d2146758be27ca2d9f4995b3e7a21cb4f879d0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 22:01:13 +0200 Subject: [PATCH 077/162] remove column rename --- src/database/entities/user.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index ec54156..9dc7947 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -32,7 +32,6 @@ pub struct User { pub data: sqlx::types::Json, pub deleted: bool, pub fingerprints: String, // TODO: Simple-array, should actually be a vec - #[sqlx(rename = "settingsIndex")] pub settings_index: PgU64, pub rights: Rights, #[sqlx(skip)] From fbbda1260529e5c116939d081c724dafaf559035 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 22:05:11 +0200 Subject: [PATCH 078/162] firedbg init --- firedbg/target-unit-test.json | 6 ++++++ firedbg/target.json | 12 ++++++++++++ firedbg/version.toml | 1 + 3 files changed, 19 insertions(+) create mode 100644 firedbg/target-unit-test.json create mode 100644 firedbg/target.json create mode 100644 firedbg/version.toml diff --git a/firedbg/target-unit-test.json b/firedbg/target-unit-test.json new file mode 100644 index 0000000..636ddd0 --- /dev/null +++ b/firedbg/target-unit-test.json @@ -0,0 +1,6 @@ +{ + "binaries": [], + "examples": [], + "integration_tests": [], + "unit_tests": [] +} \ No newline at end of file diff --git a/firedbg/target.json b/firedbg/target.json new file mode 100644 index 0000000..01e5221 --- /dev/null +++ b/firedbg/target.json @@ -0,0 +1,12 @@ +{ + "binaries": [ + { + "name": "symfonia", + "src_path": "/home/star/Codespace/polyphony/symfonia/src/main.rs", + "required_features": [] + } + ], + "examples": [], + "integration_tests": [], + "unit_tests": [] +} \ No newline at end of file diff --git a/firedbg/version.toml b/firedbg/version.toml new file mode 100644 index 0000000..f0b26f9 --- /dev/null +++ b/firedbg/version.toml @@ -0,0 +1 @@ +firedbg_cli = "1.80.0" From ee9b2261d1c75ce0c02a53908ec19238d121c2d7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 22:12:41 +0200 Subject: [PATCH 079/162] exclude firedbg dir --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6895844..6dc62b0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ Cargo.lock .env /log -/.idea \ No newline at end of file +/.idea +/firedbg \ No newline at end of file From e2aeeb7739f0e07b03b20caffaa6d4d3dfd26c43 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 22:21:02 +0200 Subject: [PATCH 080/162] add todo message for myself --- src/gateway/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index bb7d864..a8a1060 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -127,6 +127,7 @@ pub async fn start_gateway( publisher_map: SharedEventPublisherMap, config: Config, ) -> Result<(), Error> { + // TODO(bitfl0wer): Add log messages throughout the method for debugging the gateway info!(target: "symfonia::gateway", "Starting gateway server"); let bind = std::env::var("GATEWAY_BIND").unwrap_or_else(|_| String::from("localhost:3003")); From b3c0a2a8ca91c2c3744fe355a766d6a6ad9e291a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 25 Aug 2024 23:48:47 +0200 Subject: [PATCH 081/162] Add and remove some logging lines --- src/gateway/mod.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index a8a1060..795e774 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -140,6 +140,7 @@ pub async fn start_gateway( let resumeable_clients: ResumableClientsStore = Arc::new(Mutex::new(BTreeMap::new())); tokio::task::spawn(async { purge_expired_disconnects(resumeable_clients) }); while let Ok((stream, _)) = listener.accept().await { + log::trace!(target: "symfonia::gateway", "New connection received"); let connection_result = match tokio::task::spawn(establish_connection( stream, db.clone(), @@ -173,7 +174,7 @@ fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { let mut removed_elements_last_minute: u128 = 0; loop { sleep(Duration::from_secs(5)); - log::trace!(target: "symfonia::gateway::purge_expired_disconnects", "Removing stale disconnected sessions from list of resumeable sessions"); + // log::trace!(target: "symfonia::gateway::purge_expired_disconnects", "Removing stale disconnected sessions from list of resumeable sessions"); let current_unix_timestamp = std::time::SystemTime::now() .duration_since(std::time::SystemTime::UNIX_EPOCH) .expect("Check the clock/time settings on the host machine") @@ -221,17 +222,19 @@ async fn establish_connection( .sender .send(Message::Text(json!(GatewayHello::default()).to_string())) .await?; - let raw_message = match connection.receiver.next().await { + let raw_message_result = match connection.receiver.next().await { Some(next) => next, None => return Err(GatewayError::Timeout.into()), - }?; + }; + log::trace!(target: "symfonia::gateway::establish_connection", "Received first message: {:?}", &raw_message_result); + let raw_message = raw_message_result?; if let Ok(resume_message) = from_str::(&raw_message.to_string()) { - log::debug!(target: "symfonia::gateway::establish_connection", "[{}] Received GatewayResume. Trying to resume gateway connection", &resume_message.session_id); + log::trace!(target: "symfonia::gateway::establish_connection", "[{}] Received GatewayResume. Trying to resume gateway connection", &resume_message.session_id); return resume_connection(connection, resume_message).await; } let _heartbeat_message = from_str::(&raw_message.to_string()) // TODO: Implement hearbeating and sequence handling .map_err(|_| GatewayError::UnexpectedMessage)?; - log::debug!(target: "symfonia::gateway::establish_connection", "Received GatewayHeartbeat. Continuing to build fresh gateway connection"); + log::trace!(target: "symfonia::gateway::establish_connection", "Received GatewayHeartbeat. Continuing to build fresh gateway connection"); let raw_identify = match connection.receiver.next().await { Some(next) => next, None => return Err(GatewayError::Timeout.into()), From 817ba0396d1ecc7aa337cd5f7adccf138214bf0f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 26 Aug 2024 10:45:26 +0200 Subject: [PATCH 082/162] Prepare sqlx query metadata --- ...5cbb8488020c74eed1bb8e68777dd12d551f9.json | 22 +++++++++++++++++++ ...3a347a745d2ea64ded9c7021c21cb56a2ee86.json | 22 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .sqlx/query-00eb44be0581509631734824a9d5cbb8488020c74eed1bb8e68777dd12d551f9.json create mode 100644 .sqlx/query-0d7b2cee7e790ce2f620a2def603a347a745d2ea64ded9c7021c21cb56a2ee86.json diff --git a/.sqlx/query-00eb44be0581509631734824a9d5cbb8488020c74eed1bb8e68777dd12d551f9.json b/.sqlx/query-00eb44be0581509631734824a9d5cbb8488020c74eed1bb8e68777dd12d551f9.json new file mode 100644 index 0000000..f36c175 --- /dev/null +++ b/.sqlx/query-00eb44be0581509631734824a9d5cbb8488020c74eed1bb8e68777dd12d551f9.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO users (id, username, discriminator, email, data, fingerprints, premium, premium_type, created_at, flags, public_flags, purchased_flags, premium_usage_flags, rights, extended_settings, settings_index) VALUES ($1, $2, $3, $4, $5, $6, false, 0, $7, 0, 0, 0, 0, $8, '{}', $9)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Numeric", + "Varchar", + "Varchar", + "Varchar", + "Json", + "Text", + "Timestamp", + "Numeric", + "Numeric" + ] + }, + "nullable": [] + }, + "hash": "00eb44be0581509631734824a9d5cbb8488020c74eed1bb8e68777dd12d551f9" +} diff --git a/.sqlx/query-0d7b2cee7e790ce2f620a2def603a347a745d2ea64ded9c7021c21cb56a2ee86.json b/.sqlx/query-0d7b2cee7e790ce2f620a2def603a347a745d2ea64ded9c7021c21cb56a2ee86.json new file mode 100644 index 0000000..11a74e1 --- /dev/null +++ b/.sqlx/query-0d7b2cee7e790ce2f620a2def603a347a745d2ea64ded9c7021c21cb56a2ee86.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_settings (locale) VALUES ($1) RETURNING index as inner", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "inner", + "type_info": "Numeric" + } + ], + "parameters": { + "Left": [ + "Varchar" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0d7b2cee7e790ce2f620a2def603a347a745d2ea64ded9c7021c21cb56a2ee86" +} From 09bbc8d03ab6271cc21a63466f541342e4a1f60d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 26 Aug 2024 10:50:18 +0200 Subject: [PATCH 083/162] force SQLX_OFFLINE when building Read: https://github.com/launchbadge/sqlx/blob/main/sqlx-cli/README.md#enable-building-in-offline-mode-with-query --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 19400c7..b09843f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ FROM chef AS builder COPY --from=planner /app/recipe.json recipe.json RUN cargo chef cook --release --recipe-path recipe.json COPY . . -RUN cargo build --release +RUN SQLX_OFFLINE=true cargo build --release FROM debian:latest AS runtime @@ -38,4 +38,4 @@ COPY --from=builder --chown=symfonia:symfonia /app/target/release/symfonia /app/ USER symfonia:symfonia WORKDIR /app/ -ENTRYPOINT ["/app/symfonia"] \ No newline at end of file +ENTRYPOINT ["/app/symfonia"] From 10e5077174ffe08924665cf6448871e1b46592bf Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 26 Aug 2024 16:24:20 +0200 Subject: [PATCH 084/162] cargo update chorus, fix String needing to be &str --- src/api/routes/auth/login.rs | 2 +- src/api/routes/auth/register.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/routes/auth/login.rs b/src/api/routes/auth/login.rs index 011c9ba..7888745 100644 --- a/src/api/routes/auth/login.rs +++ b/src/api/routes/auth/login.rs @@ -92,7 +92,7 @@ pub async fn login( let token = jwt::generate_token( &user.id, - user.email.clone().unwrap_or_default(), + user.email.clone().unwrap_or_default().as_str(), &cfg.security.jwt_secret, ); diff --git a/src/api/routes/auth/register.rs b/src/api/routes/auth/register.rs index 1f8680c..9ce1370 100644 --- a/src/api/routes/auth/register.rs +++ b/src/api/routes/auth/register.rs @@ -53,7 +53,7 @@ pub async fn register( let token = generate_token( &user.id, - user.email.clone().unwrap(), + user.email.clone().unwrap().as_str(), &cfg.security.jwt_secret, ); From 3d180e54e2096687ab9992f8747fe244a807719d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 26 Aug 2024 18:05:15 +0200 Subject: [PATCH 085/162] Update non-docker build instructions --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c8efa5a..3dedcf7 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Whether you are using Docker or not, you will need to have the following install ### Non-Docker -1. Install [PostgreSQL](https://www.postgresql.org/download/) +1. Install and host a [PostgreSQL database](https://www.postgresql.org/download/) 2. Create a new database, and a user that has full access to that database 3. Create a `.env` file in the root of the project with the following contents: @@ -43,7 +43,9 @@ DATABASE_PASSWORD=[Your Postgres password] DATABASE_NAME=[Your Postgres database name] ``` -4. Run the project with `cargo run`. +4. Install the sqlx CLI with `cargo install sqlx-cli` +5. Run `cargo sqlx migrate run` from within the project directory to run the migrations +6. Run the project with `cargo run`. ### Docker From 7a6dfcea07e1e1155649e7d3d341885bb5a99855 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 26 Aug 2024 23:25:14 +0200 Subject: [PATCH 086/162] todos --- src/gateway/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 795e774..c18a673 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -216,6 +216,13 @@ async fn establish_connection( gateway_users_store: GatewayUsersStore, config: Config, ) -> Result { + /* TODO(bitfl0wer) + - The first heartbeat only needs to be received $HEARTBEAT_INTERVAL seconds after the hello + event has been sent. Heartbeat task is a good idea. + - Identify or resume is mandatory, though. If we don't receive one of those, we should close + the connection. + */ + let ws_stream = accept_async(stream).await?; let mut connection: Connection = ws_stream.split().into(); connection From dcd3e0e67e3d6b1cfe93a0cc83b8f07ec006c006 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 28 Aug 2024 00:38:12 +0200 Subject: [PATCH 087/162] gateway 2: electric boogaloo --- src/gateway/establish_connection.rs | 76 ++++++++++++++++ src/gateway/gateway_task.rs | 11 +++ src/gateway/mod.rs | 132 +++++----------------------- src/gateway/resume_connection.rs | 21 +++++ 4 files changed, 130 insertions(+), 110 deletions(-) create mode 100644 src/gateway/establish_connection.rs create mode 100644 src/gateway/gateway_task.rs create mode 100644 src/gateway/resume_connection.rs diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs new file mode 100644 index 0000000..0a8ba52 --- /dev/null +++ b/src/gateway/establish_connection.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use chorus::types::{ + GatewayHeartbeat, GatewayHeartbeatAck, GatewayHello, GatewayIdentifyPayload, GatewayResume, +}; +use futures::{SinkExt, StreamExt}; +use serde_json::{from_str, json}; +use sqlx::PgPool; +use tokio::net::TcpStream; +use tokio::sync::Mutex; +use tokio_tungstenite::accept_async; +use tokio_tungstenite::tungstenite::Message; + +use crate::database::entities::Config; +use crate::errors::{Error, GatewayError}; +use crate::gateway::resume_connection::resume_connection; + +use super::{Connection, GatewayClient, NewConnection}; + +/// `establish_connection` is the entrypoint method that gets called when a client tries to connect +/// to the WebSocket server. +/// +/// If successful, returns a [NewConnection] with a new [Arc>] and a +/// [GatewayClient], whose `.parent` field contains a [Weak] reference to the new [GatewayUser]. +pub(super) async fn establish_connection( + stream: TcpStream, + db: PgPool, // TODO: Do we need db here? + config: Config, +) -> Result { + let ws_stream = accept_async(stream).await?; + let mut connection: Connection = ws_stream.split().into(); + // Hello message + connection + .sender + .send(Message::Text(json!(GatewayHello::default()).to_string())) + .await?; + + let connection = Arc::new(Mutex::new(connection)); + + let mut received_identify_or_resume = false; + + loop { + if received_identify_or_resume { + break; + } + + let raw_message = match connection.lock().await.receiver.next().await { + Some(next) => next, + None => return Err(GatewayError::Timeout.into()), + }?; + + if let Ok(heartbeat) = from_str::(&raw_message.to_string()) { + log::trace!(target: "symfonia::gateway::establish_connection", "Received heartbeat"); + connection + .lock() + .await + .sender + .send(Message::Text( + json!(GatewayHeartbeatAck::default()).to_string(), + )) + .await?; + } else if let Ok(identify) = from_str::(&raw_message.to_string()) { + received_identify_or_resume = true; + log::trace!(target: "symfonia::gateway::establish_connection", "Received identify payload"); + // TODO: Verify token, build NewConnection + } else if let Ok(resume) = from_str::(&raw_message.to_string()) { + received_identify_or_resume = true; + log::trace!(target: "symfonia::gateway::establish_connection", "Received resume payload"); + return resume_connection(connection, db, config, resume).await; + } else { + return Err(GatewayError::UnexpectedMessage.into()); + } + } + + todo!() +} diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs new file mode 100644 index 0000000..535507d --- /dev/null +++ b/src/gateway/gateway_task.rs @@ -0,0 +1,11 @@ +use std::sync::{Arc, Mutex}; + +use super::GatewayClient; + +/// Handles all messages a client sends to the gateway post-handshake. +pub(super) async fn gateway_task( + client: Arc>, +) -> Result<(), crate::errors::Error> { + // TODO + todo!() +} diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index c18a673..7228a0b 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -6,6 +6,9 @@ static RESUME_RECONNECT_WINDOW_SECONDS: u8 = 90; +mod establish_connection; +mod gateway_task; +mod resume_connection; mod types; use std::collections::BTreeMap; @@ -64,29 +67,32 @@ GatewayUser is subscribed to /// time. struct GatewayUser { /// Sessions a User is connected with. - pub clients: Vec>>, + clients: Vec>>, /// The Snowflake ID of the User. - pub id: Snowflake, + id: Snowflake, /// A collection of [Subscribers](Subscriber) to [Event] [Publishers](pubserve::Publisher). /// /// A GatewayUser may have many [GatewayClients](GatewayClient), but he only gets subscribed to /// all relevant [Publishers](pubserve::Publisher) *once* to save resources. - pub subscriptions: Vec>>, + subscriptions: Vec>>, } /// A concrete session, that a [GatewayUser] is connected to the Gateway with. struct GatewayClient { + connection: Arc>, /// A [Weak] reference to the [GatewayUser] this client belongs to. - pub parent: Weak>, - /// The [SplitSink] and [SplitStream] for this clients' WebSocket session - pub connection: Connection, - pub session_id: String, - pub last_sequence: u64, + parent: Weak>, + // Handle to the main Gateway task for this client + main_task_handle: tokio::task::JoinHandle>, + // Handle to the heartbeat task for this client + heartbeat_task_handle: tokio::task::JoinHandle>, + // Kill switch to disconnect the client + kill_send: tokio::sync::broadcast::Sender<()>, } struct Connection { - pub sender: WebSocketSend, - pub receiver: WebSocketReceive, + sender: WebSocketSend, + receiver: WebSocketReceive, } struct DisconnectInfo { @@ -116,7 +122,7 @@ impl struct NewConnection { user: Arc>, - client: GatewayClient, + client: Arc>, } type ResumableClientsStore = Arc>>; @@ -141,12 +147,9 @@ pub async fn start_gateway( tokio::task::spawn(async { purge_expired_disconnects(resumeable_clients) }); while let Ok((stream, _)) = listener.accept().await { log::trace!(target: "symfonia::gateway", "New connection received"); - let connection_result = match tokio::task::spawn(establish_connection( - stream, - db.clone(), - gateway_users.clone(), - config.clone(), - )) + let connection_result = match tokio::task::spawn( + establish_connection::establish_connection(stream, db.clone(), config.clone()), + ) .await { Ok(result) => result, @@ -205,97 +208,6 @@ fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { } } -/// `establish_connection` is the entrypoint method that gets called when a client tries to connect -/// to the WebSocket server. -/// -/// If successful, returns a [NewConnection] with a new [Arc>] and a -/// [GatewayClient], whose `.parent` field contains a [Weak] reference to the new [GatewayUser]. -async fn establish_connection( - stream: TcpStream, - db: PgPool, - gateway_users_store: GatewayUsersStore, - config: Config, -) -> Result { - /* TODO(bitfl0wer) - - The first heartbeat only needs to be received $HEARTBEAT_INTERVAL seconds after the hello - event has been sent. Heartbeat task is a good idea. - - Identify or resume is mandatory, though. If we don't receive one of those, we should close - the connection. - */ - - let ws_stream = accept_async(stream).await?; - let mut connection: Connection = ws_stream.split().into(); - connection - .sender - .send(Message::Text(json!(GatewayHello::default()).to_string())) - .await?; - let raw_message_result = match connection.receiver.next().await { - Some(next) => next, - None => return Err(GatewayError::Timeout.into()), - }; - log::trace!(target: "symfonia::gateway::establish_connection", "Received first message: {:?}", &raw_message_result); - let raw_message = raw_message_result?; - if let Ok(resume_message) = from_str::(&raw_message.to_string()) { - log::trace!(target: "symfonia::gateway::establish_connection", "[{}] Received GatewayResume. Trying to resume gateway connection", &resume_message.session_id); - return resume_connection(connection, resume_message).await; - } - let _heartbeat_message = from_str::(&raw_message.to_string()) // TODO: Implement hearbeating and sequence handling - .map_err(|_| GatewayError::UnexpectedMessage)?; - log::trace!(target: "symfonia::gateway::establish_connection", "Received GatewayHeartbeat. Continuing to build fresh gateway connection"); - let raw_identify = match connection.receiver.next().await { - Some(next) => next, - None => return Err(GatewayError::Timeout.into()), - }?; - let identify = match from_str::(&raw_identify.to_string()) { - Ok(identify) => identify, - Err(e) => { - log::debug!(target: "symfonia::gateway::establish_connection", "Expected GatewayIdentifyPayload, received wrong data: {e}"); - return Err(GatewayError::UnexpectedMessage.into()); - } - }; - - // Retrieve the token without the "Bearer " prefix - let token = identify.token.trim_start_matches("Bearer "); - // Check the token and retrieve the valid claims - let claims = check_token(&db, token, &config.security.jwt_secret).await?; - let id = claims.id; - // Check if the user is already connected. If so, add the new client to the existing user - if let Some(user) = gateway_users_store.lock().unwrap().get(&id) { - log::debug!(target: "symfonia::gateway::establish_connection", "User with ID {id} already connected. Adding new client to existing user"); - let client = GatewayClient { - parent: Arc::downgrade(user), - connection, - session_id: identify.token, - last_sequence: 0, - }; - Ok(NewConnection { - user: user.clone(), - client, - }) - } else { - log::debug!(target: "symfonia::gateway::establish_connection", "User with ID {id} not connected yet. Creating new user and client"); - let user = Arc::new(Mutex::new(GatewayUser { - clients: Vec::new(), - id, - subscriptions: Vec::new(), // TODO: Subscribe to relevant publishers - })); - let client = GatewayClient { - parent: Arc::downgrade(&user), - connection, - session_id: identify.token, - last_sequence: 0, - }; - Ok(NewConnection { user, client }) - } -} - -async fn resume_connection( - connection: Connection, - resume_message: GatewayResume, -) -> Result { - todo!() // TODO Implement resuming connections -} - /// Adds the contents of a [NewConnection] struct to a `gateway_users` map in a "checked" manner. /// /// If the `NewConnection` contains a [GatewayUser] which is already in `gateway_users`, then @@ -319,12 +231,12 @@ fn checked_add_new_connection( // the `clients` field of our existing user. if locked_map.contains_key(&new_connection_user.id) { let existing_user = locked_map.get(&new_connection_user.id).unwrap(); - new_connection.client.parent = Arc::downgrade(existing_user); + new_connection.client.lock().unwrap().parent = Arc::downgrade(existing_user); existing_user .lock() .unwrap() .clients - .push(Arc::new(Mutex::new(new_connection.client))); + .push(new_connection.client); } else { // We cannot do `locked_map.insert(id, new_connection.user)` if new_connection is still // locked. Just bind the id we need to a new variable, then drop the lock. diff --git a/src/gateway/resume_connection.rs b/src/gateway/resume_connection.rs new file mode 100644 index 0000000..860ef0a --- /dev/null +++ b/src/gateway/resume_connection.rs @@ -0,0 +1,21 @@ +use std::sync::Arc; + +use chorus::types::GatewayResume; +use sqlx::PgPool; +use tokio::net::TcpStream; +use tokio::sync::Mutex; + +use crate::database::entities::Config; +use crate::errors::Error; + +use super::{Connection, NewConnection}; + +pub(super) async fn resume_connection( + connection: Arc>, + db: PgPool, + config: Config, + resume_message: GatewayResume, +) -> Result { + // TODO + todo!() +} From 3a5179fa86d45157e27c08af300a3b5f618bedf5 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 28 Aug 2024 18:31:14 +0200 Subject: [PATCH 088/162] Write HeartbeatHandler --- src/gateway/heartbeat.rs | 115 +++++++++++++++++++++++++++++++++++++++ src/gateway/mod.rs | 1 + 2 files changed, 116 insertions(+) create mode 100644 src/gateway/heartbeat.rs diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs new file mode 100644 index 0000000..b4ade51 --- /dev/null +++ b/src/gateway/heartbeat.rs @@ -0,0 +1,115 @@ +use std::sync::Arc; + +use chorus::types::GatewayHeartbeat; +use futures::SinkExt; +use serde_json::json; +use tokio::sync::Mutex; + +use super::Connection; + +struct HeartbeatHandler { + connection: Arc>, + kill_receive: tokio::sync::broadcast::Receiver<()>, + kill_send: tokio::sync::broadcast::Sender<()>, + message_receive: tokio::sync::mpsc::Receiver, + last_heartbeat: std::time::Instant, +} + +impl HeartbeatHandler { + /// Constructs a new `HeartbeatHandler` instance. + /// + /// This method initializes a new heartbeat handler with the provided connection, kill signals, and message receiver. It sets up the internal state for tracking the last heartbeat time. + /// + /// # Parameters + /// - `connection`: A shared reference to a mutex-protected connection object. + /// - `kill_receive`: A channel receiver for signaling the shutdown of the heartbeat handler. + /// - `kill_send`: A channel sender for sending signals to shut down the heartbeat handler. + /// - `message_receive`: An MPSC (Multiple Producer Single Consumer) channel receiver for receiving heartbeat messages. + /// + /// # Returns + /// The newly created `HeartbeatHandler` instance. + /// + /// # Example + /// ```rust + /// use std::sync::Arc; + /// use tokio::sync::broadcast; + /// use tokio::sync::mpsc; + /// use chorus::types::GatewayHeartbeat; + /// use super::Connection; + /// use super::HeartbeatHandler; + /// + /// let connection = Arc::new(Mutex::new(Connection::new())); + /// let (kill_send, kill_receive) = broadcast::channel(1); + /// let (message_send, message_receive) = mpsc::channel(16); + /// + /// let heartbeat_handler = HeartbeatHandler::new(connection, kill_receive, kill_send, message_receive).await; + /// ``` + pub(super) async fn new( + connection: Arc>, + kill_receive: tokio::sync::broadcast::Receiver<()>, + kill_send: tokio::sync::broadcast::Sender<()>, + message_receive: tokio::sync::mpsc::Receiver, + ) -> Self { + Self { + connection, + kill_receive, + kill_send, + message_receive, + last_heartbeat: std::time::Instant::now(), + } + } + + /// Continuously listens for messages and handles heartbeat logic until instructed to shut down. + /// + /// This asynchronous method maintains an infinite loop that waits for signals to either receive + /// a new heartbeat message or check if it should terminate. It updates the last heartbeat time + /// upon receiving a new heartbeat, sends a ping over the WebSocket connection periodically, and + /// terminates itself if no heartbeats are received within 45 seconds. Because this method is + /// running an "infinite" loop, the [HeartbeatHandler] should be moved to a separate task using + /// `tokio::spawn`, where the method should be executed. + /// + /// ## Termination + /// The loop terminates when: + /// - A shutdown signal is received through `kill_receive`. + /// - An error occurs during WebSocket communication or channel reception. + /// + /// + /// ## Example + /// ```rust + /// use std::sync::Arc; + /// use tokio::sync::broadcast; + /// use tokio::sync::mpsc; + /// use chorus::types::GatewayHeartbeat; + /// use super::Connection; + /// use super::HeartbeatHandler; + /// + /// let connection = Arc::new(Mutex::new(Connection::new())); + /// let (kill_send, kill_receive) = broadcast::channel(1); + /// let (message_send, message_receive) = mpsc::channel(16); + /// + /// let mut handler = HeartbeatHandler::new(connection, kill_receive, kill_send, message_receive).await; + /// tokio::spawn(async move { + /// handler.run().await; + /// }); + /// ``` + pub(super) async fn run(&mut self) { + loop { + tokio::select! { + _ = self.kill_receive.recv() => { + break; + } + Some(heartbeat) = self.message_receive.recv() => { + self.last_heartbeat = std::time::Instant::now(); + self.connection.lock().await.sender.send(tokio_tungstenite::tungstenite::Message::Text(json!(heartbeat).to_string())).await.unwrap(); + } + else => { + let elapsed = std::time::Instant::now() - self.last_heartbeat; + if elapsed > std::time::Duration::from_secs(45) { + self.kill_send.send(()).unwrap(); + break; + } + } + } + } + } +} diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 7228a0b..85f0b82 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -8,6 +8,7 @@ static RESUME_RECONNECT_WINDOW_SECONDS: u8 = 90; mod establish_connection; mod gateway_task; +mod heartbeat; mod resume_connection; mod types; From c49cbe49bcdcf7c655af07137742e733a04f1ef6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 28 Aug 2024 18:45:40 +0200 Subject: [PATCH 089/162] Update docstring --- src/gateway/heartbeat.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index b4ade51..c1d3d83 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -73,6 +73,12 @@ impl HeartbeatHandler { /// - A shutdown signal is received through `kill_receive`. /// - An error occurs during WebSocket communication or channel reception. /// + /// Termination is signaled by sending a message through `kill_send` to the main task. This + /// `kill_send` channel is created by the main task and passed to the `HeartbeatHandler` during + /// initialization. The corresponding `kill_receive` can be used by other tasks to signal that + /// the Gateway connection should be closed. In the context of symfonia, this is being done to + /// close the [GatewayTask]. + /// /// /// ## Example /// ```rust @@ -89,7 +95,7 @@ impl HeartbeatHandler { /// /// let mut handler = HeartbeatHandler::new(connection, kill_receive, kill_send, message_receive).await; /// tokio::spawn(async move { - /// handler.run().await; + /// handler.run(); /// }); /// ``` pub(super) async fn run(&mut self) { From 167ed221bcd8e44c33282593435f23392d0709c8 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 28 Aug 2024 19:19:21 +0200 Subject: [PATCH 090/162] more stuff!!! --- src/gateway/heartbeat.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index c1d3d83..66adb3e 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -1,11 +1,12 @@ use std::sync::Arc; -use chorus::types::GatewayHeartbeat; +use chorus::types::{GatewayHeartbeat, GatewayHeartbeatAck}; use futures::SinkExt; +use rand::seq; use serde_json::json; use tokio::sync::Mutex; -use super::Connection; +use super::{Connection, GatewayClient}; struct HeartbeatHandler { connection: Arc>, @@ -13,6 +14,8 @@ struct HeartbeatHandler { kill_send: tokio::sync::broadcast::Sender<()>, message_receive: tokio::sync::mpsc::Receiver, last_heartbeat: std::time::Instant, + /// The current sequence number of the gateway connection. + sequence_number: Arc>, } impl HeartbeatHandler { @@ -49,6 +52,7 @@ impl HeartbeatHandler { kill_receive: tokio::sync::broadcast::Receiver<()>, kill_send: tokio::sync::broadcast::Sender<()>, message_receive: tokio::sync::mpsc::Receiver, + sequence_number: Arc>, ) -> Self { Self { connection, @@ -56,6 +60,7 @@ impl HeartbeatHandler { kill_send, message_receive, last_heartbeat: std::time::Instant::now(), + sequence_number, } } @@ -98,15 +103,31 @@ impl HeartbeatHandler { /// handler.run(); /// }); /// ``` - pub(super) async fn run(&mut self) { + pub(super) async fn run(&mut self, client: Arc>) { + let mut sequence = 0u64; loop { tokio::select! { _ = self.kill_receive.recv() => { break; } Some(heartbeat) = self.message_receive.recv() => { + if let Some(received_sequence_number) = heartbeat.d { + let mut sequence = self.sequence_number.lock().await; + // TODO: ..wait do we actually even *receive* sequence numbers, or do we just send them? + match *sequence + 1 == received_sequence_number { + true => { + *sequence = received_sequence_number; + } + false => { + // TODO Send disconnect message + self.connection.lock().await.sender.send().await.unwrap(); + self.kill_send.send(()).unwrap(); + break; + } + } + } self.last_heartbeat = std::time::Instant::now(); - self.connection.lock().await.sender.send(tokio_tungstenite::tungstenite::Message::Text(json!(heartbeat).to_string())).await.unwrap(); + self.connection.lock().await.sender.send(tokio_tungstenite::tungstenite::Message::Text(json!(GatewayHeartbeatAck::default()).to_string())).await.unwrap(); } else => { let elapsed = std::time::Instant::now() - self.last_heartbeat; From a364e667b4b12895be86075526f007d61af8e7c6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 28 Aug 2024 21:59:42 +0200 Subject: [PATCH 091/162] use chorus stable 0.16.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f3dbffc..fdcbdc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ sentry = { version = "0.34.0", default-features = false, features = [ clap = { version = "4.5.4", features = ["derive"] } chorus = { features = [ "backend", -], default-features = false, git = "https://github.com/polyphony-chat/chorus", branch = "dev" } # git = "ssh://git@github.com/Quat3rnion/chorus" # path = "../chorus" git = "ssh://git@github.com/polyphony-chat/chorus" +], default-features = false, version = "0.16.0" } # git = "ssh://git@github.com/Quat3rnion/chorus" # path = "../chorus" git = "ssh://git@github.com/polyphony-chat/chorus" serde_path_to_error = "0.1.16" percent-encoding = "2.3.1" hex = "0.4.3" From 679b4e6e1b6dbbce34e456d8da739d1a4736ce34 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 28 Aug 2024 22:00:27 +0200 Subject: [PATCH 092/162] Add logging, correctly kill everything if something unexpected fails, remove client arg from run method --- src/gateway/heartbeat.rs | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index 66adb3e..9cde4e9 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -2,13 +2,18 @@ use std::sync::Arc; use chorus::types::{GatewayHeartbeat, GatewayHeartbeatAck}; use futures::SinkExt; +use log::*; use rand::seq; use serde_json::json; use tokio::sync::Mutex; +use tokio_tungstenite::tungstenite::Message; use super::{Connection, GatewayClient}; -struct HeartbeatHandler { +static HEARTBEAT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(45); +static LATENCY_BUFFER: std::time::Duration = std::time::Duration::from_secs(5); + +pub(super) struct HeartbeatHandler { connection: Arc>, kill_receive: tokio::sync::broadcast::Receiver<()>, kill_send: tokio::sync::broadcast::Sender<()>, @@ -47,7 +52,7 @@ impl HeartbeatHandler { /// /// let heartbeat_handler = HeartbeatHandler::new(connection, kill_receive, kill_send, message_receive).await; /// ``` - pub(super) async fn new( + pub(super) fn new( connection: Arc>, kill_receive: tokio::sync::broadcast::Receiver<()>, kill_send: tokio::sync::broadcast::Sender<()>, @@ -103,14 +108,17 @@ impl HeartbeatHandler { /// handler.run(); /// }); /// ``` - pub(super) async fn run(&mut self, client: Arc>) { + pub(super) async fn run(&mut self) { + // TODO: On death of this task, create and store disconnect info in gateway client object let mut sequence = 0u64; loop { tokio::select! { _ = self.kill_receive.recv() => { + trace!("Received kill signal in heartbeat_handler. Stopping heartbeat handler"); break; } Some(heartbeat) = self.message_receive.recv() => { + trace!("Received heartbeat message in heartbeat_handler"); if let Some(received_sequence_number) = heartbeat.d { let mut sequence = self.sequence_number.lock().await; // TODO: ..wait do we actually even *receive* sequence numbers, or do we just send them? @@ -127,12 +135,25 @@ impl HeartbeatHandler { } } self.last_heartbeat = std::time::Instant::now(); - self.connection.lock().await.sender.send(tokio_tungstenite::tungstenite::Message::Text(json!(GatewayHeartbeatAck::default()).to_string())).await.unwrap(); + self.connection + .lock() + .await + .sender + .send(Message::Text( + json!(GatewayHeartbeatAck::default()).to_string(), + )) + .await.unwrap_or_else(|_| { + trace!("Failed to send heartbeat ack in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); + self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); + return; + } + ); } else => { let elapsed = std::time::Instant::now() - self.last_heartbeat; if elapsed > std::time::Duration::from_secs(45) { - self.kill_send.send(()).unwrap(); + trace!("Heartbeat timed out in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); + self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler");; break; } } From 36f7a066f5cec7ae180ab531a92df048b1e09d42 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 28 Aug 2024 22:00:56 +0200 Subject: [PATCH 093/162] Change heartbeat_task_handle to be JoinHandle<()> instead of JoinHandle> --- src/gateway/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 85f0b82..6de2cac 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -86,7 +86,7 @@ struct GatewayClient { // Handle to the main Gateway task for this client main_task_handle: tokio::task::JoinHandle>, // Handle to the heartbeat task for this client - heartbeat_task_handle: tokio::task::JoinHandle>, + heartbeat_task_handle: tokio::task::JoinHandle<()>, // Kill switch to disconnect the client kill_send: tokio::sync::broadcast::Sender<()>, } From 65d6f9e3a2349f5f70fb675318863fa13609dbb9 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 28 Aug 2024 22:01:15 +0200 Subject: [PATCH 094/162] Use heartbeat handler to... handle the heartbeat :shocked: --- src/gateway/establish_connection.rs | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 0a8ba52..a3e015f 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -2,18 +2,22 @@ use std::sync::Arc; use chorus::types::{ GatewayHeartbeat, GatewayHeartbeatAck, GatewayHello, GatewayIdentifyPayload, GatewayResume, + Snowflake, }; use futures::{SinkExt, StreamExt}; use serde_json::{from_str, json}; use sqlx::PgPool; use tokio::net::TcpStream; use tokio::sync::Mutex; +use tokio::task::JoinHandle; use tokio_tungstenite::accept_async; use tokio_tungstenite::tungstenite::Message; use crate::database::entities::Config; use crate::errors::{Error, GatewayError}; +use crate::gateway::heartbeat::HeartbeatHandler; use crate::gateway::resume_connection::resume_connection; +use crate::gateway::GatewayUser; use super::{Connection, GatewayClient, NewConnection}; @@ -39,6 +43,21 @@ pub(super) async fn establish_connection( let mut received_identify_or_resume = false; + let (kill_send, kill_receive) = tokio::sync::broadcast::channel(1); + let (message_send, message_receive) = tokio::sync::mpsc::channel(4); + let sequence_number = Arc::new(Mutex::new(0u64)); + let mut heartbeat_handler = HeartbeatHandler::new( + connection.clone(), + kill_receive.resubscribe(), + kill_send.clone(), + message_receive, + sequence_number.clone(), + ); + + // This JoinHandle `.is_some()` if we receive a heartbeat message *before* we receive an + // identify or resume message. + let heartbeat_handler_handle: Option>; + loop { if received_identify_or_resume { break; @@ -51,14 +70,7 @@ pub(super) async fn establish_connection( if let Ok(heartbeat) = from_str::(&raw_message.to_string()) { log::trace!(target: "symfonia::gateway::establish_connection", "Received heartbeat"); - connection - .lock() - .await - .sender - .send(Message::Text( - json!(GatewayHeartbeatAck::default()).to_string(), - )) - .await?; + heartbeat_handler_handle = Some(tokio::spawn(heartbeat_handler.run())); } else if let Ok(identify) = from_str::(&raw_message.to_string()) { received_identify_or_resume = true; log::trace!(target: "symfonia::gateway::establish_connection", "Received identify payload"); From ef9b69cab5c06d1bcd9c9922f7ee0c71bdb56936 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 29 Aug 2024 20:02:19 +0200 Subject: [PATCH 095/162] correct opcode number, rename fields --- src/gateway/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 6de2cac..ac5ed53 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -98,8 +98,8 @@ struct Connection { struct DisconnectInfo { session_id: String, - disconnected_at: u64, - with_opcode: u16, + disconnected_at_sequence: u64, + with_opcode: u32, } impl From 227a878a0f35e2638670a549296860cd01b7850f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 29 Aug 2024 20:02:30 +0200 Subject: [PATCH 096/162] considerations about heartbeats --- src/gateway/heartbeat.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index 9cde4e9..f527e1e 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -112,6 +112,12 @@ impl HeartbeatHandler { // TODO: On death of this task, create and store disconnect info in gateway client object let mut sequence = 0u64; loop { + // When receiving heartbeats, we need to consider the following cases: + // - Heartbeat sequence number is correct + // - Heartbeat sequence number is slightly off, likely because a new packet was sent before the heartbeat was received + // - Heartbeat sequence number is way off, likely because the connection has high latency or is unstable + // + // I would consider "way off" to be a difference of more than or equal to 3. tokio::select! { _ = self.kill_receive.recv() => { trace!("Received kill signal in heartbeat_handler. Stopping heartbeat handler"); From 4730681fd293477d4debb157cebe7e55e14ebf2d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 30 Aug 2024 00:07:58 +0200 Subject: [PATCH 097/162] Logic for checking received sequence number against last known sequence number --- src/gateway/heartbeat.rs | 61 ++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index f527e1e..7de3c8d 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -1,13 +1,17 @@ use std::sync::Arc; -use chorus::types::{GatewayHeartbeat, GatewayHeartbeatAck}; +use chorus::types::{GatewayHeartbeat, GatewayHeartbeatAck, GatewayReconnect}; use futures::SinkExt; use log::*; use rand::seq; use serde_json::json; use tokio::sync::Mutex; +use tokio_tungstenite::tungstenite::protocol::frame::coding::OpCode; +use tokio_tungstenite::tungstenite::protocol::CloseFrame; use tokio_tungstenite::tungstenite::Message; +use crate::gateway::DisconnectInfo; + use super::{Connection, GatewayClient}; static HEARTBEAT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(45); @@ -17,10 +21,11 @@ pub(super) struct HeartbeatHandler { connection: Arc>, kill_receive: tokio::sync::broadcast::Receiver<()>, kill_send: tokio::sync::broadcast::Sender<()>, - message_receive: tokio::sync::mpsc::Receiver, + message_receive: tokio::sync::broadcast::Receiver, last_heartbeat: std::time::Instant, /// The current sequence number of the gateway connection. sequence_number: Arc>, + session_id_receive: tokio::sync::broadcast::Receiver, } impl HeartbeatHandler { @@ -33,6 +38,10 @@ impl HeartbeatHandler { /// - `kill_receive`: A channel receiver for signaling the shutdown of the heartbeat handler. /// - `kill_send`: A channel sender for sending signals to shut down the heartbeat handler. /// - `message_receive`: An MPSC (Multiple Producer Single Consumer) channel receiver for receiving heartbeat messages. + /// - `session_id_receive`: A oneshot channel receiver for receiving the session ID. The heartbeat handler may start + /// running before an identify or resume message with a session ID is received, so this channel is used to wait for + /// the session ID. If a session ID has been received, the heartbeat handler can use it to store a DisconnectInfo + /// object in the appropriate `GatewayClient` when the connection is closed. /// /// # Returns /// The newly created `HeartbeatHandler` instance. @@ -56,8 +65,9 @@ impl HeartbeatHandler { connection: Arc>, kill_receive: tokio::sync::broadcast::Receiver<()>, kill_send: tokio::sync::broadcast::Sender<()>, - message_receive: tokio::sync::mpsc::Receiver, - sequence_number: Arc>, + message_receive: tokio::sync::broadcast::Receiver, + last_sequence_number: Arc>, + session_id_receive: tokio::sync::broadcast::Receiver, ) -> Self { Self { connection, @@ -65,7 +75,8 @@ impl HeartbeatHandler { kill_send, message_receive, last_heartbeat: std::time::Instant::now(), - sequence_number, + sequence_number: last_sequence_number, + session_id_receive, } } @@ -128,15 +139,25 @@ impl HeartbeatHandler { if let Some(received_sequence_number) = heartbeat.d { let mut sequence = self.sequence_number.lock().await; // TODO: ..wait do we actually even *receive* sequence numbers, or do we just send them? - match *sequence + 1 == received_sequence_number { - true => { + // TODO: Actually send Acks + match Self::compare_sequence_numbers(*sequence, received_sequence_number) { + SequenceNumberComparison::Correct => { *sequence = received_sequence_number; } - false => { - // TODO Send disconnect message - self.connection.lock().await.sender.send().await.unwrap(); - self.kill_send.send(()).unwrap(); - break; + SequenceNumberComparison::SlightlyOff(diff) => { + *sequence = received_sequence_number; + trace!(target: "symfonia::gateway::heartbeat_handler", "Received heartbeat sequence number is slightly off by {}. This may be due to latency or a new packet being sent before the current one got received.", diff); + } + SequenceNumberComparison::WayOff(diff) => { + *sequence = received_sequence_number; + trace!(target: "symfonia::gateway::heartbeat_handler", "Received heartbeat sequence number is way off by {}. This may be due to latency.", diff); + self.connection.lock().await.sender.send(Message::Text(json!(GatewayReconnect::default()).to_string())).await.unwrap_or_else(|_| { + trace!("Failed to send reconnect message in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); + self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); + return; + }); + self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); + return; } } } @@ -166,4 +187,20 @@ impl HeartbeatHandler { } } } + + fn compare_sequence_numbers(one: u64, two: u64) -> SequenceNumberComparison { + let max = std::cmp::max(one, two); + let min = std::cmp::min(one, two); + match max - min { + 0 => SequenceNumberComparison::Correct, + 1..2 => SequenceNumberComparison::SlightlyOff(max - min), + _ => SequenceNumberComparison::WayOff(max - min), + } + } +} + +enum SequenceNumberComparison { + Correct, + SlightlyOff(u64), + WayOff(u64), } From 132d0e31c3dd4b68810e33673b517df62eb7ac97 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 30 Aug 2024 00:08:39 +0200 Subject: [PATCH 098/162] Create and move heartbeat_handler only when it is being created --- src/gateway/establish_connection.rs | 42 ++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index a3e015f..55944a4 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -44,19 +44,13 @@ pub(super) async fn establish_connection( let mut received_identify_or_resume = false; let (kill_send, kill_receive) = tokio::sync::broadcast::channel(1); - let (message_send, message_receive) = tokio::sync::mpsc::channel(4); + let (message_send, message_receive) = tokio::sync::broadcast::channel(4); let sequence_number = Arc::new(Mutex::new(0u64)); - let mut heartbeat_handler = HeartbeatHandler::new( - connection.clone(), - kill_receive.resubscribe(), - kill_send.clone(), - message_receive, - sequence_number.clone(), - ); + let (session_id_send, session_id_receive) = tokio::sync::broadcast::channel(1); // This JoinHandle `.is_some()` if we receive a heartbeat message *before* we receive an // identify or resume message. - let heartbeat_handler_handle: Option>; + let mut heartbeat_handler_handle: Option> = None; loop { if received_identify_or_resume { @@ -70,7 +64,35 @@ pub(super) async fn establish_connection( if let Ok(heartbeat) = from_str::(&raw_message.to_string()) { log::trace!(target: "symfonia::gateway::establish_connection", "Received heartbeat"); - heartbeat_handler_handle = Some(tokio::spawn(heartbeat_handler.run())); + match heartbeat_handler_handle { + None => { + // This only happens *once*. You will find that we have to `.resubscribe()` to + // the channels to make the borrow checker happy, because the channels are otherwise + // moved into the spawned task, which, *technically* could occur multiple times, + // due to the loop {} construct. However, this is not the case, because this code + // executes only if heartbeat_handler_handle is None, which is only true once, + // as we set it to Some(_) in this block. We could perhaps make this a little + // nicer by using unsafe rust magic, which would also allow us to use more appropriate + // channel types such as `oneshot` for the session_id_receive channel. However, + // I don't see that this is needed at the moment. + heartbeat_handler_handle = Some(tokio::spawn({ + let mut heartbeat_handler = HeartbeatHandler::new( + connection.clone(), + kill_receive.resubscribe(), + kill_send.clone(), + message_receive.resubscribe(), + sequence_number.clone(), + session_id_receive.resubscribe(), + ); + async move { + heartbeat_handler.run().await; + } + })) + } + Some(_) => { + message_send.send(heartbeat); + } + } } else if let Ok(identify) = from_str::(&raw_message.to_string()) { received_identify_or_resume = true; log::trace!(target: "symfonia::gateway::establish_connection", "Received identify payload"); From 401f0fd1e8041ebd66237091994601d6fec67beb Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 31 Aug 2024 00:41:45 +0200 Subject: [PATCH 099/162] Send HeartbeatAck --- src/gateway/heartbeat.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index 7de3c8d..62be863 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use chorus::types::{GatewayHeartbeat, GatewayHeartbeatAck, GatewayReconnect}; -use futures::SinkExt; +use futures::{SinkExt, TryFutureExt}; use log::*; use rand::seq; use serde_json::json; @@ -134,22 +134,20 @@ impl HeartbeatHandler { trace!("Received kill signal in heartbeat_handler. Stopping heartbeat handler"); break; } - Some(heartbeat) = self.message_receive.recv() => { + Ok(heartbeat) = self.message_receive.recv() => { trace!("Received heartbeat message in heartbeat_handler"); if let Some(received_sequence_number) = heartbeat.d { let mut sequence = self.sequence_number.lock().await; - // TODO: ..wait do we actually even *receive* sequence numbers, or do we just send them? // TODO: Actually send Acks match Self::compare_sequence_numbers(*sequence, received_sequence_number) { SequenceNumberComparison::Correct => { - *sequence = received_sequence_number; + self.send_ack().await; } SequenceNumberComparison::SlightlyOff(diff) => { - *sequence = received_sequence_number; trace!(target: "symfonia::gateway::heartbeat_handler", "Received heartbeat sequence number is slightly off by {}. This may be due to latency or a new packet being sent before the current one got received.", diff); + self.send_ack().await; } SequenceNumberComparison::WayOff(diff) => { - *sequence = received_sequence_number; trace!(target: "symfonia::gateway::heartbeat_handler", "Received heartbeat sequence number is way off by {}. This may be due to latency.", diff); self.connection.lock().await.sender.send(Message::Text(json!(GatewayReconnect::default()).to_string())).await.unwrap_or_else(|_| { trace!("Failed to send reconnect message in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); @@ -197,6 +195,14 @@ impl HeartbeatHandler { _ => SequenceNumberComparison::WayOff(max - min), } } + + async fn send_ack(&self) { + self.connection.lock().await.sender.send(Message::Text(json!(GatewayHeartbeatAck::default()).to_string())).await.unwrap_or_else(|_| { + trace!("Failed to send heartbeat ack in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); + self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); + return; + }); + } } enum SequenceNumberComparison { From 63a6b4927734b1d6e429dc9927cba4675f772d47 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 31 Aug 2024 00:50:54 +0200 Subject: [PATCH 100/162] Change type ResumableClientsStore, change resuming logic a little --- src/gateway/mod.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index ac5ed53..5aacc71 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -89,6 +89,8 @@ struct GatewayClient { heartbeat_task_handle: tokio::task::JoinHandle<()>, // Kill switch to disconnect the client kill_send: tokio::sync::broadcast::Sender<()>, + // Disconnect info for resuming the session + disconnect_info: Option, } struct Connection { @@ -126,7 +128,7 @@ struct NewConnection { client: Arc>, } -type ResumableClientsStore = Arc>>; +type ResumableClientsStore = Arc>>; type GatewayUsersStore = Arc>>>>; pub async fn start_gateway( @@ -184,9 +186,17 @@ fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { .expect("Check the clock/time settings on the host machine") .as_secs(); let mut to_remove = Vec::new(); - let mut lock = resumeable_clients.lock().unwrap(); - for (disconnected_session_id, disconnected_session_info) in lock.iter() { - if current_unix_timestamp - disconnected_session_info.disconnected_at + let mut resumeable_clients_lock = resumeable_clients.lock().unwrap(); + for (disconnected_session_id, disconnected_session) in resumeable_clients_lock.iter() { + let disconnect_info = match disconnected_session.disconnect_info.as_ref() { + Some(d) => d, + None => { + // This shouldn't happen, but if it does, remove the session from the list + to_remove.push(disconnected_session_id.clone()); + continue; + } + }; + if current_unix_timestamp - disconnect_info.disconnected_at_sequence > RESUME_RECONNECT_WINDOW_SECONDS as u64 { to_remove.push(disconnected_session_id.clone()); @@ -197,9 +207,9 @@ fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { .checked_add(len as u128) .unwrap_or(u128::MAX); for session_id in to_remove.iter() { - lock.remove(session_id); + resumeable_clients_lock.remove(session_id); } - drop(lock); + drop(resumeable_clients_lock); minutely_log_timer += 1; if minutely_log_timer == 12 { log::debug!(target: "symfonia::gateway::purge_expired_disconnects", "Removed {} stale sessions in the last 60 seconds", removed_elements_last_minute); From fc07fd1cf05e382ab0b67336796eae09401a21f0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 31 Aug 2024 00:52:28 +0200 Subject: [PATCH 101/162] Remove unneeded return --- src/gateway/heartbeat.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index 62be863..9d44f0d 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -152,7 +152,6 @@ impl HeartbeatHandler { self.connection.lock().await.sender.send(Message::Text(json!(GatewayReconnect::default()).to_string())).await.unwrap_or_else(|_| { trace!("Failed to send reconnect message in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); - return; }); self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); return; @@ -170,7 +169,6 @@ impl HeartbeatHandler { .await.unwrap_or_else(|_| { trace!("Failed to send heartbeat ack in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); - return; } ); } From ce4a53d75ad880640911357737f064dd99857f4f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 31 Aug 2024 00:54:59 +0200 Subject: [PATCH 102/162] todos --- src/gateway/heartbeat.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index 9d44f0d..11c0c50 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -138,7 +138,6 @@ impl HeartbeatHandler { trace!("Received heartbeat message in heartbeat_handler"); if let Some(received_sequence_number) = heartbeat.d { let mut sequence = self.sequence_number.lock().await; - // TODO: Actually send Acks match Self::compare_sequence_numbers(*sequence, received_sequence_number) { SequenceNumberComparison::Correct => { self.send_ack().await; @@ -148,6 +147,8 @@ impl HeartbeatHandler { self.send_ack().await; } SequenceNumberComparison::WayOff(diff) => { + // TODO: We could potentially send a heartbeat to the client, prompting it to send a new heartbeat. + // This would require more logic though. trace!(target: "symfonia::gateway::heartbeat_handler", "Received heartbeat sequence number is way off by {}. This may be due to latency.", diff); self.connection.lock().await.sender.send(Message::Text(json!(GatewayReconnect::default()).to_string())).await.unwrap_or_else(|_| { trace!("Failed to send reconnect message in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); @@ -173,6 +174,8 @@ impl HeartbeatHandler { ); } else => { + // TODO: We could potentially send a heartbeat if we haven't received one in ~40 seconds, + // to try and keep the session from disconnecting. let elapsed = std::time::Instant::now() - self.last_heartbeat; if elapsed > std::time::Duration::from_secs(45) { trace!("Heartbeat timed out in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); @@ -198,7 +201,6 @@ impl HeartbeatHandler { self.connection.lock().await.sender.send(Message::Text(json!(GatewayHeartbeatAck::default()).to_string())).await.unwrap_or_else(|_| { trace!("Failed to send heartbeat ack in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); - return; }); } } From 081cac02b2253c4a3a36f715a3e131fdeaa2c654 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 31 Aug 2024 01:07:00 +0200 Subject: [PATCH 103/162] establish_connection: Die after 10s of inactivity or if kill signal has been received --- src/errors.rs | 3 + src/gateway/establish_connection.rs | 102 +++++++++++++++------------- 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/src/errors.rs b/src/errors.rs index 09de8a8..1b89634 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -69,6 +69,8 @@ pub enum GatewayError { UnexpectedMessage, #[error("TIMEOUT")] Timeout, + #[error("CLOSED")] + Closed, } #[derive(Debug, thiserror::Error)] @@ -236,6 +238,7 @@ impl ResponseError for Error { // TODO: Check if the associated statuscodes are okay GatewayError::UnexpectedMessage => StatusCode::BAD_REQUEST, GatewayError::Timeout => StatusCode::BAD_REQUEST, + GatewayError::Closed => StatusCode::BAD_REQUEST, }, Error::SqlxPgUint(_) => StatusCode::BAD_REQUEST, } diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 55944a4..998eaa2 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -43,7 +43,7 @@ pub(super) async fn establish_connection( let mut received_identify_or_resume = false; - let (kill_send, kill_receive) = tokio::sync::broadcast::channel(1); + let (kill_send, mut kill_receive) = tokio::sync::broadcast::channel(1); let (message_send, message_receive) = tokio::sync::broadcast::channel(4); let sequence_number = Arc::new(Mutex::new(0u64)); let (session_id_send, session_id_receive) = tokio::sync::broadcast::channel(1); @@ -52,57 +52,67 @@ pub(super) async fn establish_connection( // identify or resume message. let mut heartbeat_handler_handle: Option> = None; - loop { - if received_identify_or_resume { - break; + tokio::select! { + _ = kill_receive.recv() => { + return Err(GatewayError::Closed.into()); } + _ = tokio::time::sleep(std::time::Duration::from_secs(10)) => { + return Err(GatewayError::Timeout.into()); + } + else => { + loop { + if received_identify_or_resume { + break; + } - let raw_message = match connection.lock().await.receiver.next().await { - Some(next) => next, - None => return Err(GatewayError::Timeout.into()), - }?; + let raw_message = match connection.lock().await.receiver.next().await { + Some(next) => next, + None => return Err(GatewayError::Timeout.into()), + }?; - if let Ok(heartbeat) = from_str::(&raw_message.to_string()) { - log::trace!(target: "symfonia::gateway::establish_connection", "Received heartbeat"); - match heartbeat_handler_handle { - None => { - // This only happens *once*. You will find that we have to `.resubscribe()` to - // the channels to make the borrow checker happy, because the channels are otherwise - // moved into the spawned task, which, *technically* could occur multiple times, - // due to the loop {} construct. However, this is not the case, because this code - // executes only if heartbeat_handler_handle is None, which is only true once, - // as we set it to Some(_) in this block. We could perhaps make this a little - // nicer by using unsafe rust magic, which would also allow us to use more appropriate - // channel types such as `oneshot` for the session_id_receive channel. However, - // I don't see that this is needed at the moment. - heartbeat_handler_handle = Some(tokio::spawn({ - let mut heartbeat_handler = HeartbeatHandler::new( - connection.clone(), - kill_receive.resubscribe(), - kill_send.clone(), - message_receive.resubscribe(), - sequence_number.clone(), - session_id_receive.resubscribe(), - ); - async move { - heartbeat_handler.run().await; + if let Ok(heartbeat) = from_str::(&raw_message.to_string()) { + log::trace!(target: "symfonia::gateway::establish_connection", "Received heartbeat"); + match heartbeat_handler_handle { + None => { + // This only happens *once*. You will find that we have to `.resubscribe()` to + // the channels to make the borrow checker happy, because the channels are otherwise + // moved into the spawned task, which, *technically* could occur multiple times, + // due to the loop {} construct. However, this is not the case, because this code + // executes only if heartbeat_handler_handle is None, which is only true once, + // as we set it to Some(_) in this block. We could perhaps make this a little + // nicer by using unsafe rust magic, which would also allow us to use more appropriate + // channel types such as `oneshot` for the session_id_receive channel. However, + // I don't see that this is needed at the moment. + heartbeat_handler_handle = Some(tokio::spawn({ + let mut heartbeat_handler = HeartbeatHandler::new( + connection.clone(), + kill_receive.resubscribe(), + kill_send.clone(), + message_receive.resubscribe(), + sequence_number.clone(), + session_id_receive.resubscribe(), + ); + async move { + heartbeat_handler.run().await; + } + })) } - })) - } - Some(_) => { - message_send.send(heartbeat); + Some(_) => { + message_send.send(heartbeat); + } + } + } else if let Ok(identify) = from_str::(&raw_message.to_string()) { + received_identify_or_resume = true; + log::trace!(target: "symfonia::gateway::establish_connection", "Received identify payload"); + // TODO: Verify token, build NewConnection + } else if let Ok(resume) = from_str::(&raw_message.to_string()) { + received_identify_or_resume = true; + log::trace!(target: "symfonia::gateway::establish_connection", "Received resume payload"); + return resume_connection(connection, db, config, resume).await; + } else { + return Err(GatewayError::UnexpectedMessage.into()); } } - } else if let Ok(identify) = from_str::(&raw_message.to_string()) { - received_identify_or_resume = true; - log::trace!(target: "symfonia::gateway::establish_connection", "Received identify payload"); - // TODO: Verify token, build NewConnection - } else if let Ok(resume) = from_str::(&raw_message.to_string()) { - received_identify_or_resume = true; - log::trace!(target: "symfonia::gateway::establish_connection", "Received resume payload"); - return resume_connection(connection, db, config, resume).await; - } else { - return Err(GatewayError::UnexpectedMessage.into()); } } From 506ca7f007b037ee467720f6e1da37cf7b4f49ce Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 31 Aug 2024 01:08:48 +0200 Subject: [PATCH 104/162] doc --- src/gateway/heartbeat.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index 11c0c50..e507acd 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -187,6 +187,7 @@ impl HeartbeatHandler { } } + /// Compares two sequence numbers and returns a comparison result of type [SequenceNumberComparison]. fn compare_sequence_numbers(one: u64, two: u64) -> SequenceNumberComparison { let max = std::cmp::max(one, two); let min = std::cmp::min(one, two); @@ -197,6 +198,7 @@ impl HeartbeatHandler { } } + /// Shorthand for sending a heartbeat ack message. async fn send_ack(&self) { self.connection.lock().await.sender.send(Message::Text(json!(GatewayHeartbeatAck::default()).to_string())).await.unwrap_or_else(|_| { trace!("Failed to send heartbeat ack in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); @@ -205,8 +207,12 @@ impl HeartbeatHandler { } } +/// Granular comparison of two sequence numbers. enum SequenceNumberComparison { + /// The sequence numbers are identical. Correct, + /// The sequence numbers have a difference of more than 0 and less than 3. SlightlyOff(u64), + // The sequence numbers have a difference of 3 or more. WayOff(u64), } From b3750d90c14d01934ee33799d96bcdf9b63b65be Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 31 Aug 2024 23:01:36 +0200 Subject: [PATCH 105/162] Indicate session token stored in memory to be hashed --- Cargo.toml | 1 + src/gateway/mod.rs | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fdcbdc5..7be9bb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,5 +73,6 @@ pubserve = { version = "1.1.0", features = ["async", "send"] } parking_lot = { version = "0.12.3", features = ["deadlock_detection"] } sqlx-pg-uint = { version = "0.5.0", features = ["serde"] } +blake3 = "1.5.4" [dev-dependencies] rusty-hook = "0.11.2" diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 5aacc71..daed034 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -99,7 +99,9 @@ struct Connection { } struct DisconnectInfo { - session_id: String, + // TODO: Token stored in memory must be hashed!! + /// [blake3] hash of the session token that was used for this connection + session_token_hash_blake3: String, disconnected_at_sequence: u64, with_opcode: u32, } @@ -128,6 +130,9 @@ struct NewConnection { client: Arc>, } +// TODO: If the string is supposed to be a token, the token must be hashed before storing it in memory!! +/// A thread-shareable map of resumable clients. The key is a [blake3] hash of the session token used +/// for the connection. The value is a [GatewayClient] that can be resumed. type ResumableClientsStore = Arc>>; type GatewayUsersStore = Arc>>>>; From 5b94632fece2a53ef6484f539cbfdac87c3cf955 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 31 Aug 2024 23:28:20 +0200 Subject: [PATCH 106/162] Verify claims in token --- src/gateway/establish_connection.rs | 35 +++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 998eaa2..5e72a81 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -18,8 +18,9 @@ use crate::errors::{Error, GatewayError}; use crate::gateway::heartbeat::HeartbeatHandler; use crate::gateway::resume_connection::resume_connection; use crate::gateway::GatewayUser; +use crate::util::token::check_token; -use super::{Connection, GatewayClient, NewConnection}; +use super::{Connection, GatewayClient, GatewayUsersStore, NewConnection}; /// `establish_connection` is the entrypoint method that gets called when a client tries to connect /// to the WebSocket server. @@ -30,6 +31,7 @@ pub(super) async fn establish_connection( stream: TcpStream, db: PgPool, // TODO: Do we need db here? config: Config, + gateway_users_store: GatewayUsersStore, ) -> Result { let ws_stream = accept_async(stream).await?; let mut connection: Connection = ws_stream.split().into(); @@ -56,7 +58,8 @@ pub(super) async fn establish_connection( _ = kill_receive.recv() => { return Err(GatewayError::Closed.into()); } - _ = tokio::time::sleep(std::time::Duration::from_secs(10)) => { + // If we do not receive an identifying or resuming message within 30 seconds, we close the connection. + _ = tokio::time::sleep(std::time::Duration::from_secs(30)) => { return Err(GatewayError::Timeout.into()); } else => { @@ -105,6 +108,15 @@ pub(super) async fn establish_connection( received_identify_or_resume = true; log::trace!(target: "symfonia::gateway::establish_connection", "Received identify payload"); // TODO: Verify token, build NewConnection + let claims = match check_token(&db, &identify.token, &config.security.jwt_secret).await { + Ok(claims) => claims, + Err(e) => { + log::trace!(target: "symfonia::gateway::establish_connection", "Failed to verify token: {}", e); + kill_send.send(()).expect("Failed to send kill signal"); + return Err(crate::errors::UserError::InvalidToken.into()); + } + }; + let gateway_user = get_or_new_gateway_user(claims.id, gateway_users_store.clone()).await; } else if let Ok(resume) = from_str::(&raw_message.to_string()) { received_identify_or_resume = true; log::trace!(target: "symfonia::gateway::establish_connection", "Received resume payload"); @@ -118,3 +130,22 @@ pub(super) async fn establish_connection( todo!() } + +/// `get_or_new_gateway_user` is a helper function that retrieves a [GatewayUser] from the store if it exists, +/// or creates a new user, stores it in the store and then returns it, if it does not exist. +async fn get_or_new_gateway_user( + user_id: Snowflake, + store: GatewayUsersStore, +) -> Arc> { + let mut store = store.lock().await; + if let Some(user) = store.get(&user_id) { + return user.clone(); + } + let user = Arc::new(Mutex::new(GatewayUser { + id: user_id, + clients: Vec::new(), + subscriptions: Vec::new(), + })); + store.insert(user_id, user.clone()); + user +} From 329d8fed9641c01e9a953622d95015744d8c367b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 31 Aug 2024 23:29:43 +0200 Subject: [PATCH 107/162] replace std mutex with tokio mutex We *need* async-aware mutexes because of the cache-like structures that are GatewayUsersStore and ResumableClientsStore, where it cannot be avoided that Mutex locks may be held across .await points. --- src/gateway/mod.rs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index daed034..91da332 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -13,7 +13,7 @@ mod resume_connection; mod types; use std::collections::BTreeMap; -use std::sync::{Arc, Mutex, Weak}; +use std::sync::{Arc, Weak}; use std::thread::sleep; use std::time::Duration; @@ -27,6 +27,7 @@ use pubserve::Subscriber; use serde_json::{from_str, json}; use sqlx::PgPool; use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::Mutex; use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::{accept_async, WebSocketStream}; @@ -91,6 +92,8 @@ struct GatewayClient { kill_send: tokio::sync::broadcast::Sender<()>, // Disconnect info for resuming the session disconnect_info: Option, + /// [blake3] token hash of the session token used for this connection + session_token_hash_blake3: String, } struct Connection { @@ -152,11 +155,16 @@ pub async fn start_gateway( let gateway_users: GatewayUsersStore = Arc::new(Mutex::new(BTreeMap::new())); let resumeable_clients: ResumableClientsStore = Arc::new(Mutex::new(BTreeMap::new())); - tokio::task::spawn(async { purge_expired_disconnects(resumeable_clients) }); + tokio::task::spawn(async { purge_expired_disconnects(resumeable_clients).await }); while let Ok((stream, _)) = listener.accept().await { log::trace!(target: "symfonia::gateway", "New connection received"); let connection_result = match tokio::task::spawn( - establish_connection::establish_connection(stream, db.clone(), config.clone()), + establish_connection::establish_connection( + stream, + db.clone(), + config.clone(), + gateway_users.clone(), + ), ) .await { @@ -167,7 +175,9 @@ pub async fn start_gateway( } }; match connection_result { - Ok(new_connection) => checked_add_new_connection(gateway_users.clone(), new_connection), + Ok(new_connection) => { + checked_add_new_connection(gateway_users.clone(), new_connection).await + } Err(e) => { log::debug!(target: "symfonia::gateway::establish_connection", "User gateway connection could not be established: {e}"); continue; @@ -180,7 +190,7 @@ pub async fn start_gateway( /// A disconnected, resumable session can only be resumed within `RESUME_RECONNECT_WINDOW_SECONDS` /// seconds after a disconnect occurs. Sessions that can be resumed are stored in a `Map`. The /// purpose of this method is to periodically throw out expired sessions from that map. -fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { +async fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { let mut minutely_log_timer = 0; let mut removed_elements_last_minute: u128 = 0; loop { @@ -191,7 +201,7 @@ fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { .expect("Check the clock/time settings on the host machine") .as_secs(); let mut to_remove = Vec::new(); - let mut resumeable_clients_lock = resumeable_clients.lock().unwrap(); + let mut resumeable_clients_lock = resumeable_clients.lock().await; for (disconnected_session_id, disconnected_session) in resumeable_clients_lock.iter() { let disconnect_info = match disconnected_session.disconnect_info.as_ref() { Some(d) => d, @@ -232,7 +242,7 @@ fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { /// `gateway_users``. /// /// Else, add the [new GatewayUser] and the new [GatewayClient] into `gateway_users` as-is. -fn checked_add_new_connection( +async fn checked_add_new_connection( gateway_users: Arc>>>>, new_connection: NewConnection, ) { @@ -240,17 +250,17 @@ fn checked_add_new_connection( let mut new_connection = new_connection; // To avoid having to get the lock a lot of times, lock once here and hold this lock for most // of the way through this method - let new_connection_user = new_connection.user.lock().unwrap(); - let mut locked_map = gateway_users.lock().unwrap(); + let new_connection_user = new_connection.user.lock().await; + let mut locked_map = gateway_users.lock().await; // If our map contains the user from `new_connection` already, modify the `parent` of the `client` // of `new_connection` to point to the user already in our map, then insert that `client` into // the `clients` field of our existing user. if locked_map.contains_key(&new_connection_user.id) { let existing_user = locked_map.get(&new_connection_user.id).unwrap(); - new_connection.client.lock().unwrap().parent = Arc::downgrade(existing_user); + new_connection.client.lock().await.parent = Arc::downgrade(existing_user); existing_user .lock() - .unwrap() + .await .clients .push(new_connection.client); } else { From a9c4f135594bfe36b586121e27bec84ef1b4a960 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 1 Sep 2024 23:42:20 +0200 Subject: [PATCH 108/162] hashing not required --- Cargo.toml | 3 +-- src/gateway/mod.rs | 12 +++++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7be9bb9..cdc0126 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,8 +71,7 @@ tokio-tungstenite = { version = "0.23.1", features = [ ] } pubserve = { version = "1.1.0", features = ["async", "send"] } parking_lot = { version = "0.12.3", features = ["deadlock_detection"] } - sqlx-pg-uint = { version = "0.5.0", features = ["serde"] } -blake3 = "1.5.4" + [dev-dependencies] rusty-hook = "0.11.2" diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 91da332..8102855 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -92,8 +92,8 @@ struct GatewayClient { kill_send: tokio::sync::broadcast::Sender<()>, // Disconnect info for resuming the session disconnect_info: Option, - /// [blake3] token hash of the session token used for this connection - session_token_hash_blake3: String, + /// Token of the session token used for this connection + session_token: String, } struct Connection { @@ -102,9 +102,8 @@ struct Connection { } struct DisconnectInfo { - // TODO: Token stored in memory must be hashed!! - /// [blake3] hash of the session token that was used for this connection - session_token_hash_blake3: String, + /// session token that was used for this connection + session_token: String, disconnected_at_sequence: u64, with_opcode: u32, } @@ -133,8 +132,7 @@ struct NewConnection { client: Arc>, } -// TODO: If the string is supposed to be a token, the token must be hashed before storing it in memory!! -/// A thread-shareable map of resumable clients. The key is a [blake3] hash of the session token used +/// A thread-shareable map of resumable clients. The key is the session token used /// for the connection. The value is a [GatewayClient] that can be resumed. type ResumableClientsStore = Arc>>; type GatewayUsersStore = Arc>>>>; From ee7bf970b62db56dc6975570217af32daa9b81e0 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 2 Sep 2024 22:10:51 +0200 Subject: [PATCH 109/162] development changes to chorus, bump sqlx-pg-uint --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cdc0126..b7fa818 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ sentry = { version = "0.34.0", default-features = false, features = [ clap = { version = "4.5.4", features = ["derive"] } chorus = { features = [ "backend", -], default-features = false, version = "0.16.0" } # git = "ssh://git@github.com/Quat3rnion/chorus" # path = "../chorus" git = "ssh://git@github.com/polyphony-chat/chorus" +], default-features = false, path = "../chorus" } serde_path_to_error = "0.1.16" percent-encoding = "2.3.1" hex = "0.4.3" @@ -71,7 +71,7 @@ tokio-tungstenite = { version = "0.23.1", features = [ ] } pubserve = { version = "1.1.0", features = ["async", "send"] } parking_lot = { version = "0.12.3", features = ["deadlock_detection"] } -sqlx-pg-uint = { version = "0.5.0", features = ["serde"] } +sqlx-pg-uint = { version = "0.7.1", features = ["serde"] } [dev-dependencies] rusty-hook = "0.11.2" From 309865b1ec78cda61f6084778a51f1cc735bfbb1 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 2 Sep 2024 22:11:23 +0200 Subject: [PATCH 110/162] First implementation of establish_connection and finish_connecting --- src/gateway/establish_connection.rs | 207 +++++++++++++++++++--------- 1 file changed, 139 insertions(+), 68 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 5e72a81..1ce3d5b 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -5,9 +5,12 @@ use chorus::types::{ Snowflake, }; use futures::{SinkExt, StreamExt}; +use log::trace; +use rand::seq; use serde_json::{from_str, json}; use sqlx::PgPool; use tokio::net::TcpStream; +use tokio::sync::broadcast::Sender; use tokio::sync::Mutex; use tokio::task::JoinHandle; use tokio_tungstenite::accept_async; @@ -17,7 +20,7 @@ use crate::database::entities::Config; use crate::errors::{Error, GatewayError}; use crate::gateway::heartbeat::HeartbeatHandler; use crate::gateway::resume_connection::resume_connection; -use crate::gateway::GatewayUser; +use crate::gateway::{gateway_task, GatewayUser}; use crate::util::token::check_token; use super::{Connection, GatewayClient, GatewayUsersStore, NewConnection}; @@ -33,98 +36,56 @@ pub(super) async fn establish_connection( config: Config, gateway_users_store: GatewayUsersStore, ) -> Result { + trace!(target: "symfonia::gateway::establish_connection", "Beginning process to establish connection (handshake)"); let ws_stream = accept_async(stream).await?; let mut connection: Connection = ws_stream.split().into(); + trace!(target: "symfonia::gateway::establish_connection", "Sending hello message"); // Hello message connection .sender .send(Message::Text(json!(GatewayHello::default()).to_string())) .await?; + trace!(target: "symfonia::gateway::establish_connection", "Sent hello message"); let connection = Arc::new(Mutex::new(connection)); let mut received_identify_or_resume = false; - let (kill_send, mut kill_receive) = tokio::sync::broadcast::channel(1); - let (message_send, message_receive) = tokio::sync::broadcast::channel(4); + let (kill_send, mut kill_receive) = tokio::sync::broadcast::channel::<()>(1); + let (message_send, message_receive) = tokio::sync::broadcast::channel::(4); let sequence_number = Arc::new(Mutex::new(0u64)); - let (session_id_send, session_id_receive) = tokio::sync::broadcast::channel(1); + let (session_id_send, session_id_receive) = tokio::sync::broadcast::channel::(1); // This JoinHandle `.is_some()` if we receive a heartbeat message *before* we receive an // identify or resume message. let mut heartbeat_handler_handle: Option> = None; + trace!(target: "symfonia::gateway::establish_connection", "Waiting for next message, timeout or kill signal..."); + let mut second_kill_receive = kill_receive.resubscribe(); tokio::select! { - _ = kill_receive.recv() => { + _ = second_kill_receive.recv() => { + trace!(target: "symfonia::gateway::establish_connection", "Connection was closed before we could establish it"); return Err(GatewayError::Closed.into()); } // If we do not receive an identifying or resuming message within 30 seconds, we close the connection. _ = tokio::time::sleep(std::time::Duration::from_secs(30)) => { + trace!(target: "symfonia::gateway::establish_connection", "Connection timed out: No message received within 30 seconds"); return Err(GatewayError::Timeout.into()); } - else => { - loop { - if received_identify_or_resume { - break; - } - - let raw_message = match connection.lock().await.receiver.next().await { - Some(next) => next, - None => return Err(GatewayError::Timeout.into()), - }?; - - if let Ok(heartbeat) = from_str::(&raw_message.to_string()) { - log::trace!(target: "symfonia::gateway::establish_connection", "Received heartbeat"); - match heartbeat_handler_handle { - None => { - // This only happens *once*. You will find that we have to `.resubscribe()` to - // the channels to make the borrow checker happy, because the channels are otherwise - // moved into the spawned task, which, *technically* could occur multiple times, - // due to the loop {} construct. However, this is not the case, because this code - // executes only if heartbeat_handler_handle is None, which is only true once, - // as we set it to Some(_) in this block. We could perhaps make this a little - // nicer by using unsafe rust magic, which would also allow us to use more appropriate - // channel types such as `oneshot` for the session_id_receive channel. However, - // I don't see that this is needed at the moment. - heartbeat_handler_handle = Some(tokio::spawn({ - let mut heartbeat_handler = HeartbeatHandler::new( - connection.clone(), - kill_receive.resubscribe(), - kill_send.clone(), - message_receive.resubscribe(), - sequence_number.clone(), - session_id_receive.resubscribe(), - ); - async move { - heartbeat_handler.run().await; - } - })) - } - Some(_) => { - message_send.send(heartbeat); - } - } - } else if let Ok(identify) = from_str::(&raw_message.to_string()) { - received_identify_or_resume = true; - log::trace!(target: "symfonia::gateway::establish_connection", "Received identify payload"); - // TODO: Verify token, build NewConnection - let claims = match check_token(&db, &identify.token, &config.security.jwt_secret).await { - Ok(claims) => claims, - Err(e) => { - log::trace!(target: "symfonia::gateway::establish_connection", "Failed to verify token: {}", e); - kill_send.send(()).expect("Failed to send kill signal"); - return Err(crate::errors::UserError::InvalidToken.into()); - } - }; - let gateway_user = get_or_new_gateway_user(claims.id, gateway_users_store.clone()).await; - } else if let Ok(resume) = from_str::(&raw_message.to_string()) { - received_identify_or_resume = true; - log::trace!(target: "symfonia::gateway::establish_connection", "Received resume payload"); - return resume_connection(connection, db, config, resume).await; - } else { - return Err(GatewayError::UnexpectedMessage.into()); - } - } + new_connection = finish_connecting( + connection.clone(), + heartbeat_handler_handle, + kill_receive, + kill_send, + message_receive, + message_send, + sequence_number, + session_id_receive, + db, + &config, + gateway_users_store.clone(), + ) => { + return new_connection; } } @@ -149,3 +110,113 @@ async fn get_or_new_gateway_user( store.insert(user_id, user.clone()); user } + +async fn finish_connecting( + connection: Arc>, + mut heartbeat_handler_handle: Option>, + kill_receive: tokio::sync::broadcast::Receiver<()>, + kill_send: tokio::sync::broadcast::Sender<()>, + message_receive: tokio::sync::broadcast::Receiver, + message_send: tokio::sync::broadcast::Sender, + sequence_number: Arc>, + session_id_receive: tokio::sync::broadcast::Receiver, + db: PgPool, + config: &Config, + gateway_users_store: GatewayUsersStore, +) -> Result { + loop { + trace!(target: "symfonia::gateway::establish_connection", "No resume or identify message received yet, waiting for next message..."); + trace!(target: "symfonia::gateway::establish_connection", "Waiting for next message..."); + let raw_message = match connection.lock().await.receiver.next().await { + Some(next) => next, + None => return Err(GatewayError::Timeout.into()), + }?; + trace!(target: "symfonia::gateway::establish_connection", "Received message: {:?}", raw_message); + + if let Ok(heartbeat) = from_str::(&raw_message.to_string()) { + log::trace!(target: "symfonia::gateway::establish_connection", "Received heartbeat"); + match heartbeat_handler_handle { + None => { + // This only happens *once*. You will find that we have to `.resubscribe()` to + // the channels to make the borrow checker happy, because the channels are otherwise + // moved into the spawned task, which, *technically* could occur multiple times, + // due to the loop {} construct. However, this is not the case, because this code + // executes only if heartbeat_handler_handle is None, which is only true once, + // as we set it to Some(_) in this block. We could perhaps make this a little + // nicer by using unsafe rust magic, which would also allow us to use more appropriate + // channel types such as `oneshot` for the session_id_receive channel. However, + // I don't see that this is needed at the moment. + heartbeat_handler_handle = Some(tokio::spawn({ + let mut heartbeat_handler = HeartbeatHandler::new( + connection.clone(), + kill_receive.resubscribe(), + kill_send.clone(), + message_receive.resubscribe(), + sequence_number.clone(), + session_id_receive.resubscribe(), + ); + async move { + heartbeat_handler.run().await; + } + })) + } + Some(_) => { + message_send.send(heartbeat); + } + } + } else if let Ok(identify) = from_str::(&raw_message.to_string()) { + log::trace!(target: "symfonia::gateway::establish_connection", "Received identify payload"); + let claims = match check_token(&db, &identify.token, &config.security.jwt_secret).await + { + Ok(claims) => claims, + Err(_) => { + log::trace!(target: "symfonia::gateway::establish_connection", "Failed to verify token"); + kill_send.send(()).expect("Failed to send kill signal"); + return Err(crate::errors::UserError::InvalidToken.into()); + } + }; + let mut gateway_user = + get_or_new_gateway_user(claims.id, gateway_users_store.clone()).await; + let gateway_client = GatewayClient { + parent: Arc::downgrade(&gateway_user), + connection: connection.clone(), + main_task_handle: tokio::spawn(gateway_task::gateway_task(connection.clone())), + heartbeat_task_handle: match heartbeat_handler_handle { + Some(handle) => handle, + None => tokio::spawn({ + let mut heartbeat_handler = HeartbeatHandler::new( + connection.clone(), + kill_receive.resubscribe(), + kill_send.clone(), + message_receive.resubscribe(), + sequence_number.clone(), + session_id_receive.resubscribe(), + ); + async move { + heartbeat_handler.run().await; + } + }), + }, + kill_send, + disconnect_info: None, + session_token: identify.token, + }; + let gateway_client_arc_mutex = Arc::new(Mutex::new(gateway_client)); + gateway_user + .lock() + .await + .clients + .push(gateway_client_arc_mutex.clone()); + return Ok(NewConnection { + user: gateway_user, + client: gateway_client_arc_mutex.clone(), + }); + } else if let Ok(resume) = from_str::(&raw_message.to_string()) { + log::trace!(target: "symfonia::gateway::establish_connection", "Received resume payload"); + return resume_connection(connection, db, config.to_owned(), resume).await; + } else { + trace!(target: "symfonia::gateway::establish_connection", "Received unexpected message: {:?}", raw_message); + return Err(GatewayError::UnexpectedMessage.into()); + } + } +} From 76e5475e412ff10651264ee7da31b34c7e32a985 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 2 Sep 2024 22:12:34 +0200 Subject: [PATCH 111/162] change gateway_task to take connection instead of gateway client to avoid cyclic dependency --- src/gateway/gateway_task.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs index 535507d..6b9e160 100644 --- a/src/gateway/gateway_task.rs +++ b/src/gateway/gateway_task.rs @@ -1,10 +1,12 @@ -use std::sync::{Arc, Mutex}; +use std::sync::Arc; + +use tokio::sync::Mutex; use super::GatewayClient; /// Handles all messages a client sends to the gateway post-handshake. pub(super) async fn gateway_task( - client: Arc>, + connection: Arc>, ) -> Result<(), crate::errors::Error> { // TODO todo!() From 787fd4631a58a66df2e2e814eeb6ac313958efb8 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 2 Sep 2024 22:13:14 +0200 Subject: [PATCH 112/162] additional trace logs --- src/gateway/heartbeat.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index e507acd..4c6bfe4 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -69,6 +69,7 @@ impl HeartbeatHandler { last_sequence_number: Arc>, session_id_receive: tokio::sync::broadcast::Receiver, ) -> Self { + trace!(target: "symfonia::gateway::heartbeat_handler", "New heartbeat handler created"); Self { connection, kill_receive, @@ -120,6 +121,7 @@ impl HeartbeatHandler { /// }); /// ``` pub(super) async fn run(&mut self) { + trace!(target: "symfonia::gateway::heartbeat_handler", "Starting heartbeat handler"); // TODO: On death of this task, create and store disconnect info in gateway client object let mut sequence = 0u64; loop { From 63980159f70422c54b2ace9f14635205ac140640 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 2 Sep 2024 22:14:19 +0200 Subject: [PATCH 113/162] change direction of comparison to fix bug --- src/util/token.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/util/token.rs b/src/util/token.rs index 83c03f6..0c6a8fc 100644 --- a/src/util/token.rs +++ b/src/util/token.rs @@ -21,7 +21,9 @@ pub async fn check_token(db: &PgPool, token: &str, jwt_secret: &str) -> Result) to fix a bug. I don't know if this is correct, + // nor have I looked at this code at all to see if it's correct. + if chrono::DateTime::from_timestamp(token.claims.iat, 0).unwrap() > user.data.valid_tokens_since { return Err(Error::User(UserError::InvalidToken)); } From d649f8e3b1a7d47e52c2df79907c8d3e33be858e Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 3 Sep 2024 22:08:31 +0200 Subject: [PATCH 114/162] Add generic GatewayPayload type --- src/gateway/types.rs | 47 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/gateway/types.rs b/src/gateway/types.rs index 5e647bb..2ca6f18 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -2,6 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use ::serde::de::DeserializeOwned; use ::serde::{Deserialize, Serialize}; use chorus::types::*; @@ -126,3 +127,49 @@ pub enum Event { StageInstanceUpdate(StageInstanceUpdate), StageInstanceDelete(StageInstanceDelete), } + +#[derive(Serialize, Clone, PartialEq, Debug)] +/// A de-/serializable data payload for transmission over the gateway. +pub struct GatewayPayload +where + T: Serialize + DeserializeOwned, +{ + #[serde(rename = "op")] + pub op_code: u8, + #[serde(rename = "d")] + pub event_data: T, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "s")] + pub sequence_number: Option, + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "t")] + pub event_name: Option, +} + +impl<'de, T: DeserializeOwned + Serialize> Deserialize<'de> for GatewayPayload { + fn deserialize(deserializer: D) -> Result + where + D: ::serde::Deserializer<'de>, + { + let value = serde_json::Value::deserialize(deserializer)?; + let op_code = value["op"].as_u64().unwrap() as u8; + let event_data = match value.get("d").cloned() { + Some(data) => match serde_json::from_value(data) { + Ok(t) => t, + Err(e) => return Err(::serde::de::Error::custom(e)), + }, + None => return Err(::serde::de::Error::missing_field("d")), + }; + let sequence_number = value.get("s").cloned().map(|v| v.as_u64().unwrap()); + let event_name = match value.get("t") { + Some(v) => v.as_str().map(|v_str| v_str.to_string()), + None => None, + }; + Ok(GatewayPayload { + op_code, + event_data, + sequence_number, + event_name, + }) + } +} From 4bad024b32d064118f5d5cef65b32142c65f01b4 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 3 Sep 2024 22:20:15 +0200 Subject: [PATCH 115/162] More log granularity --- src/gateway/establish_connection.rs | 57 +++++++++++++++++++---------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 1ce3d5b..8b52ea5 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -5,7 +5,7 @@ use chorus::types::{ Snowflake, }; use futures::{SinkExt, StreamExt}; -use log::trace; +use log::{debug, trace}; use rand::seq; use serde_json::{from_str, json}; use sqlx::PgPool; @@ -20,7 +20,7 @@ use crate::database::entities::Config; use crate::errors::{Error, GatewayError}; use crate::gateway::heartbeat::HeartbeatHandler; use crate::gateway::resume_connection::resume_connection; -use crate::gateway::{gateway_task, GatewayUser}; +use crate::gateway::{gateway_task, GatewayPayload, GatewayUser}; use crate::util::token::check_token; use super::{Connection, GatewayClient, GatewayUsersStore, NewConnection}; @@ -36,16 +36,16 @@ pub(super) async fn establish_connection( config: Config, gateway_users_store: GatewayUsersStore, ) -> Result { - trace!(target: "symfonia::gateway::establish_connection", "Beginning process to establish connection (handshake)"); + trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Beginning process to establish connection (handshake)"); let ws_stream = accept_async(stream).await?; let mut connection: Connection = ws_stream.split().into(); - trace!(target: "symfonia::gateway::establish_connection", "Sending hello message"); + trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Sending hello message"); // Hello message connection .sender .send(Message::Text(json!(GatewayHello::default()).to_string())) .await?; - trace!(target: "symfonia::gateway::establish_connection", "Sent hello message"); + trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Sent hello message"); let connection = Arc::new(Mutex::new(connection)); @@ -60,18 +60,20 @@ pub(super) async fn establish_connection( // identify or resume message. let mut heartbeat_handler_handle: Option> = None; - trace!(target: "symfonia::gateway::establish_connection", "Waiting for next message, timeout or kill signal..."); + trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Waiting for next message, timeout or kill signal..."); let mut second_kill_receive = kill_receive.resubscribe(); tokio::select! { _ = second_kill_receive.recv() => { - trace!(target: "symfonia::gateway::establish_connection", "Connection was closed before we could establish it"); + debug!(target: "symfonia::gateway::establish_connection::establish_connection", "Connection was closed before we could establish it"); return Err(GatewayError::Closed.into()); } // If we do not receive an identifying or resuming message within 30 seconds, we close the connection. _ = tokio::time::sleep(std::time::Duration::from_secs(30)) => { - trace!(target: "symfonia::gateway::establish_connection", "Connection timed out: No message received within 30 seconds"); + debug!(target: "symfonia::gateway::establish_connection::establish_connection", "Connection timed out: No message received within 30 seconds"); return Err(GatewayError::Timeout.into()); } + // Since async closures are not yet stable, we have to use a dedicated function to handle the + // connection establishment process. :( new_connection = finish_connecting( connection.clone(), heartbeat_handler_handle, @@ -111,6 +113,11 @@ async fn get_or_new_gateway_user( user } +/// `finish_connecting` is the second part of the connection establishment process. It picks up after +/// the initial `Hello` message has been sent to the client. It then waits on the next message from +/// the client, which should be either a `Heartbeat`, `Identify` or `Resume` message, handling each +/// case accordingly. +#[allow(clippy::too_many_arguments)] async fn finish_connecting( connection: Arc>, mut heartbeat_handler_handle: Option>, @@ -125,16 +132,15 @@ async fn finish_connecting( gateway_users_store: GatewayUsersStore, ) -> Result { loop { - trace!(target: "symfonia::gateway::establish_connection", "No resume or identify message received yet, waiting for next message..."); - trace!(target: "symfonia::gateway::establish_connection", "Waiting for next message..."); + trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Waiting for next message..."); let raw_message = match connection.lock().await.receiver.next().await { Some(next) => next, None => return Err(GatewayError::Timeout.into()), }?; - trace!(target: "symfonia::gateway::establish_connection", "Received message: {:?}", raw_message); + debug!(target: "symfonia::gateway::establish_connection::finish_connecting", "Received message"); if let Ok(heartbeat) = from_str::(&raw_message.to_string()) { - log::trace!(target: "symfonia::gateway::establish_connection", "Received heartbeat"); + log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Received heartbeat"); match heartbeat_handler_handle { None => { // This only happens *once*. You will find that we have to `.resubscribe()` to @@ -164,13 +170,23 @@ async fn finish_connecting( message_send.send(heartbeat); } } - } else if let Ok(identify) = from_str::(&raw_message.to_string()) { - log::trace!(target: "symfonia::gateway::establish_connection", "Received identify payload"); - let claims = match check_token(&db, &identify.token, &config.security.jwt_secret).await + } else if let Ok(identify) = + from_str::>(&raw_message.to_string()) + { + log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Received identify payload"); + let claims = match check_token( + &db, + &identify.event_data.token, + &config.security.jwt_secret, + ) + .await { - Ok(claims) => claims, + Ok(claims) => { + trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Token verified"); + claims + } Err(_) => { - log::trace!(target: "symfonia::gateway::establish_connection", "Failed to verify token"); + log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Failed to verify token"); kill_send.send(()).expect("Failed to send kill signal"); return Err(crate::errors::UserError::InvalidToken.into()); } @@ -199,7 +215,7 @@ async fn finish_connecting( }, kill_send, disconnect_info: None, - session_token: identify.token, + session_token: identify.event_data.token, }; let gateway_client_arc_mutex = Arc::new(Mutex::new(gateway_client)); gateway_user @@ -212,10 +228,11 @@ async fn finish_connecting( client: gateway_client_arc_mutex.clone(), }); } else if let Ok(resume) = from_str::(&raw_message.to_string()) { - log::trace!(target: "symfonia::gateway::establish_connection", "Received resume payload"); + log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Received resume payload"); return resume_connection(connection, db, config.to_owned(), resume).await; } else { - trace!(target: "symfonia::gateway::establish_connection", "Received unexpected message: {:?}", raw_message); + debug!(target: "symfonia::gateway::establish_connection::finish_connecting", "Message could not be decoded as resume, heartbeat or identify."); + return Err(GatewayError::UnexpectedMessage.into()); } } From 4afebd7187d50300f0bac6351b386480db005de3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 3 Sep 2024 23:00:44 +0200 Subject: [PATCH 116/162] die. (the function) --- src/gateway/establish_connection.rs | 26 +++++++++++++++++--------- src/gateway/gateway_task.rs | 4 +--- src/gateway/mod.rs | 23 ++++++++++++++++++++--- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 8b52ea5..01c572d 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -23,7 +23,7 @@ use crate::gateway::resume_connection::resume_connection; use crate::gateway::{gateway_task, GatewayPayload, GatewayUser}; use crate::util::token::check_token; -use super::{Connection, GatewayClient, GatewayUsersStore, NewConnection}; +use super::{Connection, GatewayClient, GatewayUsersStore, NewConnection, ResumableClientsStore}; /// `establish_connection` is the entrypoint method that gets called when a client tries to connect /// to the WebSocket server. @@ -35,6 +35,7 @@ pub(super) async fn establish_connection( db: PgPool, // TODO: Do we need db here? config: Config, gateway_users_store: GatewayUsersStore, + resumeable_clients_store: ResumableClientsStore, ) -> Result { trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Beginning process to establish connection (handshake)"); let ws_stream = accept_async(stream).await?; @@ -53,7 +54,7 @@ pub(super) async fn establish_connection( let (kill_send, mut kill_receive) = tokio::sync::broadcast::channel::<()>(1); let (message_send, message_receive) = tokio::sync::broadcast::channel::(4); - let sequence_number = Arc::new(Mutex::new(0u64)); + let sequence_number = Arc::new(Mutex::new(0u64)); // TODO: Actually use this, as in: Increment it when needed. Currently, this is not being done. let (session_id_send, session_id_receive) = tokio::sync::broadcast::channel::(1); // This JoinHandle `.is_some()` if we receive a heartbeat message *before* we receive an @@ -65,12 +66,12 @@ pub(super) async fn establish_connection( tokio::select! { _ = second_kill_receive.recv() => { debug!(target: "symfonia::gateway::establish_connection::establish_connection", "Connection was closed before we could establish it"); - return Err(GatewayError::Closed.into()); + Err(GatewayError::Closed.into()) } // If we do not receive an identifying or resuming message within 30 seconds, we close the connection. _ = tokio::time::sleep(std::time::Duration::from_secs(30)) => { debug!(target: "symfonia::gateway::establish_connection::establish_connection", "Connection timed out: No message received within 30 seconds"); - return Err(GatewayError::Timeout.into()); + Err(GatewayError::Timeout.into()) } // Since async closures are not yet stable, we have to use a dedicated function to handle the // connection establishment process. :( @@ -86,12 +87,11 @@ pub(super) async fn establish_connection( db, &config, gateway_users_store.clone(), + resumeable_clients_store.clone(), ) => { - return new_connection; + new_connection } } - - todo!() } /// `get_or_new_gateway_user` is a helper function that retrieves a [GatewayUser] from the store if it exists, @@ -99,6 +99,7 @@ pub(super) async fn establish_connection( async fn get_or_new_gateway_user( user_id: Snowflake, store: GatewayUsersStore, + resumeable_clients_store: ResumableClientsStore, ) -> Arc> { let mut store = store.lock().await; if let Some(user) = store.get(&user_id) { @@ -108,6 +109,7 @@ async fn get_or_new_gateway_user( id: user_id, clients: Vec::new(), subscriptions: Vec::new(), + resumeable_clients_store: Arc::downgrade(&resumeable_clients_store), })); store.insert(user_id, user.clone()); user @@ -130,6 +132,7 @@ async fn finish_connecting( db: PgPool, config: &Config, gateway_users_store: GatewayUsersStore, + resumeable_clients_store: ResumableClientsStore, ) -> Result { loop { trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Waiting for next message..."); @@ -191,8 +194,12 @@ async fn finish_connecting( return Err(crate::errors::UserError::InvalidToken.into()); } }; - let mut gateway_user = - get_or_new_gateway_user(claims.id, gateway_users_store.clone()).await; + let mut gateway_user = get_or_new_gateway_user( + claims.id, + gateway_users_store.clone(), + resumeable_clients_store.clone(), + ) + .await; let gateway_client = GatewayClient { parent: Arc::downgrade(&gateway_user), connection: connection.clone(), @@ -216,6 +223,7 @@ async fn finish_connecting( kill_send, disconnect_info: None, session_token: identify.event_data.token, + last_sequence: sequence_number.clone(), }; let gateway_client_arc_mutex = Arc::new(Mutex::new(gateway_client)); gateway_user diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs index 6b9e160..a1e166d 100644 --- a/src/gateway/gateway_task.rs +++ b/src/gateway/gateway_task.rs @@ -5,9 +5,7 @@ use tokio::sync::Mutex; use super::GatewayClient; /// Handles all messages a client sends to the gateway post-handshake. -pub(super) async fn gateway_task( - connection: Arc>, -) -> Result<(), crate::errors::Error> { +pub(super) async fn gateway_task(connection: Arc>) { // TODO todo!() } diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 8102855..837da22 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -77,6 +77,8 @@ struct GatewayUser { /// A GatewayUser may have many [GatewayClients](GatewayClient), but he only gets subscribed to /// all relevant [Publishers](pubserve::Publisher) *once* to save resources. subscriptions: Vec>>, + /// [Weak] reference to the [ResumableClientsStore]. + resumeable_clients_store: Weak>>, } /// A concrete session, that a [GatewayUser] is connected to the Gateway with. @@ -85,7 +87,7 @@ struct GatewayClient { /// A [Weak] reference to the [GatewayUser] this client belongs to. parent: Weak>, // Handle to the main Gateway task for this client - main_task_handle: tokio::task::JoinHandle>, + main_task_handle: tokio::task::JoinHandle<()>, // Handle to the heartbeat task for this client heartbeat_task_handle: tokio::task::JoinHandle<()>, // Kill switch to disconnect the client @@ -94,6 +96,20 @@ struct GatewayClient { disconnect_info: Option, /// Token of the session token used for this connection session_token: String, + /// The last sequence number received from the client. Shared between the main task, heartbeat + /// task, and this struct. + last_sequence: Arc>, +} + +impl GatewayClient { + pub async fn die(&mut self) { + self.kill_send.send(()).unwrap(); + let disconnect_info = DisconnectInfo { + session_token: self.session_token.clone(), + disconnected_at_sequence: *self.last_sequence.lock().await, + }; + // TODO: Remove self from parent's clients, add disconnect info to resumeable_clients + } } struct Connection { @@ -105,7 +121,6 @@ struct DisconnectInfo { /// session token that was used for this connection session_token: String, disconnected_at_sequence: u64, - with_opcode: u32, } impl @@ -153,7 +168,8 @@ pub async fn start_gateway( let gateway_users: GatewayUsersStore = Arc::new(Mutex::new(BTreeMap::new())); let resumeable_clients: ResumableClientsStore = Arc::new(Mutex::new(BTreeMap::new())); - tokio::task::spawn(async { purge_expired_disconnects(resumeable_clients).await }); + let resumeable_clients_clone = resumeable_clients.clone(); + tokio::task::spawn(async { purge_expired_disconnects(resumeable_clients_clone).await }); while let Ok((stream, _)) = listener.accept().await { log::trace!(target: "symfonia::gateway", "New connection received"); let connection_result = match tokio::task::spawn( @@ -162,6 +178,7 @@ pub async fn start_gateway( db.clone(), config.clone(), gateway_users.clone(), + resumeable_clients.clone(), ), ) .await From 3d568ff3bda26ae219755e154eadc67fe7b52e40 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 11 Sep 2024 17:52:19 +0200 Subject: [PATCH 117/162] Make clients prop of GatewayUser be HashMap instead of Vec --- src/gateway/establish_connection.rs | 14 +++++++------- src/gateway/mod.rs | 9 +++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 01c572d..262a8d8 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::sync::Arc; use chorus::types::{ @@ -32,7 +33,7 @@ use super::{Connection, GatewayClient, GatewayUsersStore, NewConnection, Resumab /// [GatewayClient], whose `.parent` field contains a [Weak] reference to the new [GatewayUser]. pub(super) async fn establish_connection( stream: TcpStream, - db: PgPool, // TODO: Do we need db here? + db: PgPool, config: Config, gateway_users_store: GatewayUsersStore, resumeable_clients_store: ResumableClientsStore, @@ -107,7 +108,7 @@ async fn get_or_new_gateway_user( } let user = Arc::new(Mutex::new(GatewayUser { id: user_id, - clients: Vec::new(), + clients: HashMap::new(), subscriptions: Vec::new(), resumeable_clients_store: Arc::downgrade(&resumeable_clients_store), })); @@ -226,11 +227,10 @@ async fn finish_connecting( last_sequence: sequence_number.clone(), }; let gateway_client_arc_mutex = Arc::new(Mutex::new(gateway_client)); - gateway_user - .lock() - .await - .clients - .push(gateway_client_arc_mutex.clone()); + gateway_user.lock().await.clients.insert( + gateway_client_arc_mutex.lock().await.session_token.clone(), + gateway_client_arc_mutex.clone(), + ); return Ok(NewConnection { user: gateway_user, client: gateway_client_arc_mutex.clone(), diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 837da22..fbea1ea 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -12,7 +12,7 @@ mod heartbeat; mod resume_connection; mod types; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashMap}; use std::sync::{Arc, Weak}; use std::thread::sleep; use std::time::Duration; @@ -68,8 +68,8 @@ GatewayUser is subscribed to /// A single identifiable User connected to the Gateway - possibly using many clients at the same /// time. struct GatewayUser { - /// Sessions a User is connected with. - clients: Vec>>, + /// Sessions a User is connected with. HashMap of SessionToken -> GatewayClient + clients: HashMap>>, /// The Snowflake ID of the User. id: Snowflake, /// A collection of [Subscribers](Subscriber) to [Event] [Publishers](pubserve::Publisher). @@ -266,6 +266,7 @@ async fn checked_add_new_connection( // To avoid having to get the lock a lot of times, lock once here and hold this lock for most // of the way through this method let new_connection_user = new_connection.user.lock().await; + let new_connection_token = new_connection.client.lock().await.session_token.clone(); let mut locked_map = gateway_users.lock().await; // If our map contains the user from `new_connection` already, modify the `parent` of the `client` // of `new_connection` to point to the user already in our map, then insert that `client` into @@ -277,7 +278,7 @@ async fn checked_add_new_connection( .lock() .await .clients - .push(new_connection.client); + .insert(new_connection_token, new_connection.client); } else { // We cannot do `locked_map.insert(id, new_connection.user)` if new_connection is still // locked. Just bind the id we need to a new variable, then drop the lock. From 567e89d754ceddfb928ce463c223710a1c86a32c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 11 Sep 2024 22:11:29 +0200 Subject: [PATCH 118/162] things --- src/gateway/establish_connection.rs | 1 - src/gateway/mod.rs | 20 ++++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 262a8d8..964034c 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -222,7 +222,6 @@ async fn finish_connecting( }), }, kill_send, - disconnect_info: None, session_token: identify.event_data.token, last_sequence: sequence_number.clone(), }; diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index fbea1ea..811b865 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -92,8 +92,6 @@ struct GatewayClient { heartbeat_task_handle: tokio::task::JoinHandle<()>, // Kill switch to disconnect the client kill_send: tokio::sync::broadcast::Sender<()>, - // Disconnect info for resuming the session - disconnect_info: Option, /// Token of the session token used for this connection session_token: String, /// The last sequence number received from the client. Shared between the main task, heartbeat @@ -102,13 +100,24 @@ struct GatewayClient { } impl GatewayClient { - pub async fn die(&mut self) { + pub async fn die(mut self, resumeable_clients: ResumableClientsStore) { self.kill_send.send(()).unwrap(); let disconnect_info = DisconnectInfo { session_token: self.session_token.clone(), disconnected_at_sequence: *self.last_sequence.lock().await, }; // TODO: Remove self from parent's clients, add disconnect info to resumeable_clients + self.parent + .upgrade() + .unwrap() + .lock() + .await + .clients + .remove(&self.session_token); + resumeable_clients + .lock() + .await + .insert(self.session_token.clone(), self); } } @@ -121,6 +130,7 @@ struct DisconnectInfo { /// session token that was used for this connection session_token: String, disconnected_at_sequence: u64, + parent: Weak>, } impl @@ -149,7 +159,9 @@ struct NewConnection { /// A thread-shareable map of resumable clients. The key is the session token used /// for the connection. The value is a [GatewayClient] that can be resumed. -type ResumableClientsStore = Arc>>; +// TODO: this is stupid. it should be a map of string and DisconnectInfo. there is no need to store +// the whole GatewayClient, nor would it make sense to do so. +type ResumableClientsStore = Arc>>; type GatewayUsersStore = Arc>>>>; pub async fn start_gateway( From 7dbdcb9d1784bdc6bbdedb01eb596e501f1d3061 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 14 Sep 2024 23:24:34 +0200 Subject: [PATCH 119/162] fix mod.rs issues --- src/gateway/mod.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 811b865..bd14355 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -105,8 +105,8 @@ impl GatewayClient { let disconnect_info = DisconnectInfo { session_token: self.session_token.clone(), disconnected_at_sequence: *self.last_sequence.lock().await, + parent: self.parent.clone(), }; - // TODO: Remove self from parent's clients, add disconnect info to resumeable_clients self.parent .upgrade() .unwrap() @@ -117,7 +117,7 @@ impl GatewayClient { resumeable_clients .lock() .await - .insert(self.session_token.clone(), self); + .insert(self.session_token.clone(), disconnect_info); } } @@ -230,15 +230,8 @@ async fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { let mut to_remove = Vec::new(); let mut resumeable_clients_lock = resumeable_clients.lock().await; for (disconnected_session_id, disconnected_session) in resumeable_clients_lock.iter() { - let disconnect_info = match disconnected_session.disconnect_info.as_ref() { - Some(d) => d, - None => { - // This shouldn't happen, but if it does, remove the session from the list - to_remove.push(disconnected_session_id.clone()); - continue; - } - }; - if current_unix_timestamp - disconnect_info.disconnected_at_sequence + // TODO(bitfl0wer): What are we calculating here? At least, this should be commented + if current_unix_timestamp - disconnected_session.disconnected_at_sequence > RESUME_RECONNECT_WINDOW_SECONDS as u64 { to_remove.push(disconnected_session_id.clone()); From b284b406cee91f839a1c0f55fb62e2b8bb65e708 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sat, 14 Sep 2024 23:26:04 +0200 Subject: [PATCH 120/162] add todo to get_or_new_gateway_user --- src/gateway/establish_connection.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 964034c..8563f5f 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -97,6 +97,7 @@ pub(super) async fn establish_connection( /// `get_or_new_gateway_user` is a helper function that retrieves a [GatewayUser] from the store if it exists, /// or creates a new user, stores it in the store and then returns it, if it does not exist. +// TODO: Refactor this function according to the new `ResumeableClientsStore` definition. async fn get_or_new_gateway_user( user_id: Snowflake, store: GatewayUsersStore, From f5da07aa0deb96f76ecc78c42a4464f32538739d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 16 Sep 2024 16:30:50 +0200 Subject: [PATCH 121/162] Remove resume_connection for now --- src/gateway/establish_connection.rs | 46 +++++++++++++++++++---------- src/gateway/heartbeat.rs | 9 +++--- src/gateway/mod.rs | 36 ++++++++++++---------- src/gateway/resume_connection.rs | 21 ------------- 4 files changed, 57 insertions(+), 55 deletions(-) delete mode 100644 src/gateway/resume_connection.rs diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 8563f5f..4303f43 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -1,5 +1,4 @@ -use std::collections::HashMap; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; use chorus::types::{ GatewayHeartbeat, GatewayHeartbeatAck, GatewayHello, GatewayIdentifyPayload, GatewayResume, @@ -10,19 +9,25 @@ use log::{debug, trace}; use rand::seq; use serde_json::{from_str, json}; use sqlx::PgPool; -use tokio::net::TcpStream; -use tokio::sync::broadcast::Sender; -use tokio::sync::Mutex; -use tokio::task::JoinHandle; -use tokio_tungstenite::accept_async; -use tokio_tungstenite::tungstenite::Message; +use tokio::{ + net::TcpStream, + sync::{broadcast::Sender, Mutex}, + task::JoinHandle, +}; +use tokio_tungstenite::{ + accept_async, + tungstenite::{ + protocol::{frame::coding::CloseCode, CloseFrame}, + Message, + }, +}; -use crate::database::entities::Config; -use crate::errors::{Error, GatewayError}; -use crate::gateway::heartbeat::HeartbeatHandler; -use crate::gateway::resume_connection::resume_connection; -use crate::gateway::{gateway_task, GatewayPayload, GatewayUser}; -use crate::util::token::check_token; +use crate::{ + database::entities::Config, + errors::{Error, GatewayError}, + gateway::{gateway_task, heartbeat::HeartbeatHandler, GatewayPayload, GatewayUser}, + util::token::check_token, +}; use super::{Connection, GatewayClient, GatewayUsersStore, NewConnection, ResumableClientsStore}; @@ -237,7 +242,18 @@ async fn finish_connecting( }); } else if let Ok(resume) = from_str::(&raw_message.to_string()) { log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Received resume payload"); - return resume_connection(connection, db, config.to_owned(), resume).await; + log::warn!(target: "symfonia::gateway::establish_connection::finish_connecting", "Resuming connections is not yet implemented. Telling client to identify instead."); + connection + .lock() + .await + .sender + .send(Message::Close(Some(CloseFrame { + code: CloseCode::from(4000u16), + reason: "Resuming connections is not yet implemented. Please identify instead." + .into(), + }))) + .await?; + kill_send.send(()).expect("Failed to send kill signal"); } else { debug!(target: "symfonia::gateway::establish_connection::finish_connecting", "Message could not be decoded as resume, heartbeat or identify."); diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index 4c6bfe4..7078cc3 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -6,9 +6,10 @@ use log::*; use rand::seq; use serde_json::json; use tokio::sync::Mutex; -use tokio_tungstenite::tungstenite::protocol::frame::coding::OpCode; -use tokio_tungstenite::tungstenite::protocol::CloseFrame; -use tokio_tungstenite::tungstenite::Message; +use tokio_tungstenite::tungstenite::{ + protocol::{frame::coding::OpCode, CloseFrame}, + Message, +}; use crate::gateway::DisconnectInfo; @@ -195,7 +196,7 @@ impl HeartbeatHandler { let min = std::cmp::min(one, two); match max - min { 0 => SequenceNumberComparison::Correct, - 1..2 => SequenceNumberComparison::SlightlyOff(max - min), + 1..=2 => SequenceNumberComparison::SlightlyOff(max - min), _ => SequenceNumberComparison::WayOff(max - min), } } diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index bd14355..ccd3b1a 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -9,37 +9,43 @@ static RESUME_RECONNECT_WINDOW_SECONDS: u8 = 90; mod establish_connection; mod gateway_task; mod heartbeat; -mod resume_connection; mod types; -use std::collections::{BTreeMap, HashMap}; -use std::sync::{Arc, Weak}; -use std::thread::sleep; -use std::time::Duration; +use std::{ + collections::{BTreeMap, HashMap}, + sync::{Arc, Weak}, + thread::sleep, + time::Duration, +}; use chorus::types::{ GatewayHeartbeat, GatewayHello, GatewayIdentifyPayload, GatewayResume, Snowflake, }; -use futures::stream::{SplitSink, SplitStream}; -use futures::{SinkExt, StreamExt}; +use futures::{ + stream::{SplitSink, SplitStream}, + SinkExt, StreamExt, +}; use log::info; use pubserve::Subscriber; use serde_json::{from_str, json}; use sqlx::PgPool; -use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::Mutex; +use tokio::{ + net::{TcpListener, TcpStream}, + sync::Mutex, +}; -use tokio_tungstenite::tungstenite::Message; -use tokio_tungstenite::{accept_async, WebSocketStream}; +use tokio_tungstenite::{accept_async, tungstenite::Message, WebSocketStream}; pub use types::*; use crate::database::entities::Config; // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use crate::errors::{Error, GatewayError}; -use crate::util::token::check_token; -use crate::{SharedEventPublisherMap, WebSocketReceive, WebSocketSend}; +use crate::{ + errors::{Error, GatewayError}, + util::token::check_token, + SharedEventPublisherMap, WebSocketReceive, WebSocketSend, +}; /* NOTES (bitfl0wer) [These will be removed] The gateway is supposed to be highly concurrent. It will be handling a lot of connections at once. @@ -78,7 +84,7 @@ struct GatewayUser { /// all relevant [Publishers](pubserve::Publisher) *once* to save resources. subscriptions: Vec>>, /// [Weak] reference to the [ResumableClientsStore]. - resumeable_clients_store: Weak>>, + resumeable_clients_store: Weak>>, } /// A concrete session, that a [GatewayUser] is connected to the Gateway with. diff --git a/src/gateway/resume_connection.rs b/src/gateway/resume_connection.rs deleted file mode 100644 index 860ef0a..0000000 --- a/src/gateway/resume_connection.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::sync::Arc; - -use chorus::types::GatewayResume; -use sqlx::PgPool; -use tokio::net::TcpStream; -use tokio::sync::Mutex; - -use crate::database::entities::Config; -use crate::errors::Error; - -use super::{Connection, NewConnection}; - -pub(super) async fn resume_connection( - connection: Arc>, - db: PgPool, - config: Config, - resume_message: GatewayResume, -) -> Result { - // TODO - todo!() -} From 10f86e7f6ea6c78915df9bda525706fbeb8d3c17 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 16 Sep 2024 16:45:25 +0200 Subject: [PATCH 122/162] change close code to 4007 https://discord.com/developers/docs/topics/opcodes-and-status-codes#gateway-gateway-close-event-codes --- src/gateway/establish_connection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 4303f43..095a7f0 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -248,7 +248,7 @@ async fn finish_connecting( .await .sender .send(Message::Close(Some(CloseFrame { - code: CloseCode::from(4000u16), + code: CloseCode::from(4007u16), reason: "Resuming connections is not yet implemented. Please identify instead." .into(), }))) From 0ce6b933f4fd5eff519d5909a8b724bcffa51848 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 16 Sep 2024 22:04:50 +0200 Subject: [PATCH 123/162] State struct and more documentation and ideas --- src/gateway/establish_connection.rs | 134 +++++++++++++++++----------- 1 file changed, 84 insertions(+), 50 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 095a7f0..8cc7588 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -31,6 +31,23 @@ use crate::{ use super::{Connection, GatewayClient, GatewayUsersStore, NewConnection, ResumableClientsStore}; +/// Internal use only state struct to pass around data to the `finish_connecting` function. +struct State { + connection: Arc>, + db: PgPool, + config: Config, + gateway_users_store: GatewayUsersStore, + resumeable_clients_store: ResumableClientsStore, + sequence_number: Arc>, + kill_send: Sender<()>, + kill_receive: tokio::sync::broadcast::Receiver<()>, + /// Receiver for heartbeat messages. The `HeartbeatHandler` will receive messages from this channel. + heartbeat_receive: tokio::sync::broadcast::Receiver, + /// Sender for heartbeat messages. The main gateway task will send messages to this channel for the `HeartbeatHandler` to receive and handle. + heartbeat_send: tokio::sync::broadcast::Sender, + session_id_receive: tokio::sync::broadcast::Receiver, +} + /// `establish_connection` is the entrypoint method that gets called when a client tries to connect /// to the WebSocket server. /// @@ -44,6 +61,7 @@ pub(super) async fn establish_connection( resumeable_clients_store: ResumableClientsStore, ) -> Result { trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Beginning process to establish connection (handshake)"); + // Accept the connection and split it into its sender and receiver halves. let ws_stream = accept_async(stream).await?; let mut connection: Connection = ws_stream.split().into(); trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Sending hello message"); @@ -54,21 +72,50 @@ pub(super) async fn establish_connection( .await?; trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Sent hello message"); + // Wrap the connection in an Arc> to allow for shared ownership and mutation. + // For example, the `HeartbeatHandler` and `GatewayClient` tasks will need to access the connection + // to send messages. let connection = Arc::new(Mutex::new(connection)); let mut received_identify_or_resume = false; let (kill_send, mut kill_receive) = tokio::sync::broadcast::channel::<()>(1); + // Inter-task communication channels. The main gateway task will send received heartbeat related messages + // to the `HeartbeatHandler` task via the `message_send` channel, which the `HeartbeatHandler` task will + // then receive and handle. + // + // TODO: The HeartbeatHandler theoretically does not need a full connection object, but either only the sender + // or just these message_* channels. Using the latter approach, the HeartbeatHandler could send Heartbeat + // responses to the main gateway task, which in turn would send them to the client. This way, the + // connection object might not need to be wraped in an `Arc>`. let (message_send, message_receive) = tokio::sync::broadcast::channel::(4); + let sequence_number = Arc::new(Mutex::new(0u64)); // TODO: Actually use this, as in: Increment it when needed. Currently, this is not being done. + + // Used to inform the `HeartbeatHandler` task of the session_id of the client, if we receive it after a heartbeat handler task has been spawned. let (session_id_send, session_id_receive) = tokio::sync::broadcast::channel::(1); + let state = State { + connection: connection.clone(), + db: db.clone(), + config: config.clone(), + gateway_users_store: gateway_users_store.clone(), + resumeable_clients_store: resumeable_clients_store.clone(), + sequence_number: sequence_number.clone(), + kill_send: kill_send.clone(), + kill_receive: kill_receive.resubscribe(), + heartbeat_receive: message_receive.resubscribe(), + heartbeat_send: message_send.clone(), + session_id_receive: session_id_receive.resubscribe(), + }; + // This JoinHandle `.is_some()` if we receive a heartbeat message *before* we receive an // identify or resume message. let mut heartbeat_handler_handle: Option> = None; trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Waiting for next message, timeout or kill signal..."); let mut second_kill_receive = kill_receive.resubscribe(); + // Either we time out, the connection is killed, or we receive succesful output from `finish_connecting`. tokio::select! { _ = second_kill_receive.recv() => { debug!(target: "symfonia::gateway::establish_connection::establish_connection", "Connection was closed before we could establish it"); @@ -81,20 +128,8 @@ pub(super) async fn establish_connection( } // Since async closures are not yet stable, we have to use a dedicated function to handle the // connection establishment process. :( - new_connection = finish_connecting( - connection.clone(), - heartbeat_handler_handle, - kill_receive, - kill_send, - message_receive, - message_send, - sequence_number, - session_id_receive, - db, - &config, - gateway_users_store.clone(), - resumeable_clients_store.clone(), - ) => { + new_connection = finish_connecting(heartbeat_handler_handle, state) + => { new_connection } } @@ -128,22 +163,12 @@ async fn get_or_new_gateway_user( /// case accordingly. #[allow(clippy::too_many_arguments)] async fn finish_connecting( - connection: Arc>, mut heartbeat_handler_handle: Option>, - kill_receive: tokio::sync::broadcast::Receiver<()>, - kill_send: tokio::sync::broadcast::Sender<()>, - message_receive: tokio::sync::broadcast::Receiver, - message_send: tokio::sync::broadcast::Sender, - sequence_number: Arc>, - session_id_receive: tokio::sync::broadcast::Receiver, - db: PgPool, - config: &Config, - gateway_users_store: GatewayUsersStore, - resumeable_clients_store: ResumableClientsStore, + state: State, ) -> Result { loop { trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Waiting for next message..."); - let raw_message = match connection.lock().await.receiver.next().await { + let raw_message = match state.connection.lock().await.receiver.next().await { Some(next) => next, None => return Err(GatewayError::Timeout.into()), }?; @@ -164,12 +189,12 @@ async fn finish_connecting( // I don't see that this is needed at the moment. heartbeat_handler_handle = Some(tokio::spawn({ let mut heartbeat_handler = HeartbeatHandler::new( - connection.clone(), - kill_receive.resubscribe(), - kill_send.clone(), - message_receive.resubscribe(), - sequence_number.clone(), - session_id_receive.resubscribe(), + state.connection.clone(), + state.kill_receive.resubscribe(), + state.kill_send.clone(), + state.heartbeat_receive.resubscribe(), + state.sequence_number.clone(), + state.session_id_receive.resubscribe(), ); async move { heartbeat_handler.run().await; @@ -177,7 +202,7 @@ async fn finish_connecting( })) } Some(_) => { - message_send.send(heartbeat); + state.heartbeat_send.send(heartbeat); } } } else if let Ok(identify) = @@ -185,9 +210,9 @@ async fn finish_connecting( { log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Received identify payload"); let claims = match check_token( - &db, + &state.db, &identify.event_data.token, - &config.security.jwt_secret, + &state.config.security.jwt_secret, ) .await { @@ -197,39 +222,44 @@ async fn finish_connecting( } Err(_) => { log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Failed to verify token"); - kill_send.send(()).expect("Failed to send kill signal"); + state + .kill_send + .send(()) + .expect("Failed to send kill signal"); return Err(crate::errors::UserError::InvalidToken.into()); } }; let mut gateway_user = get_or_new_gateway_user( claims.id, - gateway_users_store.clone(), - resumeable_clients_store.clone(), + state.gateway_users_store.clone(), + state.resumeable_clients_store.clone(), ) .await; let gateway_client = GatewayClient { parent: Arc::downgrade(&gateway_user), - connection: connection.clone(), - main_task_handle: tokio::spawn(gateway_task::gateway_task(connection.clone())), + connection: state.connection.clone(), + main_task_handle: tokio::spawn(gateway_task::gateway_task( + state.connection.clone(), + )), heartbeat_task_handle: match heartbeat_handler_handle { Some(handle) => handle, None => tokio::spawn({ let mut heartbeat_handler = HeartbeatHandler::new( - connection.clone(), - kill_receive.resubscribe(), - kill_send.clone(), - message_receive.resubscribe(), - sequence_number.clone(), - session_id_receive.resubscribe(), + state.connection.clone(), + state.kill_receive.resubscribe(), + state.kill_send.clone(), + state.heartbeat_receive.resubscribe(), + state.sequence_number.clone(), + state.session_id_receive.resubscribe(), ); async move { heartbeat_handler.run().await; } }), }, - kill_send, + kill_send: state.kill_send.clone(), session_token: identify.event_data.token, - last_sequence: sequence_number.clone(), + last_sequence: state.sequence_number.clone(), }; let gateway_client_arc_mutex = Arc::new(Mutex::new(gateway_client)); gateway_user.lock().await.clients.insert( @@ -243,7 +273,8 @@ async fn finish_connecting( } else if let Ok(resume) = from_str::(&raw_message.to_string()) { log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Received resume payload"); log::warn!(target: "symfonia::gateway::establish_connection::finish_connecting", "Resuming connections is not yet implemented. Telling client to identify instead."); - connection + state + .connection .lock() .await .sender @@ -253,7 +284,10 @@ async fn finish_connecting( .into(), }))) .await?; - kill_send.send(()).expect("Failed to send kill signal"); + state + .kill_send + .send(()) + .expect("Failed to send kill signal"); } else { debug!(target: "symfonia::gateway::establish_connection::finish_connecting", "Message could not be decoded as resume, heartbeat or identify."); From 5ed9a59a1e6cbde35e0227f84125e3bb4ff8442e Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 17 Sep 2024 16:04:01 +0200 Subject: [PATCH 124/162] Add excalidraw whiteboard --- docs/Event Publishing in Symfonia.excalidraw | 1035 ++++++++++++++++++ 1 file changed, 1035 insertions(+) create mode 100644 docs/Event Publishing in Symfonia.excalidraw diff --git a/docs/Event Publishing in Symfonia.excalidraw b/docs/Event Publishing in Symfonia.excalidraw new file mode 100644 index 0000000..b7884e0 --- /dev/null +++ b/docs/Event Publishing in Symfonia.excalidraw @@ -0,0 +1,1035 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "2MVXKRrbf5jvhORRI9URX", + "type": "rectangle", + "x": 367, + "y": 168, + "width": 423, + "height": 217, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": null, + "seed": 1484259885, + "version": 151, + "versionNonce": 1669709965, + "isDeleted": false, + "boundElements": [ + { + "id": "hvS4AEabLaqrr1-YIMYjQ", + "type": "arrow" + }, + { + "id": "0xjS1TWpsg8zIrQa57fCf", + "type": "arrow" + } + ], + "updated": 1726580075733, + "link": null, + "locked": false + }, + { + "id": "OTBzzVqwEY6os024JScwh", + "type": "text", + "x": 514, + "y": 179, + "width": 132.3333282470703, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": null, + "seed": 2120332483, + "version": 39, + "versionNonce": 1228797325, + "isDeleted": false, + "boundElements": null, + "updated": 1726580015230, + "link": null, + "locked": false, + "text": "API Endpoint", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "API Endpoint", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "4Sf2ujjlCt5_fuSqXkiLB", + "type": "rectangle", + "x": 401, + "y": 262, + "width": 344, + "height": 72, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": null, + "seed": 1424712877, + "version": 185, + "versionNonce": 1352161827, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "FZtE7MMsPYRo6B5BjG9_E" + }, + { + "id": "0xjS1TWpsg8zIrQa57fCf", + "type": "arrow" + }, + { + "id": "hvS4AEabLaqrr1-YIMYjQ", + "type": "arrow" + } + ], + "updated": 1726580084302, + "link": null, + "locked": false + }, + { + "id": "FZtE7MMsPYRo6B5BjG9_E", + "type": "text", + "x": 496.35, + "y": 285.5, + "width": 153.3, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2V", + "roundness": null, + "seed": 361869059, + "version": 122, + "versionNonce": 1788505357, + "isDeleted": false, + "boundElements": null, + "updated": 1726580028380, + "link": null, + "locked": false, + "text": "Event Publisher", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "4Sf2ujjlCt5_fuSqXkiLB", + "originalText": "Event Publisher", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "1JFUtbSSfm7QWM0KjMPE2", + "type": "ellipse", + "x": 351, + "y": 499, + "width": 163.99999999999994, + "height": 73, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4", + "roundness": null, + "seed": 106388611, + "version": 570, + "versionNonce": 456852013, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "_Pw8i03cpH5wSwGx3Ri9P" + }, + { + "id": "hvS4AEabLaqrr1-YIMYjQ", + "type": "arrow" + } + ], + "updated": 1726580071965, + "link": null, + "locked": false + }, + { + "id": "_Pw8i03cpH5wSwGx3Ri9P", + "type": "text", + "x": 383.1172439427031, + "y": 523.190602486691, + "width": 99.8, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a5", + "roundness": null, + "seed": 511424259, + "version": 527, + "versionNonce": 1499572557, + "isDeleted": false, + "boundElements": null, + "updated": 1726580067832, + "link": null, + "locked": false, + "text": "Subscriber", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "1JFUtbSSfm7QWM0KjMPE2", + "originalText": "Subscriber", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "ellipse", + "version": 586, + "versionNonce": 2000129997, + "index": "a6", + "isDeleted": false, + "id": "A4UGJKAYTwb60t4OudBqR", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 623, + "y": 500.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 163.99999999999994, + "height": 73, + "seed": 92011949, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "3TPexlcGphxPOeYq8__TX" + }, + { + "id": "0xjS1TWpsg8zIrQa57fCf", + "type": "arrow" + } + ], + "updated": 1726580075733, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 543, + "versionNonce": 296617101, + "index": "a7", + "isDeleted": false, + "id": "3TPexlcGphxPOeYq8__TX", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 655.1172439427031, + "y": 524.690602486691, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 99.8, + "height": 25, + "seed": 703184909, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726580065571, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 5, + "text": "Subscriber", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "A4UGJKAYTwb60t4OudBqR", + "originalText": "Subscriber", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "hvS4AEabLaqrr1-YIMYjQ", + "type": "arrow", + "x": 429, + "y": 493, + "width": 130, + "height": 153, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a8", + "roundness": { + "type": 2 + }, + "seed": 928577453, + "version": 91, + "versionNonce": 1834928387, + "isDeleted": false, + "boundElements": null, + "updated": 1726580084628, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 130, + -153 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "1JFUtbSSfm7QWM0KjMPE2", + "focus": -0.4575302916701412, + "gap": 6.042070325377999, + "fixedPoint": null + }, + "endBinding": { + "elementId": "4Sf2ujjlCt5_fuSqXkiLB", + "focus": -0.1070460704607046, + "gap": 6, + "fixedPoint": null + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "0xjS1TWpsg8zIrQa57fCf", + "type": "arrow", + "x": 704, + "y": 496, + "width": 127, + "height": 159, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a9", + "roundness": { + "type": 2 + }, + "seed": 779244035, + "version": 135, + "versionNonce": 348745741, + "isDeleted": false, + "boundElements": null, + "updated": 1726580082162, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -127, + -159 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "A4UGJKAYTwb60t4OudBqR", + "focus": 0.3648049259477906, + "gap": 4.502649494138325, + "fixedPoint": null + }, + "endBinding": { + "elementId": "4Sf2ujjlCt5_fuSqXkiLB", + "focus": 0.13524436090225567, + "gap": 3, + "fixedPoint": null + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "jL7ecVW1c_uet6ZE5_v-G", + "type": "rectangle", + "x": 1035, + "y": 198, + "width": 181, + "height": 76, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aF", + "roundness": null, + "seed": 1955642147, + "version": 104, + "versionNonce": 1687109133, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "PdP90lnYk5InEtHZQIWRw" + } + ], + "updated": 1726580196593, + "link": null, + "locked": false + }, + { + "id": "PdP90lnYk5InEtHZQIWRw", + "type": "text", + "x": 1058.4083333333333, + "y": 223.5, + "width": 134.18333333333334, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aG", + "roundness": null, + "seed": 30884451, + "version": 59, + "versionNonce": 1500902509, + "isDeleted": false, + "boundElements": null, + "updated": 1726580196593, + "link": null, + "locked": false, + "text": "User Updates", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "jL7ecVW1c_uet6ZE5_v-G", + "originalText": "User Updates", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "A4F4pjFUhGoZqAx_Ph_XU", + "type": "text", + "x": 1037, + "y": 294, + "width": 134.23333333333332, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aH", + "roundness": null, + "seed": 361037955, + "version": 31, + "versionNonce": 1800405101, + "isDeleted": false, + "boundElements": null, + "updated": 1726580204623, + "link": null, + "locked": false, + "text": "- Relationship\n- Avatar\n- Bio", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- Relationship\n- Avatar\n- Bio", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "oWA9AtvfD6_HSuXMUtzr4", + "type": "rectangle", + "x": 1236, + "y": 198, + "width": 186, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aI", + "roundness": null, + "seed": 1165134125, + "version": 70, + "versionNonce": 631617805, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "cI-tGafUxksU_I5Mpd0ga" + } + ], + "updated": 1726580212286, + "link": null, + "locked": false + }, + { + "id": "cI-tGafUxksU_I5Mpd0ga", + "type": "text", + "x": 1259.6, + "y": 223, + "width": 138.8, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aJ", + "roundness": null, + "seed": 1054914211, + "version": 15, + "versionNonce": 478390531, + "isDeleted": false, + "boundElements": null, + "updated": 1726580214702, + "link": null, + "locked": false, + "text": "Guild Updates", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "oWA9AtvfD6_HSuXMUtzr4", + "originalText": "Guild Updates", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "RfSnEaRHOpA5YddANm-4Q", + "type": "text", + "x": 1236, + "y": 299, + "width": 186, + "height": 100, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aK", + "roundness": null, + "seed": 1187124589, + "version": 113, + "versionNonce": 1233723011, + "isDeleted": false, + "boundElements": null, + "updated": 1726580243950, + "link": null, + "locked": false, + "text": "- Name Change\n- Icon Change\n- Other public \nattribute change", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- Name Change\n- Icon Change\n- Other public attribute change", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "ch7tfkBfIKlUfgWsBmyTw", + "type": "rectangle", + "x": 1441, + "y": 198, + "width": 199, + "height": 73, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aL", + "roundness": null, + "seed": 2113903043, + "version": 118, + "versionNonce": 1301851395, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "3hE5gwZr8o6CjWkvfQ9zv" + } + ], + "updated": 1726580252706, + "link": null, + "locked": false + }, + { + "id": "3hE5gwZr8o6CjWkvfQ9zv", + "type": "text", + "x": 1461.875, + "y": 209.5, + "width": 157.25, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aLV", + "roundness": null, + "seed": 1942465101, + "version": 30, + "versionNonce": 828585411, + "isDeleted": false, + "boundElements": null, + "updated": 1726580257575, + "link": null, + "locked": false, + "text": "Extended Guild \nUpdates", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ch7tfkBfIKlUfgWsBmyTw", + "originalText": "Extended Guild Updates", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "BOkSrfJNV_yDCdj7-YJct", + "type": "text", + "x": 1442, + "y": 301, + "width": 195, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aN", + "roundness": null, + "seed": 1219020557, + "version": 102, + "versionNonce": 1636452195, + "isDeleted": false, + "boundElements": null, + "updated": 1726580288877, + "link": null, + "locked": false, + "text": "- Full Audit Log \nwith Usernames", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- Full Audit Log with Usernames", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "-M-N8zfYU15qrMGHtXLOg", + "type": "rectangle", + "x": 1036, + "y": 462, + "width": 212, + "height": 73, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aO", + "roundness": null, + "seed": 380897955, + "version": 259, + "versionNonce": 2058274413, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "q46wzLGHNKS-PDdoV2Yt-" + } + ], + "updated": 1726581313729, + "link": null, + "locked": false + }, + { + "id": "q46wzLGHNKS-PDdoV2Yt-", + "type": "text", + "x": 1061.8833312988281, + "y": 473.5, + "width": 160.23333740234375, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aP", + "roundness": null, + "seed": 1089230851, + "version": 146, + "versionNonce": 706434285, + "isDeleted": false, + "boundElements": null, + "updated": 1726581340415, + "link": null, + "locked": false, + "text": "Passive Channel \nUpdates", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "-M-N8zfYU15qrMGHtXLOg", + "originalText": "Passive Channel Updates", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "BD_37_o-zsNIxKV-GP_gh", + "type": "text", + "x": 1038, + "y": 566, + "width": 207, + "height": 200, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aQ", + "roundness": null, + "seed": 1198423533, + "version": 387, + "versionNonce": 1421354851, + "isDeleted": false, + "boundElements": null, + "updated": 1726581629052, + "link": null, + "locked": false, + "text": "- Permission Changes\n(ones that do not \nin-/exclude the user \nto/from the channel)\n- Channel \nname/description \nchanges\n-", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- Permission Changes\n(ones that do not in-/exclude the user to/from the channel)\n- Channel name/description changes\n-", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "dgmOCsrSsYfsKwLF33TK1", + "type": "text", + "x": 359, + "y": 753, + "width": 421, + "height": 200, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aS", + "roundness": null, + "seed": 1967997485, + "version": 379, + "versionNonce": 315055085, + "isDeleted": false, + "boundElements": null, + "updated": 1726580954205, + "link": null, + "locked": false, + "text": "Goals in Mind:\n\n- Support multiple chats opened at the \nsame time\n- Send the least amount of messages \nrequired without sacrificing UX\n- Do not modify Discord API/WS \nbehaviour", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Goals in Mind:\n\n- Support multiple chats opened at the same time\n- Send the least amount of messages required without sacrificing UX\n- Do not modify Discord API/WS behaviour", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "KUumgEMrgSNz1yHP1SAwp", + "type": "text", + "x": 359, + "y": 987, + "width": 395, + "height": 575, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aT", + "roundness": null, + "seed": 1310238883, + "version": 1172, + "versionNonce": 245211683, + "isDeleted": false, + "boundElements": null, + "updated": 1726581303535, + "link": null, + "locked": false, + "text": "Idea:\n- Always send events from the latest \nchannel scope that has been accessed. \nWhen a user clicks on a channel, they \nwill send a get messages request to the\nchannel. We will use that request to \ndetermine which channel the user is \nviewing.\n- Send events from all other channels \nthe user has accessed via HTTP API in\nthe last XY seconds. When the user \naccesses a new channel, the \"old\" \nchannel is pushed back into a sort of \nqueue, where after a set amount of \ntime, the user will be unsubscribed from \nthese channels' \"detailed events\". The \nunsubscribing can be stopped if the \nuser sends a WS payload which \ndescribes the intent to keep receiving \nevents from that channel. This way, one\ncan view multiple channels at the same \ntime using custom clients, while single-\nview clients are also fully supported.", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Idea:\n- Always send events from the latest channel scope that has been accessed. When a user clicks on a channel, they will send a get messages request to the channel. We will use that request to determine which channel the user is viewing.\n- Send events from all other channels the user has accessed via HTTP API in the last XY seconds. When the user accesses a new channel, the \"old\" channel is pushed back into a sort of queue, where after a set amount of time, the user will be unsubscribed from these channels' \"detailed events\". The unsubscribing can be stopped if the user sends a WS payload which describes the intent to keep receiving events from that channel. This way, one can view multiple channels at the same time using custom clients, while single-view clients are also fully supported.", + "autoResize": false, + "lineHeight": 1.25 + }, + { + "id": "2sjonxYVzFv7deYkoQoX5", + "type": "rectangle", + "x": 1259, + "y": 460, + "width": 214, + "height": 74, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aU", + "roundness": null, + "seed": 1759587, + "version": 120, + "versionNonce": 1701385027, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "fq34t956YSgc0zsG_zZeu" + } + ], + "updated": 1726581341666, + "link": null, + "locked": false + }, + { + "id": "fq34t956YSgc0zsG_zZeu", + "type": "text", + "x": 1291.7583312988281, + "y": 472, + "width": 148.48333740234375, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aU0G", + "roundness": null, + "seed": 1130921997, + "version": 43, + "versionNonce": 377703981, + "isDeleted": false, + "boundElements": null, + "updated": 1726581348263, + "link": null, + "locked": false, + "text": "Active Channel \nUpdates", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "2sjonxYVzFv7deYkoQoX5", + "originalText": "Active Channel Updates", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "B14jWAlwXTnH4FLL-5KOB", + "type": "text", + "x": 1262, + "y": 571, + "width": 211, + "height": 200, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aZ", + "roundness": null, + "seed": 2043685229, + "version": 315, + "versionNonce": 1614546253, + "isDeleted": false, + "boundElements": null, + "updated": 1726581617515, + "link": null, + "locked": false, + "text": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user \nupdates (See User \nUpdates)\n- Passive Channel \nUpdates", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user updates (See User Updates)\n- Passive Channel Updates", + "autoResize": false, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file From d5a241ec26a93457e45a3fd381bde5fb47b0f636 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 17 Sep 2024 17:55:25 +0200 Subject: [PATCH 125/162] update diagram --- docs/Event Publishing in Symfonia.excalidraw | 1092 +++++++++++------- 1 file changed, 666 insertions(+), 426 deletions(-) diff --git a/docs/Event Publishing in Symfonia.excalidraw b/docs/Event Publishing in Symfonia.excalidraw index b7884e0..b94be98 100644 --- a/docs/Event Publishing in Symfonia.excalidraw +++ b/docs/Event Publishing in Symfonia.excalidraw @@ -1,31 +1,30 @@ { "type": "excalidraw", "version": 2, - "source": "https://excalidraw.com", + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", "elements": [ { - "id": "2MVXKRrbf5jvhORRI9URX", "type": "rectangle", - "x": 367, - "y": 168, - "width": 423, - "height": 217, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 154, + "versionNonce": 824718855, + "isDeleted": false, + "id": "2MVXKRrbf5jvhORRI9URX", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 367, + "y": 168, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 423, + "height": 217, + "seed": 1484259885, "groupIds": [], "frameId": null, - "index": "a0", "roundness": null, - "seed": 1484259885, - "version": 151, - "versionNonce": 1669709965, - "isDeleted": false, "boundElements": [ { "id": "hvS4AEabLaqrr1-YIMYjQ", @@ -36,70 +35,68 @@ "type": "arrow" } ], - "updated": 1726580075733, + "updated": 1726588459812, "link": null, "locked": false }, { - "id": "OTBzzVqwEY6os024JScwh", "type": "text", - "x": 514, - "y": 179, - "width": 132.3333282470703, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 43, + "versionNonce": 2099466729, + "isDeleted": false, + "id": "OTBzzVqwEY6os024JScwh", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 514, + "y": 179, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 110.556640625, + "height": 25, + "seed": 2120332483, "groupIds": [], "frameId": null, - "index": "a1", "roundness": null, - "seed": 2120332483, - "version": 39, - "versionNonce": 1228797325, - "isDeleted": false, - "boundElements": null, - "updated": 1726580015230, + "boundElements": [], + "updated": 1726588459812, "link": null, "locked": false, - "text": "API Endpoint", "fontSize": 20, "fontFamily": 5, + "text": "API Endpoint", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "API Endpoint", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { - "id": "4Sf2ujjlCt5_fuSqXkiLB", "type": "rectangle", - "x": 401, - "y": 262, - "width": 344, - "height": 72, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 188, + "versionNonce": 1457642791, + "isDeleted": false, + "id": "4Sf2ujjlCt5_fuSqXkiLB", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 401, + "y": 262, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 344, + "height": 72, + "seed": 1424712877, "groupIds": [], "frameId": null, - "index": "a2", "roundness": null, - "seed": 1424712877, - "version": 185, - "versionNonce": 1352161827, - "isDeleted": false, "boundElements": [ { "type": "text", @@ -114,70 +111,68 @@ "type": "arrow" } ], - "updated": 1726580084302, + "updated": 1726588459812, "link": null, "locked": false }, { - "id": "FZtE7MMsPYRo6B5BjG9_E", "type": "text", - "x": 496.35, - "y": 285.5, - "width": 153.3, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 125, + "versionNonce": 1720921289, + "isDeleted": false, + "id": "FZtE7MMsPYRo6B5BjG9_E", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 496.35, + "y": 285.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 153.3, + "height": 25, + "seed": 361869059, "groupIds": [], "frameId": null, - "index": "a2V", "roundness": null, - "seed": 361869059, - "version": 122, - "versionNonce": 1788505357, - "isDeleted": false, - "boundElements": null, - "updated": 1726580028380, + "boundElements": [], + "updated": 1726588459812, "link": null, "locked": false, - "text": "Event Publisher", "fontSize": 20, "fontFamily": 5, + "text": "Event Publisher", "textAlign": "center", "verticalAlign": "middle", "containerId": "4Sf2ujjlCt5_fuSqXkiLB", "originalText": "Event Publisher", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { - "id": "1JFUtbSSfm7QWM0KjMPE2", "type": "ellipse", - "x": 351, - "y": 499, - "width": 163.99999999999994, - "height": 73, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 573, + "versionNonce": 1629069383, + "isDeleted": false, + "id": "1JFUtbSSfm7QWM0KjMPE2", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 351, + "y": 499, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 163.99999999999994, + "height": 73, + "seed": 106388611, "groupIds": [], "frameId": null, - "index": "a4", "roundness": null, - "seed": 106388611, - "version": 570, - "versionNonce": 456852013, - "isDeleted": false, "boundElements": [ { "type": "text", @@ -188,52 +183,50 @@ "type": "arrow" } ], - "updated": 1726580071965, + "updated": 1726588459812, "link": null, "locked": false }, { - "id": "_Pw8i03cpH5wSwGx3Ri9P", "type": "text", - "x": 383.1172439427031, - "y": 523.190602486691, - "width": 99.8, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 530, + "versionNonce": 251964329, + "isDeleted": false, + "id": "_Pw8i03cpH5wSwGx3Ri9P", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 383.1172439427031, + "y": 523.190602486691, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 99.8, + "height": 25, + "seed": 511424259, "groupIds": [], "frameId": null, - "index": "a5", "roundness": null, - "seed": 511424259, - "version": 527, - "versionNonce": 1499572557, - "isDeleted": false, - "boundElements": null, - "updated": 1726580067832, + "boundElements": [], + "updated": 1726588459812, "link": null, "locked": false, - "text": "Subscriber", "fontSize": 20, "fontFamily": 5, + "text": "Subscriber", "textAlign": "center", "verticalAlign": "middle", "containerId": "1JFUtbSSfm7QWM0KjMPE2", "originalText": "Subscriber", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { "type": "ellipse", - "version": 586, - "versionNonce": 2000129997, - "index": "a6", + "version": 589, + "versionNonce": 1370226535, "isDeleted": false, "id": "A4UGJKAYTwb60t4OudBqR", "fillStyle": "solid", @@ -262,15 +255,14 @@ "type": "arrow" } ], - "updated": 1726580075733, + "updated": 1726588459812, "link": null, "locked": false }, { "type": "text", - "version": 543, - "versionNonce": 296617101, - "index": "a7", + "version": 546, + "versionNonce": 1856196233, "isDeleted": false, "id": "3TPexlcGphxPOeYq8__TX", "fillStyle": "solid", @@ -290,7 +282,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726580065571, + "updated": 1726588459812, "link": null, "locked": false, "fontSize": 20, @@ -300,49 +292,37 @@ "verticalAlign": "middle", "containerId": "A4UGJKAYTwb60t4OudBqR", "originalText": "Subscriber", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { - "id": "hvS4AEabLaqrr1-YIMYjQ", "type": "arrow", - "x": 429, - "y": 493, - "width": 130, - "height": 153, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 94, + "versionNonce": 988117639, + "isDeleted": false, + "id": "hvS4AEabLaqrr1-YIMYjQ", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 429, + "y": 493, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 130, + "height": 153, + "seed": 928577453, "groupIds": [], "frameId": null, - "index": "a8", "roundness": { "type": 2 }, - "seed": 928577453, - "version": 91, - "versionNonce": 1834928387, - "isDeleted": false, - "boundElements": null, - "updated": 1726580084628, + "boundElements": [], + "updated": 1726588459812, "link": null, "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - 130, - -153 - ] - ], - "lastCommittedPoint": null, "startBinding": { "elementId": "1JFUtbSSfm7QWM0KjMPE2", "focus": -0.4575302916701412, @@ -355,50 +335,48 @@ "gap": 6, "fixedPoint": null }, + "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", - "elbowed": false + "points": [ + [ + 0, + 0 + ], + [ + 130, + -153 + ] + ] }, { - "id": "0xjS1TWpsg8zIrQa57fCf", "type": "arrow", - "x": 704, - "y": 496, - "width": 127, - "height": 159, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 138, + "versionNonce": 1814730089, + "isDeleted": false, + "id": "0xjS1TWpsg8zIrQa57fCf", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 704, + "y": 496, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 127, + "height": 159, + "seed": 779244035, "groupIds": [], "frameId": null, - "index": "a9", "roundness": { "type": 2 }, - "seed": 779244035, - "version": 135, - "versionNonce": 348745741, - "isDeleted": false, - "boundElements": null, - "updated": 1726580082162, + "boundElements": [], + "updated": 1726588459812, "link": null, "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -127, - -159 - ] - ], - "lastCommittedPoint": null, "startBinding": { "elementId": "A4UGJKAYTwb60t4OudBqR", "focus": 0.3648049259477906, @@ -411,408 +389,619 @@ "gap": 3, "fixedPoint": null }, + "lastCommittedPoint": null, "startArrowhead": null, "endArrowhead": "arrow", - "elbowed": false + "points": [ + [ + 0, + 0 + ], + [ + -127, + -159 + ] + ] }, { - "id": "jL7ecVW1c_uet6ZE5_v-G", "type": "rectangle", - "x": 1035, - "y": 198, - "width": 181, - "height": 76, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 107, + "versionNonce": 830640551, + "isDeleted": false, + "id": "jL7ecVW1c_uet6ZE5_v-G", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1035, + "y": 198, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 181, + "height": 76, + "seed": 1955642147, "groupIds": [], "frameId": null, - "index": "aF", "roundness": null, - "seed": 1955642147, - "version": 104, - "versionNonce": 1687109133, - "isDeleted": false, "boundElements": [ { "type": "text", "id": "PdP90lnYk5InEtHZQIWRw" } ], - "updated": 1726580196593, + "updated": 1726588459812, "link": null, "locked": false }, { - "id": "PdP90lnYk5InEtHZQIWRw", "type": "text", - "x": 1058.4083333333333, - "y": 223.5, - "width": 134.18333333333334, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 62, + "versionNonce": 182845513, + "isDeleted": false, + "id": "PdP90lnYk5InEtHZQIWRw", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aG", - "roundness": null, - "seed": 30884451, - "version": 59, - "versionNonce": 1500902509, - "isDeleted": false, - "boundElements": null, - "updated": 1726580196593, + "angle": 0, + "x": 1058.4083333333333, + "y": 223.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 134.18333333333334, + "height": 25, + "seed": 30884451, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726588459812, "link": null, "locked": false, - "text": "User Updates", "fontSize": 20, "fontFamily": 5, + "text": "User Updates", "textAlign": "center", "verticalAlign": "middle", "containerId": "jL7ecVW1c_uet6ZE5_v-G", "originalText": "User Updates", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { - "id": "A4F4pjFUhGoZqAx_Ph_XU", "type": "text", - "x": 1037, - "y": 294, - "width": 134.23333333333332, - "height": 75, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 92, + "versionNonce": 2125448391, + "isDeleted": false, + "id": "A4F4pjFUhGoZqAx_Ph_XU", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1037, + "y": 294, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 112.763671875, + "height": 75, + "seed": 361037955, "groupIds": [], "frameId": null, - "index": "aH", "roundness": null, - "seed": 361037955, - "version": 31, - "versionNonce": 1800405101, - "isDeleted": false, - "boundElements": null, - "updated": 1726580204623, + "boundElements": [], + "updated": 1726588459812, "link": null, "locked": false, - "text": "- Relationship\n- Avatar\n- Bio", "fontSize": 20, "fontFamily": 5, + "text": "- Relationship\n- Avatar\n- Biography", "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "- Relationship\n- Avatar\n- Bio", - "autoResize": true, - "lineHeight": 1.25 + "originalText": "- Relationship\n- Avatar\n- Biography", + "lineHeight": 1.25, + "baseline": 69 }, { - "id": "oWA9AtvfD6_HSuXMUtzr4", "type": "rectangle", - "x": 1236, - "y": 198, - "width": 186, - "height": 75, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 73, + "versionNonce": 1487998761, + "isDeleted": false, + "id": "oWA9AtvfD6_HSuXMUtzr4", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1236, + "y": 198, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 186, + "height": 75, + "seed": 1165134125, "groupIds": [], "frameId": null, - "index": "aI", "roundness": null, - "seed": 1165134125, - "version": 70, - "versionNonce": 631617805, - "isDeleted": false, "boundElements": [ { "type": "text", "id": "cI-tGafUxksU_I5Mpd0ga" } ], - "updated": 1726580212286, + "updated": 1726588459812, "link": null, "locked": false }, { - "id": "cI-tGafUxksU_I5Mpd0ga", "type": "text", - "x": 1259.6, - "y": 223, - "width": 138.8, - "height": 25, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 18, + "versionNonce": 302617575, + "isDeleted": false, + "id": "cI-tGafUxksU_I5Mpd0ga", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1259.6, + "y": 223, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 138.8, + "height": 25, + "seed": 1054914211, "groupIds": [], "frameId": null, - "index": "aJ", "roundness": null, - "seed": 1054914211, - "version": 15, - "versionNonce": 478390531, - "isDeleted": false, - "boundElements": null, - "updated": 1726580214702, + "boundElements": [], + "updated": 1726588459812, "link": null, "locked": false, - "text": "Guild Updates", "fontSize": 20, "fontFamily": 5, + "text": "Guild Updates", "textAlign": "center", "verticalAlign": "middle", "containerId": "oWA9AtvfD6_HSuXMUtzr4", "originalText": "Guild Updates", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { - "id": "RfSnEaRHOpA5YddANm-4Q", "type": "text", - "x": 1236, - "y": 299, - "width": 186, - "height": 100, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 126, + "versionNonce": 469189129, + "isDeleted": false, + "id": "RfSnEaRHOpA5YddANm-4Q", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1240, + "y": 300, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 253.2421875, + "height": 75, + "seed": 1187124589, "groupIds": [], "frameId": null, - "index": "aK", "roundness": null, - "seed": 1187124589, - "version": 113, - "versionNonce": 1233723011, - "isDeleted": false, - "boundElements": null, - "updated": 1726580243950, + "boundElements": [], + "updated": 1726588459812, "link": null, "locked": false, - "text": "- Name Change\n- Icon Change\n- Other public \nattribute change", "fontSize": 20, "fontFamily": 5, + "text": "- Name Change\n- Icon Change\n- Other public attribute changes", "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "- Name Change\n- Icon Change\n- Other public attribute change", - "autoResize": false, - "lineHeight": 1.25 + "originalText": "- Name Change\n- Icon Change\n- Other public attribute changes", + "lineHeight": 1.25, + "baseline": 69 }, { - "id": "ch7tfkBfIKlUfgWsBmyTw", "type": "rectangle", - "x": 1441, - "y": 198, - "width": 199, - "height": 73, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 121, + "versionNonce": 865894151, + "isDeleted": false, + "id": "ch7tfkBfIKlUfgWsBmyTw", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1441, + "y": 198, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 199, + "height": 73, + "seed": 2113903043, "groupIds": [], "frameId": null, - "index": "aL", "roundness": null, - "seed": 2113903043, - "version": 118, - "versionNonce": 1301851395, - "isDeleted": false, "boundElements": [ { "type": "text", "id": "3hE5gwZr8o6CjWkvfQ9zv" } ], - "updated": 1726580252706, + "updated": 1726588459812, "link": null, "locked": false }, { - "id": "3hE5gwZr8o6CjWkvfQ9zv", "type": "text", + "version": 33, + "versionNonce": 1102321897, + "isDeleted": false, + "id": "3hE5gwZr8o6CjWkvfQ9zv", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, "x": 1461.875, "y": 209.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", "width": 157.25, "height": 50, + "seed": 1942465101, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726588459812, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 5, + "text": "Extended Guild \nUpdates", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ch7tfkBfIKlUfgWsBmyTw", + "originalText": "Extended Guild Updates", + "lineHeight": 1.25, + "baseline": 44 + }, + { + "type": "text", + "version": 106, + "versionNonce": 1977943591, + "isDeleted": false, + "id": "BOkSrfJNV_yDCdj7-YJct", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, "angle": 0, + "x": 1442, + "y": 301, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", + "width": 136.669921875, + "height": 50, + "seed": 1219020557, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726588459812, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 5, + "text": "- Full Audit Log \nwith Usernames", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- Full Audit Log with Usernames", + "lineHeight": 1.25, + "baseline": 44 + }, + { + "type": "rectangle", + "version": 262, + "versionNonce": 192091081, + "isDeleted": false, + "id": "-M-N8zfYU15qrMGHtXLOg", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1036, + "y": 462, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 212, + "height": 73, + "seed": 380897955, "groupIds": [], "frameId": null, - "index": "aLV", "roundness": null, - "seed": 1942465101, - "version": 30, - "versionNonce": 828585411, + "boundElements": [ + { + "type": "text", + "id": "q46wzLGHNKS-PDdoV2Yt-" + } + ], + "updated": 1726588459812, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 149, + "versionNonce": 1741662535, "isDeleted": false, - "boundElements": null, - "updated": 1726580257575, + "id": "q46wzLGHNKS-PDdoV2Yt-", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1061.8833312988281, + "y": 473.5, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 160.23333740234375, + "height": 50, + "seed": 1089230851, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726588459812, "link": null, "locked": false, - "text": "Extended Guild \nUpdates", "fontSize": 20, "fontFamily": 5, + "text": "Passive Channel \nUpdates", "textAlign": "center", "verticalAlign": "middle", - "containerId": "ch7tfkBfIKlUfgWsBmyTw", - "originalText": "Extended Guild Updates", - "autoResize": true, - "lineHeight": 1.25 + "containerId": "-M-N8zfYU15qrMGHtXLOg", + "originalText": "Passive Channel Updates", + "lineHeight": 1.25, + "baseline": 44 }, { - "id": "BOkSrfJNV_yDCdj7-YJct", "type": "text", - "x": 1442, - "y": 301, - "width": 195, - "height": 50, + "version": 391, + "versionNonce": 2102375081, + "isDeleted": false, + "id": "BD_37_o-zsNIxKV-GP_gh", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, "angle": 0, + "x": 1038, + "y": 566, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", + "width": 174.43359375, + "height": 200, + "seed": 1198423533, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726588459812, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 5, + "text": "- Permission Changes\n(ones that do not \nin-/exclude the user \nto/from the channel)\n- Channel \nname/description \nchanges\n-", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- Permission Changes\n(ones that do not in-/exclude the user to/from the channel)\n- Channel name/description changes\n-", + "lineHeight": 1.25, + "baseline": 194 + }, + { + "type": "text", + "version": 383, + "versionNonce": 112781415, + "isDeleted": false, + "id": "dgmOCsrSsYfsKwLF33TK1", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 359, + "y": 753, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 309.375, + "height": 200, + "seed": 1967997485, "groupIds": [], "frameId": null, - "index": "aN", "roundness": null, - "seed": 1219020557, - "version": 102, - "versionNonce": 1636452195, - "isDeleted": false, - "boundElements": null, - "updated": 1726580288877, + "boundElements": [], + "updated": 1726588459813, "link": null, "locked": false, - "text": "- Full Audit Log \nwith Usernames", "fontSize": 20, "fontFamily": 5, + "text": "Goals in Mind:\n\n- Support multiple chats opened at the \nsame time\n- Send the least amount of messages \nrequired without sacrificing UX\n- Do not modify Discord API/WS \nbehaviour", "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "- Full Audit Log with Usernames", - "autoResize": false, - "lineHeight": 1.25 + "originalText": "Goals in Mind:\n\n- Support multiple chats opened at the same time\n- Send the least amount of messages required without sacrificing UX\n- Do not modify Discord API/WS behaviour", + "lineHeight": 1.25, + "baseline": 194 }, { - "id": "-M-N8zfYU15qrMGHtXLOg", - "type": "rectangle", - "x": 1036, - "y": 462, - "width": 212, - "height": 73, + "type": "text", + "version": 1177, + "versionNonce": 200635785, + "isDeleted": false, + "id": "KUumgEMrgSNz1yHP1SAwp", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, "angle": 0, + "x": 359, + "y": 987, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", + "width": 327.705078125, + "height": 575, + "seed": 1310238883, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726588459813, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 5, + "text": "Idea:\n- Always send events from the latest \nchannel scope that has been accessed. \nWhen a user clicks on a channel, they \nwill send a get messages request to the\nchannel. We will use that request to \ndetermine which channel the user is \nviewing.\n- Send events from all other channels \nthe user has accessed via HTTP API in\nthe last XY seconds. When the user \naccesses a new channel, the \"old\" \nchannel is pushed back into a sort of \nqueue, where after a set amount of \ntime, the user will be unsubscribed from \nthese channels' \"detailed events\". The \nunsubscribing can be stopped if the \nuser sends a WS payload which \ndescribes the intent to keep receiving \nevents from that channel. This way, one\ncan view multiple channels at the same \ntime using custom clients, while single-\nview clients are also fully supported.", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Idea:\n- Always send events from the latest channel scope that has been accessed. When a user clicks on a channel, they will send a get messages request to the channel. We will use that request to determine which channel the user is viewing.\n- Send events from all other channels the user has accessed via HTTP API in the last XY seconds. When the user accesses a new channel, the \"old\" channel is pushed back into a sort of queue, where after a set amount of time, the user will be unsubscribed from these channels' \"detailed events\". The unsubscribing can be stopped if the user sends a WS payload which describes the intent to keep receiving events from that channel. This way, one can view multiple channels at the same time using custom clients, while single-view clients are also fully supported.", + "lineHeight": 1.25, + "baseline": 569 + }, + { + "type": "rectangle", + "version": 123, + "versionNonce": 1886208903, + "isDeleted": false, + "id": "2sjonxYVzFv7deYkoQoX5", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1259, + "y": 460, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 214, + "height": 74, + "seed": 1759587, "groupIds": [], "frameId": null, - "index": "aO", "roundness": null, - "seed": 380897955, - "version": 259, - "versionNonce": 2058274413, - "isDeleted": false, "boundElements": [ { "type": "text", - "id": "q46wzLGHNKS-PDdoV2Yt-" + "id": "fq34t956YSgc0zsG_zZeu" } ], - "updated": 1726581313729, + "updated": 1726588459813, "link": null, "locked": false }, { - "id": "q46wzLGHNKS-PDdoV2Yt-", "type": "text", - "x": 1061.8833312988281, - "y": 473.5, - "width": 160.23333740234375, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 46, + "versionNonce": 178248809, + "isDeleted": false, + "id": "fq34t956YSgc0zsG_zZeu", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1291.7583312988281, + "y": 472, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 148.48333740234375, + "height": 50, + "seed": 1130921997, "groupIds": [], "frameId": null, - "index": "aP", "roundness": null, - "seed": 1089230851, - "version": 146, - "versionNonce": 706434285, - "isDeleted": false, - "boundElements": null, - "updated": 1726581340415, + "boundElements": [], + "updated": 1726588459813, "link": null, "locked": false, - "text": "Passive Channel \nUpdates", "fontSize": 20, "fontFamily": 5, + "text": "Active Channel \nUpdates", "textAlign": "center", "verticalAlign": "middle", - "containerId": "-M-N8zfYU15qrMGHtXLOg", - "originalText": "Passive Channel Updates", - "autoResize": true, - "lineHeight": 1.25 + "containerId": "2sjonxYVzFv7deYkoQoX5", + "originalText": "Active Channel Updates", + "lineHeight": 1.25, + "baseline": 44 }, { - "id": "BD_37_o-zsNIxKV-GP_gh", "type": "text", - "x": 1038, - "y": 566, - "width": 207, + "version": 319, + "versionNonce": 1428765351, + "isDeleted": false, + "id": "B14jWAlwXTnH4FLL-5KOB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1262, + "y": 571, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 163.291015625, "height": 200, + "seed": 2043685229, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726588459813, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 5, + "text": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user \nupdates (See User \nUpdates)\n- Passive Channel \nUpdates", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user updates (See User Updates)\n- Passive Channel Updates", + "lineHeight": 1.25, + "baseline": 194 + }, + { + "id": "7Nqx9LYOEEzgetXjNa2_b", + "type": "rectangle", + "x": 1000, + "y": 900, + "width": 700, + "height": 500, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -823,33 +1012,91 @@ "opacity": 100, "groupIds": [], "frameId": null, - "index": "aQ", "roundness": null, - "seed": 1198423533, - "version": 387, - "versionNonce": 1421354851, + "seed": 741046727, + "version": 37, + "versionNonce": 1494902601, + "isDeleted": false, + "boundElements": null, + "updated": 1726588459813, + "link": null, + "locked": false + }, + { + "id": "E9kU-lMID9gyW-OY4I5Un", + "type": "text", + "x": 1260, + "y": 900, + "width": 179.059814453125, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 529728071, + "version": 29, + "versionNonce": 697912775, "isDeleted": false, "boundElements": null, - "updated": 1726581629052, + "updated": 1726588459813, "link": null, "locked": false, - "text": "- Permission Changes\n(ones that do not \nin-/exclude the user \nto/from the channel)\n- Channel \nname/description \nchanges\n-", + "text": "User Subscriptions", "fontSize": 20, - "fontFamily": 5, + "fontFamily": 1, "textAlign": "left", "verticalAlign": "top", + "baseline": 18, "containerId": null, - "originalText": "- Permission Changes\n(ones that do not in-/exclude the user to/from the channel)\n- Channel name/description changes\n-", - "autoResize": false, + "originalText": "User Subscriptions", "lineHeight": 1.25 }, { - "id": "dgmOCsrSsYfsKwLF33TK1", + "id": "SFCx_f3SYUvjWr1siydbq", + "type": "rectangle", + "x": 1020, + "y": 960, + "width": 140, + "height": 60, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "roundness": null, + "seed": 911025735, + "version": 9, + "versionNonce": 1371575849, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "qcvyp3WuBaAPIQ-5fqR3H" + } + ], + "updated": 1726588459813, + "link": null, + "locked": false + }, + { + "id": "qcvyp3WuBaAPIQ-5fqR3H", "type": "text", - "x": 359, - "y": 753, - "width": 421, - "height": 200, + "x": 1049.020034790039, + "y": 965, + "width": 81.95993041992188, + "height": 50, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -860,33 +1107,32 @@ "opacity": 100, "groupIds": [], "frameId": null, - "index": "aS", "roundness": null, - "seed": 1967997485, - "version": 379, - "versionNonce": 315055085, + "seed": 740720969, + "version": 16, + "versionNonce": 416939239, "isDeleted": false, "boundElements": null, - "updated": 1726580954205, + "updated": 1726588459813, "link": null, "locked": false, - "text": "Goals in Mind:\n\n- Support multiple chats opened at the \nsame time\n- Send the least amount of messages \nrequired without sacrificing UX\n- Do not modify Discord API/WS \nbehaviour", + "text": "Guild \nUpdates", "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "Goals in Mind:\n\n- Support multiple chats opened at the same time\n- Send the least amount of messages required without sacrificing UX\n- Do not modify Discord API/WS behaviour", - "autoResize": false, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 43, + "containerId": "SFCx_f3SYUvjWr1siydbq", + "originalText": "Guild Updates", "lineHeight": 1.25 }, { - "id": "KUumgEMrgSNz1yHP1SAwp", + "id": "CrENgJMFm5et4g_0hTvHx", "type": "text", - "x": 359, - "y": 987, - "width": 395, - "height": 575, + "x": 1021.1800482818668, + "y": 1038.9178098974699, + "width": 191.09982299804688, + "height": 25, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -897,33 +1143,32 @@ "opacity": 100, "groupIds": [], "frameId": null, - "index": "aT", "roundness": null, - "seed": 1310238883, - "version": 1172, - "versionNonce": 245211683, + "seed": 345871591, + "version": 33, + "versionNonce": 913819655, "isDeleted": false, "boundElements": null, - "updated": 1726581303535, + "updated": 1726588459813, "link": null, "locked": false, - "text": "Idea:\n- Always send events from the latest \nchannel scope that has been accessed. \nWhen a user clicks on a channel, they \nwill send a get messages request to the\nchannel. We will use that request to \ndetermine which channel the user is \nviewing.\n- Send events from all other channels \nthe user has accessed via HTTP API in\nthe last XY seconds. When the user \naccesses a new channel, the \"old\" \nchannel is pushed back into a sort of \nqueue, where after a set amount of \ntime, the user will be unsubscribed from \nthese channels' \"detailed events\". The \nunsubscribing can be stopped if the \nuser sends a WS payload which \ndescribes the intent to keep receiving \nevents from that channel. This way, one\ncan view multiple channels at the same \ntime using custom clients, while single-\nview clients are also fully supported.", + "text": "- Always subscribed", "fontSize": 20, - "fontFamily": 5, + "fontFamily": 1, "textAlign": "left", "verticalAlign": "top", + "baseline": 18, "containerId": null, - "originalText": "Idea:\n- Always send events from the latest channel scope that has been accessed. When a user clicks on a channel, they will send a get messages request to the channel. We will use that request to determine which channel the user is viewing.\n- Send events from all other channels the user has accessed via HTTP API in the last XY seconds. When the user accesses a new channel, the \"old\" channel is pushed back into a sort of queue, where after a set amount of time, the user will be unsubscribed from these channels' \"detailed events\". The unsubscribing can be stopped if the user sends a WS payload which describes the intent to keep receiving events from that channel. This way, one can view multiple channels at the same time using custom clients, while single-view clients are also fully supported.", - "autoResize": false, + "originalText": "- Always subscribed", "lineHeight": 1.25 }, { - "id": "2sjonxYVzFv7deYkoQoX5", + "id": "Z9pK_w-Js_hASq2cNWM8Z", "type": "rectangle", - "x": 1259, - "y": 460, - "width": 214, - "height": 74, + "x": 1240, + "y": 960, + "width": 140, + "height": 60, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -934,28 +1179,27 @@ "opacity": 100, "groupIds": [], "frameId": null, - "index": "aU", "roundness": null, - "seed": 1759587, - "version": 120, - "versionNonce": 1701385027, + "seed": 190047209, + "version": 12, + "versionNonce": 1674436585, "isDeleted": false, "boundElements": [ { "type": "text", - "id": "fq34t956YSgc0zsG_zZeu" + "id": "IgHGpcaA-ptVAN4_bbFL_" } ], - "updated": 1726581341666, + "updated": 1726588459813, "link": null, "locked": false }, { - "id": "fq34t956YSgc0zsG_zZeu", + "id": "IgHGpcaA-ptVAN4_bbFL_", "type": "text", - "x": 1291.7583312988281, - "y": 472, - "width": 148.48333740234375, + "x": 1269.020034790039, + "y": 965, + "width": 81.95993041992188, "height": 50, "angle": 0, "strokeColor": "#1e1e1e", @@ -967,33 +1211,32 @@ "opacity": 100, "groupIds": [], "frameId": null, - "index": "aU0G", "roundness": null, - "seed": 1130921997, - "version": 43, - "versionNonce": 377703981, + "seed": 669337481, + "version": 15, + "versionNonce": 1886334759, "isDeleted": false, "boundElements": null, - "updated": 1726581348263, + "updated": 1726588459813, "link": null, "locked": false, - "text": "Active Channel \nUpdates", + "text": "User \nUpdates", "fontSize": 20, - "fontFamily": 5, + "fontFamily": 1, "textAlign": "center", "verticalAlign": "middle", - "containerId": "2sjonxYVzFv7deYkoQoX5", - "originalText": "Active Channel Updates", - "autoResize": true, + "baseline": 43, + "containerId": "Z9pK_w-Js_hASq2cNWM8Z", + "originalText": "User Updates", "lineHeight": 1.25 }, { - "id": "B14jWAlwXTnH4FLL-5KOB", + "id": "Zt9bVlywuPXkMqJ81QcUV", "type": "text", - "x": 1262, - "y": 571, - "width": 211, - "height": 200, + "x": 1240, + "y": 1040, + "width": 198.49984741210938, + "height": 175, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -1004,31 +1247,28 @@ "opacity": 100, "groupIds": [], "frameId": null, - "index": "aZ", "roundness": null, - "seed": 2043685229, - "version": 315, - "versionNonce": 1614546253, + "seed": 1862542215, + "version": 226, + "versionNonce": 1958476489, "isDeleted": false, "boundElements": null, - "updated": 1726581617515, + "updated": 1726588459813, "link": null, "locked": false, - "text": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user \nupdates (See User \nUpdates)\n- Passive Channel \nUpdates", + "text": "- Always subscribed\nto Friends\n- Subscribed to\nmax. 10 participants\nin a channel(?)\n- Not subscribed to\ndm people", "fontSize": 20, - "fontFamily": 5, + "fontFamily": 1, "textAlign": "left", "verticalAlign": "top", + "baseline": 168, "containerId": null, - "originalText": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user updates (See User Updates)\n- Passive Channel Updates", - "autoResize": false, + "originalText": "- Always subscribed\nto Friends\n- Subscribed to\nmax. 10 participants\nin a channel(?)\n- Not subscribed to\ndm people", "lineHeight": 1.25 } ], "appState": { "gridSize": 20, - "gridStep": 5, - "gridModeEnabled": false, "viewBackgroundColor": "#ffffff" }, "files": {} From bfd312d37f23df4e8a35e13561450e436261bc7d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 17 Sep 2024 21:27:07 +0200 Subject: [PATCH 126/162] update whiteboard --- docs/Event Publishing in Symfonia.excalidraw | 804 ++++++++++++++----- 1 file changed, 581 insertions(+), 223 deletions(-) diff --git a/docs/Event Publishing in Symfonia.excalidraw b/docs/Event Publishing in Symfonia.excalidraw index b94be98..5852a2d 100644 --- a/docs/Event Publishing in Symfonia.excalidraw +++ b/docs/Event Publishing in Symfonia.excalidraw @@ -1,12 +1,13 @@ { "type": "excalidraw", "version": 2, - "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", + "source": "https://excalidraw.com", "elements": [ { "type": "rectangle", - "version": 154, - "versionNonce": 824718855, + "version": 155, + "versionNonce": 2135653332, + "index": "a0", "isDeleted": false, "id": "2MVXKRrbf5jvhORRI9URX", "fillStyle": "solid", @@ -35,14 +36,15 @@ "type": "arrow" } ], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false }, { "type": "text", - "version": 43, - "versionNonce": 2099466729, + "version": 44, + "versionNonce": 169466732, + "index": "a1", "isDeleted": false, "id": "OTBzzVqwEY6os024JScwh", "fillStyle": "solid", @@ -62,7 +64,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false, "fontSize": 20, @@ -72,13 +74,14 @@ "verticalAlign": "top", "containerId": null, "originalText": "API Endpoint", - "lineHeight": 1.25, - "baseline": 19 + "autoResize": true, + "lineHeight": 1.25 }, { "type": "rectangle", - "version": 188, - "versionNonce": 1457642791, + "version": 189, + "versionNonce": 563405140, + "index": "a2", "isDeleted": false, "id": "4Sf2ujjlCt5_fuSqXkiLB", "fillStyle": "solid", @@ -111,14 +114,15 @@ "type": "arrow" } ], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false }, { "type": "text", - "version": 125, - "versionNonce": 1720921289, + "version": 126, + "versionNonce": 396594668, + "index": "a3", "isDeleted": false, "id": "FZtE7MMsPYRo6B5BjG9_E", "fillStyle": "solid", @@ -138,7 +142,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false, "fontSize": 20, @@ -148,13 +152,14 @@ "verticalAlign": "middle", "containerId": "4Sf2ujjlCt5_fuSqXkiLB", "originalText": "Event Publisher", - "lineHeight": 1.25, - "baseline": 19 + "autoResize": true, + "lineHeight": 1.25 }, { "type": "ellipse", - "version": 573, - "versionNonce": 1629069383, + "version": 574, + "versionNonce": 1351980756, + "index": "a4", "isDeleted": false, "id": "1JFUtbSSfm7QWM0KjMPE2", "fillStyle": "solid", @@ -183,14 +188,15 @@ "type": "arrow" } ], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false }, { "type": "text", - "version": 530, - "versionNonce": 251964329, + "version": 531, + "versionNonce": 1582493804, + "index": "a5", "isDeleted": false, "id": "_Pw8i03cpH5wSwGx3Ri9P", "fillStyle": "solid", @@ -210,7 +216,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false, "fontSize": 20, @@ -220,13 +226,14 @@ "verticalAlign": "middle", "containerId": "1JFUtbSSfm7QWM0KjMPE2", "originalText": "Subscriber", - "lineHeight": 1.25, - "baseline": 19 + "autoResize": true, + "lineHeight": 1.25 }, { "type": "ellipse", - "version": 589, - "versionNonce": 1370226535, + "version": 590, + "versionNonce": 417569876, + "index": "a6", "isDeleted": false, "id": "A4UGJKAYTwb60t4OudBqR", "fillStyle": "solid", @@ -255,14 +262,15 @@ "type": "arrow" } ], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false }, { "type": "text", - "version": 546, - "versionNonce": 1856196233, + "version": 547, + "versionNonce": 233964268, + "index": "a7", "isDeleted": false, "id": "3TPexlcGphxPOeYq8__TX", "fillStyle": "solid", @@ -282,7 +290,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false, "fontSize": 20, @@ -292,13 +300,14 @@ "verticalAlign": "middle", "containerId": "A4UGJKAYTwb60t4OudBqR", "originalText": "Subscriber", - "lineHeight": 1.25, - "baseline": 19 + "autoResize": true, + "lineHeight": 1.25 }, { "type": "arrow", - "version": 94, - "versionNonce": 988117639, + "version": 95, + "versionNonce": 72675796, + "index": "a8", "isDeleted": false, "id": "hvS4AEabLaqrr1-YIMYjQ", "fillStyle": "solid", @@ -320,7 +329,7 @@ "type": 2 }, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false, "startBinding": { @@ -351,8 +360,9 @@ }, { "type": "arrow", - "version": 138, - "versionNonce": 1814730089, + "version": 139, + "versionNonce": 1292551532, + "index": "a9", "isDeleted": false, "id": "0xjS1TWpsg8zIrQa57fCf", "fillStyle": "solid", @@ -374,7 +384,7 @@ "type": 2 }, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false, "startBinding": { @@ -405,8 +415,9 @@ }, { "type": "rectangle", - "version": 107, - "versionNonce": 830640551, + "version": 108, + "versionNonce": 1912575828, + "index": "aA", "isDeleted": false, "id": "jL7ecVW1c_uet6ZE5_v-G", "fillStyle": "solid", @@ -431,14 +442,15 @@ "id": "PdP90lnYk5InEtHZQIWRw" } ], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false }, { "type": "text", - "version": 62, - "versionNonce": 182845513, + "version": 63, + "versionNonce": 1625765868, + "index": "aB", "isDeleted": false, "id": "PdP90lnYk5InEtHZQIWRw", "fillStyle": "solid", @@ -458,7 +470,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false, "fontSize": 20, @@ -468,13 +480,14 @@ "verticalAlign": "middle", "containerId": "jL7ecVW1c_uet6ZE5_v-G", "originalText": "User Updates", - "lineHeight": 1.25, - "baseline": 19 + "autoResize": true, + "lineHeight": 1.25 }, { "type": "text", - "version": 92, - "versionNonce": 2125448391, + "version": 94, + "versionNonce": 1950026324, + "index": "aC", "isDeleted": false, "id": "A4F4pjFUhGoZqAx_Ph_XU", "fillStyle": "solid", @@ -487,14 +500,14 @@ "y": 294, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 112.763671875, + "width": 134.23333333333332, "height": 75, "seed": 361037955, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598793116, "link": null, "locked": false, "fontSize": 20, @@ -504,13 +517,14 @@ "verticalAlign": "top", "containerId": null, "originalText": "- Relationship\n- Avatar\n- Biography", - "lineHeight": 1.25, - "baseline": 69 + "autoResize": true, + "lineHeight": 1.25 }, { "type": "rectangle", - "version": 73, - "versionNonce": 1487998761, + "version": 74, + "versionNonce": 1435589228, + "index": "aD", "isDeleted": false, "id": "oWA9AtvfD6_HSuXMUtzr4", "fillStyle": "solid", @@ -535,14 +549,15 @@ "id": "cI-tGafUxksU_I5Mpd0ga" } ], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false }, { "type": "text", - "version": 18, - "versionNonce": 302617575, + "version": 19, + "versionNonce": 177787476, + "index": "aE", "isDeleted": false, "id": "cI-tGafUxksU_I5Mpd0ga", "fillStyle": "solid", @@ -562,7 +577,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false, "fontSize": 20, @@ -572,13 +587,14 @@ "verticalAlign": "middle", "containerId": "oWA9AtvfD6_HSuXMUtzr4", "originalText": "Guild Updates", - "lineHeight": 1.25, - "baseline": 19 + "autoResize": true, + "lineHeight": 1.25 }, { "type": "text", - "version": 126, - "versionNonce": 469189129, + "version": 130, + "versionNonce": 797734484, + "index": "aF", "isDeleted": false, "id": "RfSnEaRHOpA5YddANm-4Q", "fillStyle": "solid", @@ -591,30 +607,31 @@ "y": 300, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 253.2421875, - "height": 75, + "width": 175.31666666666666, + "height": 100, "seed": 1187124589, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598804565, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "- Name Change\n- Icon Change\n- Other public attribute changes", + "text": "- Name Change\n- Icon Change\n- Other public\nattribute changes", "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "- Name Change\n- Icon Change\n- Other public attribute changes", - "lineHeight": 1.25, - "baseline": 69 + "originalText": "- Name Change\n- Icon Change\n- Other public\nattribute changes", + "autoResize": true, + "lineHeight": 1.25 }, { "type": "rectangle", - "version": 121, - "versionNonce": 865894151, + "version": 122, + "versionNonce": 905144276, + "index": "aG", "isDeleted": false, "id": "ch7tfkBfIKlUfgWsBmyTw", "fillStyle": "solid", @@ -639,14 +656,15 @@ "id": "3hE5gwZr8o6CjWkvfQ9zv" } ], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false }, { "type": "text", - "version": 33, - "versionNonce": 1102321897, + "version": 34, + "versionNonce": 1664528236, + "index": "aH", "isDeleted": false, "id": "3hE5gwZr8o6CjWkvfQ9zv", "fillStyle": "solid", @@ -666,7 +684,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false, "fontSize": 20, @@ -676,13 +694,14 @@ "verticalAlign": "middle", "containerId": "ch7tfkBfIKlUfgWsBmyTw", "originalText": "Extended Guild Updates", - "lineHeight": 1.25, - "baseline": 44 + "autoResize": true, + "lineHeight": 1.25 }, { "type": "text", - "version": 106, - "versionNonce": 1977943591, + "version": 147, + "versionNonce": 1738131540, + "index": "aI", "isDeleted": false, "id": "BOkSrfJNV_yDCdj7-YJct", "fillStyle": "solid", @@ -695,14 +714,14 @@ "y": 301, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 136.669921875, + "width": 193.26223450443933, "height": 50, "seed": 1219020557, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598809537, "link": null, "locked": false, "fontSize": 20, @@ -712,13 +731,14 @@ "verticalAlign": "top", "containerId": null, "originalText": "- Full Audit Log with Usernames", - "lineHeight": 1.25, - "baseline": 44 + "autoResize": false, + "lineHeight": 1.25 }, { "type": "rectangle", - "version": 262, - "versionNonce": 192091081, + "version": 263, + "versionNonce": 1577443820, + "index": "aJ", "isDeleted": false, "id": "-M-N8zfYU15qrMGHtXLOg", "fillStyle": "solid", @@ -743,14 +763,15 @@ "id": "q46wzLGHNKS-PDdoV2Yt-" } ], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false }, { "type": "text", - "version": 149, - "versionNonce": 1741662535, + "version": 150, + "versionNonce": 1462928084, + "index": "aK", "isDeleted": false, "id": "q46wzLGHNKS-PDdoV2Yt-", "fillStyle": "solid", @@ -770,7 +791,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726598715928, "link": null, "locked": false, "fontSize": 20, @@ -780,13 +801,14 @@ "verticalAlign": "middle", "containerId": "-M-N8zfYU15qrMGHtXLOg", "originalText": "Passive Channel Updates", - "lineHeight": 1.25, - "baseline": 44 + "autoResize": true, + "lineHeight": 1.25 }, { "type": "text", - "version": 391, - "versionNonce": 2102375081, + "version": 443, + "versionNonce": 1749396588, + "index": "aL", "isDeleted": false, "id": "BD_37_o-zsNIxKV-GP_gh", "fillStyle": "solid", @@ -799,30 +821,31 @@ "y": 566, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 174.43359375, - "height": 200, + "width": 204.3002887347182, + "height": 125, "seed": 1198423533, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459812, + "updated": 1726600771451, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "- Permission Changes\n(ones that do not \nin-/exclude the user \nto/from the channel)\n- Channel \nname/description \nchanges\n-", + "text": "- Permission Changes\n- Channel \nname/description \nchanges\n- @mentions", "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "- Permission Changes\n(ones that do not in-/exclude the user to/from the channel)\n- Channel name/description changes\n-", - "lineHeight": 1.25, - "baseline": 194 + "originalText": "- Permission Changes\n- Channel name/description changes\n- @mentions", + "autoResize": false, + "lineHeight": 1.25 }, { "type": "text", - "version": 383, - "versionNonce": 112781415, + "version": 491, + "versionNonce": 2109563220, + "index": "aM", "isDeleted": false, "id": "dgmOCsrSsYfsKwLF33TK1", "fillStyle": "solid", @@ -835,30 +858,31 @@ "y": 753, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 309.375, - "height": 200, + "width": 425.52325827390416, + "height": 175, "seed": 1967997485, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459813, + "updated": 1726599083888, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "Goals in Mind:\n\n- Support multiple chats opened at the \nsame time\n- Send the least amount of messages \nrequired without sacrificing UX\n- Do not modify Discord API/WS \nbehaviour", + "text": "Goals in Mind:\n\n- Support multiple chats opened at the \nsame time\n- Send the least amount of messages \nrequired without sacrificing UX\n- Do not modify Discord API/WS behaviour", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Goals in Mind:\n\n- Support multiple chats opened at the same time\n- Send the least amount of messages required without sacrificing UX\n- Do not modify Discord API/WS behaviour", - "lineHeight": 1.25, - "baseline": 194 + "autoResize": false, + "lineHeight": 1.25 }, { "type": "text", - "version": 1177, - "versionNonce": 200635785, + "version": 1239, + "versionNonce": 977772244, + "index": "aN", "isDeleted": false, "id": "KUumgEMrgSNz1yHP1SAwp", "fillStyle": "solid", @@ -871,30 +895,31 @@ "y": 987, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 327.705078125, - "height": 575, + "width": 420.6236847441232, + "height": 550, "seed": 1310238883, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459813, + "updated": 1726599088418, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "Idea:\n- Always send events from the latest \nchannel scope that has been accessed. \nWhen a user clicks on a channel, they \nwill send a get messages request to the\nchannel. We will use that request to \ndetermine which channel the user is \nviewing.\n- Send events from all other channels \nthe user has accessed via HTTP API in\nthe last XY seconds. When the user \naccesses a new channel, the \"old\" \nchannel is pushed back into a sort of \nqueue, where after a set amount of \ntime, the user will be unsubscribed from \nthese channels' \"detailed events\". The \nunsubscribing can be stopped if the \nuser sends a WS payload which \ndescribes the intent to keep receiving \nevents from that channel. This way, one\ncan view multiple channels at the same \ntime using custom clients, while single-\nview clients are also fully supported.", + "text": "Idea:\n- Always send events from the latest \nchannel scope that has been accessed. \nWhen a user clicks on a channel, they will \nsend a get messages request to the \nchannel. We will use that request to \ndetermine which channel the user is \nviewing.\n- Send events from all other channels the \nuser has accessed via HTTP API in the \nlast XY seconds. When the user accesses a\nnew channel, the \"old\" channel is pushed \nback into a sort of queue, where after a \nset amount of time, the user will be \nunsubscribed from these channels' \n\"detailed events\". The unsubscribing can be\nstopped if the user sends a WS payload \nwhich describes the intent to keep \nreceiving events from that channel. This \nway, one can view multiple channels at the \nsame time using custom clients, while \nsingle-view clients are also fully supported.", "textAlign": "left", "verticalAlign": "top", "containerId": null, "originalText": "Idea:\n- Always send events from the latest channel scope that has been accessed. When a user clicks on a channel, they will send a get messages request to the channel. We will use that request to determine which channel the user is viewing.\n- Send events from all other channels the user has accessed via HTTP API in the last XY seconds. When the user accesses a new channel, the \"old\" channel is pushed back into a sort of queue, where after a set amount of time, the user will be unsubscribed from these channels' \"detailed events\". The unsubscribing can be stopped if the user sends a WS payload which describes the intent to keep receiving events from that channel. This way, one can view multiple channels at the same time using custom clients, while single-view clients are also fully supported.", - "lineHeight": 1.25, - "baseline": 569 + "autoResize": false, + "lineHeight": 1.25 }, { "type": "rectangle", - "version": 123, - "versionNonce": 1886208903, + "version": 124, + "versionNonce": 564583892, + "index": "aO", "isDeleted": false, "id": "2sjonxYVzFv7deYkoQoX5", "fillStyle": "solid", @@ -919,14 +944,15 @@ "id": "fq34t956YSgc0zsG_zZeu" } ], - "updated": 1726588459813, + "updated": 1726598715928, "link": null, "locked": false }, { "type": "text", - "version": 46, - "versionNonce": 178248809, + "version": 47, + "versionNonce": 1461557612, + "index": "aP", "isDeleted": false, "id": "fq34t956YSgc0zsG_zZeu", "fillStyle": "solid", @@ -946,7 +972,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459813, + "updated": 1726598715928, "link": null, "locked": false, "fontSize": 20, @@ -956,13 +982,14 @@ "verticalAlign": "middle", "containerId": "2sjonxYVzFv7deYkoQoX5", "originalText": "Active Channel Updates", - "lineHeight": 1.25, - "baseline": 44 + "autoResize": true, + "lineHeight": 1.25 }, { "type": "text", - "version": 319, - "versionNonce": 1428765351, + "version": 421, + "versionNonce": 578730068, + "index": "aQ", "isDeleted": false, "id": "B14jWAlwXTnH4FLL-5KOB", "fillStyle": "solid", @@ -975,127 +1002,344 @@ "y": 571, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 163.291015625, - "height": 200, + "width": 208.64414504623892, + "height": 275, "seed": 2043685229, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726588459813, + "updated": 1726600670944, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user \nupdates (See User \nUpdates)\n- Passive Channel \nUpdates", + "text": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user \nupdates (See User \nUpdates)\n- New \nThreads/Deleted \nThreads\n- Passive Channel \nUpdates", "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user updates (See User Updates)\n- Passive Channel Updates", - "lineHeight": 1.25, - "baseline": 194 + "originalText": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user updates (See User Updates)\n- New Threads/Deleted Threads\n- Passive Channel Updates", + "autoResize": false, + "lineHeight": 1.25 }, { - "id": "7Nqx9LYOEEzgetXjNa2_b", "type": "rectangle", + "version": 146, + "versionNonce": 1027198060, + "index": "aR", + "isDeleted": false, + "id": "7Nqx9LYOEEzgetXjNa2_b", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, "x": 1000, "y": 900, - "width": 700, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1188.92885863872, "height": 500, + "seed": 741046727, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726600639440, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 30, + "versionNonce": 956332244, + "index": "aS", + "isDeleted": false, + "id": "E9kU-lMID9gyW-OY4I5Un", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, "angle": 0, + "x": 1260, + "y": 900, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", + "width": 179.059814453125, + "height": 25, + "seed": 529728071, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726598715928, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "User Subscriptions", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "User Subscriptions", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "rectangle", + "version": 10, + "versionNonce": 805252716, + "index": "aT", + "isDeleted": false, + "id": "SFCx_f3SYUvjWr1siydbq", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1020, + "y": 960, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 140, + "height": 60, + "seed": 911025735, "groupIds": [], "frameId": null, "roundness": null, - "seed": 741046727, - "version": 37, - "versionNonce": 1494902601, - "isDeleted": false, - "boundElements": null, - "updated": 1726588459813, + "boundElements": [ + { + "type": "text", + "id": "qcvyp3WuBaAPIQ-5fqR3H" + } + ], + "updated": 1726598715928, "link": null, "locked": false }, { - "id": "E9kU-lMID9gyW-OY4I5Un", "type": "text", - "x": 1260, - "y": 900, - "width": 179.059814453125, - "height": 25, + "version": 17, + "versionNonce": 899825236, + "index": "aU", + "isDeleted": false, + "id": "qcvyp3WuBaAPIQ-5fqR3H", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, "angle": 0, + "x": 1049.020034790039, + "y": 965, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", + "width": 81.95993041992188, + "height": 50, + "seed": 740720969, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726598715928, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "Guild \nUpdates", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "SFCx_f3SYUvjWr1siydbq", + "originalText": "Guild Updates", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "text", + "version": 34, + "versionNonce": 459702508, + "index": "aV", + "isDeleted": false, + "id": "CrENgJMFm5et4g_0hTvHx", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1021.1800482818668, + "y": 1038.9178098974699, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 191.09982299804688, + "height": 25, + "seed": 345871591, "groupIds": [], "frameId": null, "roundness": null, - "seed": 529728071, - "version": 29, - "versionNonce": 697912775, - "isDeleted": false, - "boundElements": null, - "updated": 1726588459813, + "boundElements": [], + "updated": 1726598715928, "link": null, "locked": false, - "text": "User Subscriptions", "fontSize": 20, "fontFamily": 1, + "text": "- Always subscribed", "textAlign": "left", "verticalAlign": "top", - "baseline": 18, "containerId": null, - "originalText": "User Subscriptions", + "originalText": "- Always subscribed", + "autoResize": true, "lineHeight": 1.25 }, { - "id": "SFCx_f3SYUvjWr1siydbq", "type": "rectangle", - "x": 1020, + "version": 13, + "versionNonce": 351228884, + "index": "aW", + "isDeleted": false, + "id": "Z9pK_w-Js_hASq2cNWM8Z", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1240, "y": 960, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", "width": 140, "height": 60, + "seed": 190047209, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "IgHGpcaA-ptVAN4_bbFL_" + } + ], + "updated": 1726598715928, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 16, + "versionNonce": 1933542252, + "index": "aX", + "isDeleted": false, + "id": "IgHGpcaA-ptVAN4_bbFL_", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, "angle": 0, + "x": 1269.020034790039, + "y": 965, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", + "width": 81.95993041992188, + "height": 50, + "seed": 669337481, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726598715928, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "User \nUpdates", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Z9pK_w-Js_hASq2cNWM8Z", + "originalText": "User Updates", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "type": "text", + "version": 230, + "versionNonce": 542704236, + "index": "aY", + "isDeleted": false, + "id": "Zt9bVlywuPXkMqJ81QcUV", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, + "angle": 0, + "x": 1240, + "y": 1040, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 198.63333333333333, + "height": 175, + "seed": 1862542215, "groupIds": [], "frameId": null, "roundness": null, - "seed": 911025735, - "version": 9, - "versionNonce": 1371575849, + "boundElements": [], + "updated": 1726598746109, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 1, + "text": "- Always subscribed\nto Friends\n- Subscribed to\nmax. 10 participants\nin a channel(?)\n- Not subscribed to\nDM people", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- Always subscribed\nto Friends\n- Subscribed to\nmax. 10 participants\nin a channel(?)\n- Not subscribed to\nDM people", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "A7Hdhub5PVx-5ZA7a6e8x", + "type": "rectangle", + "x": 1483.0463180382053, + "y": 959.1795524862915, + "width": 149.3334749235912, + "height": 63.31852166496879, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aa", + "roundness": null, + "seed": 1879765588, + "version": 157, + "versionNonce": 324374124, "isDeleted": false, "boundElements": [ { "type": "text", - "id": "qcvyp3WuBaAPIQ-5fqR3H" + "id": "a__xxvCZDR7K9FGOwsbI0" } ], - "updated": 1726588459813, + "updated": 1726599019432, "link": null, "locked": false }, { - "id": "qcvyp3WuBaAPIQ-5fqR3H", + "id": "a__xxvCZDR7K9FGOwsbI0", "type": "text", - "x": 1049.020034790039, - "y": 965, - "width": 81.95993041992188, + "x": 1488.313055500001, + "y": 965.8388133187759, + "width": 138.8, "height": 50, "angle": 0, "strokeColor": "#1e1e1e", @@ -1107,31 +1351,139 @@ "opacity": 100, "groupIds": [], "frameId": null, + "index": "aa2", "roundness": null, - "seed": 740720969, - "version": 16, - "versionNonce": 416939239, + "seed": 888381268, + "version": 65, + "versionNonce": 82895084, "isDeleted": false, "boundElements": null, - "updated": 1726588459813, + "updated": 1726599019432, "link": null, "locked": false, - "text": "Guild \nUpdates", + "text": "Extended \nGuild Updates", "fontSize": 20, - "fontFamily": 1, + "fontFamily": 5, "textAlign": "center", "verticalAlign": "middle", - "baseline": 43, - "containerId": "SFCx_f3SYUvjWr1siydbq", - "originalText": "Guild Updates", + "containerId": "A7Hdhub5PVx-5ZA7a6e8x", + "originalText": "Extended Guild Updates", + "autoResize": true, "lineHeight": 1.25 }, { - "id": "CrENgJMFm5et4g_0hTvHx", + "id": "3HvfXk7LSKslLUwOPx1g1", "type": "text", - "x": 1021.1800482818668, - "y": 1038.9178098974699, - "width": 191.09982299804688, + "x": 1484.2586658148514, + "y": 1039.266071876967, + "width": 142, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ac", + "roundness": null, + "seed": 371454956, + "version": 40, + "versionNonce": 752428396, + "isDeleted": false, + "boundElements": null, + "updated": 1726598893667, + "link": null, + "locked": false, + "text": "- Subscribed if\neligible", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- Subscribed if\neligible", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "0s0v0Ph79R3KV2D6b1p89", + "type": "rectangle", + "x": 1664.5650096114834, + "y": 958.5153780293955, + "width": 163.71373547178882, + "height": 63.05191163440509, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ad", + "roundness": null, + "seed": 1300696788, + "version": 118, + "versionNonce": 1920810580, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "X2DYwzvMmzYfm_C8EYUfZ" + } + ], + "updated": 1726599032470, + "link": null, + "locked": false + }, + { + "id": "X2DYwzvMmzYfm_C8EYUfZ", + "type": "text", + "x": 1670.0552106807113, + "y": 965.041333846598, + "width": 152.73333333333332, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "ae", + "roundness": null, + "seed": 1760833132, + "version": 28, + "versionNonce": 265753708, + "isDeleted": false, + "boundElements": null, + "updated": 1726599036977, + "link": null, + "locked": false, + "text": "Passive Channel\nUpdates", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "0s0v0Ph79R3KV2D6b1p89", + "originalText": "Passive Channel Updates", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "NmUZHPnkX3HyWYoAyEAQb", + "type": "text", + "x": 1666.7773573881293, + "y": 1039.266071876967, + "width": 189.81666564941406, "height": 25, "angle": 0, "strokeColor": "#1e1e1e", @@ -1143,32 +1495,33 @@ "opacity": 100, "groupIds": [], "frameId": null, + "index": "af", "roundness": null, - "seed": 345871591, - "version": 33, - "versionNonce": 913819655, + "seed": 2127948140, + "version": 184, + "versionNonce": 1680085740, "isDeleted": false, "boundElements": null, - "updated": 1726588459813, + "updated": 1726600631188, "link": null, "locked": false, "text": "- Always subscribed", "fontSize": 20, - "fontFamily": 1, + "fontFamily": 5, "textAlign": "left", "verticalAlign": "top", - "baseline": 18, "containerId": null, "originalText": "- Always subscribed", + "autoResize": true, "lineHeight": 1.25 }, { - "id": "Z9pK_w-Js_hASq2cNWM8Z", + "id": "dXRiLAxAGjQsMZJou8SsK", "type": "rectangle", - "x": 1240, - "y": 960, - "width": 140, - "height": 60, + "x": 1903.4985694892282, + "y": 957.4092041410731, + "width": 158.18286603017418, + "height": 65.26425941105094, "angle": 0, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -1179,27 +1532,28 @@ "opacity": 100, "groupIds": [], "frameId": null, + "index": "ag", "roundness": null, - "seed": 190047209, - "version": 12, - "versionNonce": 1674436585, + "seed": 1174667476, + "version": 86, + "versionNonce": 572168940, "isDeleted": false, "boundElements": [ { "type": "text", - "id": "IgHGpcaA-ptVAN4_bbFL_" + "id": "VWbtks-OQBuNFRwUPRL6D" } ], - "updated": 1726588459813, + "updated": 1726600742156, "link": null, "locked": false }, { - "id": "IgHGpcaA-ptVAN4_bbFL_", + "id": "VWbtks-OQBuNFRwUPRL6D", "type": "text", - "x": 1269.020034790039, - "y": 965, - "width": 81.95993041992188, + "x": 1912.3483338031433, + "y": 965.0413338465986, + "width": 140.48333740234375, "height": 50, "angle": 0, "strokeColor": "#1e1e1e", @@ -1211,34 +1565,35 @@ "opacity": 100, "groupIds": [], "frameId": null, + "index": "agV", "roundness": null, - "seed": 669337481, - "version": 15, - "versionNonce": 1886334759, + "seed": 1823310932, + "version": 25, + "versionNonce": 1364958676, "isDeleted": false, "boundElements": null, - "updated": 1726588459813, + "updated": 1726600746259, "link": null, "locked": false, - "text": "User \nUpdates", + "text": "Active Channel\nUpdates", "fontSize": 20, - "fontFamily": 1, + "fontFamily": 5, "textAlign": "center", "verticalAlign": "middle", - "baseline": 43, - "containerId": "Z9pK_w-Js_hASq2cNWM8Z", - "originalText": "User Updates", + "containerId": "dXRiLAxAGjQsMZJou8SsK", + "originalText": "Active Channel Updates", + "autoResize": true, "lineHeight": 1.25 }, { - "id": "Zt9bVlywuPXkMqJ81QcUV", + "id": "ElxDUoNGS-gEJhuVxp-Ru", "type": "text", - "x": 1240, - "y": 1040, - "width": 198.49984741210938, - "height": 175, + "x": 1719.8737040276271, + "y": 443.038346070926, + "width": 471.5, + "height": 100, "angle": 0, - "strokeColor": "#1e1e1e", + "strokeColor": "#f08c00", "backgroundColor": "transparent", "fillStyle": "solid", "strokeWidth": 2, @@ -1247,28 +1602,31 @@ "opacity": 100, "groupIds": [], "frameId": null, + "index": "ai", "roundness": null, - "seed": 1862542215, - "version": 226, - "versionNonce": 1958476489, + "seed": 1068866388, + "version": 168, + "versionNonce": 88154860, "isDeleted": false, "boundElements": null, - "updated": 1726588459813, + "updated": 1726601168303, "link": null, "locked": false, - "text": "- Always subscribed\nto Friends\n- Subscribed to\nmax. 10 participants\nin a channel(?)\n- Not subscribed to\ndm people", + "text": "How does Discord handle the \"unread\nMessages\" indicator in Guilds and Channels?\nDo we just receive all the messages all the time,\nor how does it work?", "fontSize": 20, - "fontFamily": 1, + "fontFamily": 5, "textAlign": "left", "verticalAlign": "top", - "baseline": 168, "containerId": null, - "originalText": "- Always subscribed\nto Friends\n- Subscribed to\nmax. 10 participants\nin a channel(?)\n- Not subscribed to\ndm people", + "originalText": "How does Discord handle the \"unread\nMessages\" indicator in Guilds and Channels?\nDo we just receive all the messages all the time,\nor how does it work?", + "autoResize": true, "lineHeight": 1.25 } ], "appState": { "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, "viewBackgroundColor": "#ffffff" }, "files": {} From e1eb0c937237d8d9cbf2493810dd902976a5afc5 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Sun, 22 Sep 2024 01:01:10 +0200 Subject: [PATCH 127/162] Update whiteboard --- docs/Event Publishing in Symfonia.excalidraw | 23616 ++++++++++++++++- 1 file changed, 22875 insertions(+), 741 deletions(-) diff --git a/docs/Event Publishing in Symfonia.excalidraw b/docs/Event Publishing in Symfonia.excalidraw index 5852a2d..f792b6d 100644 --- a/docs/Event Publishing in Symfonia.excalidraw +++ b/docs/Event Publishing in Symfonia.excalidraw @@ -3,6 +3,220 @@ "version": 2, "source": "https://excalidraw.com", "elements": [ + { + "type": "rectangle", + "version": 930, + "versionNonce": 508696171, + "index": "Zs", + "isDeleted": false, + "id": "Yef_zHBLDgJgcHaAYtii3", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 992.8531459400365, + "y": 2219.8733700419616, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1839.1026870295834, + "height": 565.5943080887822, + "seed": 1583650603, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "Y-VdE-5JZCbVRtwiRhZM0" + } + ], + "updated": 1726919645376, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 551, + "versionNonce": 1306663083, + "index": "Zt", + "isDeleted": false, + "id": "Y-VdE-5JZCbVRtwiRhZM0", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1818.3544864030705, + "y": 2224.8733700419616, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 188.10000610351562, + "height": 21.6, + "seed": 1533350667, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919381684, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Server-/Outside boundary", + "textAlign": "center", + "verticalAlign": "top", + "containerId": "Yef_zHBLDgJgcHaAYtii3", + "originalText": "Server-/Outside boundary", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "rectangle", + "version": 677, + "versionNonce": 1255271621, + "index": "Zu", + "isDeleted": false, + "id": "pWnZ5m49e23wgtjp43Cjk", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1007.4070365241471, + "y": 2265.70346669144, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1798.6009389590572, + "height": 493.0980439136671, + "seed": 532042917, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "d___i3s4lLCSsCIGrwJcj" + } + ], + "updated": 1726919642616, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 374, + "versionNonce": 1946962219, + "index": "Zv", + "isDeleted": false, + "id": "d___i3s4lLCSsCIGrwJcj", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1840.8325060036757, + "y": 2270.70346669144, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 131.75, + "height": 21.6, + "seed": 940294693, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919467527, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "User-specific Task", + "textAlign": "center", + "verticalAlign": "top", + "containerId": "pWnZ5m49e23wgtjp43Cjk", + "originalText": "User-specific Task", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "rectangle", + "version": 746, + "versionNonce": 1014573035, + "index": "Zy", + "isDeleted": false, + "id": "aBcRnfi5WZIFdLks0fcNk", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1043.6701333544931, + "y": 2303.0614322921792, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 778.8793335223208, + "height": 431.6222461467981, + "seed": 1146378469, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "oJsFTZtKwwj41ba8qbaAF" + }, + { + "id": "NFrs7hBfcfS6Sh6nfbpdz", + "type": "arrow" + } + ], + "updated": 1726919565141, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 542, + "versionNonce": 1048769163, + "index": "Zz", + "isDeleted": false, + "id": "oJsFTZtKwwj41ba8qbaAF", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1363.0514657650676, + "y": 2308.0614322921792, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 140.11666870117188, + "height": 21.6, + "seed": 1608907077, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919565141, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Client-specific Task", + "textAlign": "center", + "verticalAlign": "top", + "containerId": "aBcRnfi5WZIFdLks0fcNk", + "originalText": "Client-specific Task", + "autoResize": true, + "lineHeight": 1.35 + }, { "type": "rectangle", "version": 155, @@ -42,8 +256,8 @@ }, { "type": "text", - "version": 44, - "versionNonce": 169466732, + "version": 49, + "versionNonce": 882960718, "index": "a1", "isDeleted": false, "id": "OTBzzVqwEY6os024JScwh", @@ -53,18 +267,18 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 514, + "x": 510, "y": 179, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 110.556640625, + "width": 132.3333282470703, "height": 25, "seed": 2120332483, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598715928, + "updated": 1726872241417, "link": null, "locked": false, "fontSize": 20, @@ -120,8 +334,8 @@ }, { "type": "text", - "version": 126, - "versionNonce": 396594668, + "version": 160, + "versionNonce": 1254865810, "index": "a3", "isDeleted": false, "id": "FZtE7MMsPYRo6B5BjG9_E", @@ -131,27 +345,27 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 496.35, + "x": 473.1166666666667, "y": 285.5, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 153.3, + "width": 199.76666666666665, "height": 25, "seed": 361869059, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598715928, + "updated": 1726872231628, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "Event Publisher", + "text": "tokio channel sender", "textAlign": "center", "verticalAlign": "middle", "containerId": "4Sf2ujjlCt5_fuSqXkiLB", - "originalText": "Event Publisher", + "originalText": "tokio channel sender", "autoResize": true, "lineHeight": 1.25 }, @@ -194,8 +408,8 @@ }, { "type": "text", - "version": 531, - "versionNonce": 1582493804, + "version": 540, + "versionNonce": 1962865486, "index": "a5", "isDeleted": false, "id": "_Pw8i03cpH5wSwGx3Ri9P", @@ -205,34 +419,34 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 383.1172439427031, + "x": 391.8255772760364, "y": 523.190602486691, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 99.8, + "width": 82.38333333333333, "height": 25, "seed": 511424259, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598715928, + "updated": 1726872234593, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "Subscriber", + "text": "Receiver", "textAlign": "center", "verticalAlign": "middle", "containerId": "1JFUtbSSfm7QWM0KjMPE2", - "originalText": "Subscriber", + "originalText": "Receiver", "autoResize": true, "lineHeight": 1.25 }, { "type": "ellipse", - "version": 590, - "versionNonce": 417569876, + "version": 591, + "versionNonce": 1037629390, "index": "a6", "isDeleted": false, "id": "A4UGJKAYTwb60t4OudBqR", @@ -242,7 +456,7 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 623, + "x": 622, "y": 500.5, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -262,14 +476,14 @@ "type": "arrow" } ], - "updated": 1726598715928, + "updated": 1726872234758, "link": null, "locked": false }, { "type": "text", - "version": 547, - "versionNonce": 233964268, + "version": 557, + "versionNonce": 165177618, "index": "a7", "isDeleted": false, "id": "3TPexlcGphxPOeYq8__TX", @@ -279,27 +493,27 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 655.1172439427031, + "x": 662.8255772760365, "y": 524.690602486691, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 99.8, + "width": 82.38333333333333, "height": 25, "seed": 703184909, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598715928, + "updated": 1726872236943, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "Subscriber", + "text": "Receiver", "textAlign": "center", "verticalAlign": "middle", "containerId": "A4UGJKAYTwb60t4OudBqR", - "originalText": "Subscriber", + "originalText": "Receiver", "autoResize": true, "lineHeight": 1.25 }, @@ -360,8 +574,8 @@ }, { "type": "arrow", - "version": 139, - "versionNonce": 1292551532, + "version": 140, + "versionNonce": 173792334, "index": "a9", "isDeleted": false, "id": "0xjS1TWpsg8zIrQa57fCf", @@ -371,12 +585,12 @@ "roughness": 1, "opacity": 100, "angle": 0, - "x": 704, - "y": 496, + "x": 703.1862546352941, + "y": 495.9991648086568, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 127, - "height": 159, + "width": 126.18625463529406, + "height": 158.99916480865681, "seed": 779244035, "groupIds": [], "frameId": null, @@ -384,7 +598,7 @@ "type": 2 }, "boundElements": [], - "updated": 1726598715928, + "updated": 1726872234759, "link": null, "locked": false, "startBinding": { @@ -408,1219 +622,23139 @@ 0 ], [ - -127, - -159 + -126.18625463529406, + -158.99916480865681 ] ] }, - { - "type": "rectangle", - "version": 108, - "versionNonce": 1912575828, - "index": "aA", - "isDeleted": false, - "id": "jL7ecVW1c_uet6ZE5_v-G", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1035, - "y": 198, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 181, - "height": 76, - "seed": 1955642147, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [ - { - "type": "text", - "id": "PdP90lnYk5InEtHZQIWRw" - } - ], - "updated": 1726598715928, - "link": null, - "locked": false - }, { "type": "text", - "version": 63, - "versionNonce": 1625765868, - "index": "aB", + "version": 491, + "versionNonce": 2109563220, + "index": "aM", "isDeleted": false, - "id": "PdP90lnYk5InEtHZQIWRw", + "id": "dgmOCsrSsYfsKwLF33TK1", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1058.4083333333333, - "y": 223.5, + "x": 359, + "y": 753, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 134.18333333333334, - "height": 25, - "seed": 30884451, + "width": 425.52325827390416, + "height": 175, + "seed": 1967997485, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598715928, + "updated": 1726599083888, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "User Updates", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "jL7ecVW1c_uet6ZE5_v-G", - "originalText": "User Updates", - "autoResize": true, + "text": "Goals in Mind:\n\n- Support multiple chats opened at the \nsame time\n- Send the least amount of messages \nrequired without sacrificing UX\n- Do not modify Discord API/WS behaviour", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Goals in Mind:\n\n- Support multiple chats opened at the same time\n- Send the least amount of messages required without sacrificing UX\n- Do not modify Discord API/WS behaviour", + "autoResize": false, "lineHeight": 1.25 }, { "type": "text", - "version": 94, - "versionNonce": 1950026324, - "index": "aC", + "version": 1239, + "versionNonce": 977772244, + "index": "aN", "isDeleted": false, - "id": "A4F4pjFUhGoZqAx_Ph_XU", + "id": "KUumgEMrgSNz1yHP1SAwp", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1037, - "y": 294, + "x": 359, + "y": 987, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 134.23333333333332, - "height": 75, - "seed": 361037955, + "width": 420.6236847441232, + "height": 550, + "seed": 1310238883, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598793116, + "updated": 1726599088418, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "- Relationship\n- Avatar\n- Biography", + "text": "Idea:\n- Always send events from the latest \nchannel scope that has been accessed. \nWhen a user clicks on a channel, they will \nsend a get messages request to the \nchannel. We will use that request to \ndetermine which channel the user is \nviewing.\n- Send events from all other channels the \nuser has accessed via HTTP API in the \nlast XY seconds. When the user accesses a\nnew channel, the \"old\" channel is pushed \nback into a sort of queue, where after a \nset amount of time, the user will be \nunsubscribed from these channels' \n\"detailed events\". The unsubscribing can be\nstopped if the user sends a WS payload \nwhich describes the intent to keep \nreceiving events from that channel. This \nway, one can view multiple channels at the \nsame time using custom clients, while \nsingle-view clients are also fully supported.", "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "- Relationship\n- Avatar\n- Biography", - "autoResize": true, + "originalText": "Idea:\n- Always send events from the latest channel scope that has been accessed. When a user clicks on a channel, they will send a get messages request to the channel. We will use that request to determine which channel the user is viewing.\n- Send events from all other channels the user has accessed via HTTP API in the last XY seconds. When the user accesses a new channel, the \"old\" channel is pushed back into a sort of queue, where after a set amount of time, the user will be unsubscribed from these channels' \"detailed events\". The unsubscribing can be stopped if the user sends a WS payload which describes the intent to keep receiving events from that channel. This way, one can view multiple channels at the same time using custom clients, while single-view clients are also fully supported.", + "autoResize": false, "lineHeight": 1.25 }, { "type": "rectangle", - "version": 74, - "versionNonce": 1435589228, - "index": "aD", + "version": 80, + "versionNonce": 336456782, + "index": "aj", "isDeleted": false, - "id": "oWA9AtvfD6_HSuXMUtzr4", + "id": "uctAc53nuACE9HNLZ4vY5", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1236, - "y": 198, + "x": 1076.167209201389, + "y": 231, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 186, - "height": 75, - "seed": 1165134125, + "width": 293.75, + "height": 71.25, + "seed": 1014903826, "groupIds": [], "frameId": null, - "roundness": null, + "roundness": { + "type": 3 + }, "boundElements": [ { "type": "text", - "id": "cI-tGafUxksU_I5Mpd0ga" + "id": "2DuWtSihpY-GZ0JNUaQjX" } ], - "updated": 1726598715928, + "updated": 1726871124109, "link": null, "locked": false }, { "type": "text", - "version": 19, - "versionNonce": 177787476, - "index": "aE", + "version": 15, + "versionNonce": 888406542, + "index": "aj4", "isDeleted": false, - "id": "cI-tGafUxksU_I5Mpd0ga", + "id": "2DuWtSihpY-GZ0JNUaQjX", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1259.6, - "y": 223, + "x": 1169.4588758680557, + "y": 254.125, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 138.8, + "width": 107.16666666666667, "height": 25, - "seed": 1054914211, + "seed": 1687572946, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598715928, + "updated": 1726871129106, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "Guild Updates", + "text": "Role based", "textAlign": "center", "verticalAlign": "middle", - "containerId": "oWA9AtvfD6_HSuXMUtzr4", - "originalText": "Guild Updates", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "type": "text", - "version": 130, - "versionNonce": 797734484, - "index": "aF", - "isDeleted": false, - "id": "RfSnEaRHOpA5YddANm-4Q", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "angle": 0, - "x": 1240, - "y": 300, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 175.31666666666666, - "height": 100, - "seed": 1187124589, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1726598804565, - "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 5, - "text": "- Name Change\n- Icon Change\n- Other public\nattribute changes", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "- Name Change\n- Icon Change\n- Other public\nattribute changes", + "containerId": "uctAc53nuACE9HNLZ4vY5", + "originalText": "Role based", "autoResize": true, "lineHeight": 1.25 }, { "type": "rectangle", - "version": 122, - "versionNonce": 905144276, - "index": "aG", + "version": 157, + "versionNonce": 1760617102, + "index": "al", "isDeleted": false, - "id": "ch7tfkBfIKlUfgWsBmyTw", + "id": "ieZZu2zGIwLcqyTV5a4CE", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1441, - "y": 198, + "x": 1483.042209201389, + "y": 230.375, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 199, - "height": 73, - "seed": 2113903043, + "width": 293.75, + "height": 71.25, + "seed": 306947858, "groupIds": [], "frameId": null, - "roundness": null, + "roundness": { + "type": 3 + }, "boundElements": [ { "type": "text", - "id": "3hE5gwZr8o6CjWkvfQ9zv" + "id": "MvOCYSEPCLzw-s5kiK43w" } ], - "updated": 1726598715928, + "updated": 1726871139501, "link": null, "locked": false }, { "type": "text", - "version": 34, - "versionNonce": 1664528236, - "index": "aH", + "version": 113, + "versionNonce": 638673102, + "index": "am", "isDeleted": false, - "id": "3hE5gwZr8o6CjWkvfQ9zv", + "id": "MvOCYSEPCLzw-s5kiK43w", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1461.875, - "y": 209.5, + "x": 1558.292209201389, + "y": 253.5, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 157.25, - "height": 50, - "seed": 1942465101, + "width": 143.25, + "height": 25, + "seed": 2083058898, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598715928, + "updated": 1726871139501, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "Extended Guild \nUpdates", + "text": "User ID based", "textAlign": "center", "verticalAlign": "middle", - "containerId": "ch7tfkBfIKlUfgWsBmyTw", - "originalText": "Extended Guild Updates", + "containerId": "ieZZu2zGIwLcqyTV5a4CE", + "originalText": "User ID based", "autoResize": true, "lineHeight": 1.25 }, { "type": "text", - "version": 147, - "versionNonce": 1738131540, - "index": "aI", + "version": 152, + "versionNonce": 784276302, + "index": "an", "isDeleted": false, - "id": "BOkSrfJNV_yDCdj7-YJct", + "id": "jf2ngP537Oa8kowc55JAi", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1442, - "y": 301, + "x": 1068.667209201389, + "y": 326, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 193.26223450443933, - "height": 50, - "seed": 1219020557, + "width": 312.9, + "height": 75, + "seed": 743310670, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598809537, + "updated": 1726871176942, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "- Full Audit Log \nwith Usernames", + "text": "- Guild updates: @everyone\n- New channel: Send to everyone\nwho can view the channel", "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "- Full Audit Log with Usernames", - "autoResize": false, + "originalText": "- Guild updates: @everyone\n- New channel: Send to everyone\nwho can view the channel", + "autoResize": true, "lineHeight": 1.25 }, { - "type": "rectangle", - "version": 263, - "versionNonce": 1577443820, - "index": "aJ", + "type": "text", + "version": 62, + "versionNonce": 1518014862, + "index": "ao", "isDeleted": false, - "id": "-M-N8zfYU15qrMGHtXLOg", + "id": "ZQ68mgcM38iWZKi5gb2F3", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1036, - "y": 462, + "x": 1489.917209201389, + "y": 331, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 212, - "height": 73, - "seed": 380897955, + "width": 144.96666666666667, + "height": 25, + "seed": 1403985870, "groupIds": [], "frameId": null, "roundness": null, - "boundElements": [ - { - "type": "text", - "id": "q46wzLGHNKS-PDdoV2Yt-" - } - ], - "updated": 1726598715928, + "boundElements": [], + "updated": 1726871235792, "link": null, - "locked": false + "locked": false, + "fontSize": 20, + "fontFamily": 5, + "text": "- Relationships", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "- Relationships", + "autoResize": true, + "lineHeight": 1.25 }, { "type": "text", - "version": 150, - "versionNonce": 1462928084, - "index": "aK", + "version": 315, + "versionNonce": 110446162, + "index": "ap", "isDeleted": false, - "id": "q46wzLGHNKS-PDdoV2Yt-", + "id": "3PN2ZjYb9D5Pj-SKax1Xc", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 1, "opacity": 100, "angle": 0, - "x": 1061.8833312988281, - "y": 473.5, + "x": 1071.167209201389, + "y": 429.75, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 160.23333740234375, - "height": 50, - "seed": 1089230851, + "width": 812.6333618164062, + "height": 75, + "seed": 1889819986, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598715928, + "updated": 1726871290983, "link": null, "locked": false, "fontSize": 20, "fontFamily": 5, - "text": "Passive Channel \nUpdates", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "-M-N8zfYU15qrMGHtXLOg", - "originalText": "Passive Channel Updates", + "text": "If multiple roles receive the same event, we need a Mathematical Set-like construct\nwhere each recipient can only exist once in the set, to not send the event multiple\ntimes.", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "If multiple roles receive the same event, we need a Mathematical Set-like construct\nwhere each recipient can only exist once in the set, to not send the event multiple\ntimes.", "autoResize": true, "lineHeight": 1.25 }, { "type": "text", - "version": 443, - "versionNonce": 1749396588, - "index": "aL", + "version": 385, + "versionNonce": 1779127122, + "index": "b04", "isDeleted": false, - "id": "BD_37_o-zsNIxKV-GP_gh", + "id": "YUxJv_gEUatv301rCZRUy", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1038, - "y": 566, + "x": 1072.667209201389, + "y": 544.8888888888889, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 204.3002887347182, - "height": 125, - "seed": 1198423533, + "backgroundColor": "#a5d8ff", + "width": 346.3833333333333, + "height": 129.60000000000002, + "seed": 1317886862, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726600771451, + "updated": 1726871949681, "link": null, "locked": false, - "fontSize": 20, - "fontFamily": 5, - "text": "- Permission Changes\n- Channel \nname/description \nchanges\n- @mentions", + "fontSize": 16, + "fontFamily": 6, + "text": "What do we *want*?\n- Do not query database often\n- Re-/storeable state from/to Database\n- Minimum amount of comparisons/computation\n- Deduplication: Only send event once, even if\nrecipient qualifies multiple times", "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "- Permission Changes\n- Channel name/description changes\n- @mentions", - "autoResize": false, - "lineHeight": 1.25 + "originalText": "What do we *want*?\n- Do not query database often\n- Re-/storeable state from/to Database\n- Minimum amount of comparisons/computation\n- Deduplication: Only send event once, even if\nrecipient qualifies multiple times", + "autoResize": true, + "lineHeight": 1.35 }, { - "type": "text", - "version": 491, - "versionNonce": 2109563220, - "index": "aM", + "type": "freedraw", + "version": 242, + "versionNonce": 1014662859, + "index": "b05", "isDeleted": false, - "id": "dgmOCsrSsYfsKwLF33TK1", + "id": "CHpg-ObEveeR8Mt-gucHW", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 359, - "y": 753, + "x": 1189.667209201389, + "y": 1208.888888888889, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 425.52325827390416, - "height": 175, - "seed": 1967997485, - "groupIds": [], + "backgroundColor": "#a5d8ff", + "width": 28, + "height": 22, + "seed": 1449237867, + "groupIds": [ + "22DR3CnQQnsnNkwU4IOoG" + ], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726599083888, + "updated": 1726918175611, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1, + 0 + ], + [ + -2, + 1 + ], + [ + -3, + 1 + ], + [ + -4, + 2 + ], + [ + -5, + 2 + ], + [ + -5, + 3 + ], + [ + -6, + 3 + ], + [ + -6, + 4 + ], + [ + -7, + 4 + ], + [ + -7, + 5 + ], + [ + -8, + 5 + ], + [ + -8, + 6 + ], + [ + -9, + 7 + ], + [ + -9, + 8 + ], + [ + -10, + 9 + ], + [ + -10, + 10 + ], + [ + -10, + 11 + ], + [ + -9, + 13 + ], + [ + -9, + 14 + ], + [ + -9, + 16 + ], + [ + -8, + 16 + ], + [ + -8, + 17 + ], + [ + -7, + 17 + ], + [ + -6, + 18 + ], + [ + -5, + 19 + ], + [ + -4, + 19 + ], + [ + -3, + 20 + ], + [ + -2, + 20 + ], + [ + -1, + 21 + ], + [ + 0, + 21 + ], + [ + 1, + 21 + ], + [ + 2, + 21 + ], + [ + 3, + 21 + ], + [ + 4, + 21 + ], + [ + 5, + 21 + ], + [ + 6, + 21 + ], + [ + 8, + 21 + ], + [ + 9, + 21 + ], + [ + 11, + 21 + ], + [ + 12, + 21 + ], + [ + 14, + 21 + ], + [ + 15, + 20 + ], + [ + 16, + 20 + ], + [ + 16, + 19 + ], + [ + 16, + 18 + ], + [ + 17, + 18 + ], + [ + 17, + 17 + ], + [ + 17, + 16 + ], + [ + 17, + 15 + ], + [ + 18, + 14 + ], + [ + 18, + 13 + ], + [ + 18, + 12 + ], + [ + 18, + 11 + ], + [ + 17, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 7 + ], + [ + 16, + 6 + ], + [ + 16, + 5 + ], + [ + 16, + 4 + ], + [ + 15, + 4 + ], + [ + 15, + 3 + ], + [ + 14, + 3 + ], + [ + 13, + 2 + ], + [ + 12, + 2 + ], + [ + 12, + 1 + ], + [ + 11, + 1 + ], + [ + 10, + 1 + ], + [ + 10, + 0 + ], + [ + 9, + 0 + ], + [ + 8, + 0 + ], + [ + 7, + 0 + ], + [ + 5, + 0 + ], + [ + 4, + -1 + ], + [ + 3, + -1 + ], + [ + 2, + -1 + ], + [ + 1, + -1 + ], + [ + 0, + 0 + ], + [ + -1, + 1 + ], + [ + -1, + 2 + ], + [ + -2, + 2 + ], + [ + -2, + 3 + ], + [ + -3, + 3 + ], + [ + -3, + 4 + ], + [ + -3, + 5 + ], + [ + -3, + 6 + ], + [ + -3, + 7 + ], + [ + -3, + 8 + ], + [ + -3, + 9 + ], + [ + -2, + 9 + ], + [ + -2, + 10 + ], + [ + -1, + 10 + ], + [ + -1, + 11 + ], + [ + 0, + 11 + ], + [ + 1, + 11 + ], + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 12 + ], + [ + 4, + 13 + ], + [ + 5, + 13 + ], + [ + 5, + 12 + ], + [ + 6, + 12 + ], + [ + 6, + 11 + ], + [ + 6, + 10 + ], + [ + 5, + 9 + ], + [ + 5, + 8 + ], + [ + 4, + 8 + ], + [ + 3, + 8 + ], + [ + 2, + 8 + ], + [ + 2, + 9 + ], + [ + 2, + 10 + ], + [ + 2, + 11 + ], + [ + 3, + 12 + ], + [ + 3, + 13 + ], + [ + 4, + 13 + ], + [ + 4, + 14 + ], + [ + 5, + 14 + ], + [ + 6, + 14 + ], + [ + 7, + 14 + ], + [ + 8, + 14 + ], + [ + 9, + 14 + ], + [ + 9, + 13 + ], + [ + 10, + 13 + ], + [ + 10, + 12 + ], + [ + 10, + 11 + ], + [ + 10, + 10 + ], + [ + 9, + 10 + ], + [ + 8, + 10 + ], + [ + 8, + 10 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 417, + "versionNonce": 951416171, + "index": "b06", + "isDeleted": false, + "id": "Sj7f5Exu4Ok9tG5tDK7M3", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1163.667209201389, + "y": 1293.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 58, + "height": 42, + "seed": 1884859781, + "groupIds": [ + "22DR3CnQQnsnNkwU4IOoG" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918175611, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -1 + ], + [ + 0, + -2 + ], + [ + -1, + -2 + ], + [ + -1, + -3 + ], + [ + -1, + -4 + ], + [ + -1, + -5 + ], + [ + -1, + -6 + ], + [ + -1, + -7 + ], + [ + -1, + -8 + ], + [ + -1, + -9 + ], + [ + -1, + -10 + ], + [ + -1, + -11 + ], + [ + -1, + -12 + ], + [ + 0, + -12 + ], + [ + 0, + -13 + ], + [ + 0, + -14 + ], + [ + 0, + -15 + ], + [ + 1, + -16 + ], + [ + 1, + -17 + ], + [ + 1, + -18 + ], + [ + 2, + -19 + ], + [ + 2, + -20 + ], + [ + 3, + -21 + ], + [ + 3, + -22 + ], + [ + 4, + -22 + ], + [ + 4, + -23 + ], + [ + 5, + -24 + ], + [ + 6, + -25 + ], + [ + 7, + -26 + ], + [ + 7, + -27 + ], + [ + 9, + -28 + ], + [ + 9, + -29 + ], + [ + 10, + -29 + ], + [ + 11, + -30 + ], + [ + 11, + -31 + ], + [ + 12, + -31 + ], + [ + 13, + -32 + ], + [ + 14, + -33 + ], + [ + 15, + -34 + ], + [ + 16, + -34 + ], + [ + 16, + -35 + ], + [ + 17, + -35 + ], + [ + 18, + -35 + ], + [ + 19, + -36 + ], + [ + 20, + -36 + ], + [ + 21, + -37 + ], + [ + 22, + -37 + ], + [ + 23, + -38 + ], + [ + 24, + -38 + ], + [ + 24, + -39 + ], + [ + 25, + -39 + ], + [ + 26, + -40 + ], + [ + 27, + -40 + ], + [ + 28, + -41 + ], + [ + 29, + -41 + ], + [ + 30, + -41 + ], + [ + 31, + -42 + ], + [ + 32, + -42 + ], + [ + 33, + -42 + ], + [ + 34, + -42 + ], + [ + 35, + -42 + ], + [ + 36, + -42 + ], + [ + 37, + -42 + ], + [ + 38, + -41 + ], + [ + 40, + -41 + ], + [ + 41, + -40 + ], + [ + 42, + -40 + ], + [ + 43, + -39 + ], + [ + 44, + -39 + ], + [ + 45, + -38 + ], + [ + 46, + -38 + ], + [ + 47, + -38 + ], + [ + 47, + -37 + ], + [ + 48, + -36 + ], + [ + 48, + -35 + ], + [ + 49, + -34 + ], + [ + 49, + -33 + ], + [ + 50, + -32 + ], + [ + 50, + -30 + ], + [ + 51, + -30 + ], + [ + 52, + -29 + ], + [ + 52, + -28 + ], + [ + 53, + -27 + ], + [ + 53, + -26 + ], + [ + 53, + -25 + ], + [ + 54, + -25 + ], + [ + 54, + -24 + ], + [ + 54, + -23 + ], + [ + 54, + -22 + ], + [ + 55, + -21 + ], + [ + 55, + -20 + ], + [ + 55, + -19 + ], + [ + 55, + -18 + ], + [ + 55, + -17 + ], + [ + 56, + -17 + ], + [ + 56, + -16 + ], + [ + 56, + -15 + ], + [ + 56, + -14 + ], + [ + 56, + -13 + ], + [ + 56, + -12 + ], + [ + 56, + -11 + ], + [ + 56, + -10 + ], + [ + 56, + -9 + ], + [ + 56, + -8 + ], + [ + 56, + -7 + ], + [ + 56, + -6 + ], + [ + 56, + -5 + ], + [ + 57, + -4 + ], + [ + 57, + -3 + ], + [ + 57, + -2 + ], + [ + 56, + -2 + ], + [ + 55, + -2 + ], + [ + 54, + -2 + ], + [ + 52, + -2 + ], + [ + 51, + -2 + ], + [ + 50, + -2 + ], + [ + 48, + -2 + ], + [ + 47, + -2 + ], + [ + 45, + -2 + ], + [ + 44, + -1 + ], + [ + 42, + -1 + ], + [ + 41, + -1 + ], + [ + 40, + -1 + ], + [ + 39, + 0 + ], + [ + 38, + 0 + ], + [ + 37, + 0 + ], + [ + 36, + 0 + ], + [ + 35, + 0 + ], + [ + 33, + 0 + ], + [ + 32, + 0 + ], + [ + 31, + 0 + ], + [ + 29, + 0 + ], + [ + 28, + 0 + ], + [ + 26, + 0 + ], + [ + 25, + 0 + ], + [ + 24, + 0 + ], + [ + 23, + 0 + ], + [ + 22, + 0 + ], + [ + 21, + 0 + ], + [ + 20, + 0 + ], + [ + 19, + 0 + ], + [ + 18, + 0 + ], + [ + 17, + 0 + ], + [ + 16, + 0 + ], + [ + 14, + 0 + ], + [ + 13, + 0 + ], + [ + 12, + 0 + ], + [ + 11, + 0 + ], + [ + 10, + 0 + ], + [ + 9, + -1 + ], + [ + 7, + -1 + ], + [ + 6, + -1 + ], + [ + 5, + -1 + ], + [ + 4, + -1 + ], + [ + 4, + -2 + ], + [ + 4, + -3 + ], + [ + 4, + -4 + ], + [ + 4, + -5 + ], + [ + 5, + -6 + ], + [ + 5, + -7 + ], + [ + 5, + -8 + ], + [ + 5, + -9 + ], + [ + 5, + -10 + ], + [ + 6, + -11 + ], + [ + 6, + -12 + ], + [ + 7, + -13 + ], + [ + 7, + -14 + ], + [ + 8, + -15 + ], + [ + 9, + -16 + ], + [ + 10, + -17 + ], + [ + 11, + -18 + ], + [ + 11, + -19 + ], + [ + 12, + -19 + ], + [ + 13, + -20 + ], + [ + 14, + -21 + ], + [ + 15, + -22 + ], + [ + 16, + -22 + ], + [ + 17, + -23 + ], + [ + 18, + -24 + ], + [ + 19, + -24 + ], + [ + 19, + -25 + ], + [ + 20, + -25 + ], + [ + 20, + -26 + ], + [ + 21, + -27 + ], + [ + 22, + -27 + ], + [ + 23, + -27 + ], + [ + 23, + -28 + ], + [ + 24, + -28 + ], + [ + 25, + -28 + ], + [ + 26, + -29 + ], + [ + 27, + -29 + ], + [ + 28, + -29 + ], + [ + 29, + -30 + ], + [ + 30, + -30 + ], + [ + 31, + -30 + ], + [ + 32, + -30 + ], + [ + 33, + -30 + ], + [ + 34, + -30 + ], + [ + 35, + -30 + ], + [ + 36, + -29 + ], + [ + 36, + -28 + ], + [ + 37, + -27 + ], + [ + 38, + -26 + ], + [ + 38, + -25 + ], + [ + 39, + -24 + ], + [ + 40, + -23 + ], + [ + 40, + -22 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 42, + -20 + ], + [ + 42, + -19 + ], + [ + 42, + -18 + ], + [ + 42, + -17 + ], + [ + 41, + -17 + ], + [ + 41, + -16 + ], + [ + 40, + -16 + ], + [ + 40, + -15 + ], + [ + 39, + -15 + ], + [ + 38, + -14 + ], + [ + 36, + -14 + ], + [ + 35, + -14 + ], + [ + 33, + -14 + ], + [ + 32, + -14 + ], + [ + 30, + -13 + ], + [ + 29, + -12 + ], + [ + 26, + -12 + ], + [ + 24, + -11 + ], + [ + 23, + -11 + ], + [ + 21, + -11 + ], + [ + 20, + -11 + ], + [ + 19, + -11 + ], + [ + 18, + -11 + ], + [ + 17, + -11 + ], + [ + 16, + -11 + ], + [ + 15, + -11 + ], + [ + 14, + -12 + ], + [ + 14, + -13 + ], + [ + 15, + -14 + ], + [ + 16, + -14 + ], + [ + 17, + -15 + ], + [ + 18, + -15 + ], + [ + 20, + -16 + ], + [ + 21, + -16 + ], + [ + 23, + -16 + ], + [ + 24, + -16 + ], + [ + 26, + -17 + ], + [ + 27, + -17 + ], + [ + 29, + -17 + ], + [ + 30, + -17 + ], + [ + 34, + -17 + ], + [ + 37, + -17 + ], + [ + 40, + -16 + ], + [ + 41, + -15 + ], + [ + 41, + -14 + ], + [ + 41, + -13 + ], + [ + 40, + -13 + ], + [ + 40, + -12 + ], + [ + 39, + -12 + ], + [ + 38, + -12 + ], + [ + 37, + -12 + ], + [ + 36, + -12 + ], + [ + 34, + -13 + ], + [ + 33, + -13 + ], + [ + 32, + -14 + ], + [ + 31, + -14 + ], + [ + 30, + -14 + ], + [ + 29, + -14 + ], + [ + 29, + -15 + ], + [ + 29, + -16 + ], + [ + 29, + -17 + ], + [ + 29, + -18 + ], + [ + 30, + -19 + ], + [ + 31, + -19 + ], + [ + 33, + -20 + ], + [ + 35, + -20 + ], + [ + 37, + -20 + ], + [ + 38, + -20 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 43, + -21 + ], + [ + 44, + -21 + ], + [ + 45, + -21 + ], + [ + 45, + -20 + ], + [ + 45, + -19 + ], + [ + 45, + -18 + ], + [ + 45, + -17 + ], + [ + 45, + -16 + ], + [ + 45, + -15 + ], + [ + 45, + -14 + ], + [ + 45, + -13 + ], + [ + 45, + -12 + ], + [ + 44, + -12 + ], + [ + 44, + -11 + ], + [ + 44, + -12 + ], + [ + 45, + -12 + ], + [ + 46, + -12 + ], + [ + 47, + -13 + ], + [ + 48, + -13 + ], + [ + 49, + -13 + ], + [ + 50, + -13 + ], + [ + 50, + -12 + ], + [ + 50, + -11 + ], + [ + 50, + -11 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 333, + "versionNonce": 1490653195, + "index": "b08", + "isDeleted": false, + "id": "T-4BfE8nTqvjRm4Bpj2hL", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1369.667209201389, + "y": 1216.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 28, + "height": 22, + "seed": 218242443, + "groupIds": [ + "Ma3CKEzFrW3wCSeKNn_Rp" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918175611, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1, + 0 + ], + [ + -2, + 1 + ], + [ + -3, + 1 + ], + [ + -4, + 2 + ], + [ + -5, + 2 + ], + [ + -5, + 3 + ], + [ + -6, + 3 + ], + [ + -6, + 4 + ], + [ + -7, + 4 + ], + [ + -7, + 5 + ], + [ + -8, + 5 + ], + [ + -8, + 6 + ], + [ + -9, + 7 + ], + [ + -9, + 8 + ], + [ + -10, + 9 + ], + [ + -10, + 10 + ], + [ + -10, + 11 + ], + [ + -9, + 13 + ], + [ + -9, + 14 + ], + [ + -9, + 16 + ], + [ + -8, + 16 + ], + [ + -8, + 17 + ], + [ + -7, + 17 + ], + [ + -6, + 18 + ], + [ + -5, + 19 + ], + [ + -4, + 19 + ], + [ + -3, + 20 + ], + [ + -2, + 20 + ], + [ + -1, + 21 + ], + [ + 0, + 21 + ], + [ + 1, + 21 + ], + [ + 2, + 21 + ], + [ + 3, + 21 + ], + [ + 4, + 21 + ], + [ + 5, + 21 + ], + [ + 6, + 21 + ], + [ + 8, + 21 + ], + [ + 9, + 21 + ], + [ + 11, + 21 + ], + [ + 12, + 21 + ], + [ + 14, + 21 + ], + [ + 15, + 20 + ], + [ + 16, + 20 + ], + [ + 16, + 19 + ], + [ + 16, + 18 + ], + [ + 17, + 18 + ], + [ + 17, + 17 + ], + [ + 17, + 16 + ], + [ + 17, + 15 + ], + [ + 18, + 14 + ], + [ + 18, + 13 + ], + [ + 18, + 12 + ], + [ + 18, + 11 + ], + [ + 17, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 7 + ], + [ + 16, + 6 + ], + [ + 16, + 5 + ], + [ + 16, + 4 + ], + [ + 15, + 4 + ], + [ + 15, + 3 + ], + [ + 14, + 3 + ], + [ + 13, + 2 + ], + [ + 12, + 2 + ], + [ + 12, + 1 + ], + [ + 11, + 1 + ], + [ + 10, + 1 + ], + [ + 10, + 0 + ], + [ + 9, + 0 + ], + [ + 8, + 0 + ], + [ + 7, + 0 + ], + [ + 5, + 0 + ], + [ + 4, + -1 + ], + [ + 3, + -1 + ], + [ + 2, + -1 + ], + [ + 1, + -1 + ], + [ + 0, + 0 + ], + [ + -1, + 1 + ], + [ + -1, + 2 + ], + [ + -2, + 2 + ], + [ + -2, + 3 + ], + [ + -3, + 3 + ], + [ + -3, + 4 + ], + [ + -3, + 5 + ], + [ + -3, + 6 + ], + [ + -3, + 7 + ], + [ + -3, + 8 + ], + [ + -3, + 9 + ], + [ + -2, + 9 + ], + [ + -2, + 10 + ], + [ + -1, + 10 + ], + [ + -1, + 11 + ], + [ + 0, + 11 + ], + [ + 1, + 11 + ], + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 12 + ], + [ + 4, + 13 + ], + [ + 5, + 13 + ], + [ + 5, + 12 + ], + [ + 6, + 12 + ], + [ + 6, + 11 + ], + [ + 6, + 10 + ], + [ + 5, + 9 + ], + [ + 5, + 8 + ], + [ + 4, + 8 + ], + [ + 3, + 8 + ], + [ + 2, + 8 + ], + [ + 2, + 9 + ], + [ + 2, + 10 + ], + [ + 2, + 11 + ], + [ + 3, + 12 + ], + [ + 3, + 13 + ], + [ + 4, + 13 + ], + [ + 4, + 14 + ], + [ + 5, + 14 + ], + [ + 6, + 14 + ], + [ + 7, + 14 + ], + [ + 8, + 14 + ], + [ + 9, + 14 + ], + [ + 9, + 13 + ], + [ + 10, + 13 + ], + [ + 10, + 12 + ], + [ + 10, + 11 + ], + [ + 10, + 10 + ], + [ + 9, + 10 + ], + [ + 8, + 10 + ], + [ + 8, + 10 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 508, + "versionNonce": 518084267, + "index": "b09", + "isDeleted": false, + "id": "FReSJkhy6BcPPAKErFY7N", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1343.667209201389, + "y": 1301.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 58, + "height": 42, + "seed": 698102827, + "groupIds": [ + "Ma3CKEzFrW3wCSeKNn_Rp" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918175611, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -1 + ], + [ + 0, + -2 + ], + [ + -1, + -2 + ], + [ + -1, + -3 + ], + [ + -1, + -4 + ], + [ + -1, + -5 + ], + [ + -1, + -6 + ], + [ + -1, + -7 + ], + [ + -1, + -8 + ], + [ + -1, + -9 + ], + [ + -1, + -10 + ], + [ + -1, + -11 + ], + [ + -1, + -12 + ], + [ + 0, + -12 + ], + [ + 0, + -13 + ], + [ + 0, + -14 + ], + [ + 0, + -15 + ], + [ + 1, + -16 + ], + [ + 1, + -17 + ], + [ + 1, + -18 + ], + [ + 2, + -19 + ], + [ + 2, + -20 + ], + [ + 3, + -21 + ], + [ + 3, + -22 + ], + [ + 4, + -22 + ], + [ + 4, + -23 + ], + [ + 5, + -24 + ], + [ + 6, + -25 + ], + [ + 7, + -26 + ], + [ + 7, + -27 + ], + [ + 9, + -28 + ], + [ + 9, + -29 + ], + [ + 10, + -29 + ], + [ + 11, + -30 + ], + [ + 11, + -31 + ], + [ + 12, + -31 + ], + [ + 13, + -32 + ], + [ + 14, + -33 + ], + [ + 15, + -34 + ], + [ + 16, + -34 + ], + [ + 16, + -35 + ], + [ + 17, + -35 + ], + [ + 18, + -35 + ], + [ + 19, + -36 + ], + [ + 20, + -36 + ], + [ + 21, + -37 + ], + [ + 22, + -37 + ], + [ + 23, + -38 + ], + [ + 24, + -38 + ], + [ + 24, + -39 + ], + [ + 25, + -39 + ], + [ + 26, + -40 + ], + [ + 27, + -40 + ], + [ + 28, + -41 + ], + [ + 29, + -41 + ], + [ + 30, + -41 + ], + [ + 31, + -42 + ], + [ + 32, + -42 + ], + [ + 33, + -42 + ], + [ + 34, + -42 + ], + [ + 35, + -42 + ], + [ + 36, + -42 + ], + [ + 37, + -42 + ], + [ + 38, + -41 + ], + [ + 40, + -41 + ], + [ + 41, + -40 + ], + [ + 42, + -40 + ], + [ + 43, + -39 + ], + [ + 44, + -39 + ], + [ + 45, + -38 + ], + [ + 46, + -38 + ], + [ + 47, + -38 + ], + [ + 47, + -37 + ], + [ + 48, + -36 + ], + [ + 48, + -35 + ], + [ + 49, + -34 + ], + [ + 49, + -33 + ], + [ + 50, + -32 + ], + [ + 50, + -30 + ], + [ + 51, + -30 + ], + [ + 52, + -29 + ], + [ + 52, + -28 + ], + [ + 53, + -27 + ], + [ + 53, + -26 + ], + [ + 53, + -25 + ], + [ + 54, + -25 + ], + [ + 54, + -24 + ], + [ + 54, + -23 + ], + [ + 54, + -22 + ], + [ + 55, + -21 + ], + [ + 55, + -20 + ], + [ + 55, + -19 + ], + [ + 55, + -18 + ], + [ + 55, + -17 + ], + [ + 56, + -17 + ], + [ + 56, + -16 + ], + [ + 56, + -15 + ], + [ + 56, + -14 + ], + [ + 56, + -13 + ], + [ + 56, + -12 + ], + [ + 56, + -11 + ], + [ + 56, + -10 + ], + [ + 56, + -9 + ], + [ + 56, + -8 + ], + [ + 56, + -7 + ], + [ + 56, + -6 + ], + [ + 56, + -5 + ], + [ + 57, + -4 + ], + [ + 57, + -3 + ], + [ + 57, + -2 + ], + [ + 56, + -2 + ], + [ + 55, + -2 + ], + [ + 54, + -2 + ], + [ + 52, + -2 + ], + [ + 51, + -2 + ], + [ + 50, + -2 + ], + [ + 48, + -2 + ], + [ + 47, + -2 + ], + [ + 45, + -2 + ], + [ + 44, + -1 + ], + [ + 42, + -1 + ], + [ + 41, + -1 + ], + [ + 40, + -1 + ], + [ + 39, + 0 + ], + [ + 38, + 0 + ], + [ + 37, + 0 + ], + [ + 36, + 0 + ], + [ + 35, + 0 + ], + [ + 33, + 0 + ], + [ + 32, + 0 + ], + [ + 31, + 0 + ], + [ + 29, + 0 + ], + [ + 28, + 0 + ], + [ + 26, + 0 + ], + [ + 25, + 0 + ], + [ + 24, + 0 + ], + [ + 23, + 0 + ], + [ + 22, + 0 + ], + [ + 21, + 0 + ], + [ + 20, + 0 + ], + [ + 19, + 0 + ], + [ + 18, + 0 + ], + [ + 17, + 0 + ], + [ + 16, + 0 + ], + [ + 14, + 0 + ], + [ + 13, + 0 + ], + [ + 12, + 0 + ], + [ + 11, + 0 + ], + [ + 10, + 0 + ], + [ + 9, + -1 + ], + [ + 7, + -1 + ], + [ + 6, + -1 + ], + [ + 5, + -1 + ], + [ + 4, + -1 + ], + [ + 4, + -2 + ], + [ + 4, + -3 + ], + [ + 4, + -4 + ], + [ + 4, + -5 + ], + [ + 5, + -6 + ], + [ + 5, + -7 + ], + [ + 5, + -8 + ], + [ + 5, + -9 + ], + [ + 5, + -10 + ], + [ + 6, + -11 + ], + [ + 6, + -12 + ], + [ + 7, + -13 + ], + [ + 7, + -14 + ], + [ + 8, + -15 + ], + [ + 9, + -16 + ], + [ + 10, + -17 + ], + [ + 11, + -18 + ], + [ + 11, + -19 + ], + [ + 12, + -19 + ], + [ + 13, + -20 + ], + [ + 14, + -21 + ], + [ + 15, + -22 + ], + [ + 16, + -22 + ], + [ + 17, + -23 + ], + [ + 18, + -24 + ], + [ + 19, + -24 + ], + [ + 19, + -25 + ], + [ + 20, + -25 + ], + [ + 20, + -26 + ], + [ + 21, + -27 + ], + [ + 22, + -27 + ], + [ + 23, + -27 + ], + [ + 23, + -28 + ], + [ + 24, + -28 + ], + [ + 25, + -28 + ], + [ + 26, + -29 + ], + [ + 27, + -29 + ], + [ + 28, + -29 + ], + [ + 29, + -30 + ], + [ + 30, + -30 + ], + [ + 31, + -30 + ], + [ + 32, + -30 + ], + [ + 33, + -30 + ], + [ + 34, + -30 + ], + [ + 35, + -30 + ], + [ + 36, + -29 + ], + [ + 36, + -28 + ], + [ + 37, + -27 + ], + [ + 38, + -26 + ], + [ + 38, + -25 + ], + [ + 39, + -24 + ], + [ + 40, + -23 + ], + [ + 40, + -22 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 42, + -20 + ], + [ + 42, + -19 + ], + [ + 42, + -18 + ], + [ + 42, + -17 + ], + [ + 41, + -17 + ], + [ + 41, + -16 + ], + [ + 40, + -16 + ], + [ + 40, + -15 + ], + [ + 39, + -15 + ], + [ + 38, + -14 + ], + [ + 36, + -14 + ], + [ + 35, + -14 + ], + [ + 33, + -14 + ], + [ + 32, + -14 + ], + [ + 30, + -13 + ], + [ + 29, + -12 + ], + [ + 26, + -12 + ], + [ + 24, + -11 + ], + [ + 23, + -11 + ], + [ + 21, + -11 + ], + [ + 20, + -11 + ], + [ + 19, + -11 + ], + [ + 18, + -11 + ], + [ + 17, + -11 + ], + [ + 16, + -11 + ], + [ + 15, + -11 + ], + [ + 14, + -12 + ], + [ + 14, + -13 + ], + [ + 15, + -14 + ], + [ + 16, + -14 + ], + [ + 17, + -15 + ], + [ + 18, + -15 + ], + [ + 20, + -16 + ], + [ + 21, + -16 + ], + [ + 23, + -16 + ], + [ + 24, + -16 + ], + [ + 26, + -17 + ], + [ + 27, + -17 + ], + [ + 29, + -17 + ], + [ + 30, + -17 + ], + [ + 34, + -17 + ], + [ + 37, + -17 + ], + [ + 40, + -16 + ], + [ + 41, + -15 + ], + [ + 41, + -14 + ], + [ + 41, + -13 + ], + [ + 40, + -13 + ], + [ + 40, + -12 + ], + [ + 39, + -12 + ], + [ + 38, + -12 + ], + [ + 37, + -12 + ], + [ + 36, + -12 + ], + [ + 34, + -13 + ], + [ + 33, + -13 + ], + [ + 32, + -14 + ], + [ + 31, + -14 + ], + [ + 30, + -14 + ], + [ + 29, + -14 + ], + [ + 29, + -15 + ], + [ + 29, + -16 + ], + [ + 29, + -17 + ], + [ + 29, + -18 + ], + [ + 30, + -19 + ], + [ + 31, + -19 + ], + [ + 33, + -20 + ], + [ + 35, + -20 + ], + [ + 37, + -20 + ], + [ + 38, + -20 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 43, + -21 + ], + [ + 44, + -21 + ], + [ + 45, + -21 + ], + [ + 45, + -20 + ], + [ + 45, + -19 + ], + [ + 45, + -18 + ], + [ + 45, + -17 + ], + [ + 45, + -16 + ], + [ + 45, + -15 + ], + [ + 45, + -14 + ], + [ + 45, + -13 + ], + [ + 45, + -12 + ], + [ + 44, + -12 + ], + [ + 44, + -11 + ], + [ + 44, + -12 + ], + [ + 45, + -12 + ], + [ + 46, + -12 + ], + [ + 47, + -13 + ], + [ + 48, + -13 + ], + [ + 49, + -13 + ], + [ + 50, + -13 + ], + [ + 50, + -12 + ], + [ + 50, + -11 + ], + [ + 50, + -11 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 327, + "versionNonce": 1769425227, + "index": "b0A", + "isDeleted": false, + "id": "Bcw6QA7doNzI8yQ81Zq7U", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1526.667209201389, + "y": 1215.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 28, + "height": 22, + "seed": 1162207813, + "groupIds": [ + "FvnwMnudqpwVcMpkv2gct" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918175611, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1, + 0 + ], + [ + -2, + 1 + ], + [ + -3, + 1 + ], + [ + -4, + 2 + ], + [ + -5, + 2 + ], + [ + -5, + 3 + ], + [ + -6, + 3 + ], + [ + -6, + 4 + ], + [ + -7, + 4 + ], + [ + -7, + 5 + ], + [ + -8, + 5 + ], + [ + -8, + 6 + ], + [ + -9, + 7 + ], + [ + -9, + 8 + ], + [ + -10, + 9 + ], + [ + -10, + 10 + ], + [ + -10, + 11 + ], + [ + -9, + 13 + ], + [ + -9, + 14 + ], + [ + -9, + 16 + ], + [ + -8, + 16 + ], + [ + -8, + 17 + ], + [ + -7, + 17 + ], + [ + -6, + 18 + ], + [ + -5, + 19 + ], + [ + -4, + 19 + ], + [ + -3, + 20 + ], + [ + -2, + 20 + ], + [ + -1, + 21 + ], + [ + 0, + 21 + ], + [ + 1, + 21 + ], + [ + 2, + 21 + ], + [ + 3, + 21 + ], + [ + 4, + 21 + ], + [ + 5, + 21 + ], + [ + 6, + 21 + ], + [ + 8, + 21 + ], + [ + 9, + 21 + ], + [ + 11, + 21 + ], + [ + 12, + 21 + ], + [ + 14, + 21 + ], + [ + 15, + 20 + ], + [ + 16, + 20 + ], + [ + 16, + 19 + ], + [ + 16, + 18 + ], + [ + 17, + 18 + ], + [ + 17, + 17 + ], + [ + 17, + 16 + ], + [ + 17, + 15 + ], + [ + 18, + 14 + ], + [ + 18, + 13 + ], + [ + 18, + 12 + ], + [ + 18, + 11 + ], + [ + 17, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 7 + ], + [ + 16, + 6 + ], + [ + 16, + 5 + ], + [ + 16, + 4 + ], + [ + 15, + 4 + ], + [ + 15, + 3 + ], + [ + 14, + 3 + ], + [ + 13, + 2 + ], + [ + 12, + 2 + ], + [ + 12, + 1 + ], + [ + 11, + 1 + ], + [ + 10, + 1 + ], + [ + 10, + 0 + ], + [ + 9, + 0 + ], + [ + 8, + 0 + ], + [ + 7, + 0 + ], + [ + 5, + 0 + ], + [ + 4, + -1 + ], + [ + 3, + -1 + ], + [ + 2, + -1 + ], + [ + 1, + -1 + ], + [ + 0, + 0 + ], + [ + -1, + 1 + ], + [ + -1, + 2 + ], + [ + -2, + 2 + ], + [ + -2, + 3 + ], + [ + -3, + 3 + ], + [ + -3, + 4 + ], + [ + -3, + 5 + ], + [ + -3, + 6 + ], + [ + -3, + 7 + ], + [ + -3, + 8 + ], + [ + -3, + 9 + ], + [ + -2, + 9 + ], + [ + -2, + 10 + ], + [ + -1, + 10 + ], + [ + -1, + 11 + ], + [ + 0, + 11 + ], + [ + 1, + 11 + ], + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 12 + ], + [ + 4, + 13 + ], + [ + 5, + 13 + ], + [ + 5, + 12 + ], + [ + 6, + 12 + ], + [ + 6, + 11 + ], + [ + 6, + 10 + ], + [ + 5, + 9 + ], + [ + 5, + 8 + ], + [ + 4, + 8 + ], + [ + 3, + 8 + ], + [ + 2, + 8 + ], + [ + 2, + 9 + ], + [ + 2, + 10 + ], + [ + 2, + 11 + ], + [ + 3, + 12 + ], + [ + 3, + 13 + ], + [ + 4, + 13 + ], + [ + 4, + 14 + ], + [ + 5, + 14 + ], + [ + 6, + 14 + ], + [ + 7, + 14 + ], + [ + 8, + 14 + ], + [ + 9, + 14 + ], + [ + 9, + 13 + ], + [ + 10, + 13 + ], + [ + 10, + 12 + ], + [ + 10, + 11 + ], + [ + 10, + 10 + ], + [ + 9, + 10 + ], + [ + 8, + 10 + ], + [ + 8, + 10 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 502, + "versionNonce": 1576396779, + "index": "b0B", + "isDeleted": false, + "id": "5oqix7IsPdeANxSpXRorI", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1500.667209201389, + "y": 1300.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 58, + "height": 42, + "seed": 356209061, + "groupIds": [ + "FvnwMnudqpwVcMpkv2gct" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918175611, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -1 + ], + [ + 0, + -2 + ], + [ + -1, + -2 + ], + [ + -1, + -3 + ], + [ + -1, + -4 + ], + [ + -1, + -5 + ], + [ + -1, + -6 + ], + [ + -1, + -7 + ], + [ + -1, + -8 + ], + [ + -1, + -9 + ], + [ + -1, + -10 + ], + [ + -1, + -11 + ], + [ + -1, + -12 + ], + [ + 0, + -12 + ], + [ + 0, + -13 + ], + [ + 0, + -14 + ], + [ + 0, + -15 + ], + [ + 1, + -16 + ], + [ + 1, + -17 + ], + [ + 1, + -18 + ], + [ + 2, + -19 + ], + [ + 2, + -20 + ], + [ + 3, + -21 + ], + [ + 3, + -22 + ], + [ + 4, + -22 + ], + [ + 4, + -23 + ], + [ + 5, + -24 + ], + [ + 6, + -25 + ], + [ + 7, + -26 + ], + [ + 7, + -27 + ], + [ + 9, + -28 + ], + [ + 9, + -29 + ], + [ + 10, + -29 + ], + [ + 11, + -30 + ], + [ + 11, + -31 + ], + [ + 12, + -31 + ], + [ + 13, + -32 + ], + [ + 14, + -33 + ], + [ + 15, + -34 + ], + [ + 16, + -34 + ], + [ + 16, + -35 + ], + [ + 17, + -35 + ], + [ + 18, + -35 + ], + [ + 19, + -36 + ], + [ + 20, + -36 + ], + [ + 21, + -37 + ], + [ + 22, + -37 + ], + [ + 23, + -38 + ], + [ + 24, + -38 + ], + [ + 24, + -39 + ], + [ + 25, + -39 + ], + [ + 26, + -40 + ], + [ + 27, + -40 + ], + [ + 28, + -41 + ], + [ + 29, + -41 + ], + [ + 30, + -41 + ], + [ + 31, + -42 + ], + [ + 32, + -42 + ], + [ + 33, + -42 + ], + [ + 34, + -42 + ], + [ + 35, + -42 + ], + [ + 36, + -42 + ], + [ + 37, + -42 + ], + [ + 38, + -41 + ], + [ + 40, + -41 + ], + [ + 41, + -40 + ], + [ + 42, + -40 + ], + [ + 43, + -39 + ], + [ + 44, + -39 + ], + [ + 45, + -38 + ], + [ + 46, + -38 + ], + [ + 47, + -38 + ], + [ + 47, + -37 + ], + [ + 48, + -36 + ], + [ + 48, + -35 + ], + [ + 49, + -34 + ], + [ + 49, + -33 + ], + [ + 50, + -32 + ], + [ + 50, + -30 + ], + [ + 51, + -30 + ], + [ + 52, + -29 + ], + [ + 52, + -28 + ], + [ + 53, + -27 + ], + [ + 53, + -26 + ], + [ + 53, + -25 + ], + [ + 54, + -25 + ], + [ + 54, + -24 + ], + [ + 54, + -23 + ], + [ + 54, + -22 + ], + [ + 55, + -21 + ], + [ + 55, + -20 + ], + [ + 55, + -19 + ], + [ + 55, + -18 + ], + [ + 55, + -17 + ], + [ + 56, + -17 + ], + [ + 56, + -16 + ], + [ + 56, + -15 + ], + [ + 56, + -14 + ], + [ + 56, + -13 + ], + [ + 56, + -12 + ], + [ + 56, + -11 + ], + [ + 56, + -10 + ], + [ + 56, + -9 + ], + [ + 56, + -8 + ], + [ + 56, + -7 + ], + [ + 56, + -6 + ], + [ + 56, + -5 + ], + [ + 57, + -4 + ], + [ + 57, + -3 + ], + [ + 57, + -2 + ], + [ + 56, + -2 + ], + [ + 55, + -2 + ], + [ + 54, + -2 + ], + [ + 52, + -2 + ], + [ + 51, + -2 + ], + [ + 50, + -2 + ], + [ + 48, + -2 + ], + [ + 47, + -2 + ], + [ + 45, + -2 + ], + [ + 44, + -1 + ], + [ + 42, + -1 + ], + [ + 41, + -1 + ], + [ + 40, + -1 + ], + [ + 39, + 0 + ], + [ + 38, + 0 + ], + [ + 37, + 0 + ], + [ + 36, + 0 + ], + [ + 35, + 0 + ], + [ + 33, + 0 + ], + [ + 32, + 0 + ], + [ + 31, + 0 + ], + [ + 29, + 0 + ], + [ + 28, + 0 + ], + [ + 26, + 0 + ], + [ + 25, + 0 + ], + [ + 24, + 0 + ], + [ + 23, + 0 + ], + [ + 22, + 0 + ], + [ + 21, + 0 + ], + [ + 20, + 0 + ], + [ + 19, + 0 + ], + [ + 18, + 0 + ], + [ + 17, + 0 + ], + [ + 16, + 0 + ], + [ + 14, + 0 + ], + [ + 13, + 0 + ], + [ + 12, + 0 + ], + [ + 11, + 0 + ], + [ + 10, + 0 + ], + [ + 9, + -1 + ], + [ + 7, + -1 + ], + [ + 6, + -1 + ], + [ + 5, + -1 + ], + [ + 4, + -1 + ], + [ + 4, + -2 + ], + [ + 4, + -3 + ], + [ + 4, + -4 + ], + [ + 4, + -5 + ], + [ + 5, + -6 + ], + [ + 5, + -7 + ], + [ + 5, + -8 + ], + [ + 5, + -9 + ], + [ + 5, + -10 + ], + [ + 6, + -11 + ], + [ + 6, + -12 + ], + [ + 7, + -13 + ], + [ + 7, + -14 + ], + [ + 8, + -15 + ], + [ + 9, + -16 + ], + [ + 10, + -17 + ], + [ + 11, + -18 + ], + [ + 11, + -19 + ], + [ + 12, + -19 + ], + [ + 13, + -20 + ], + [ + 14, + -21 + ], + [ + 15, + -22 + ], + [ + 16, + -22 + ], + [ + 17, + -23 + ], + [ + 18, + -24 + ], + [ + 19, + -24 + ], + [ + 19, + -25 + ], + [ + 20, + -25 + ], + [ + 20, + -26 + ], + [ + 21, + -27 + ], + [ + 22, + -27 + ], + [ + 23, + -27 + ], + [ + 23, + -28 + ], + [ + 24, + -28 + ], + [ + 25, + -28 + ], + [ + 26, + -29 + ], + [ + 27, + -29 + ], + [ + 28, + -29 + ], + [ + 29, + -30 + ], + [ + 30, + -30 + ], + [ + 31, + -30 + ], + [ + 32, + -30 + ], + [ + 33, + -30 + ], + [ + 34, + -30 + ], + [ + 35, + -30 + ], + [ + 36, + -29 + ], + [ + 36, + -28 + ], + [ + 37, + -27 + ], + [ + 38, + -26 + ], + [ + 38, + -25 + ], + [ + 39, + -24 + ], + [ + 40, + -23 + ], + [ + 40, + -22 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 42, + -20 + ], + [ + 42, + -19 + ], + [ + 42, + -18 + ], + [ + 42, + -17 + ], + [ + 41, + -17 + ], + [ + 41, + -16 + ], + [ + 40, + -16 + ], + [ + 40, + -15 + ], + [ + 39, + -15 + ], + [ + 38, + -14 + ], + [ + 36, + -14 + ], + [ + 35, + -14 + ], + [ + 33, + -14 + ], + [ + 32, + -14 + ], + [ + 30, + -13 + ], + [ + 29, + -12 + ], + [ + 26, + -12 + ], + [ + 24, + -11 + ], + [ + 23, + -11 + ], + [ + 21, + -11 + ], + [ + 20, + -11 + ], + [ + 19, + -11 + ], + [ + 18, + -11 + ], + [ + 17, + -11 + ], + [ + 16, + -11 + ], + [ + 15, + -11 + ], + [ + 14, + -12 + ], + [ + 14, + -13 + ], + [ + 15, + -14 + ], + [ + 16, + -14 + ], + [ + 17, + -15 + ], + [ + 18, + -15 + ], + [ + 20, + -16 + ], + [ + 21, + -16 + ], + [ + 23, + -16 + ], + [ + 24, + -16 + ], + [ + 26, + -17 + ], + [ + 27, + -17 + ], + [ + 29, + -17 + ], + [ + 30, + -17 + ], + [ + 34, + -17 + ], + [ + 37, + -17 + ], + [ + 40, + -16 + ], + [ + 41, + -15 + ], + [ + 41, + -14 + ], + [ + 41, + -13 + ], + [ + 40, + -13 + ], + [ + 40, + -12 + ], + [ + 39, + -12 + ], + [ + 38, + -12 + ], + [ + 37, + -12 + ], + [ + 36, + -12 + ], + [ + 34, + -13 + ], + [ + 33, + -13 + ], + [ + 32, + -14 + ], + [ + 31, + -14 + ], + [ + 30, + -14 + ], + [ + 29, + -14 + ], + [ + 29, + -15 + ], + [ + 29, + -16 + ], + [ + 29, + -17 + ], + [ + 29, + -18 + ], + [ + 30, + -19 + ], + [ + 31, + -19 + ], + [ + 33, + -20 + ], + [ + 35, + -20 + ], + [ + 37, + -20 + ], + [ + 38, + -20 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 43, + -21 + ], + [ + 44, + -21 + ], + [ + 45, + -21 + ], + [ + 45, + -20 + ], + [ + 45, + -19 + ], + [ + 45, + -18 + ], + [ + 45, + -17 + ], + [ + 45, + -16 + ], + [ + 45, + -15 + ], + [ + 45, + -14 + ], + [ + 45, + -13 + ], + [ + 45, + -12 + ], + [ + 44, + -12 + ], + [ + 44, + -11 + ], + [ + 44, + -12 + ], + [ + 45, + -12 + ], + [ + 46, + -12 + ], + [ + 47, + -13 + ], + [ + 48, + -13 + ], + [ + 49, + -13 + ], + [ + 50, + -13 + ], + [ + 50, + -12 + ], + [ + 50, + -11 + ], + [ + 50, + -11 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 296, + "versionNonce": 308785803, + "index": "b0C", + "isDeleted": false, + "id": "bGO7CwBudzshOJXqkwenw", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1692.667209201389, + "y": 1218.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 28, + "height": 22, + "seed": 1946783787, + "groupIds": [ + "Q3cZJO-k37DIooUiwYoiJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918175611, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1, + 0 + ], + [ + -2, + 1 + ], + [ + -3, + 1 + ], + [ + -4, + 2 + ], + [ + -5, + 2 + ], + [ + -5, + 3 + ], + [ + -6, + 3 + ], + [ + -6, + 4 + ], + [ + -7, + 4 + ], + [ + -7, + 5 + ], + [ + -8, + 5 + ], + [ + -8, + 6 + ], + [ + -9, + 7 + ], + [ + -9, + 8 + ], + [ + -10, + 9 + ], + [ + -10, + 10 + ], + [ + -10, + 11 + ], + [ + -9, + 13 + ], + [ + -9, + 14 + ], + [ + -9, + 16 + ], + [ + -8, + 16 + ], + [ + -8, + 17 + ], + [ + -7, + 17 + ], + [ + -6, + 18 + ], + [ + -5, + 19 + ], + [ + -4, + 19 + ], + [ + -3, + 20 + ], + [ + -2, + 20 + ], + [ + -1, + 21 + ], + [ + 0, + 21 + ], + [ + 1, + 21 + ], + [ + 2, + 21 + ], + [ + 3, + 21 + ], + [ + 4, + 21 + ], + [ + 5, + 21 + ], + [ + 6, + 21 + ], + [ + 8, + 21 + ], + [ + 9, + 21 + ], + [ + 11, + 21 + ], + [ + 12, + 21 + ], + [ + 14, + 21 + ], + [ + 15, + 20 + ], + [ + 16, + 20 + ], + [ + 16, + 19 + ], + [ + 16, + 18 + ], + [ + 17, + 18 + ], + [ + 17, + 17 + ], + [ + 17, + 16 + ], + [ + 17, + 15 + ], + [ + 18, + 14 + ], + [ + 18, + 13 + ], + [ + 18, + 12 + ], + [ + 18, + 11 + ], + [ + 17, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 7 + ], + [ + 16, + 6 + ], + [ + 16, + 5 + ], + [ + 16, + 4 + ], + [ + 15, + 4 + ], + [ + 15, + 3 + ], + [ + 14, + 3 + ], + [ + 13, + 2 + ], + [ + 12, + 2 + ], + [ + 12, + 1 + ], + [ + 11, + 1 + ], + [ + 10, + 1 + ], + [ + 10, + 0 + ], + [ + 9, + 0 + ], + [ + 8, + 0 + ], + [ + 7, + 0 + ], + [ + 5, + 0 + ], + [ + 4, + -1 + ], + [ + 3, + -1 + ], + [ + 2, + -1 + ], + [ + 1, + -1 + ], + [ + 0, + 0 + ], + [ + -1, + 1 + ], + [ + -1, + 2 + ], + [ + -2, + 2 + ], + [ + -2, + 3 + ], + [ + -3, + 3 + ], + [ + -3, + 4 + ], + [ + -3, + 5 + ], + [ + -3, + 6 + ], + [ + -3, + 7 + ], + [ + -3, + 8 + ], + [ + -3, + 9 + ], + [ + -2, + 9 + ], + [ + -2, + 10 + ], + [ + -1, + 10 + ], + [ + -1, + 11 + ], + [ + 0, + 11 + ], + [ + 1, + 11 + ], + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 12 + ], + [ + 4, + 13 + ], + [ + 5, + 13 + ], + [ + 5, + 12 + ], + [ + 6, + 12 + ], + [ + 6, + 11 + ], + [ + 6, + 10 + ], + [ + 5, + 9 + ], + [ + 5, + 8 + ], + [ + 4, + 8 + ], + [ + 3, + 8 + ], + [ + 2, + 8 + ], + [ + 2, + 9 + ], + [ + 2, + 10 + ], + [ + 2, + 11 + ], + [ + 3, + 12 + ], + [ + 3, + 13 + ], + [ + 4, + 13 + ], + [ + 4, + 14 + ], + [ + 5, + 14 + ], + [ + 6, + 14 + ], + [ + 7, + 14 + ], + [ + 8, + 14 + ], + [ + 9, + 14 + ], + [ + 9, + 13 + ], + [ + 10, + 13 + ], + [ + 10, + 12 + ], + [ + 10, + 11 + ], + [ + 10, + 10 + ], + [ + 9, + 10 + ], + [ + 8, + 10 + ], + [ + 8, + 10 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 471, + "versionNonce": 2005346603, + "index": "b0D", + "isDeleted": false, + "id": "xeZeu1Xtow0ELNDfxV7BK", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1666.667209201389, + "y": 1303.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 58, + "height": 42, + "seed": 811356875, + "groupIds": [ + "Q3cZJO-k37DIooUiwYoiJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918175611, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -1 + ], + [ + 0, + -2 + ], + [ + -1, + -2 + ], + [ + -1, + -3 + ], + [ + -1, + -4 + ], + [ + -1, + -5 + ], + [ + -1, + -6 + ], + [ + -1, + -7 + ], + [ + -1, + -8 + ], + [ + -1, + -9 + ], + [ + -1, + -10 + ], + [ + -1, + -11 + ], + [ + -1, + -12 + ], + [ + 0, + -12 + ], + [ + 0, + -13 + ], + [ + 0, + -14 + ], + [ + 0, + -15 + ], + [ + 1, + -16 + ], + [ + 1, + -17 + ], + [ + 1, + -18 + ], + [ + 2, + -19 + ], + [ + 2, + -20 + ], + [ + 3, + -21 + ], + [ + 3, + -22 + ], + [ + 4, + -22 + ], + [ + 4, + -23 + ], + [ + 5, + -24 + ], + [ + 6, + -25 + ], + [ + 7, + -26 + ], + [ + 7, + -27 + ], + [ + 9, + -28 + ], + [ + 9, + -29 + ], + [ + 10, + -29 + ], + [ + 11, + -30 + ], + [ + 11, + -31 + ], + [ + 12, + -31 + ], + [ + 13, + -32 + ], + [ + 14, + -33 + ], + [ + 15, + -34 + ], + [ + 16, + -34 + ], + [ + 16, + -35 + ], + [ + 17, + -35 + ], + [ + 18, + -35 + ], + [ + 19, + -36 + ], + [ + 20, + -36 + ], + [ + 21, + -37 + ], + [ + 22, + -37 + ], + [ + 23, + -38 + ], + [ + 24, + -38 + ], + [ + 24, + -39 + ], + [ + 25, + -39 + ], + [ + 26, + -40 + ], + [ + 27, + -40 + ], + [ + 28, + -41 + ], + [ + 29, + -41 + ], + [ + 30, + -41 + ], + [ + 31, + -42 + ], + [ + 32, + -42 + ], + [ + 33, + -42 + ], + [ + 34, + -42 + ], + [ + 35, + -42 + ], + [ + 36, + -42 + ], + [ + 37, + -42 + ], + [ + 38, + -41 + ], + [ + 40, + -41 + ], + [ + 41, + -40 + ], + [ + 42, + -40 + ], + [ + 43, + -39 + ], + [ + 44, + -39 + ], + [ + 45, + -38 + ], + [ + 46, + -38 + ], + [ + 47, + -38 + ], + [ + 47, + -37 + ], + [ + 48, + -36 + ], + [ + 48, + -35 + ], + [ + 49, + -34 + ], + [ + 49, + -33 + ], + [ + 50, + -32 + ], + [ + 50, + -30 + ], + [ + 51, + -30 + ], + [ + 52, + -29 + ], + [ + 52, + -28 + ], + [ + 53, + -27 + ], + [ + 53, + -26 + ], + [ + 53, + -25 + ], + [ + 54, + -25 + ], + [ + 54, + -24 + ], + [ + 54, + -23 + ], + [ + 54, + -22 + ], + [ + 55, + -21 + ], + [ + 55, + -20 + ], + [ + 55, + -19 + ], + [ + 55, + -18 + ], + [ + 55, + -17 + ], + [ + 56, + -17 + ], + [ + 56, + -16 + ], + [ + 56, + -15 + ], + [ + 56, + -14 + ], + [ + 56, + -13 + ], + [ + 56, + -12 + ], + [ + 56, + -11 + ], + [ + 56, + -10 + ], + [ + 56, + -9 + ], + [ + 56, + -8 + ], + [ + 56, + -7 + ], + [ + 56, + -6 + ], + [ + 56, + -5 + ], + [ + 57, + -4 + ], + [ + 57, + -3 + ], + [ + 57, + -2 + ], + [ + 56, + -2 + ], + [ + 55, + -2 + ], + [ + 54, + -2 + ], + [ + 52, + -2 + ], + [ + 51, + -2 + ], + [ + 50, + -2 + ], + [ + 48, + -2 + ], + [ + 47, + -2 + ], + [ + 45, + -2 + ], + [ + 44, + -1 + ], + [ + 42, + -1 + ], + [ + 41, + -1 + ], + [ + 40, + -1 + ], + [ + 39, + 0 + ], + [ + 38, + 0 + ], + [ + 37, + 0 + ], + [ + 36, + 0 + ], + [ + 35, + 0 + ], + [ + 33, + 0 + ], + [ + 32, + 0 + ], + [ + 31, + 0 + ], + [ + 29, + 0 + ], + [ + 28, + 0 + ], + [ + 26, + 0 + ], + [ + 25, + 0 + ], + [ + 24, + 0 + ], + [ + 23, + 0 + ], + [ + 22, + 0 + ], + [ + 21, + 0 + ], + [ + 20, + 0 + ], + [ + 19, + 0 + ], + [ + 18, + 0 + ], + [ + 17, + 0 + ], + [ + 16, + 0 + ], + [ + 14, + 0 + ], + [ + 13, + 0 + ], + [ + 12, + 0 + ], + [ + 11, + 0 + ], + [ + 10, + 0 + ], + [ + 9, + -1 + ], + [ + 7, + -1 + ], + [ + 6, + -1 + ], + [ + 5, + -1 + ], + [ + 4, + -1 + ], + [ + 4, + -2 + ], + [ + 4, + -3 + ], + [ + 4, + -4 + ], + [ + 4, + -5 + ], + [ + 5, + -6 + ], + [ + 5, + -7 + ], + [ + 5, + -8 + ], + [ + 5, + -9 + ], + [ + 5, + -10 + ], + [ + 6, + -11 + ], + [ + 6, + -12 + ], + [ + 7, + -13 + ], + [ + 7, + -14 + ], + [ + 8, + -15 + ], + [ + 9, + -16 + ], + [ + 10, + -17 + ], + [ + 11, + -18 + ], + [ + 11, + -19 + ], + [ + 12, + -19 + ], + [ + 13, + -20 + ], + [ + 14, + -21 + ], + [ + 15, + -22 + ], + [ + 16, + -22 + ], + [ + 17, + -23 + ], + [ + 18, + -24 + ], + [ + 19, + -24 + ], + [ + 19, + -25 + ], + [ + 20, + -25 + ], + [ + 20, + -26 + ], + [ + 21, + -27 + ], + [ + 22, + -27 + ], + [ + 23, + -27 + ], + [ + 23, + -28 + ], + [ + 24, + -28 + ], + [ + 25, + -28 + ], + [ + 26, + -29 + ], + [ + 27, + -29 + ], + [ + 28, + -29 + ], + [ + 29, + -30 + ], + [ + 30, + -30 + ], + [ + 31, + -30 + ], + [ + 32, + -30 + ], + [ + 33, + -30 + ], + [ + 34, + -30 + ], + [ + 35, + -30 + ], + [ + 36, + -29 + ], + [ + 36, + -28 + ], + [ + 37, + -27 + ], + [ + 38, + -26 + ], + [ + 38, + -25 + ], + [ + 39, + -24 + ], + [ + 40, + -23 + ], + [ + 40, + -22 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 42, + -20 + ], + [ + 42, + -19 + ], + [ + 42, + -18 + ], + [ + 42, + -17 + ], + [ + 41, + -17 + ], + [ + 41, + -16 + ], + [ + 40, + -16 + ], + [ + 40, + -15 + ], + [ + 39, + -15 + ], + [ + 38, + -14 + ], + [ + 36, + -14 + ], + [ + 35, + -14 + ], + [ + 33, + -14 + ], + [ + 32, + -14 + ], + [ + 30, + -13 + ], + [ + 29, + -12 + ], + [ + 26, + -12 + ], + [ + 24, + -11 + ], + [ + 23, + -11 + ], + [ + 21, + -11 + ], + [ + 20, + -11 + ], + [ + 19, + -11 + ], + [ + 18, + -11 + ], + [ + 17, + -11 + ], + [ + 16, + -11 + ], + [ + 15, + -11 + ], + [ + 14, + -12 + ], + [ + 14, + -13 + ], + [ + 15, + -14 + ], + [ + 16, + -14 + ], + [ + 17, + -15 + ], + [ + 18, + -15 + ], + [ + 20, + -16 + ], + [ + 21, + -16 + ], + [ + 23, + -16 + ], + [ + 24, + -16 + ], + [ + 26, + -17 + ], + [ + 27, + -17 + ], + [ + 29, + -17 + ], + [ + 30, + -17 + ], + [ + 34, + -17 + ], + [ + 37, + -17 + ], + [ + 40, + -16 + ], + [ + 41, + -15 + ], + [ + 41, + -14 + ], + [ + 41, + -13 + ], + [ + 40, + -13 + ], + [ + 40, + -12 + ], + [ + 39, + -12 + ], + [ + 38, + -12 + ], + [ + 37, + -12 + ], + [ + 36, + -12 + ], + [ + 34, + -13 + ], + [ + 33, + -13 + ], + [ + 32, + -14 + ], + [ + 31, + -14 + ], + [ + 30, + -14 + ], + [ + 29, + -14 + ], + [ + 29, + -15 + ], + [ + 29, + -16 + ], + [ + 29, + -17 + ], + [ + 29, + -18 + ], + [ + 30, + -19 + ], + [ + 31, + -19 + ], + [ + 33, + -20 + ], + [ + 35, + -20 + ], + [ + 37, + -20 + ], + [ + 38, + -20 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 43, + -21 + ], + [ + 44, + -21 + ], + [ + 45, + -21 + ], + [ + 45, + -20 + ], + [ + 45, + -19 + ], + [ + 45, + -18 + ], + [ + 45, + -17 + ], + [ + 45, + -16 + ], + [ + 45, + -15 + ], + [ + 45, + -14 + ], + [ + 45, + -13 + ], + [ + 45, + -12 + ], + [ + 44, + -12 + ], + [ + 44, + -11 + ], + [ + 44, + -12 + ], + [ + 45, + -12 + ], + [ + 46, + -12 + ], + [ + 47, + -13 + ], + [ + 48, + -13 + ], + [ + 49, + -13 + ], + [ + 50, + -13 + ], + [ + 50, + -12 + ], + [ + 50, + -11 + ], + [ + 50, + -11 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "text", + "version": 172, + "versionNonce": 46058443, + "index": "b0E", + "isDeleted": false, + "id": "8EzYrC-dXIrmkiBP6umfl", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1149.667209201389, + "y": 1311.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 81.43333435058594, + "height": 64.80000000000001, + "seed": 176266507, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918175611, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "@everyone\nrole1\nrole2", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "@everyone\nrole1\nrole2", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "text", + "version": 228, + "versionNonce": 405346923, + "index": "b0F", + "isDeleted": false, + "id": "xk4XNQq63sET4n4cVfEiZ", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1332.950542026096, + "y": 1319.0888888888892, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 81.43333435058594, + "height": 43.2, + "seed": 1674891019, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918175611, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "@everyone\nrole1", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "@everyone\nrole1", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "text", + "version": 283, + "versionNonce": 881890571, + "index": "b0G", + "isDeleted": false, + "id": "JUEgPZtV5KL2V38fpjHz4", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1489.950542026096, + "y": 1324.0888888888887, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 81.43333435058594, + "height": 43.2, + "seed": 627094373, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918175611, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "@everyone\nrole2", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "@everyone\nrole2", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "text", + "version": 338, + "versionNonce": 1466940331, + "index": "b0H", + "isDeleted": false, + "id": "CPiCG41jzAmrPZzI14zP7", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1650.950542026096, + "y": 1323.0888888888892, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 81.43333435058594, + "height": 21.6, + "seed": 2122129163, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918175611, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "@everyone", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "@everyone", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "ellipse", + "version": 187, + "versionNonce": 863988491, + "index": "b0J", + "isDeleted": false, + "id": "lTLUoLMALkBdsgu2m-6Lc", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1187.667209201389, + "y": 805.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 189, + "height": 106, + "seed": 923905989, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "type": "text", + "id": "X4wb25xwsRKeAYW0d9Vm1" + }, + { + "id": "e4Nz2Zcd-PFg1RzFl7Rkb", + "type": "arrow" + }, + { + "id": "zZ5CdQAjnna-l8gWxCqca", + "type": "arrow" + }, + { + "id": "NjFBOI_LqII2FGV8XIwS8", + "type": "arrow" + }, + { + "id": "Bi7VjwGIRtHm94qnuHwDw", + "type": "arrow" + } + ], + "updated": 1726918258851, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 159, + "versionNonce": 331224267, + "index": "b0K", + "isDeleted": false, + "id": "X4wb25xwsRKeAYW0d9Vm1", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1226.5789519669067, + "y": 826.512229486002, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 111.53333282470703, + "height": 64.80000000000001, + "seed": 1455024843, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918208342, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "channel update\nevent for\nrole1, role2", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "lTLUoLMALkBdsgu2m-6Lc", + "originalText": "channel update\nevent for\nrole1, role2", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 105, + "versionNonce": 780049707, + "index": "b0L", + "isDeleted": false, + "id": "e4Nz2Zcd-PFg1RzFl7Rkb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1244.6984292533898, + "y": 920.7575664479319, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 33.03122005200089, + "height": 266.131322440957, + "seed": 1662098955, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "8eFl_bz4o1x_Gi_OrjgzY" + } + ], + "updated": 1726918264834, + "link": null, + "locked": false, + "startBinding": { + "elementId": "lTLUoLMALkBdsgu2m-6Lc", + "focus": 0.31447575443708436, + "gap": 12.872356729493255, + "fixedPoint": null + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -33.03122005200089, + 266.131322440957 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 8, + "versionNonce": 292886693, + "index": "b0M", + "isDeleted": false, + "id": "8eFl_bz4o1x_Gi_OrjgzY", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1194.1755425347224, + "y": 1042.588888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 37.983333333333334, + "height": 21.6, + "seed": 111725989, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918220540, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "role1", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "e4Nz2Zcd-PFg1RzFl7Rkb", + "originalText": "role1", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 162, + "versionNonce": 2064364165, + "index": "b0N", + "isDeleted": false, + "id": "zZ5CdQAjnna-l8gWxCqca", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1285.666912715616, + "y": 921.8878713204338, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 81.00029648577288, + "height": 278.00101756845515, + "seed": 1985880427, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "8OUPBM2su9Op99_hGVMNT" + } + ], + "updated": 1726918258851, + "link": null, + "locked": false, + "startBinding": { + "elementId": "lTLUoLMALkBdsgu2m-6Lc", + "focus": 0.15514969316181598, + "gap": 10.034318118154864, + "fixedPoint": null + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 81.00029648577288, + 278.00101756845515 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 8, + "versionNonce": 782494795, + "index": "b0O", + "isDeleted": false, + "id": "8OUPBM2su9Op99_hGVMNT", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1307.1755425347224, + "y": 1050.088888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 37.983333333333334, + "height": 21.6, + "seed": 1885873995, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918230724, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "role1", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "zZ5CdQAjnna-l8gWxCqca", + "originalText": "role1", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 189, + "versionNonce": 1666546891, + "index": "b0P", + "isDeleted": false, + "id": "NjFBOI_LqII2FGV8XIwS8", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1364.632374156974, + "y": 897.8161124903663, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 167.03483504441488, + "height": 307.0727763985226, + "seed": 755932325, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "SKaYSFFXuptpGH-z-KnGI" + } + ], + "updated": 1726918304358, + "link": null, + "locked": false, + "startBinding": { + "elementId": "lTLUoLMALkBdsgu2m-6Lc", + "focus": -0.6203489373990365, + "gap": 9.828595619787677, + "fixedPoint": null + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 167.03483504441488, + 307.0727763985226 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 11, + "versionNonce": 1596781675, + "index": "b0Q", + "isDeleted": false, + "id": "SKaYSFFXuptpGH-z-KnGI", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1434.1755425347224, + "y": 1030.088888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 37.983333333333334, + "height": 21.6, + "seed": 34981771, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918240436, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "role2", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "NjFBOI_LqII2FGV8XIwS8", + "originalText": "role2", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 117, + "versionNonce": 457943589, + "index": "b0R", + "isDeleted": false, + "id": "Bi7VjwGIRtHm94qnuHwDw", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1201.7109185021475, + "y": 899.4721935549908, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 30.043709300758564, + "height": 286.41669533389813, + "seed": 1263703525, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "GUT5oAc7DPQFATEjGMFrR" + } + ], + "updated": 1726918261979, + "link": null, + "locked": false, + "startBinding": { + "elementId": "lTLUoLMALkBdsgu2m-6Lc", + "focus": 0.8049500531717398, + "gap": 10.31758056448777, + "fixedPoint": null + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -30.043709300758564, + 286.41669533389813 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 9, + "versionNonce": 1911717733, + "index": "b0S", + "isDeleted": false, + "id": "GUT5oAc7DPQFATEjGMFrR", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1167.6973971851016, + "y": 1031.88054122194, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 37.983333333333334, + "height": 21.6, + "seed": 1119965733, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918261109, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "role2", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Bi7VjwGIRtHm94qnuHwDw", + "originalText": "role2", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 182, + "versionNonce": 1317884075, + "index": "b0T", + "isDeleted": false, + "id": "oL3Rhg7nAySLp3xks-Exz", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1070.667209201389, + "y": 960.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 76, + "height": 111, + "seed": 1725140587, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726918273722, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 7, + 69 + ], + [ + 76, + 111 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 50, + "versionNonce": 1434019403, + "index": "b0V", + "isDeleted": false, + "id": "CkAKDa8507tqAyhE5X430", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1017.6672092013889, + "y": 925.1888888888889, + "strokeColor": "#f08c00", + "backgroundColor": "#a5d8ff", + "width": 104.75, + "height": 27, + "seed": 381538405, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918286078, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 6, + "text": "duplication!", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "duplication!", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "rectangle", + "version": 262, + "versionNonce": 6611333, + "index": "b0W", + "isDeleted": false, + "id": "qwvPWBT-AMLng4YbFJgE5", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2199.6672092013887, + "y": 943.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 211.99999999999977, + "height": 98, + "seed": 1913738437, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "tM0soXn7oP_i2OGsLUJAq" + }, + { + "id": "VSp2nxUoFQQvjUVEjNcqU", + "type": "arrow" + }, + { + "id": "d2KqmWXSWVpK1rau0p4Ly", + "type": "arrow" + }, + { + "id": "suRrGqAz0cEyuwlqJcpHg", + "type": "arrow" + }, + { + "id": "321eNbFnBxl8j-kWXkY8s", + "type": "arrow" + }, + { + "id": "39o_Q6zDovd5a5GljOMno", + "type": "arrow" + }, + { + "id": "x0GvUbEbHaO3epjVckQko", + "type": "arrow" + } + ], + "updated": 1726918540204, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 233, + "versionNonce": 288101, + "index": "b0X", + "isDeleted": false, + "id": "tM0soXn7oP_i2OGsLUJAq", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2215.5338758680555, + "y": 965.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 180.26666666666665, + "height": 54, + "seed": 1798075179, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918367688, + "link": null, + "locked": false, + "fontSize": 20, + "fontFamily": 6, + "text": "Mathematical Set\nContaining User IDs", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "qwvPWBT-AMLng4YbFJgE5", + "originalText": "Mathematical Set\nContaining User IDs", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "freedraw", + "version": 425, + "versionNonce": 1762815141, + "index": "b0Y", + "isDeleted": false, + "id": "I3rz4sapMk720uV--byCf", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2045.3088756137422, + "y": 1185.4888888888888, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 28, + "height": 22, + "seed": 1896991493, + "groupIds": [ + "Vr-kJcJ59f8GIpVjxSffg" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1, + 0 + ], + [ + -2, + 1 + ], + [ + -3, + 1 + ], + [ + -4, + 2 + ], + [ + -5, + 2 + ], + [ + -5, + 3 + ], + [ + -6, + 3 + ], + [ + -6, + 4 + ], + [ + -7, + 4 + ], + [ + -7, + 5 + ], + [ + -8, + 5 + ], + [ + -8, + 6 + ], + [ + -9, + 7 + ], + [ + -9, + 8 + ], + [ + -10, + 9 + ], + [ + -10, + 10 + ], + [ + -10, + 11 + ], + [ + -9, + 13 + ], + [ + -9, + 14 + ], + [ + -9, + 16 + ], + [ + -8, + 16 + ], + [ + -8, + 17 + ], + [ + -7, + 17 + ], + [ + -6, + 18 + ], + [ + -5, + 19 + ], + [ + -4, + 19 + ], + [ + -3, + 20 + ], + [ + -2, + 20 + ], + [ + -1, + 21 + ], + [ + 0, + 21 + ], + [ + 1, + 21 + ], + [ + 2, + 21 + ], + [ + 3, + 21 + ], + [ + 4, + 21 + ], + [ + 5, + 21 + ], + [ + 6, + 21 + ], + [ + 8, + 21 + ], + [ + 9, + 21 + ], + [ + 11, + 21 + ], + [ + 12, + 21 + ], + [ + 14, + 21 + ], + [ + 15, + 20 + ], + [ + 16, + 20 + ], + [ + 16, + 19 + ], + [ + 16, + 18 + ], + [ + 17, + 18 + ], + [ + 17, + 17 + ], + [ + 17, + 16 + ], + [ + 17, + 15 + ], + [ + 18, + 14 + ], + [ + 18, + 13 + ], + [ + 18, + 12 + ], + [ + 18, + 11 + ], + [ + 17, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 7 + ], + [ + 16, + 6 + ], + [ + 16, + 5 + ], + [ + 16, + 4 + ], + [ + 15, + 4 + ], + [ + 15, + 3 + ], + [ + 14, + 3 + ], + [ + 13, + 2 + ], + [ + 12, + 2 + ], + [ + 12, + 1 + ], + [ + 11, + 1 + ], + [ + 10, + 1 + ], + [ + 10, + 0 + ], + [ + 9, + 0 + ], + [ + 8, + 0 + ], + [ + 7, + 0 + ], + [ + 5, + 0 + ], + [ + 4, + -1 + ], + [ + 3, + -1 + ], + [ + 2, + -1 + ], + [ + 1, + -1 + ], + [ + 0, + 0 + ], + [ + -1, + 1 + ], + [ + -1, + 2 + ], + [ + -2, + 2 + ], + [ + -2, + 3 + ], + [ + -3, + 3 + ], + [ + -3, + 4 + ], + [ + -3, + 5 + ], + [ + -3, + 6 + ], + [ + -3, + 7 + ], + [ + -3, + 8 + ], + [ + -3, + 9 + ], + [ + -2, + 9 + ], + [ + -2, + 10 + ], + [ + -1, + 10 + ], + [ + -1, + 11 + ], + [ + 0, + 11 + ], + [ + 1, + 11 + ], + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 12 + ], + [ + 4, + 13 + ], + [ + 5, + 13 + ], + [ + 5, + 12 + ], + [ + 6, + 12 + ], + [ + 6, + 11 + ], + [ + 6, + 10 + ], + [ + 5, + 9 + ], + [ + 5, + 8 + ], + [ + 4, + 8 + ], + [ + 3, + 8 + ], + [ + 2, + 8 + ], + [ + 2, + 9 + ], + [ + 2, + 10 + ], + [ + 2, + 11 + ], + [ + 3, + 12 + ], + [ + 3, + 13 + ], + [ + 4, + 13 + ], + [ + 4, + 14 + ], + [ + 5, + 14 + ], + [ + 6, + 14 + ], + [ + 7, + 14 + ], + [ + 8, + 14 + ], + [ + 9, + 14 + ], + [ + 9, + 13 + ], + [ + 10, + 13 + ], + [ + 10, + 12 + ], + [ + 10, + 11 + ], + [ + 10, + 10 + ], + [ + 9, + 10 + ], + [ + 8, + 10 + ], + [ + 8, + 10 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 600, + "versionNonce": 2009567237, + "index": "b0Z", + "isDeleted": false, + "id": "c6iR_MiueBJ5rn0R-g8Bz", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2019.3088756137422, + "y": 1270.4888888888888, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 58, + "height": 42, + "seed": 1493967461, + "groupIds": [ + "Vr-kJcJ59f8GIpVjxSffg" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -1 + ], + [ + 0, + -2 + ], + [ + -1, + -2 + ], + [ + -1, + -3 + ], + [ + -1, + -4 + ], + [ + -1, + -5 + ], + [ + -1, + -6 + ], + [ + -1, + -7 + ], + [ + -1, + -8 + ], + [ + -1, + -9 + ], + [ + -1, + -10 + ], + [ + -1, + -11 + ], + [ + -1, + -12 + ], + [ + 0, + -12 + ], + [ + 0, + -13 + ], + [ + 0, + -14 + ], + [ + 0, + -15 + ], + [ + 1, + -16 + ], + [ + 1, + -17 + ], + [ + 1, + -18 + ], + [ + 2, + -19 + ], + [ + 2, + -20 + ], + [ + 3, + -21 + ], + [ + 3, + -22 + ], + [ + 4, + -22 + ], + [ + 4, + -23 + ], + [ + 5, + -24 + ], + [ + 6, + -25 + ], + [ + 7, + -26 + ], + [ + 7, + -27 + ], + [ + 9, + -28 + ], + [ + 9, + -29 + ], + [ + 10, + -29 + ], + [ + 11, + -30 + ], + [ + 11, + -31 + ], + [ + 12, + -31 + ], + [ + 13, + -32 + ], + [ + 14, + -33 + ], + [ + 15, + -34 + ], + [ + 16, + -34 + ], + [ + 16, + -35 + ], + [ + 17, + -35 + ], + [ + 18, + -35 + ], + [ + 19, + -36 + ], + [ + 20, + -36 + ], + [ + 21, + -37 + ], + [ + 22, + -37 + ], + [ + 23, + -38 + ], + [ + 24, + -38 + ], + [ + 24, + -39 + ], + [ + 25, + -39 + ], + [ + 26, + -40 + ], + [ + 27, + -40 + ], + [ + 28, + -41 + ], + [ + 29, + -41 + ], + [ + 30, + -41 + ], + [ + 31, + -42 + ], + [ + 32, + -42 + ], + [ + 33, + -42 + ], + [ + 34, + -42 + ], + [ + 35, + -42 + ], + [ + 36, + -42 + ], + [ + 37, + -42 + ], + [ + 38, + -41 + ], + [ + 40, + -41 + ], + [ + 41, + -40 + ], + [ + 42, + -40 + ], + [ + 43, + -39 + ], + [ + 44, + -39 + ], + [ + 45, + -38 + ], + [ + 46, + -38 + ], + [ + 47, + -38 + ], + [ + 47, + -37 + ], + [ + 48, + -36 + ], + [ + 48, + -35 + ], + [ + 49, + -34 + ], + [ + 49, + -33 + ], + [ + 50, + -32 + ], + [ + 50, + -30 + ], + [ + 51, + -30 + ], + [ + 52, + -29 + ], + [ + 52, + -28 + ], + [ + 53, + -27 + ], + [ + 53, + -26 + ], + [ + 53, + -25 + ], + [ + 54, + -25 + ], + [ + 54, + -24 + ], + [ + 54, + -23 + ], + [ + 54, + -22 + ], + [ + 55, + -21 + ], + [ + 55, + -20 + ], + [ + 55, + -19 + ], + [ + 55, + -18 + ], + [ + 55, + -17 + ], + [ + 56, + -17 + ], + [ + 56, + -16 + ], + [ + 56, + -15 + ], + [ + 56, + -14 + ], + [ + 56, + -13 + ], + [ + 56, + -12 + ], + [ + 56, + -11 + ], + [ + 56, + -10 + ], + [ + 56, + -9 + ], + [ + 56, + -8 + ], + [ + 56, + -7 + ], + [ + 56, + -6 + ], + [ + 56, + -5 + ], + [ + 57, + -4 + ], + [ + 57, + -3 + ], + [ + 57, + -2 + ], + [ + 56, + -2 + ], + [ + 55, + -2 + ], + [ + 54, + -2 + ], + [ + 52, + -2 + ], + [ + 51, + -2 + ], + [ + 50, + -2 + ], + [ + 48, + -2 + ], + [ + 47, + -2 + ], + [ + 45, + -2 + ], + [ + 44, + -1 + ], + [ + 42, + -1 + ], + [ + 41, + -1 + ], + [ + 40, + -1 + ], + [ + 39, + 0 + ], + [ + 38, + 0 + ], + [ + 37, + 0 + ], + [ + 36, + 0 + ], + [ + 35, + 0 + ], + [ + 33, + 0 + ], + [ + 32, + 0 + ], + [ + 31, + 0 + ], + [ + 29, + 0 + ], + [ + 28, + 0 + ], + [ + 26, + 0 + ], + [ + 25, + 0 + ], + [ + 24, + 0 + ], + [ + 23, + 0 + ], + [ + 22, + 0 + ], + [ + 21, + 0 + ], + [ + 20, + 0 + ], + [ + 19, + 0 + ], + [ + 18, + 0 + ], + [ + 17, + 0 + ], + [ + 16, + 0 + ], + [ + 14, + 0 + ], + [ + 13, + 0 + ], + [ + 12, + 0 + ], + [ + 11, + 0 + ], + [ + 10, + 0 + ], + [ + 9, + -1 + ], + [ + 7, + -1 + ], + [ + 6, + -1 + ], + [ + 5, + -1 + ], + [ + 4, + -1 + ], + [ + 4, + -2 + ], + [ + 4, + -3 + ], + [ + 4, + -4 + ], + [ + 4, + -5 + ], + [ + 5, + -6 + ], + [ + 5, + -7 + ], + [ + 5, + -8 + ], + [ + 5, + -9 + ], + [ + 5, + -10 + ], + [ + 6, + -11 + ], + [ + 6, + -12 + ], + [ + 7, + -13 + ], + [ + 7, + -14 + ], + [ + 8, + -15 + ], + [ + 9, + -16 + ], + [ + 10, + -17 + ], + [ + 11, + -18 + ], + [ + 11, + -19 + ], + [ + 12, + -19 + ], + [ + 13, + -20 + ], + [ + 14, + -21 + ], + [ + 15, + -22 + ], + [ + 16, + -22 + ], + [ + 17, + -23 + ], + [ + 18, + -24 + ], + [ + 19, + -24 + ], + [ + 19, + -25 + ], + [ + 20, + -25 + ], + [ + 20, + -26 + ], + [ + 21, + -27 + ], + [ + 22, + -27 + ], + [ + 23, + -27 + ], + [ + 23, + -28 + ], + [ + 24, + -28 + ], + [ + 25, + -28 + ], + [ + 26, + -29 + ], + [ + 27, + -29 + ], + [ + 28, + -29 + ], + [ + 29, + -30 + ], + [ + 30, + -30 + ], + [ + 31, + -30 + ], + [ + 32, + -30 + ], + [ + 33, + -30 + ], + [ + 34, + -30 + ], + [ + 35, + -30 + ], + [ + 36, + -29 + ], + [ + 36, + -28 + ], + [ + 37, + -27 + ], + [ + 38, + -26 + ], + [ + 38, + -25 + ], + [ + 39, + -24 + ], + [ + 40, + -23 + ], + [ + 40, + -22 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 42, + -20 + ], + [ + 42, + -19 + ], + [ + 42, + -18 + ], + [ + 42, + -17 + ], + [ + 41, + -17 + ], + [ + 41, + -16 + ], + [ + 40, + -16 + ], + [ + 40, + -15 + ], + [ + 39, + -15 + ], + [ + 38, + -14 + ], + [ + 36, + -14 + ], + [ + 35, + -14 + ], + [ + 33, + -14 + ], + [ + 32, + -14 + ], + [ + 30, + -13 + ], + [ + 29, + -12 + ], + [ + 26, + -12 + ], + [ + 24, + -11 + ], + [ + 23, + -11 + ], + [ + 21, + -11 + ], + [ + 20, + -11 + ], + [ + 19, + -11 + ], + [ + 18, + -11 + ], + [ + 17, + -11 + ], + [ + 16, + -11 + ], + [ + 15, + -11 + ], + [ + 14, + -12 + ], + [ + 14, + -13 + ], + [ + 15, + -14 + ], + [ + 16, + -14 + ], + [ + 17, + -15 + ], + [ + 18, + -15 + ], + [ + 20, + -16 + ], + [ + 21, + -16 + ], + [ + 23, + -16 + ], + [ + 24, + -16 + ], + [ + 26, + -17 + ], + [ + 27, + -17 + ], + [ + 29, + -17 + ], + [ + 30, + -17 + ], + [ + 34, + -17 + ], + [ + 37, + -17 + ], + [ + 40, + -16 + ], + [ + 41, + -15 + ], + [ + 41, + -14 + ], + [ + 41, + -13 + ], + [ + 40, + -13 + ], + [ + 40, + -12 + ], + [ + 39, + -12 + ], + [ + 38, + -12 + ], + [ + 37, + -12 + ], + [ + 36, + -12 + ], + [ + 34, + -13 + ], + [ + 33, + -13 + ], + [ + 32, + -14 + ], + [ + 31, + -14 + ], + [ + 30, + -14 + ], + [ + 29, + -14 + ], + [ + 29, + -15 + ], + [ + 29, + -16 + ], + [ + 29, + -17 + ], + [ + 29, + -18 + ], + [ + 30, + -19 + ], + [ + 31, + -19 + ], + [ + 33, + -20 + ], + [ + 35, + -20 + ], + [ + 37, + -20 + ], + [ + 38, + -20 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 43, + -21 + ], + [ + 44, + -21 + ], + [ + 45, + -21 + ], + [ + 45, + -20 + ], + [ + 45, + -19 + ], + [ + 45, + -18 + ], + [ + 45, + -17 + ], + [ + 45, + -16 + ], + [ + 45, + -15 + ], + [ + 45, + -14 + ], + [ + 45, + -13 + ], + [ + 45, + -12 + ], + [ + 44, + -12 + ], + [ + 44, + -11 + ], + [ + 44, + -12 + ], + [ + 45, + -12 + ], + [ + 46, + -12 + ], + [ + 47, + -13 + ], + [ + 48, + -13 + ], + [ + 49, + -13 + ], + [ + 50, + -13 + ], + [ + 50, + -12 + ], + [ + 50, + -11 + ], + [ + 50, + -11 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 516, + "versionNonce": 1858160485, + "index": "b0a", + "isDeleted": false, + "id": "hfeHrTcskEALtNTDWGVrO", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2225.308875613742, + "y": 1193.4888888888888, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 28, + "height": 22, + "seed": 847547845, + "groupIds": [ + "C8KGNpuBhzB9p4UvDLOu-" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1, + 0 + ], + [ + -2, + 1 + ], + [ + -3, + 1 + ], + [ + -4, + 2 + ], + [ + -5, + 2 + ], + [ + -5, + 3 + ], + [ + -6, + 3 + ], + [ + -6, + 4 + ], + [ + -7, + 4 + ], + [ + -7, + 5 + ], + [ + -8, + 5 + ], + [ + -8, + 6 + ], + [ + -9, + 7 + ], + [ + -9, + 8 + ], + [ + -10, + 9 + ], + [ + -10, + 10 + ], + [ + -10, + 11 + ], + [ + -9, + 13 + ], + [ + -9, + 14 + ], + [ + -9, + 16 + ], + [ + -8, + 16 + ], + [ + -8, + 17 + ], + [ + -7, + 17 + ], + [ + -6, + 18 + ], + [ + -5, + 19 + ], + [ + -4, + 19 + ], + [ + -3, + 20 + ], + [ + -2, + 20 + ], + [ + -1, + 21 + ], + [ + 0, + 21 + ], + [ + 1, + 21 + ], + [ + 2, + 21 + ], + [ + 3, + 21 + ], + [ + 4, + 21 + ], + [ + 5, + 21 + ], + [ + 6, + 21 + ], + [ + 8, + 21 + ], + [ + 9, + 21 + ], + [ + 11, + 21 + ], + [ + 12, + 21 + ], + [ + 14, + 21 + ], + [ + 15, + 20 + ], + [ + 16, + 20 + ], + [ + 16, + 19 + ], + [ + 16, + 18 + ], + [ + 17, + 18 + ], + [ + 17, + 17 + ], + [ + 17, + 16 + ], + [ + 17, + 15 + ], + [ + 18, + 14 + ], + [ + 18, + 13 + ], + [ + 18, + 12 + ], + [ + 18, + 11 + ], + [ + 17, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 7 + ], + [ + 16, + 6 + ], + [ + 16, + 5 + ], + [ + 16, + 4 + ], + [ + 15, + 4 + ], + [ + 15, + 3 + ], + [ + 14, + 3 + ], + [ + 13, + 2 + ], + [ + 12, + 2 + ], + [ + 12, + 1 + ], + [ + 11, + 1 + ], + [ + 10, + 1 + ], + [ + 10, + 0 + ], + [ + 9, + 0 + ], + [ + 8, + 0 + ], + [ + 7, + 0 + ], + [ + 5, + 0 + ], + [ + 4, + -1 + ], + [ + 3, + -1 + ], + [ + 2, + -1 + ], + [ + 1, + -1 + ], + [ + 0, + 0 + ], + [ + -1, + 1 + ], + [ + -1, + 2 + ], + [ + -2, + 2 + ], + [ + -2, + 3 + ], + [ + -3, + 3 + ], + [ + -3, + 4 + ], + [ + -3, + 5 + ], + [ + -3, + 6 + ], + [ + -3, + 7 + ], + [ + -3, + 8 + ], + [ + -3, + 9 + ], + [ + -2, + 9 + ], + [ + -2, + 10 + ], + [ + -1, + 10 + ], + [ + -1, + 11 + ], + [ + 0, + 11 + ], + [ + 1, + 11 + ], + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 12 + ], + [ + 4, + 13 + ], + [ + 5, + 13 + ], + [ + 5, + 12 + ], + [ + 6, + 12 + ], + [ + 6, + 11 + ], + [ + 6, + 10 + ], + [ + 5, + 9 + ], + [ + 5, + 8 + ], + [ + 4, + 8 + ], + [ + 3, + 8 + ], + [ + 2, + 8 + ], + [ + 2, + 9 + ], + [ + 2, + 10 + ], + [ + 2, + 11 + ], + [ + 3, + 12 + ], + [ + 3, + 13 + ], + [ + 4, + 13 + ], + [ + 4, + 14 + ], + [ + 5, + 14 + ], + [ + 6, + 14 + ], + [ + 7, + 14 + ], + [ + 8, + 14 + ], + [ + 9, + 14 + ], + [ + 9, + 13 + ], + [ + 10, + 13 + ], + [ + 10, + 12 + ], + [ + 10, + 11 + ], + [ + 10, + 10 + ], + [ + 9, + 10 + ], + [ + 8, + 10 + ], + [ + 8, + 10 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 691, + "versionNonce": 5051077, + "index": "b0b", + "isDeleted": false, + "id": "uG6Z89OJ6WxSXpR7-p7q1", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2199.308875613742, + "y": 1278.4888888888888, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 58, + "height": 42, + "seed": 1285066021, + "groupIds": [ + "C8KGNpuBhzB9p4UvDLOu-" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -1 + ], + [ + 0, + -2 + ], + [ + -1, + -2 + ], + [ + -1, + -3 + ], + [ + -1, + -4 + ], + [ + -1, + -5 + ], + [ + -1, + -6 + ], + [ + -1, + -7 + ], + [ + -1, + -8 + ], + [ + -1, + -9 + ], + [ + -1, + -10 + ], + [ + -1, + -11 + ], + [ + -1, + -12 + ], + [ + 0, + -12 + ], + [ + 0, + -13 + ], + [ + 0, + -14 + ], + [ + 0, + -15 + ], + [ + 1, + -16 + ], + [ + 1, + -17 + ], + [ + 1, + -18 + ], + [ + 2, + -19 + ], + [ + 2, + -20 + ], + [ + 3, + -21 + ], + [ + 3, + -22 + ], + [ + 4, + -22 + ], + [ + 4, + -23 + ], + [ + 5, + -24 + ], + [ + 6, + -25 + ], + [ + 7, + -26 + ], + [ + 7, + -27 + ], + [ + 9, + -28 + ], + [ + 9, + -29 + ], + [ + 10, + -29 + ], + [ + 11, + -30 + ], + [ + 11, + -31 + ], + [ + 12, + -31 + ], + [ + 13, + -32 + ], + [ + 14, + -33 + ], + [ + 15, + -34 + ], + [ + 16, + -34 + ], + [ + 16, + -35 + ], + [ + 17, + -35 + ], + [ + 18, + -35 + ], + [ + 19, + -36 + ], + [ + 20, + -36 + ], + [ + 21, + -37 + ], + [ + 22, + -37 + ], + [ + 23, + -38 + ], + [ + 24, + -38 + ], + [ + 24, + -39 + ], + [ + 25, + -39 + ], + [ + 26, + -40 + ], + [ + 27, + -40 + ], + [ + 28, + -41 + ], + [ + 29, + -41 + ], + [ + 30, + -41 + ], + [ + 31, + -42 + ], + [ + 32, + -42 + ], + [ + 33, + -42 + ], + [ + 34, + -42 + ], + [ + 35, + -42 + ], + [ + 36, + -42 + ], + [ + 37, + -42 + ], + [ + 38, + -41 + ], + [ + 40, + -41 + ], + [ + 41, + -40 + ], + [ + 42, + -40 + ], + [ + 43, + -39 + ], + [ + 44, + -39 + ], + [ + 45, + -38 + ], + [ + 46, + -38 + ], + [ + 47, + -38 + ], + [ + 47, + -37 + ], + [ + 48, + -36 + ], + [ + 48, + -35 + ], + [ + 49, + -34 + ], + [ + 49, + -33 + ], + [ + 50, + -32 + ], + [ + 50, + -30 + ], + [ + 51, + -30 + ], + [ + 52, + -29 + ], + [ + 52, + -28 + ], + [ + 53, + -27 + ], + [ + 53, + -26 + ], + [ + 53, + -25 + ], + [ + 54, + -25 + ], + [ + 54, + -24 + ], + [ + 54, + -23 + ], + [ + 54, + -22 + ], + [ + 55, + -21 + ], + [ + 55, + -20 + ], + [ + 55, + -19 + ], + [ + 55, + -18 + ], + [ + 55, + -17 + ], + [ + 56, + -17 + ], + [ + 56, + -16 + ], + [ + 56, + -15 + ], + [ + 56, + -14 + ], + [ + 56, + -13 + ], + [ + 56, + -12 + ], + [ + 56, + -11 + ], + [ + 56, + -10 + ], + [ + 56, + -9 + ], + [ + 56, + -8 + ], + [ + 56, + -7 + ], + [ + 56, + -6 + ], + [ + 56, + -5 + ], + [ + 57, + -4 + ], + [ + 57, + -3 + ], + [ + 57, + -2 + ], + [ + 56, + -2 + ], + [ + 55, + -2 + ], + [ + 54, + -2 + ], + [ + 52, + -2 + ], + [ + 51, + -2 + ], + [ + 50, + -2 + ], + [ + 48, + -2 + ], + [ + 47, + -2 + ], + [ + 45, + -2 + ], + [ + 44, + -1 + ], + [ + 42, + -1 + ], + [ + 41, + -1 + ], + [ + 40, + -1 + ], + [ + 39, + 0 + ], + [ + 38, + 0 + ], + [ + 37, + 0 + ], + [ + 36, + 0 + ], + [ + 35, + 0 + ], + [ + 33, + 0 + ], + [ + 32, + 0 + ], + [ + 31, + 0 + ], + [ + 29, + 0 + ], + [ + 28, + 0 + ], + [ + 26, + 0 + ], + [ + 25, + 0 + ], + [ + 24, + 0 + ], + [ + 23, + 0 + ], + [ + 22, + 0 + ], + [ + 21, + 0 + ], + [ + 20, + 0 + ], + [ + 19, + 0 + ], + [ + 18, + 0 + ], + [ + 17, + 0 + ], + [ + 16, + 0 + ], + [ + 14, + 0 + ], + [ + 13, + 0 + ], + [ + 12, + 0 + ], + [ + 11, + 0 + ], + [ + 10, + 0 + ], + [ + 9, + -1 + ], + [ + 7, + -1 + ], + [ + 6, + -1 + ], + [ + 5, + -1 + ], + [ + 4, + -1 + ], + [ + 4, + -2 + ], + [ + 4, + -3 + ], + [ + 4, + -4 + ], + [ + 4, + -5 + ], + [ + 5, + -6 + ], + [ + 5, + -7 + ], + [ + 5, + -8 + ], + [ + 5, + -9 + ], + [ + 5, + -10 + ], + [ + 6, + -11 + ], + [ + 6, + -12 + ], + [ + 7, + -13 + ], + [ + 7, + -14 + ], + [ + 8, + -15 + ], + [ + 9, + -16 + ], + [ + 10, + -17 + ], + [ + 11, + -18 + ], + [ + 11, + -19 + ], + [ + 12, + -19 + ], + [ + 13, + -20 + ], + [ + 14, + -21 + ], + [ + 15, + -22 + ], + [ + 16, + -22 + ], + [ + 17, + -23 + ], + [ + 18, + -24 + ], + [ + 19, + -24 + ], + [ + 19, + -25 + ], + [ + 20, + -25 + ], + [ + 20, + -26 + ], + [ + 21, + -27 + ], + [ + 22, + -27 + ], + [ + 23, + -27 + ], + [ + 23, + -28 + ], + [ + 24, + -28 + ], + [ + 25, + -28 + ], + [ + 26, + -29 + ], + [ + 27, + -29 + ], + [ + 28, + -29 + ], + [ + 29, + -30 + ], + [ + 30, + -30 + ], + [ + 31, + -30 + ], + [ + 32, + -30 + ], + [ + 33, + -30 + ], + [ + 34, + -30 + ], + [ + 35, + -30 + ], + [ + 36, + -29 + ], + [ + 36, + -28 + ], + [ + 37, + -27 + ], + [ + 38, + -26 + ], + [ + 38, + -25 + ], + [ + 39, + -24 + ], + [ + 40, + -23 + ], + [ + 40, + -22 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 42, + -20 + ], + [ + 42, + -19 + ], + [ + 42, + -18 + ], + [ + 42, + -17 + ], + [ + 41, + -17 + ], + [ + 41, + -16 + ], + [ + 40, + -16 + ], + [ + 40, + -15 + ], + [ + 39, + -15 + ], + [ + 38, + -14 + ], + [ + 36, + -14 + ], + [ + 35, + -14 + ], + [ + 33, + -14 + ], + [ + 32, + -14 + ], + [ + 30, + -13 + ], + [ + 29, + -12 + ], + [ + 26, + -12 + ], + [ + 24, + -11 + ], + [ + 23, + -11 + ], + [ + 21, + -11 + ], + [ + 20, + -11 + ], + [ + 19, + -11 + ], + [ + 18, + -11 + ], + [ + 17, + -11 + ], + [ + 16, + -11 + ], + [ + 15, + -11 + ], + [ + 14, + -12 + ], + [ + 14, + -13 + ], + [ + 15, + -14 + ], + [ + 16, + -14 + ], + [ + 17, + -15 + ], + [ + 18, + -15 + ], + [ + 20, + -16 + ], + [ + 21, + -16 + ], + [ + 23, + -16 + ], + [ + 24, + -16 + ], + [ + 26, + -17 + ], + [ + 27, + -17 + ], + [ + 29, + -17 + ], + [ + 30, + -17 + ], + [ + 34, + -17 + ], + [ + 37, + -17 + ], + [ + 40, + -16 + ], + [ + 41, + -15 + ], + [ + 41, + -14 + ], + [ + 41, + -13 + ], + [ + 40, + -13 + ], + [ + 40, + -12 + ], + [ + 39, + -12 + ], + [ + 38, + -12 + ], + [ + 37, + -12 + ], + [ + 36, + -12 + ], + [ + 34, + -13 + ], + [ + 33, + -13 + ], + [ + 32, + -14 + ], + [ + 31, + -14 + ], + [ + 30, + -14 + ], + [ + 29, + -14 + ], + [ + 29, + -15 + ], + [ + 29, + -16 + ], + [ + 29, + -17 + ], + [ + 29, + -18 + ], + [ + 30, + -19 + ], + [ + 31, + -19 + ], + [ + 33, + -20 + ], + [ + 35, + -20 + ], + [ + 37, + -20 + ], + [ + 38, + -20 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 43, + -21 + ], + [ + 44, + -21 + ], + [ + 45, + -21 + ], + [ + 45, + -20 + ], + [ + 45, + -19 + ], + [ + 45, + -18 + ], + [ + 45, + -17 + ], + [ + 45, + -16 + ], + [ + 45, + -15 + ], + [ + 45, + -14 + ], + [ + 45, + -13 + ], + [ + 45, + -12 + ], + [ + 44, + -12 + ], + [ + 44, + -11 + ], + [ + 44, + -12 + ], + [ + 45, + -12 + ], + [ + 46, + -12 + ], + [ + 47, + -13 + ], + [ + 48, + -13 + ], + [ + 49, + -13 + ], + [ + 50, + -13 + ], + [ + 50, + -12 + ], + [ + 50, + -11 + ], + [ + 50, + -11 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 510, + "versionNonce": 956854821, + "index": "b0c", + "isDeleted": false, + "id": "glksEOJMoJWHSB1RdDsyn", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2382.308875613742, + "y": 1192.4888888888888, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 28, + "height": 22, + "seed": 917872773, + "groupIds": [ + "XRwDdzD55xlcyTYX5jETa" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1, + 0 + ], + [ + -2, + 1 + ], + [ + -3, + 1 + ], + [ + -4, + 2 + ], + [ + -5, + 2 + ], + [ + -5, + 3 + ], + [ + -6, + 3 + ], + [ + -6, + 4 + ], + [ + -7, + 4 + ], + [ + -7, + 5 + ], + [ + -8, + 5 + ], + [ + -8, + 6 + ], + [ + -9, + 7 + ], + [ + -9, + 8 + ], + [ + -10, + 9 + ], + [ + -10, + 10 + ], + [ + -10, + 11 + ], + [ + -9, + 13 + ], + [ + -9, + 14 + ], + [ + -9, + 16 + ], + [ + -8, + 16 + ], + [ + -8, + 17 + ], + [ + -7, + 17 + ], + [ + -6, + 18 + ], + [ + -5, + 19 + ], + [ + -4, + 19 + ], + [ + -3, + 20 + ], + [ + -2, + 20 + ], + [ + -1, + 21 + ], + [ + 0, + 21 + ], + [ + 1, + 21 + ], + [ + 2, + 21 + ], + [ + 3, + 21 + ], + [ + 4, + 21 + ], + [ + 5, + 21 + ], + [ + 6, + 21 + ], + [ + 8, + 21 + ], + [ + 9, + 21 + ], + [ + 11, + 21 + ], + [ + 12, + 21 + ], + [ + 14, + 21 + ], + [ + 15, + 20 + ], + [ + 16, + 20 + ], + [ + 16, + 19 + ], + [ + 16, + 18 + ], + [ + 17, + 18 + ], + [ + 17, + 17 + ], + [ + 17, + 16 + ], + [ + 17, + 15 + ], + [ + 18, + 14 + ], + [ + 18, + 13 + ], + [ + 18, + 12 + ], + [ + 18, + 11 + ], + [ + 17, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 7 + ], + [ + 16, + 6 + ], + [ + 16, + 5 + ], + [ + 16, + 4 + ], + [ + 15, + 4 + ], + [ + 15, + 3 + ], + [ + 14, + 3 + ], + [ + 13, + 2 + ], + [ + 12, + 2 + ], + [ + 12, + 1 + ], + [ + 11, + 1 + ], + [ + 10, + 1 + ], + [ + 10, + 0 + ], + [ + 9, + 0 + ], + [ + 8, + 0 + ], + [ + 7, + 0 + ], + [ + 5, + 0 + ], + [ + 4, + -1 + ], + [ + 3, + -1 + ], + [ + 2, + -1 + ], + [ + 1, + -1 + ], + [ + 0, + 0 + ], + [ + -1, + 1 + ], + [ + -1, + 2 + ], + [ + -2, + 2 + ], + [ + -2, + 3 + ], + [ + -3, + 3 + ], + [ + -3, + 4 + ], + [ + -3, + 5 + ], + [ + -3, + 6 + ], + [ + -3, + 7 + ], + [ + -3, + 8 + ], + [ + -3, + 9 + ], + [ + -2, + 9 + ], + [ + -2, + 10 + ], + [ + -1, + 10 + ], + [ + -1, + 11 + ], + [ + 0, + 11 + ], + [ + 1, + 11 + ], + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 12 + ], + [ + 4, + 13 + ], + [ + 5, + 13 + ], + [ + 5, + 12 + ], + [ + 6, + 12 + ], + [ + 6, + 11 + ], + [ + 6, + 10 + ], + [ + 5, + 9 + ], + [ + 5, + 8 + ], + [ + 4, + 8 + ], + [ + 3, + 8 + ], + [ + 2, + 8 + ], + [ + 2, + 9 + ], + [ + 2, + 10 + ], + [ + 2, + 11 + ], + [ + 3, + 12 + ], + [ + 3, + 13 + ], + [ + 4, + 13 + ], + [ + 4, + 14 + ], + [ + 5, + 14 + ], + [ + 6, + 14 + ], + [ + 7, + 14 + ], + [ + 8, + 14 + ], + [ + 9, + 14 + ], + [ + 9, + 13 + ], + [ + 10, + 13 + ], + [ + 10, + 12 + ], + [ + 10, + 11 + ], + [ + 10, + 10 + ], + [ + 9, + 10 + ], + [ + 8, + 10 + ], + [ + 8, + 10 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 685, + "versionNonce": 903931269, + "index": "b0d", + "isDeleted": false, + "id": "XupIdN9gDEZBHd-_2F24j", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2356.308875613742, + "y": 1277.4888888888888, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 58, + "height": 42, + "seed": 916015077, + "groupIds": [ + "XRwDdzD55xlcyTYX5jETa" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -1 + ], + [ + 0, + -2 + ], + [ + -1, + -2 + ], + [ + -1, + -3 + ], + [ + -1, + -4 + ], + [ + -1, + -5 + ], + [ + -1, + -6 + ], + [ + -1, + -7 + ], + [ + -1, + -8 + ], + [ + -1, + -9 + ], + [ + -1, + -10 + ], + [ + -1, + -11 + ], + [ + -1, + -12 + ], + [ + 0, + -12 + ], + [ + 0, + -13 + ], + [ + 0, + -14 + ], + [ + 0, + -15 + ], + [ + 1, + -16 + ], + [ + 1, + -17 + ], + [ + 1, + -18 + ], + [ + 2, + -19 + ], + [ + 2, + -20 + ], + [ + 3, + -21 + ], + [ + 3, + -22 + ], + [ + 4, + -22 + ], + [ + 4, + -23 + ], + [ + 5, + -24 + ], + [ + 6, + -25 + ], + [ + 7, + -26 + ], + [ + 7, + -27 + ], + [ + 9, + -28 + ], + [ + 9, + -29 + ], + [ + 10, + -29 + ], + [ + 11, + -30 + ], + [ + 11, + -31 + ], + [ + 12, + -31 + ], + [ + 13, + -32 + ], + [ + 14, + -33 + ], + [ + 15, + -34 + ], + [ + 16, + -34 + ], + [ + 16, + -35 + ], + [ + 17, + -35 + ], + [ + 18, + -35 + ], + [ + 19, + -36 + ], + [ + 20, + -36 + ], + [ + 21, + -37 + ], + [ + 22, + -37 + ], + [ + 23, + -38 + ], + [ + 24, + -38 + ], + [ + 24, + -39 + ], + [ + 25, + -39 + ], + [ + 26, + -40 + ], + [ + 27, + -40 + ], + [ + 28, + -41 + ], + [ + 29, + -41 + ], + [ + 30, + -41 + ], + [ + 31, + -42 + ], + [ + 32, + -42 + ], + [ + 33, + -42 + ], + [ + 34, + -42 + ], + [ + 35, + -42 + ], + [ + 36, + -42 + ], + [ + 37, + -42 + ], + [ + 38, + -41 + ], + [ + 40, + -41 + ], + [ + 41, + -40 + ], + [ + 42, + -40 + ], + [ + 43, + -39 + ], + [ + 44, + -39 + ], + [ + 45, + -38 + ], + [ + 46, + -38 + ], + [ + 47, + -38 + ], + [ + 47, + -37 + ], + [ + 48, + -36 + ], + [ + 48, + -35 + ], + [ + 49, + -34 + ], + [ + 49, + -33 + ], + [ + 50, + -32 + ], + [ + 50, + -30 + ], + [ + 51, + -30 + ], + [ + 52, + -29 + ], + [ + 52, + -28 + ], + [ + 53, + -27 + ], + [ + 53, + -26 + ], + [ + 53, + -25 + ], + [ + 54, + -25 + ], + [ + 54, + -24 + ], + [ + 54, + -23 + ], + [ + 54, + -22 + ], + [ + 55, + -21 + ], + [ + 55, + -20 + ], + [ + 55, + -19 + ], + [ + 55, + -18 + ], + [ + 55, + -17 + ], + [ + 56, + -17 + ], + [ + 56, + -16 + ], + [ + 56, + -15 + ], + [ + 56, + -14 + ], + [ + 56, + -13 + ], + [ + 56, + -12 + ], + [ + 56, + -11 + ], + [ + 56, + -10 + ], + [ + 56, + -9 + ], + [ + 56, + -8 + ], + [ + 56, + -7 + ], + [ + 56, + -6 + ], + [ + 56, + -5 + ], + [ + 57, + -4 + ], + [ + 57, + -3 + ], + [ + 57, + -2 + ], + [ + 56, + -2 + ], + [ + 55, + -2 + ], + [ + 54, + -2 + ], + [ + 52, + -2 + ], + [ + 51, + -2 + ], + [ + 50, + -2 + ], + [ + 48, + -2 + ], + [ + 47, + -2 + ], + [ + 45, + -2 + ], + [ + 44, + -1 + ], + [ + 42, + -1 + ], + [ + 41, + -1 + ], + [ + 40, + -1 + ], + [ + 39, + 0 + ], + [ + 38, + 0 + ], + [ + 37, + 0 + ], + [ + 36, + 0 + ], + [ + 35, + 0 + ], + [ + 33, + 0 + ], + [ + 32, + 0 + ], + [ + 31, + 0 + ], + [ + 29, + 0 + ], + [ + 28, + 0 + ], + [ + 26, + 0 + ], + [ + 25, + 0 + ], + [ + 24, + 0 + ], + [ + 23, + 0 + ], + [ + 22, + 0 + ], + [ + 21, + 0 + ], + [ + 20, + 0 + ], + [ + 19, + 0 + ], + [ + 18, + 0 + ], + [ + 17, + 0 + ], + [ + 16, + 0 + ], + [ + 14, + 0 + ], + [ + 13, + 0 + ], + [ + 12, + 0 + ], + [ + 11, + 0 + ], + [ + 10, + 0 + ], + [ + 9, + -1 + ], + [ + 7, + -1 + ], + [ + 6, + -1 + ], + [ + 5, + -1 + ], + [ + 4, + -1 + ], + [ + 4, + -2 + ], + [ + 4, + -3 + ], + [ + 4, + -4 + ], + [ + 4, + -5 + ], + [ + 5, + -6 + ], + [ + 5, + -7 + ], + [ + 5, + -8 + ], + [ + 5, + -9 + ], + [ + 5, + -10 + ], + [ + 6, + -11 + ], + [ + 6, + -12 + ], + [ + 7, + -13 + ], + [ + 7, + -14 + ], + [ + 8, + -15 + ], + [ + 9, + -16 + ], + [ + 10, + -17 + ], + [ + 11, + -18 + ], + [ + 11, + -19 + ], + [ + 12, + -19 + ], + [ + 13, + -20 + ], + [ + 14, + -21 + ], + [ + 15, + -22 + ], + [ + 16, + -22 + ], + [ + 17, + -23 + ], + [ + 18, + -24 + ], + [ + 19, + -24 + ], + [ + 19, + -25 + ], + [ + 20, + -25 + ], + [ + 20, + -26 + ], + [ + 21, + -27 + ], + [ + 22, + -27 + ], + [ + 23, + -27 + ], + [ + 23, + -28 + ], + [ + 24, + -28 + ], + [ + 25, + -28 + ], + [ + 26, + -29 + ], + [ + 27, + -29 + ], + [ + 28, + -29 + ], + [ + 29, + -30 + ], + [ + 30, + -30 + ], + [ + 31, + -30 + ], + [ + 32, + -30 + ], + [ + 33, + -30 + ], + [ + 34, + -30 + ], + [ + 35, + -30 + ], + [ + 36, + -29 + ], + [ + 36, + -28 + ], + [ + 37, + -27 + ], + [ + 38, + -26 + ], + [ + 38, + -25 + ], + [ + 39, + -24 + ], + [ + 40, + -23 + ], + [ + 40, + -22 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 42, + -20 + ], + [ + 42, + -19 + ], + [ + 42, + -18 + ], + [ + 42, + -17 + ], + [ + 41, + -17 + ], + [ + 41, + -16 + ], + [ + 40, + -16 + ], + [ + 40, + -15 + ], + [ + 39, + -15 + ], + [ + 38, + -14 + ], + [ + 36, + -14 + ], + [ + 35, + -14 + ], + [ + 33, + -14 + ], + [ + 32, + -14 + ], + [ + 30, + -13 + ], + [ + 29, + -12 + ], + [ + 26, + -12 + ], + [ + 24, + -11 + ], + [ + 23, + -11 + ], + [ + 21, + -11 + ], + [ + 20, + -11 + ], + [ + 19, + -11 + ], + [ + 18, + -11 + ], + [ + 17, + -11 + ], + [ + 16, + -11 + ], + [ + 15, + -11 + ], + [ + 14, + -12 + ], + [ + 14, + -13 + ], + [ + 15, + -14 + ], + [ + 16, + -14 + ], + [ + 17, + -15 + ], + [ + 18, + -15 + ], + [ + 20, + -16 + ], + [ + 21, + -16 + ], + [ + 23, + -16 + ], + [ + 24, + -16 + ], + [ + 26, + -17 + ], + [ + 27, + -17 + ], + [ + 29, + -17 + ], + [ + 30, + -17 + ], + [ + 34, + -17 + ], + [ + 37, + -17 + ], + [ + 40, + -16 + ], + [ + 41, + -15 + ], + [ + 41, + -14 + ], + [ + 41, + -13 + ], + [ + 40, + -13 + ], + [ + 40, + -12 + ], + [ + 39, + -12 + ], + [ + 38, + -12 + ], + [ + 37, + -12 + ], + [ + 36, + -12 + ], + [ + 34, + -13 + ], + [ + 33, + -13 + ], + [ + 32, + -14 + ], + [ + 31, + -14 + ], + [ + 30, + -14 + ], + [ + 29, + -14 + ], + [ + 29, + -15 + ], + [ + 29, + -16 + ], + [ + 29, + -17 + ], + [ + 29, + -18 + ], + [ + 30, + -19 + ], + [ + 31, + -19 + ], + [ + 33, + -20 + ], + [ + 35, + -20 + ], + [ + 37, + -20 + ], + [ + 38, + -20 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 43, + -21 + ], + [ + 44, + -21 + ], + [ + 45, + -21 + ], + [ + 45, + -20 + ], + [ + 45, + -19 + ], + [ + 45, + -18 + ], + [ + 45, + -17 + ], + [ + 45, + -16 + ], + [ + 45, + -15 + ], + [ + 45, + -14 + ], + [ + 45, + -13 + ], + [ + 45, + -12 + ], + [ + 44, + -12 + ], + [ + 44, + -11 + ], + [ + 44, + -12 + ], + [ + 45, + -12 + ], + [ + 46, + -12 + ], + [ + 47, + -13 + ], + [ + 48, + -13 + ], + [ + 49, + -13 + ], + [ + 50, + -13 + ], + [ + 50, + -12 + ], + [ + 50, + -11 + ], + [ + 50, + -11 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 479, + "versionNonce": 437513445, + "index": "b0e", + "isDeleted": false, + "id": "UDvKr0RxgawP5asj6VCaM", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2548.308875613742, + "y": 1195.4888888888888, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 28, + "height": 22, + "seed": 2119238469, + "groupIds": [ + "txEXNu40-Rts7qz-Hfi7x" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1, + 0 + ], + [ + -2, + 1 + ], + [ + -3, + 1 + ], + [ + -4, + 2 + ], + [ + -5, + 2 + ], + [ + -5, + 3 + ], + [ + -6, + 3 + ], + [ + -6, + 4 + ], + [ + -7, + 4 + ], + [ + -7, + 5 + ], + [ + -8, + 5 + ], + [ + -8, + 6 + ], + [ + -9, + 7 + ], + [ + -9, + 8 + ], + [ + -10, + 9 + ], + [ + -10, + 10 + ], + [ + -10, + 11 + ], + [ + -9, + 13 + ], + [ + -9, + 14 + ], + [ + -9, + 16 + ], + [ + -8, + 16 + ], + [ + -8, + 17 + ], + [ + -7, + 17 + ], + [ + -6, + 18 + ], + [ + -5, + 19 + ], + [ + -4, + 19 + ], + [ + -3, + 20 + ], + [ + -2, + 20 + ], + [ + -1, + 21 + ], + [ + 0, + 21 + ], + [ + 1, + 21 + ], + [ + 2, + 21 + ], + [ + 3, + 21 + ], + [ + 4, + 21 + ], + [ + 5, + 21 + ], + [ + 6, + 21 + ], + [ + 8, + 21 + ], + [ + 9, + 21 + ], + [ + 11, + 21 + ], + [ + 12, + 21 + ], + [ + 14, + 21 + ], + [ + 15, + 20 + ], + [ + 16, + 20 + ], + [ + 16, + 19 + ], + [ + 16, + 18 + ], + [ + 17, + 18 + ], + [ + 17, + 17 + ], + [ + 17, + 16 + ], + [ + 17, + 15 + ], + [ + 18, + 14 + ], + [ + 18, + 13 + ], + [ + 18, + 12 + ], + [ + 18, + 11 + ], + [ + 17, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 7 + ], + [ + 16, + 6 + ], + [ + 16, + 5 + ], + [ + 16, + 4 + ], + [ + 15, + 4 + ], + [ + 15, + 3 + ], + [ + 14, + 3 + ], + [ + 13, + 2 + ], + [ + 12, + 2 + ], + [ + 12, + 1 + ], + [ + 11, + 1 + ], + [ + 10, + 1 + ], + [ + 10, + 0 + ], + [ + 9, + 0 + ], + [ + 8, + 0 + ], + [ + 7, + 0 + ], + [ + 5, + 0 + ], + [ + 4, + -1 + ], + [ + 3, + -1 + ], + [ + 2, + -1 + ], + [ + 1, + -1 + ], + [ + 0, + 0 + ], + [ + -1, + 1 + ], + [ + -1, + 2 + ], + [ + -2, + 2 + ], + [ + -2, + 3 + ], + [ + -3, + 3 + ], + [ + -3, + 4 + ], + [ + -3, + 5 + ], + [ + -3, + 6 + ], + [ + -3, + 7 + ], + [ + -3, + 8 + ], + [ + -3, + 9 + ], + [ + -2, + 9 + ], + [ + -2, + 10 + ], + [ + -1, + 10 + ], + [ + -1, + 11 + ], + [ + 0, + 11 + ], + [ + 1, + 11 + ], + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 12 + ], + [ + 4, + 13 + ], + [ + 5, + 13 + ], + [ + 5, + 12 + ], + [ + 6, + 12 + ], + [ + 6, + 11 + ], + [ + 6, + 10 + ], + [ + 5, + 9 + ], + [ + 5, + 8 + ], + [ + 4, + 8 + ], + [ + 3, + 8 + ], + [ + 2, + 8 + ], + [ + 2, + 9 + ], + [ + 2, + 10 + ], + [ + 2, + 11 + ], + [ + 3, + 12 + ], + [ + 3, + 13 + ], + [ + 4, + 13 + ], + [ + 4, + 14 + ], + [ + 5, + 14 + ], + [ + 6, + 14 + ], + [ + 7, + 14 + ], + [ + 8, + 14 + ], + [ + 9, + 14 + ], + [ + 9, + 13 + ], + [ + 10, + 13 + ], + [ + 10, + 12 + ], + [ + 10, + 11 + ], + [ + 10, + 10 + ], + [ + 9, + 10 + ], + [ + 8, + 10 + ], + [ + 8, + 10 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 654, + "versionNonce": 1160710213, + "index": "b0f", + "isDeleted": false, + "id": "EQVH9YKKVPGd4zatw84ha", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2522.308875613742, + "y": 1280.4888888888888, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 58, + "height": 42, + "seed": 574247589, + "groupIds": [ + "txEXNu40-Rts7qz-Hfi7x" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -1 + ], + [ + 0, + -2 + ], + [ + -1, + -2 + ], + [ + -1, + -3 + ], + [ + -1, + -4 + ], + [ + -1, + -5 + ], + [ + -1, + -6 + ], + [ + -1, + -7 + ], + [ + -1, + -8 + ], + [ + -1, + -9 + ], + [ + -1, + -10 + ], + [ + -1, + -11 + ], + [ + -1, + -12 + ], + [ + 0, + -12 + ], + [ + 0, + -13 + ], + [ + 0, + -14 + ], + [ + 0, + -15 + ], + [ + 1, + -16 + ], + [ + 1, + -17 + ], + [ + 1, + -18 + ], + [ + 2, + -19 + ], + [ + 2, + -20 + ], + [ + 3, + -21 + ], + [ + 3, + -22 + ], + [ + 4, + -22 + ], + [ + 4, + -23 + ], + [ + 5, + -24 + ], + [ + 6, + -25 + ], + [ + 7, + -26 + ], + [ + 7, + -27 + ], + [ + 9, + -28 + ], + [ + 9, + -29 + ], + [ + 10, + -29 + ], + [ + 11, + -30 + ], + [ + 11, + -31 + ], + [ + 12, + -31 + ], + [ + 13, + -32 + ], + [ + 14, + -33 + ], + [ + 15, + -34 + ], + [ + 16, + -34 + ], + [ + 16, + -35 + ], + [ + 17, + -35 + ], + [ + 18, + -35 + ], + [ + 19, + -36 + ], + [ + 20, + -36 + ], + [ + 21, + -37 + ], + [ + 22, + -37 + ], + [ + 23, + -38 + ], + [ + 24, + -38 + ], + [ + 24, + -39 + ], + [ + 25, + -39 + ], + [ + 26, + -40 + ], + [ + 27, + -40 + ], + [ + 28, + -41 + ], + [ + 29, + -41 + ], + [ + 30, + -41 + ], + [ + 31, + -42 + ], + [ + 32, + -42 + ], + [ + 33, + -42 + ], + [ + 34, + -42 + ], + [ + 35, + -42 + ], + [ + 36, + -42 + ], + [ + 37, + -42 + ], + [ + 38, + -41 + ], + [ + 40, + -41 + ], + [ + 41, + -40 + ], + [ + 42, + -40 + ], + [ + 43, + -39 + ], + [ + 44, + -39 + ], + [ + 45, + -38 + ], + [ + 46, + -38 + ], + [ + 47, + -38 + ], + [ + 47, + -37 + ], + [ + 48, + -36 + ], + [ + 48, + -35 + ], + [ + 49, + -34 + ], + [ + 49, + -33 + ], + [ + 50, + -32 + ], + [ + 50, + -30 + ], + [ + 51, + -30 + ], + [ + 52, + -29 + ], + [ + 52, + -28 + ], + [ + 53, + -27 + ], + [ + 53, + -26 + ], + [ + 53, + -25 + ], + [ + 54, + -25 + ], + [ + 54, + -24 + ], + [ + 54, + -23 + ], + [ + 54, + -22 + ], + [ + 55, + -21 + ], + [ + 55, + -20 + ], + [ + 55, + -19 + ], + [ + 55, + -18 + ], + [ + 55, + -17 + ], + [ + 56, + -17 + ], + [ + 56, + -16 + ], + [ + 56, + -15 + ], + [ + 56, + -14 + ], + [ + 56, + -13 + ], + [ + 56, + -12 + ], + [ + 56, + -11 + ], + [ + 56, + -10 + ], + [ + 56, + -9 + ], + [ + 56, + -8 + ], + [ + 56, + -7 + ], + [ + 56, + -6 + ], + [ + 56, + -5 + ], + [ + 57, + -4 + ], + [ + 57, + -3 + ], + [ + 57, + -2 + ], + [ + 56, + -2 + ], + [ + 55, + -2 + ], + [ + 54, + -2 + ], + [ + 52, + -2 + ], + [ + 51, + -2 + ], + [ + 50, + -2 + ], + [ + 48, + -2 + ], + [ + 47, + -2 + ], + [ + 45, + -2 + ], + [ + 44, + -1 + ], + [ + 42, + -1 + ], + [ + 41, + -1 + ], + [ + 40, + -1 + ], + [ + 39, + 0 + ], + [ + 38, + 0 + ], + [ + 37, + 0 + ], + [ + 36, + 0 + ], + [ + 35, + 0 + ], + [ + 33, + 0 + ], + [ + 32, + 0 + ], + [ + 31, + 0 + ], + [ + 29, + 0 + ], + [ + 28, + 0 + ], + [ + 26, + 0 + ], + [ + 25, + 0 + ], + [ + 24, + 0 + ], + [ + 23, + 0 + ], + [ + 22, + 0 + ], + [ + 21, + 0 + ], + [ + 20, + 0 + ], + [ + 19, + 0 + ], + [ + 18, + 0 + ], + [ + 17, + 0 + ], + [ + 16, + 0 + ], + [ + 14, + 0 + ], + [ + 13, + 0 + ], + [ + 12, + 0 + ], + [ + 11, + 0 + ], + [ + 10, + 0 + ], + [ + 9, + -1 + ], + [ + 7, + -1 + ], + [ + 6, + -1 + ], + [ + 5, + -1 + ], + [ + 4, + -1 + ], + [ + 4, + -2 + ], + [ + 4, + -3 + ], + [ + 4, + -4 + ], + [ + 4, + -5 + ], + [ + 5, + -6 + ], + [ + 5, + -7 + ], + [ + 5, + -8 + ], + [ + 5, + -9 + ], + [ + 5, + -10 + ], + [ + 6, + -11 + ], + [ + 6, + -12 + ], + [ + 7, + -13 + ], + [ + 7, + -14 + ], + [ + 8, + -15 + ], + [ + 9, + -16 + ], + [ + 10, + -17 + ], + [ + 11, + -18 + ], + [ + 11, + -19 + ], + [ + 12, + -19 + ], + [ + 13, + -20 + ], + [ + 14, + -21 + ], + [ + 15, + -22 + ], + [ + 16, + -22 + ], + [ + 17, + -23 + ], + [ + 18, + -24 + ], + [ + 19, + -24 + ], + [ + 19, + -25 + ], + [ + 20, + -25 + ], + [ + 20, + -26 + ], + [ + 21, + -27 + ], + [ + 22, + -27 + ], + [ + 23, + -27 + ], + [ + 23, + -28 + ], + [ + 24, + -28 + ], + [ + 25, + -28 + ], + [ + 26, + -29 + ], + [ + 27, + -29 + ], + [ + 28, + -29 + ], + [ + 29, + -30 + ], + [ + 30, + -30 + ], + [ + 31, + -30 + ], + [ + 32, + -30 + ], + [ + 33, + -30 + ], + [ + 34, + -30 + ], + [ + 35, + -30 + ], + [ + 36, + -29 + ], + [ + 36, + -28 + ], + [ + 37, + -27 + ], + [ + 38, + -26 + ], + [ + 38, + -25 + ], + [ + 39, + -24 + ], + [ + 40, + -23 + ], + [ + 40, + -22 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 42, + -20 + ], + [ + 42, + -19 + ], + [ + 42, + -18 + ], + [ + 42, + -17 + ], + [ + 41, + -17 + ], + [ + 41, + -16 + ], + [ + 40, + -16 + ], + [ + 40, + -15 + ], + [ + 39, + -15 + ], + [ + 38, + -14 + ], + [ + 36, + -14 + ], + [ + 35, + -14 + ], + [ + 33, + -14 + ], + [ + 32, + -14 + ], + [ + 30, + -13 + ], + [ + 29, + -12 + ], + [ + 26, + -12 + ], + [ + 24, + -11 + ], + [ + 23, + -11 + ], + [ + 21, + -11 + ], + [ + 20, + -11 + ], + [ + 19, + -11 + ], + [ + 18, + -11 + ], + [ + 17, + -11 + ], + [ + 16, + -11 + ], + [ + 15, + -11 + ], + [ + 14, + -12 + ], + [ + 14, + -13 + ], + [ + 15, + -14 + ], + [ + 16, + -14 + ], + [ + 17, + -15 + ], + [ + 18, + -15 + ], + [ + 20, + -16 + ], + [ + 21, + -16 + ], + [ + 23, + -16 + ], + [ + 24, + -16 + ], + [ + 26, + -17 + ], + [ + 27, + -17 + ], + [ + 29, + -17 + ], + [ + 30, + -17 + ], + [ + 34, + -17 + ], + [ + 37, + -17 + ], + [ + 40, + -16 + ], + [ + 41, + -15 + ], + [ + 41, + -14 + ], + [ + 41, + -13 + ], + [ + 40, + -13 + ], + [ + 40, + -12 + ], + [ + 39, + -12 + ], + [ + 38, + -12 + ], + [ + 37, + -12 + ], + [ + 36, + -12 + ], + [ + 34, + -13 + ], + [ + 33, + -13 + ], + [ + 32, + -14 + ], + [ + 31, + -14 + ], + [ + 30, + -14 + ], + [ + 29, + -14 + ], + [ + 29, + -15 + ], + [ + 29, + -16 + ], + [ + 29, + -17 + ], + [ + 29, + -18 + ], + [ + 30, + -19 + ], + [ + 31, + -19 + ], + [ + 33, + -20 + ], + [ + 35, + -20 + ], + [ + 37, + -20 + ], + [ + 38, + -20 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 43, + -21 + ], + [ + 44, + -21 + ], + [ + 45, + -21 + ], + [ + 45, + -20 + ], + [ + 45, + -19 + ], + [ + 45, + -18 + ], + [ + 45, + -17 + ], + [ + 45, + -16 + ], + [ + 45, + -15 + ], + [ + 45, + -14 + ], + [ + 45, + -13 + ], + [ + 45, + -12 + ], + [ + 44, + -12 + ], + [ + 44, + -11 + ], + [ + 44, + -12 + ], + [ + 45, + -12 + ], + [ + 46, + -12 + ], + [ + 47, + -13 + ], + [ + 48, + -13 + ], + [ + 49, + -13 + ], + [ + 50, + -13 + ], + [ + 50, + -12 + ], + [ + 50, + -11 + ], + [ + 50, + -11 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "text", + "version": 355, + "versionNonce": 1225766821, + "index": "b0g", + "isDeleted": false, + "id": "oHiI-diNFXNo4nn79ecEY", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2005.3088756137422, + "y": 1288.4888888888888, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 81.43333435058594, + "height": 64.80000000000001, + "seed": 1325577733, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "@everyone\nrole1\nrole2", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "@everyone\nrole1\nrole2", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "text", + "version": 411, + "versionNonce": 535001861, + "index": "b0h", + "isDeleted": false, + "id": "xdFtorJIquQrPdTl0BGlU", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2188.5922084384492, + "y": 1295.6888888888896, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 81.43333435058594, + "height": 43.2, + "seed": 1067953509, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "@everyone\nrole1", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "@everyone\nrole1", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "text", + "version": 466, + "versionNonce": 573035109, + "index": "b0i", + "isDeleted": false, + "id": "7emNqD2oswIWvyucmQPgq", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2345.5922084384492, + "y": 1300.6888888888886, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 81.43333435058594, + "height": 43.2, + "seed": 1937160389, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "@everyone\nrole2", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "@everyone\nrole2", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "text", + "version": 521, + "versionNonce": 1017789893, + "index": "b0j", + "isDeleted": false, + "id": "QPBL862S6bVU8ck8DKkbi", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2506.5922084384492, + "y": 1299.6888888888896, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 81.43333435058594, + "height": 21.6, + "seed": 604854309, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918366440, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "@everyone", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "@everyone", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "ellipse", + "version": 355, + "versionNonce": 1420342277, + "index": "b0k", + "isDeleted": false, + "id": "3U99Cb2Kfg8Tlwif9RVQc", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2203.1672092013887, + "y": 519.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 189, + "height": 106, + "seed": 431153995, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "type": "text", + "id": "xuUnGiT59o-8cz8a1F7LJ" + }, + { + "id": "sXyxzxgCu764FCGAZM_lx", + "type": "arrow" + }, + { + "id": "C4nv0UsyABV7pyFF4MjvB", + "type": "arrow" + } + ], + "updated": 1726918427926, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 326, + "versionNonce": 1383070725, + "index": "b0l", + "isDeleted": false, + "id": "xuUnGiT59o-8cz8a1F7LJ", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2242.0789519669065, + "y": 540.512229486002, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 111.53333282470703, + "height": 64.80000000000001, + "seed": 2007908843, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918380012, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "channel update\nevent for\nrole1, role2", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "3U99Cb2Kfg8Tlwif9RVQc", + "originalText": "channel update\nevent for\nrole1, role2", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "rectangle", + "version": 135, + "versionNonce": 1154041867, + "index": "b0m", + "isDeleted": false, + "id": "pX5ccEr5swnPSJx6hZORI", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2195.6672092013887, + "y": 753.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 194, + "height": 93, + "seed": 854806917, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "kUmeJCdbzqrKDcbWWED27" + }, + { + "id": "sXyxzxgCu764FCGAZM_lx", + "type": "arrow" + }, + { + "id": "C4nv0UsyABV7pyFF4MjvB", + "type": "arrow" + }, + { + "id": "d2KqmWXSWVpK1rau0p4Ly", + "type": "arrow" + }, + { + "id": "VSp2nxUoFQQvjUVEjNcqU", + "type": "arrow" + } + ], + "updated": 1726918468206, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 143, + "versionNonce": 53590181, + "index": "b0n", + "isDeleted": false, + "id": "kUmeJCdbzqrKDcbWWED27", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2227.6672092013887, + "y": 767.9888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 130, + "height": 64.80000000000001, + "seed": 1535401829, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918409820, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Guild Information:\nRole ID->User ID\nmapping", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "pX5ccEr5swnPSJx6hZORI", + "originalText": "Guild Information:\nRole ID->User ID\nmapping", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 62, + "versionNonce": 46705701, + "index": "b0o", + "isDeleted": false, + "id": "sXyxzxgCu764FCGAZM_lx", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2264.6672092013887, + "y": 625.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 1, + "height": 124, + "seed": 712872805, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "type": "text", + "id": "GDegdKUZ9_XlmQsn3cjzY" + } + ], + "updated": 1726918432010, + "link": null, + "locked": false, + "startBinding": { + "elementId": "3U99Cb2Kfg8Tlwif9RVQc", + "focus": 0.3446798675077111, + "gap": 3.2675346198741124, + "fixedPoint": null + }, + "endBinding": { + "elementId": "pX5ccEr5swnPSJx6hZORI", + "focus": -0.30200008281916435, + "gap": 4, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -1, + 124 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 9, + "versionNonce": 270463851, + "index": "b0oV", + "isDeleted": false, + "id": "GDegdKUZ9_XlmQsn3cjzY", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2245.175542534722, + "y": 677.088888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 37.983333333333334, + "height": 21.6, + "seed": 482377675, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918431097, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "role1", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "sXyxzxgCu764FCGAZM_lx", + "originalText": "role1", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 62, + "versionNonce": 95890757, + "index": "b0p", + "isDeleted": false, + "id": "C4nv0UsyABV7pyFF4MjvB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2326.6672092013887, + "y": 630.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 1, + "height": 115, + "seed": 348394859, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "type": "text", + "id": "ZXByhtNsQIoD3boJwK_1U" + } + ], + "updated": 1726918434302, + "link": null, + "locked": false, + "startBinding": { + "elementId": "3U99Cb2Kfg8Tlwif9RVQc", + "focus": -0.3122116080481013, + "gap": 7.442335847963641, + "fixedPoint": null + }, + "endBinding": { + "elementId": "pX5ccEr5swnPSJx6hZORI", + "focus": 0.333928491719859, + "gap": 8, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -1, + 115 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 8, + "versionNonce": 1292192843, + "index": "b0q", + "isDeleted": false, + "id": "ZXByhtNsQIoD3boJwK_1U", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2307.175542534722, + "y": 677.588888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 37.983333333333334, + "height": 21.6, + "seed": 1848215371, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918433617, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "role2", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "C4nv0UsyABV7pyFF4MjvB", + "originalText": "role2", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 286, + "versionNonce": 610867813, + "index": "b0r", + "isDeleted": false, + "id": "d2KqmWXSWVpK1rau0p4Ly", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2396.6672092013887, + "y": 761.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 85, + "height": 181, + "seed": 1867201701, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726918508964, + "link": null, + "locked": false, + "startBinding": { + "elementId": "pX5ccEr5swnPSJx6hZORI", + "focus": -0.9160669523732515, + "gap": 7, + "fixedPoint": null + }, + "endBinding": { + "elementId": "qwvPWBT-AMLng4YbFJgE5", + "focus": 0.48429206713527506, + "gap": 1, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 85, + 23 + ], + [ + 0, + 181 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 172, + "versionNonce": 1515014469, + "index": "b0s", + "isDeleted": false, + "id": "lNZOuEpmA2RQahhdkRqrN", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2491.6672092013887, + "y": 764.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 204.86666666666667, + "height": 43.2, + "seed": 226967749, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918493942, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Add everyone from Role 1 \nas recipient", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Add everyone from Role 1 as recipient", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 365, + "versionNonce": 309439365, + "index": "b0t", + "isDeleted": false, + "id": "VSp2nxUoFQQvjUVEjNcqU", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2188.6672092013887, + "y": 781.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 102, + "height": 166, + "seed": 1298872459, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726918507242, + "link": null, + "locked": false, + "startBinding": { + "elementId": "pX5ccEr5swnPSJx6hZORI", + "focus": 0.5703860019344061, + "gap": 7, + "fixedPoint": null + }, + "endBinding": { + "elementId": "qwvPWBT-AMLng4YbFJgE5", + "focus": -0.5435155208531713, + "gap": 1, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -91, + 15 + ], + [ + 11, + 166 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 97, + "versionNonce": 556104709, + "index": "b0u", + "isDeleted": false, + "id": "tmkdW5jNX7uZWaMNJsZRf", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1893.667209201389, + "y": 774.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 189.81666564941406, + "height": 43.2, + "seed": 1174821861, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918495409, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Add everyone from Role 2\nas recipient", + "textAlign": "right", + "verticalAlign": "top", + "containerId": null, + "originalText": "Add everyone from Role 2\nas recipient", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 39, + "versionNonce": 1947291973, + "index": "b0v", + "isDeleted": false, + "id": "suRrGqAz0cEyuwlqJcpHg", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2429.6672092013887, + "y": 987.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 87, + "height": 1, + "seed": 472212747, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726918515631, + "link": null, + "locked": false, + "startBinding": { + "elementId": "qwvPWBT-AMLng4YbFJgE5", + "focus": -0.1279468986037995, + "gap": 18, + "fixedPoint": null + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 87, + 1 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 64, + "versionNonce": 360055147, + "index": "b0w", + "isDeleted": false, + "id": "DkbZYT7aReffbg0UmTWGj", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2527.150542534722, + "y": 978.8888888888889, + "strokeColor": "#2f9e44", + "backgroundColor": "#a5d8ff", + "width": 92.51666666666667, + "height": 21.6, + "seed": 832989899, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918525184, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "deduplicates", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "deduplicates", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 121, + "versionNonce": 227194565, + "index": "b0x", + "isDeleted": false, + "id": "321eNbFnBxl8j-kWXkY8s", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2264.6672092013887, + "y": 1046.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 200, + "height": 118, + "seed": 1441718955, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726918533072, + "link": null, + "locked": false, + "startBinding": { + "elementId": "qwvPWBT-AMLng4YbFJgE5", + "focus": -0.2672583826429981, + "gap": 5, + "fixedPoint": null + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -200, + 118 + ] + ], + "elbowed": false + }, + { + "type": "arrow", + "version": 43, + "versionNonce": 860129739, + "index": "b0y", + "isDeleted": false, + "id": "39o_Q6zDovd5a5GljOMno", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2280.6672092013887, + "y": 1050.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 48, + "height": 128, + "seed": 384931365, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726918538208, + "link": null, + "locked": false, + "startBinding": { + "elementId": "qwvPWBT-AMLng4YbFJgE5", + "focus": 0.02613065326633168, + "gap": 9, + "fixedPoint": null + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -48, + 128 + ] + ], + "elbowed": false + }, + { + "type": "arrow", + "version": 80, + "versionNonce": 593227301, + "index": "b0z", + "isDeleted": false, + "id": "x0GvUbEbHaO3epjVckQko", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2335.6672092013887, + "y": 1042.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 35, + "height": 128, + "seed": 1695807269, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726918540204, + "link": null, + "locked": false, + "startBinding": { + "elementId": "qwvPWBT-AMLng4YbFJgE5", + "focus": -0.13675325525093254, + "gap": 1, + "fixedPoint": null + }, + "endBinding": null, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 35, + 128 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 195, + "versionNonce": 1275942021, + "index": "b10", + "isDeleted": false, + "id": "ZFK4unSk7yiswsQLXlRE0", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2183.6672092013887, + "y": 1103.888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 209.68333333333334, + "height": 21.6, + "seed": 1606759333, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918578911, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "broadcast event to recipients", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "broadcast event to recipients", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "rectangle", + "version": 341, + "versionNonce": 433404133, + "index": "b11", + "isDeleted": false, + "id": "YvrAXYERhR-VVUCxxvzwy", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2576.6672092013887, + "y": 825.8888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 408, + "height": 120, + "seed": 456582347, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "5uIKmQGCHQ4JNzkHQ2WTP" + } + ], + "updated": 1726918829164, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 430, + "versionNonce": 1457968197, + "index": "b12", + "isDeleted": false, + "id": "5uIKmQGCHQ4JNzkHQ2WTP", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2581.6672092013887, + "y": 842.6888888888889, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 373.8500061035156, + "height": 86.4, + "seed": 817003045, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918829164, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "What we need is a sort of omniscient sender struct \nwhich knows of every user id connected to the \ngateway and which holds a channel sender to send \nmessages to that user.", + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "YvrAXYERhR-VVUCxxvzwy", + "originalText": "What we need is a sort of omniscient sender struct which knows of every user id connected to the gateway and which holds a channel sender to send messages to that user.", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "rectangle", + "version": 563, + "versionNonce": 788942283, + "index": "b13", + "isDeleted": false, + "id": "2-xZdr1N7h94XCAl94juo", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1052.8519422561303, + "y": 2342.3669517142903, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 196.66666666666652, + "height": 95.55555555555566, + "seed": 655902469, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "t0kacxDg-xYJMZRhr-ORv" + }, + { + "id": "_FXruw5utykj1QDL3wURd", + "type": "arrow" + }, + { + "id": "VQweVVOoEtZ8a74fJHKXs", + "type": "arrow" + }, + { + "id": "ldtBH_Mc3-DYjw69pcUaV", + "type": "arrow" + }, + { + "id": "NFrs7hBfcfS6Sh6nfbpdz", + "type": "arrow" + }, + { + "id": "kuMt4oieo-H6xuQKoGSWP", + "type": "arrow" + }, + { + "id": "HQ69vqNQrY84lbpyc-1HF", + "type": "arrow" + } + ], + "updated": 1726919381684, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 530, + "versionNonce": 338713707, + "index": "b13V", + "isDeleted": false, + "id": "t0kacxDg-xYJMZRhr-ORv", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1100.476943527696, + "y": 2379.344729492068, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 101.41666412353516, + "height": 21.6, + "seed": 1860658405, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919381684, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Gateway Task", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "2-xZdr1N7h94XCAl94juo", + "originalText": "Gateway Task", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "rectangle", + "version": 770, + "versionNonce": 1741918923, + "index": "b15", + "isDeleted": false, + "id": "ernpMbdN54sSKmYGoM7nC", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1583.4074978116857, + "y": 2342.36695171429, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 196.66666666666652, + "height": 95.55555555555566, + "seed": 364662091, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "iFTMGnafTCa2cKPMR7lDj" + }, + { + "id": "_FXruw5utykj1QDL3wURd", + "type": "arrow" + }, + { + "id": "VQweVVOoEtZ8a74fJHKXs", + "type": "arrow" + }, + { + "id": "o1dloWTz2OdDChjfXO3W8", + "type": "arrow" + }, + { + "id": "iPGwpGNDp9qVzkdoEhrce", + "type": "arrow" + } + ], + "updated": 1726919381684, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 758, + "versionNonce": 755827051, + "index": "b16", + "isDeleted": false, + "id": "iFTMGnafTCa2cKPMR7lDj", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1625.9574978116857, + "y": 2379.344729492068, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 111.56666666666666, + "height": 21.6, + "seed": 1123707883, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919381684, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Heartbeat Task", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ernpMbdN54sSKmYGoM7nC", + "originalText": "Heartbeat Task", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 1809, + "versionNonce": 188264357, + "index": "b17", + "isDeleted": false, + "id": "_FXruw5utykj1QDL3wURd", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1257.2963867005747, + "y": 2371.255840603179, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 317.77777777777806, + "height": 0.7658583367585834, + "seed": 394883525, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "type": "text", + "id": "rVjVYzDRISZ3zbtZ2lI7r" + } + ], + "updated": 1726919383641, + "link": null, + "locked": false, + "startBinding": { + "elementId": "2-xZdr1N7h94XCAl94juo", + "focus": -0.39872362474490314, + "gap": 7.777777777777828, + "fixedPoint": null + }, + "endBinding": { + "elementId": "ernpMbdN54sSKmYGoM7nC", + "focus": 0.3720930232557988, + "gap": 8.33333333333303, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 317.77777777777806, + 0.7658583367585834 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 42, + "versionNonce": 1087064805, + "index": "b17V", + "isDeleted": false, + "id": "rVjVYzDRISZ3zbtZ2lI7r", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1626.577523609949, + "y": 1805.4973968540703, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 199.60000610351562, + "height": 43.2, + "seed": 740383013, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918885299, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "forwards heartbeat related \nmessages", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "_FXruw5utykj1QDL3wURd", + "originalText": "forwards heartbeat related messages", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 1816, + "versionNonce": 947047013, + "index": "b18", + "isDeleted": false, + "id": "VQweVVOoEtZ8a74fJHKXs", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1576.1852755894638, + "y": 2400.5573937061986, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 322.2222222222224, + "height": 0.6984468969803856, + "seed": 1426645291, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "type": "text", + "id": "vH_ahY76bpif3eJ3dl-IZ" + } + ], + "updated": 1726919383641, + "link": null, + "locked": false, + "startBinding": { + "elementId": "ernpMbdN54sSKmYGoM7nC", + "focus": -0.21220392709342042, + "gap": 7.222222222221944, + "fixedPoint": null + }, + "endBinding": { + "elementId": "2-xZdr1N7h94XCAl94juo", + "focus": 0.2361673942352932, + "gap": 4.444444444444571, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -322.2222222222224, + 0.6984468969803856 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 131, + "versionNonce": 1376495019, + "index": "b19", + "isDeleted": false, + "id": "vH_ahY76bpif3eJ3dl-IZ", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1635.7997488839292, + "y": 1845.6461262590453, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 178.93333333333334, + "height": 21.6, + "seed": 318121419, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726918903035, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "response to send to user", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "VQweVVOoEtZ8a74fJHKXs", + "originalText": "response to send to user", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "freedraw", + "version": 1165, + "versionNonce": 1606044613, + "index": "b1A", + "isDeleted": false, + "id": "DrB3Y-Z4QjXjSwPrDuRu6", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1683.9702132358989, + "y": 1777.2360377736534, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 28, + "height": 22, + "seed": 2138815653, + "groupIds": [ + "QIVflQ_PHDJZcp_Z14GCd" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919401304, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1, + 0 + ], + [ + -2, + 1 + ], + [ + -3, + 1 + ], + [ + -4, + 2 + ], + [ + -5, + 2 + ], + [ + -5, + 3 + ], + [ + -6, + 3 + ], + [ + -6, + 4 + ], + [ + -7, + 4 + ], + [ + -7, + 5 + ], + [ + -8, + 5 + ], + [ + -8, + 6 + ], + [ + -9, + 7 + ], + [ + -9, + 8 + ], + [ + -10, + 9 + ], + [ + -10, + 10 + ], + [ + -10, + 11 + ], + [ + -9, + 13 + ], + [ + -9, + 14 + ], + [ + -9, + 16 + ], + [ + -8, + 16 + ], + [ + -8, + 17 + ], + [ + -7, + 17 + ], + [ + -6, + 18 + ], + [ + -5, + 19 + ], + [ + -4, + 19 + ], + [ + -3, + 20 + ], + [ + -2, + 20 + ], + [ + -1, + 21 + ], + [ + 0, + 21 + ], + [ + 1, + 21 + ], + [ + 2, + 21 + ], + [ + 3, + 21 + ], + [ + 4, + 21 + ], + [ + 5, + 21 + ], + [ + 6, + 21 + ], + [ + 8, + 21 + ], + [ + 9, + 21 + ], + [ + 11, + 21 + ], + [ + 12, + 21 + ], + [ + 14, + 21 + ], + [ + 15, + 20 + ], + [ + 16, + 20 + ], + [ + 16, + 19 + ], + [ + 16, + 18 + ], + [ + 17, + 18 + ], + [ + 17, + 17 + ], + [ + 17, + 16 + ], + [ + 17, + 15 + ], + [ + 18, + 14 + ], + [ + 18, + 13 + ], + [ + 18, + 12 + ], + [ + 18, + 11 + ], + [ + 17, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 7 + ], + [ + 16, + 6 + ], + [ + 16, + 5 + ], + [ + 16, + 4 + ], + [ + 15, + 4 + ], + [ + 15, + 3 + ], + [ + 14, + 3 + ], + [ + 13, + 2 + ], + [ + 12, + 2 + ], + [ + 12, + 1 + ], + [ + 11, + 1 + ], + [ + 10, + 1 + ], + [ + 10, + 0 + ], + [ + 9, + 0 + ], + [ + 8, + 0 + ], + [ + 7, + 0 + ], + [ + 5, + 0 + ], + [ + 4, + -1 + ], + [ + 3, + -1 + ], + [ + 2, + -1 + ], + [ + 1, + -1 + ], + [ + 0, + 0 + ], + [ + -1, + 1 + ], + [ + -1, + 2 + ], + [ + -2, + 2 + ], + [ + -2, + 3 + ], + [ + -3, + 3 + ], + [ + -3, + 4 + ], + [ + -3, + 5 + ], + [ + -3, + 6 + ], + [ + -3, + 7 + ], + [ + -3, + 8 + ], + [ + -3, + 9 + ], + [ + -2, + 9 + ], + [ + -2, + 10 + ], + [ + -1, + 10 + ], + [ + -1, + 11 + ], + [ + 0, + 11 + ], + [ + 1, + 11 + ], + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 12 + ], + [ + 4, + 13 + ], + [ + 5, + 13 + ], + [ + 5, + 12 + ], + [ + 6, + 12 + ], + [ + 6, + 11 + ], + [ + 6, + 10 + ], + [ + 5, + 9 + ], + [ + 5, + 8 + ], + [ + 4, + 8 + ], + [ + 3, + 8 + ], + [ + 2, + 8 + ], + [ + 2, + 9 + ], + [ + 2, + 10 + ], + [ + 2, + 11 + ], + [ + 3, + 12 + ], + [ + 3, + 13 + ], + [ + 4, + 13 + ], + [ + 4, + 14 + ], + [ + 5, + 14 + ], + [ + 6, + 14 + ], + [ + 7, + 14 + ], + [ + 8, + 14 + ], + [ + 9, + 14 + ], + [ + 9, + 13 + ], + [ + 10, + 13 + ], + [ + 10, + 12 + ], + [ + 10, + 11 + ], + [ + 10, + 10 + ], + [ + 9, + 10 + ], + [ + 8, + 10 + ], + [ + 8, + 10 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 1340, + "versionNonce": 140938021, + "index": "b1B", + "isDeleted": false, + "id": "e9cm8wo2OWga0vSbbT22r", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1657.9702132358989, + "y": 1862.2360377736534, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 58, + "height": 42, + "seed": 344383493, + "groupIds": [ + "QIVflQ_PHDJZcp_Z14GCd" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919401304, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -1 + ], + [ + 0, + -2 + ], + [ + -1, + -2 + ], + [ + -1, + -3 + ], + [ + -1, + -4 + ], + [ + -1, + -5 + ], + [ + -1, + -6 + ], + [ + -1, + -7 + ], + [ + -1, + -8 + ], + [ + -1, + -9 + ], + [ + -1, + -10 + ], + [ + -1, + -11 + ], + [ + -1, + -12 + ], + [ + 0, + -12 + ], + [ + 0, + -13 + ], + [ + 0, + -14 + ], + [ + 0, + -15 + ], + [ + 1, + -16 + ], + [ + 1, + -17 + ], + [ + 1, + -18 + ], + [ + 2, + -19 + ], + [ + 2, + -20 + ], + [ + 3, + -21 + ], + [ + 3, + -22 + ], + [ + 4, + -22 + ], + [ + 4, + -23 + ], + [ + 5, + -24 + ], + [ + 6, + -25 + ], + [ + 7, + -26 + ], + [ + 7, + -27 + ], + [ + 9, + -28 + ], + [ + 9, + -29 + ], + [ + 10, + -29 + ], + [ + 11, + -30 + ], + [ + 11, + -31 + ], + [ + 12, + -31 + ], + [ + 13, + -32 + ], + [ + 14, + -33 + ], + [ + 15, + -34 + ], + [ + 16, + -34 + ], + [ + 16, + -35 + ], + [ + 17, + -35 + ], + [ + 18, + -35 + ], + [ + 19, + -36 + ], + [ + 20, + -36 + ], + [ + 21, + -37 + ], + [ + 22, + -37 + ], + [ + 23, + -38 + ], + [ + 24, + -38 + ], + [ + 24, + -39 + ], + [ + 25, + -39 + ], + [ + 26, + -40 + ], + [ + 27, + -40 + ], + [ + 28, + -41 + ], + [ + 29, + -41 + ], + [ + 30, + -41 + ], + [ + 31, + -42 + ], + [ + 32, + -42 + ], + [ + 33, + -42 + ], + [ + 34, + -42 + ], + [ + 35, + -42 + ], + [ + 36, + -42 + ], + [ + 37, + -42 + ], + [ + 38, + -41 + ], + [ + 40, + -41 + ], + [ + 41, + -40 + ], + [ + 42, + -40 + ], + [ + 43, + -39 + ], + [ + 44, + -39 + ], + [ + 45, + -38 + ], + [ + 46, + -38 + ], + [ + 47, + -38 + ], + [ + 47, + -37 + ], + [ + 48, + -36 + ], + [ + 48, + -35 + ], + [ + 49, + -34 + ], + [ + 49, + -33 + ], + [ + 50, + -32 + ], + [ + 50, + -30 + ], + [ + 51, + -30 + ], + [ + 52, + -29 + ], + [ + 52, + -28 + ], + [ + 53, + -27 + ], + [ + 53, + -26 + ], + [ + 53, + -25 + ], + [ + 54, + -25 + ], + [ + 54, + -24 + ], + [ + 54, + -23 + ], + [ + 54, + -22 + ], + [ + 55, + -21 + ], + [ + 55, + -20 + ], + [ + 55, + -19 + ], + [ + 55, + -18 + ], + [ + 55, + -17 + ], + [ + 56, + -17 + ], + [ + 56, + -16 + ], + [ + 56, + -15 + ], + [ + 56, + -14 + ], + [ + 56, + -13 + ], + [ + 56, + -12 + ], + [ + 56, + -11 + ], + [ + 56, + -10 + ], + [ + 56, + -9 + ], + [ + 56, + -8 + ], + [ + 56, + -7 + ], + [ + 56, + -6 + ], + [ + 56, + -5 + ], + [ + 57, + -4 + ], + [ + 57, + -3 + ], + [ + 57, + -2 + ], + [ + 56, + -2 + ], + [ + 55, + -2 + ], + [ + 54, + -2 + ], + [ + 52, + -2 + ], + [ + 51, + -2 + ], + [ + 50, + -2 + ], + [ + 48, + -2 + ], + [ + 47, + -2 + ], + [ + 45, + -2 + ], + [ + 44, + -1 + ], + [ + 42, + -1 + ], + [ + 41, + -1 + ], + [ + 40, + -1 + ], + [ + 39, + 0 + ], + [ + 38, + 0 + ], + [ + 37, + 0 + ], + [ + 36, + 0 + ], + [ + 35, + 0 + ], + [ + 33, + 0 + ], + [ + 32, + 0 + ], + [ + 31, + 0 + ], + [ + 29, + 0 + ], + [ + 28, + 0 + ], + [ + 26, + 0 + ], + [ + 25, + 0 + ], + [ + 24, + 0 + ], + [ + 23, + 0 + ], + [ + 22, + 0 + ], + [ + 21, + 0 + ], + [ + 20, + 0 + ], + [ + 19, + 0 + ], + [ + 18, + 0 + ], + [ + 17, + 0 + ], + [ + 16, + 0 + ], + [ + 14, + 0 + ], + [ + 13, + 0 + ], + [ + 12, + 0 + ], + [ + 11, + 0 + ], + [ + 10, + 0 + ], + [ + 9, + -1 + ], + [ + 7, + -1 + ], + [ + 6, + -1 + ], + [ + 5, + -1 + ], + [ + 4, + -1 + ], + [ + 4, + -2 + ], + [ + 4, + -3 + ], + [ + 4, + -4 + ], + [ + 4, + -5 + ], + [ + 5, + -6 + ], + [ + 5, + -7 + ], + [ + 5, + -8 + ], + [ + 5, + -9 + ], + [ + 5, + -10 + ], + [ + 6, + -11 + ], + [ + 6, + -12 + ], + [ + 7, + -13 + ], + [ + 7, + -14 + ], + [ + 8, + -15 + ], + [ + 9, + -16 + ], + [ + 10, + -17 + ], + [ + 11, + -18 + ], + [ + 11, + -19 + ], + [ + 12, + -19 + ], + [ + 13, + -20 + ], + [ + 14, + -21 + ], + [ + 15, + -22 + ], + [ + 16, + -22 + ], + [ + 17, + -23 + ], + [ + 18, + -24 + ], + [ + 19, + -24 + ], + [ + 19, + -25 + ], + [ + 20, + -25 + ], + [ + 20, + -26 + ], + [ + 21, + -27 + ], + [ + 22, + -27 + ], + [ + 23, + -27 + ], + [ + 23, + -28 + ], + [ + 24, + -28 + ], + [ + 25, + -28 + ], + [ + 26, + -29 + ], + [ + 27, + -29 + ], + [ + 28, + -29 + ], + [ + 29, + -30 + ], + [ + 30, + -30 + ], + [ + 31, + -30 + ], + [ + 32, + -30 + ], + [ + 33, + -30 + ], + [ + 34, + -30 + ], + [ + 35, + -30 + ], + [ + 36, + -29 + ], + [ + 36, + -28 + ], + [ + 37, + -27 + ], + [ + 38, + -26 + ], + [ + 38, + -25 + ], + [ + 39, + -24 + ], + [ + 40, + -23 + ], + [ + 40, + -22 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 42, + -20 + ], + [ + 42, + -19 + ], + [ + 42, + -18 + ], + [ + 42, + -17 + ], + [ + 41, + -17 + ], + [ + 41, + -16 + ], + [ + 40, + -16 + ], + [ + 40, + -15 + ], + [ + 39, + -15 + ], + [ + 38, + -14 + ], + [ + 36, + -14 + ], + [ + 35, + -14 + ], + [ + 33, + -14 + ], + [ + 32, + -14 + ], + [ + 30, + -13 + ], + [ + 29, + -12 + ], + [ + 26, + -12 + ], + [ + 24, + -11 + ], + [ + 23, + -11 + ], + [ + 21, + -11 + ], + [ + 20, + -11 + ], + [ + 19, + -11 + ], + [ + 18, + -11 + ], + [ + 17, + -11 + ], + [ + 16, + -11 + ], + [ + 15, + -11 + ], + [ + 14, + -12 + ], + [ + 14, + -13 + ], + [ + 15, + -14 + ], + [ + 16, + -14 + ], + [ + 17, + -15 + ], + [ + 18, + -15 + ], + [ + 20, + -16 + ], + [ + 21, + -16 + ], + [ + 23, + -16 + ], + [ + 24, + -16 + ], + [ + 26, + -17 + ], + [ + 27, + -17 + ], + [ + 29, + -17 + ], + [ + 30, + -17 + ], + [ + 34, + -17 + ], + [ + 37, + -17 + ], + [ + 40, + -16 + ], + [ + 41, + -15 + ], + [ + 41, + -14 + ], + [ + 41, + -13 + ], + [ + 40, + -13 + ], + [ + 40, + -12 + ], + [ + 39, + -12 + ], + [ + 38, + -12 + ], + [ + 37, + -12 + ], + [ + 36, + -12 + ], + [ + 34, + -13 + ], + [ + 33, + -13 + ], + [ + 32, + -14 + ], + [ + 31, + -14 + ], + [ + 30, + -14 + ], + [ + 29, + -14 + ], + [ + 29, + -15 + ], + [ + 29, + -16 + ], + [ + 29, + -17 + ], + [ + 29, + -18 + ], + [ + 30, + -19 + ], + [ + 31, + -19 + ], + [ + 33, + -20 + ], + [ + 35, + -20 + ], + [ + 37, + -20 + ], + [ + 38, + -20 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 43, + -21 + ], + [ + 44, + -21 + ], + [ + 45, + -21 + ], + [ + 45, + -20 + ], + [ + 45, + -19 + ], + [ + 45, + -18 + ], + [ + 45, + -17 + ], + [ + 45, + -16 + ], + [ + 45, + -15 + ], + [ + 45, + -14 + ], + [ + 45, + -13 + ], + [ + 45, + -12 + ], + [ + 44, + -12 + ], + [ + 44, + -11 + ], + [ + 44, + -12 + ], + [ + 45, + -12 + ], + [ + 46, + -12 + ], + [ + 47, + -13 + ], + [ + 48, + -13 + ], + [ + 49, + -13 + ], + [ + 50, + -13 + ], + [ + 50, + -12 + ], + [ + 50, + -11 + ], + [ + 50, + -11 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "arrow", + "version": 1097, + "versionNonce": 266216453, + "index": "b1E", + "isDeleted": false, + "id": "ldtBH_Mc3-DYjw69pcUaV", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1102.8466310158012, + "y": 2337.3669517142903, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 443.89723817272056, + "height": 333.4131343177314, + "seed": 1145680325, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726919461234, + "link": null, + "locked": false, + "startBinding": { + "elementId": "2-xZdr1N7h94XCAl94juo", + "focus": -0.7323890299850935, + "gap": 5, + "fixedPoint": null + }, + "endBinding": { + "elementId": "yrMLMaP8hB6ezngk1Y_H9", + "focus": 1.057909466192341, + "gap": 9.399299968612127, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 443.89723817272056, + -333.4131343177314 + ] + ], + "elbowed": false + }, + { + "type": "arrow", + "version": 1260, + "versionNonce": 1152907141, + "index": "b1F", + "isDeleted": false, + "id": "NFrs7hBfcfS6Sh6nfbpdz", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1572.2903640815873, + "y": 2024.2886438786966, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 426.16246819580033, + "height": 314.5440885902324, + "seed": 1362911237, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726919457459, + "link": null, + "locked": false, + "startBinding": { + "elementId": "yrMLMaP8hB6ezngk1Y_H9", + "focus": -0.267713028894834, + "gap": 2.7504596041435434, + "fixedPoint": null + }, + "endBinding": { + "elementId": "2-xZdr1N7h94XCAl94juo", + "focus": -0.4573491501323452, + "gap": 3.534219245361328, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -426.16246819580033, + 314.5440885902324 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 940, + "versionNonce": 546839115, + "index": "b1G", + "isDeleted": false, + "id": "Dn-LVSEKgJ-knjwmjqVvi", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1173.7398702543148, + "y": 2110.5413661943376, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 194.69675842101927, + "height": 43.2, + "seed": 956358117, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "NFrs7hBfcfS6Sh6nfbpdz", + "type": "arrow" + } + ], + "updated": 1726919381684, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Send/Receive messages\nto/from client", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Send/Receive messages\nto/from client", + "autoResize": false, + "lineHeight": 1.35 + }, + { + "type": "rectangle", + "version": 539, + "versionNonce": 2060753131, + "index": "b1H", + "isDeleted": false, + "id": "ylLtdS1Ye_gyVKSaQJs2x", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1352.407497811686, + "y": 2578.3669517142903, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 104, + "height": 106, + "seed": 2134947467, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "AwYqSeI9CW9aJE0REZarf" + }, + { + "id": "kuMt4oieo-H6xuQKoGSWP", + "type": "arrow" + }, + { + "id": "HQ69vqNQrY84lbpyc-1HF", + "type": "arrow" + }, + { + "id": "o1dloWTz2OdDChjfXO3W8", + "type": "arrow" + }, + { + "id": "iPGwpGNDp9qVzkdoEhrce", + "type": "arrow" + } + ], + "updated": 1726919381684, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 463, + "versionNonce": 1648903051, + "index": "b1I", + "isDeleted": false, + "id": "AwYqSeI9CW9aJE0REZarf", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1385.9158310178627, + "y": 2609.7669517142904, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 36.983333587646484, + "height": 43.2, + "seed": 2009656139, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919381684, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Kill\nSend", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ylLtdS1Ye_gyVKSaQJs2x", + "originalText": "Kill\nSend", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 1732, + "versionNonce": 2037116901, + "index": "b1J", + "isDeleted": false, + "id": "kuMt4oieo-H6xuQKoGSWP", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1220.407497811686, + "y": 2444.3669517142903, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 121, + "height": 158, + "seed": 2107898277, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726919383641, + "link": null, + "locked": false, + "startBinding": { + "elementId": "2-xZdr1N7h94XCAl94juo", + "focus": -0.20528510372146525, + "gap": 6.444444444444343, + "fixedPoint": null + }, + "endBinding": { + "elementId": "ylLtdS1Ye_gyVKSaQJs2x", + "focus": -0.44056326474810314, + "gap": 11, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 121, + 158 + ] + ], + "elbowed": false + }, + { + "type": "arrow", + "version": 1712, + "versionNonce": 2122513061, + "index": "b1K", + "isDeleted": false, + "id": "HQ69vqNQrY84lbpyc-1HF", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1379.407497811686, + "y": 2573.3669517142903, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 120, + "height": 151, + "seed": 76404517, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726919383641, + "link": null, + "locked": false, + "startBinding": { + "elementId": "ylLtdS1Ye_gyVKSaQJs2x", + "focus": 0.22410638896707005, + "gap": 5, + "fixedPoint": null + }, + "endBinding": { + "elementId": "2-xZdr1N7h94XCAl94juo", + "focus": -0.6061165546468019, + "gap": 9.888888888889142, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -120, + -151 + ] + ], + "elbowed": false + }, + { + "type": "arrow", + "version": 1705, + "versionNonce": 1340495205, + "index": "b1L", + "isDeleted": false, + "id": "o1dloWTz2OdDChjfXO3W8", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1436.407497811686, + "y": 2572.3669517142903, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 137, + "height": 138, + "seed": 1651958763, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726919383641, + "link": null, + "locked": false, + "startBinding": { + "elementId": "ylLtdS1Ye_gyVKSaQJs2x", + "focus": -0.2540001385329362, + "gap": 6, + "fixedPoint": null + }, + "endBinding": { + "elementId": "ernpMbdN54sSKmYGoM7nC", + "focus": 0.4420238621299113, + "gap": 9.999999999999773, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 137, + -138 + ] + ], + "elbowed": false + }, + { + "type": "arrow", + "version": 1736, + "versionNonce": 1656881189, + "index": "b1M", + "isDeleted": false, + "id": "iPGwpGNDp9qVzkdoEhrce", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1620.407497811686, + "y": 2445.3669517142903, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 155, + "height": 156, + "seed": 1647368325, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726919383641, + "link": null, + "locked": false, + "startBinding": { + "elementId": "ernpMbdN54sSKmYGoM7nC", + "focus": 0.04434077475452598, + "gap": 7.444444444444798, + "fixedPoint": null + }, + "endBinding": { + "elementId": "ylLtdS1Ye_gyVKSaQJs2x", + "focus": 0.29803393152446866, + "gap": 9, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -155, + 156 + ] + ], + "elbowed": false + }, + { + "type": "text", + "version": 72, + "versionNonce": 102530213, + "index": "b1S", + "isDeleted": false, + "id": "wYibwBpVWhy536011GXFm", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1643.753836412584, + "y": 1878.0368903977403, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 82.21666666666667, + "height": 21.6, + "seed": 195156773, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919402510, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "User15278", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "User15278", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "freedraw", + "version": 330, + "versionNonce": 137244139, + "index": "b1T", + "isDeleted": false, + "id": "xr7MX00xNHXe0CkTbirrn", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1574.802579386489, + "y": 1920.363233792333, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 44.57525777012642, + "height": 73.60100701579006, + "seed": 868744555, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919435707, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.0366339016308526, + 0 + ], + [ + 2.073267803261705, + 0 + ], + [ + 2.5915847540771315, + 0 + ], + [ + 4.14653560652341, + 0 + ], + [ + 5.183169508154263, + -0.5183169508154263 + ], + [ + 7.256437311415969, + -0.5183169508154263 + ], + [ + 7.7747542622313945, + -0.5183169508154263 + ], + [ + 9.329705114677674, + -0.5183169508154263 + ], + [ + 12.439606819570232, + -0.5183169508154263 + ], + [ + 15.031191573647364, + -0.5183169508154263 + ], + [ + 16.58614242609364, + -1.0366339016308526 + ], + [ + 18.65941022935535, + -1.0366339016308526 + ], + [ + 20.214361081801627, + -1.0366339016308526 + ], + [ + 22.28762888506333, + -1.0366339016308526 + ], + [ + 23.324262786694185, + -1.554950852446279 + ], + [ + 24.879213639140463, + -1.554950852446279 + ], + [ + 25.915847540771317, + -1.554950852446279 + ], + [ + 27.470798393217596, + -2.073267803261705 + ], + [ + 27.98911534403302, + -2.073267803261705 + ], + [ + 29.025749245663874, + -2.073267803261705 + ], + [ + 29.5440661964793, + -2.073267803261705 + ], + [ + 30.580700098110153, + -2.073267803261705 + ], + [ + 30.580700098110153, + -2.5915847540771315 + ], + [ + 31.099017048925578, + -2.5915847540771315 + ], + [ + 31.099017048925578, + -2.073267803261705 + ], + [ + 31.099017048925578, + -1.554950852446279 + ], + [ + 31.099017048925578, + -0.5183169508154263 + ], + [ + 31.099017048925578, + 0.5183169508154263 + ], + [ + 31.099017048925578, + 1.0366339016308526 + ], + [ + 31.617333999741007, + 3.109901704892558 + ], + [ + 31.617333999741007, + 4.664852557338837 + ], + [ + 31.617333999741007, + 5.183169508154263 + ], + [ + 32.13565095055643, + 8.29307121304682 + ], + [ + 32.13565095055643, + 10.366339016308526 + ], + [ + 32.65396790137186, + 12.43960681956999 + ], + [ + 32.65396790137186, + 14.512874622831696 + ], + [ + 32.65396790137186, + 16.067825475278216 + ], + [ + 32.65396790137186, + 16.586142426093403 + ], + [ + 32.65396790137186, + 19.177727180170532 + ], + [ + 32.65396790137186, + 20.214361081801385 + ], + [ + 32.65396790137186, + 22.28762888506309 + ], + [ + 32.65396790137186, + 23.324262786693943 + ], + [ + 32.65396790137186, + 25.39753058995565 + ], + [ + 32.65396790137186, + 25.915847540771075 + ], + [ + 32.65396790137186, + 27.98911534403278 + ], + [ + 32.65396790137186, + 28.507432294848208 + ], + [ + 32.65396790137186, + 30.58070009810991 + ], + [ + 32.65396790137186, + 31.09901704892534 + ], + [ + 32.65396790137186, + 33.17228485218704 + ], + [ + 32.65396790137186, + 33.69060180300247 + ], + [ + 32.65396790137186, + 35.24555265544875 + ], + [ + 32.65396790137186, + 36.282186557079605 + ], + [ + 32.13565095055643, + 37.83713740952588 + ], + [ + 32.13565095055643, + 38.87377131115673 + ], + [ + 32.13565095055643, + 40.42872216360301 + ], + [ + 32.13565095055643, + 41.46535606523386 + ], + [ + 31.617333999741007, + 43.020306917680145 + ], + [ + 31.617333999741007, + 44.056940819310995 + ], + [ + 31.617333999741007, + 45.61189167175728 + ], + [ + 31.617333999741007, + 46.64852557338813 + ], + [ + 31.099017048925578, + 48.20347642583441 + ], + [ + 31.099017048925578, + 49.24011032746526 + ], + [ + 30.580700098110153, + 50.79506117991154 + ], + [ + 30.580700098110153, + 51.83169508154239 + ], + [ + 30.062383147294728, + 53.38664593398867 + ], + [ + 29.5440661964793, + 55.459913737250375 + ], + [ + 29.5440661964793, + 55.9782306880658 + ], + [ + 29.025749245663874, + 58.56981544214293 + ], + [ + 28.50743229484845, + 59.08813239295836 + ], + [ + 27.98911534403302, + 61.161400196220065 + ], + [ + 27.98911534403302, + 61.67971714703525 + ], + [ + 27.470798393217596, + 63.752984950296955 + ], + [ + 27.470798393217596, + 64.27130190111262 + ], + [ + 26.95248144240217, + 65.82625275355866 + ], + [ + 26.95248144240217, + 66.34456970437408 + ], + [ + 26.434164491586742, + 66.86288665518951 + ], + [ + 26.434164491586742, + 67.38120360600495 + ], + [ + 26.434164491586742, + 67.89952055682036 + ], + [ + 26.434164491586742, + 68.4178375076358 + ], + [ + 26.434164491586742, + 68.93615445845121 + ], + [ + 26.434164491586742, + 69.45447140926665 + ], + [ + 26.434164491586742, + 69.97278836008208 + ], + [ + 26.434164491586742, + 70.4911053108975 + ], + [ + 26.434164491586742, + 71.00942226171293 + ], + [ + 25.915847540771317, + 71.00942226171293 + ], + [ + 25.39753058995589, + 70.4911053108975 + ], + [ + 24.879213639140463, + 70.4911053108975 + ], + [ + 23.84257973750961, + 70.4911053108975 + ], + [ + 23.324262786694185, + 69.97278836008208 + ], + [ + 21.25099498343248, + 69.97278836008208 + ], + [ + 19.696044130986202, + 69.97278836008208 + ], + [ + 19.177727180170773, + 69.97278836008208 + ], + [ + 16.58614242609364, + 69.45447140926665 + ], + [ + 14.512874622831937, + 69.45447140926665 + ], + [ + 13.476240721201085, + 69.45447140926665 + ], + [ + 10.366339016308526, + 69.45447140926665 + ], + [ + 7.7747542622313945, + 68.93615445845121 + ], + [ + 6.738120360600543, + 68.93615445845121 + ], + [ + 3.6282186557079843, + 68.93615445845121 + ], + [ + 2.073267803261705, + 68.93615445845121 + ], + [ + 1.55495085244652, + 68.93615445845121 + ], + [ + -0.5183169508151851, + 68.93615445845121 + ], + [ + -1.5549508524460378, + 68.93615445845121 + ], + [ + -3.109901704892558, + 69.45447140926665 + ], + [ + -4.14653560652341, + 69.45447140926665 + ], + [ + -5.183169508154263, + 69.97278836008208 + ], + [ + -5.701486458969448, + 69.97278836008208 + ], + [ + -6.738120360600301, + 69.97278836008208 + ], + [ + -7.256437311415969, + 69.97278836008208 + ], + [ + -7.774754262231154, + 70.4911053108975 + ], + [ + -8.29307121304682, + 70.4911053108975 + ], + [ + -8.811388163862006, + 70.4911053108975 + ], + [ + -9.329705114677674, + 70.4911053108975 + ], + [ + -9.84802206549286, + 70.4911053108975 + ], + [ + -10.366339016308286, + 70.4911053108975 + ], + [ + -10.884655967123711, + 70.4911053108975 + ], + [ + -11.402972917939138, + 70.4911053108975 + ], + [ + -11.921289868754565, + 70.4911053108975 + ], + [ + -11.921289868754565, + 69.97278836008208 + ], + [ + -11.921289868754565, + 69.45447140926665 + ], + [ + -11.921289868754565, + 68.4178375076358 + ], + [ + -11.921289868754565, + 67.89952055682036 + ], + [ + -11.921289868754565, + 66.86288665518951 + ], + [ + -11.402972917939138, + 65.82625275355866 + ], + [ + -11.402972917939138, + 65.30793580274347 + ], + [ + -11.402972917939138, + 64.27130190111262 + ], + [ + -10.884655967123711, + 62.716351048666105 + ], + [ + -10.884655967123711, + 62.198034097850915 + ], + [ + -10.366339016308286, + 60.6430832454044 + ], + [ + -10.366339016308286, + 59.60644934377379 + ], + [ + -9.84802206549286, + 58.05149849132751 + ], + [ + -9.84802206549286, + 57.01486458969666 + ], + [ + -9.84802206549286, + 54.94159678643495 + ], + [ + -9.84802206549286, + 53.38664593398867 + ], + [ + -9.84802206549286, + 52.35001203235782 + ], + [ + -9.84802206549286, + 50.79506117991154 + ], + [ + -9.84802206549286, + 50.27674422909611 + ], + [ + -9.84802206549286, + 48.721793376649835 + ], + [ + -9.84802206549286, + 47.68515947501898 + ], + [ + -9.84802206549286, + 46.64852557338813 + ], + [ + -9.84802206549286, + 46.1302086225727 + ], + [ + -9.84802206549286, + 44.57525777012642 + ], + [ + -9.84802206549286, + 44.056940819310995 + ], + [ + -9.84802206549286, + 42.50198996686472 + ], + [ + -9.84802206549286, + 41.983673016049295 + ], + [ + -9.84802206549286, + 40.42872216360301 + ], + [ + -9.84802206549286, + 39.91040521278759 + ], + [ + -9.84802206549286, + 38.87377131115673 + ], + [ + -9.84802206549286, + 38.355454360341305 + ], + [ + -9.84802206549286, + 36.80050350789503 + ], + [ + -9.84802206549286, + 36.282186557079605 + ], + [ + -9.84802206549286, + 35.24555265544875 + ], + [ + -9.84802206549286, + 34.72723570463332 + ], + [ + -9.84802206549286, + 33.69060180300247 + ], + [ + -9.84802206549286, + 33.17228485218704 + ], + [ + -9.84802206549286, + 31.617333999740765 + ], + [ + -9.84802206549286, + 30.58070009810991 + ], + [ + -9.84802206549286, + 30.062383147294486 + ], + [ + -9.84802206549286, + 28.507432294848208 + ], + [ + -9.84802206549286, + 27.98911534403278 + ], + [ + -9.84802206549286, + 26.4341644915865 + ], + [ + -9.84802206549286, + 25.915847540771075 + ], + [ + -9.84802206549286, + 24.360896688324797 + ], + [ + -9.84802206549286, + 23.84257973750937 + ], + [ + -9.84802206549286, + 22.28762888506309 + ], + [ + -9.84802206549286, + 21.769311934247664 + ], + [ + -9.84802206549286, + 20.214361081801385 + ], + [ + -9.84802206549286, + 19.177727180170532 + ], + [ + -9.84802206549286, + 18.14109327853968 + ], + [ + -9.84802206549286, + 17.622776327724253 + ], + [ + -9.84802206549286, + 16.586142426093403 + ], + [ + -9.84802206549286, + 16.067825475278216 + ], + [ + -9.84802206549286, + 15.031191573647364 + ], + [ + -9.84802206549286, + 14.512874622831696 + ], + [ + -9.84802206549286, + 13.476240721200844 + ], + [ + -9.84802206549286, + 12.43960681956999 + ], + [ + -9.84802206549286, + 11.921289868754805 + ], + [ + -9.84802206549286, + 10.366339016308526 + ], + [ + -9.84802206549286, + 9.329705114677674 + ], + [ + -9.84802206549286, + 7.7747542622313945 + ], + [ + -9.84802206549286, + 7.256437311415969 + ], + [ + -9.84802206549286, + 6.219803409785116 + ], + [ + -9.84802206549286, + 5.70148645896969 + ], + [ + -9.84802206549286, + 5.183169508154263 + ], + [ + -9.84802206549286, + 4.664852557338837 + ], + [ + -9.84802206549286, + 4.14653560652341 + ], + [ + -9.84802206549286, + 3.6282186557079843 + ], + [ + -9.84802206549286, + 3.109901704892558 + ], + [ + -9.84802206549286, + 2.5915847540771315 + ], + [ + -9.84802206549286, + 2.073267803261705 + ], + [ + -9.329705114677674, + 2.073267803261705 + ], + [ + -8.811388163862006, + 2.073267803261705 + ], + [ + -8.29307121304682, + 2.073267803261705 + ], + [ + -7.774754262231154, + 2.073267803261705 + ], + [ + -7.256437311415969, + 2.073267803261705 + ], + [ + -6.219803409785116, + 2.073267803261705 + ], + [ + -5.183169508154263, + 2.073267803261705 + ], + [ + -4.14653560652341, + 2.073267803261705 + ], + [ + -3.109901704892558, + 2.073267803261705 + ], + [ + -2.073267803261705, + 2.073267803261705 + ], + [ + -1.5549508524460378, + 2.073267803261705 + ], + [ + -1.0366339016308526, + 2.073267803261705 + ], + [ + -0.5183169508151851, + 2.073267803261705 + ], + [ + 0, + 2.073267803261705 + ], + [ + 0.5183169508156675, + 2.073267803261705 + ], + [ + 1.0366339016308526, + 1.554950852446279 + ], + [ + 1.55495085244652, + 1.554950852446279 + ], + [ + 2.073267803261705, + 1.554950852446279 + ], + [ + 2.5915847540771315, + 1.554950852446279 + ], + [ + 3.109901704892558, + 1.554950852446279 + ], + [ + 3.6282186557079843, + 1.554950852446279 + ], + [ + 4.14653560652341, + 1.554950852446279 + ], + [ + 4.664852557338837, + 1.554950852446279 + ], + [ + 5.183169508154263, + 1.554950852446279 + ], + [ + 5.183169508154263, + 1.554950852446279 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 130, + "versionNonce": 430389387, + "index": "b1U", + "isDeleted": false, + "id": "FUyJxHFcU72z5lvGW0Fr8", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1583.0956505995357, + "y": 1928.6563050053799, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 8.29307121304682, + "height": 0, + "seed": 1730695723, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919435707, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.5183169508154263, + 0 + ], + [ + 1.0366339016308526, + 0 + ], + [ + 1.554950852446279, + 0 + ], + [ + 2.073267803261705, + 0 + ], + [ + 2.5915847540771315, + 0 + ], + [ + 3.109901704892558, + 0 + ], + [ + 4.14653560652341, + 0 + ], + [ + 5.183169508154263, + 0 + ], + [ + 5.70148645896969, + 0 + ], + [ + 6.219803409785116, + 0 + ], + [ + 6.738120360600543, + 0 + ], + [ + 7.256437311415969, + 0 + ], + [ + 7.7747542622313945, + 0 + ], + [ + 8.29307121304682, + 0 + ], + [ + 8.29307121304682, + 0 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 182, + "versionNonce": 954593067, + "index": "b1V", + "isDeleted": false, + "id": "DMCE9Mbx4X6Tft1CScGCp", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1583.6139675503512, + "y": 1982.0429509393682, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 3.109901704892558, + "height": 4.6648525573385955, + "seed": 467249259, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919435707, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.5183169508154263, + 0 + ], + [ + -0.5183169508154263, + 0.5183169508156675 + ], + [ + -0.5183169508154263, + 1.0366339016308526 + ], + [ + 0, + 1.0366339016308526 + ], + [ + 0.5183169508154263, + 1.0366339016308526 + ], + [ + 0.5183169508154263, + 0.5183169508156675 + ], + [ + 1.0366339016308526, + 0.5183169508156675 + ], + [ + 1.0366339016308526, + 0 + ], + [ + 1.0366339016308526, + -0.5183169508151851 + ], + [ + 1.0366339016308526, + -1.0366339016308526 + ], + [ + 0.5183169508154263, + -1.0366339016308526 + ], + [ + 0.5183169508154263, + -1.5549508524460378 + ], + [ + 0, + -1.5549508524460378 + ], + [ + -0.5183169508154263, + -1.5549508524460378 + ], + [ + -1.0366339016308526, + -1.5549508524460378 + ], + [ + -1.0366339016308526, + -1.0366339016308526 + ], + [ + -0.5183169508154263, + -1.0366339016308526 + ], + [ + -0.5183169508154263, + -0.5183169508151851 + ], + [ + 0, + -0.5183169508151851 + ], + [ + 0, + 0 + ], + [ + 0.5183169508154263, + 0 + ], + [ + 1.0366339016308526, + 0 + ], + [ + 1.554950852446279, + 0 + ], + [ + 2.073267803261705, + -0.5183169508151851 + ], + [ + 2.073267803261705, + -1.0366339016308526 + ], + [ + 2.073267803261705, + -1.5549508524460378 + ], + [ + 2.073267803261705, + -2.073267803261464 + ], + [ + 2.073267803261705, + -2.5915847540768904 + ], + [ + 2.073267803261705, + -3.1099017048923168 + ], + [ + 2.073267803261705, + -3.628218655707743 + ], + [ + 1.554950852446279, + -3.628218655707743 + ], + [ + 1.0366339016308526, + -3.628218655707743 + ], + [ + 0.5183169508154263, + -3.628218655707743 + ], + [ + 0.5183169508154263, + -3.1099017048923168 + ], + [ + 0.5183169508154263, + -2.5915847540768904 + ], + [ + 0, + -2.073267803261464 + ], + [ + 0, + -1.5549508524460378 + ], + [ + 0, + -1.0366339016308526 + ], + [ + 0, + -0.5183169508151851 + ], + [ + 0.5183169508154263, + -0.5183169508151851 + ], + [ + 1.0366339016308526, + -0.5183169508151851 + ], + [ + 1.0366339016308526, + 0 + ], + [ + 1.554950852446279, + 0 + ], + [ + 2.073267803261705, + 0 + ], + [ + 2.073267803261705, + -0.5183169508151851 + ], + [ + 2.073267803261705, + -1.0366339016308526 + ], + [ + 2.073267803261705, + -1.5549508524460378 + ], + [ + 2.073267803261705, + -2.073267803261464 + ], + [ + 2.073267803261705, + -2.5915847540768904 + ], + [ + 2.073267803261705, + -3.1099017048923168 + ], + [ + 1.554950852446279, + -3.1099017048923168 + ], + [ + 1.0366339016308526, + -3.1099017048923168 + ], + [ + 0.5183169508154263, + -3.1099017048923168 + ], + [ + 0, + -3.1099017048923168 + ], + [ + -0.5183169508154263, + -3.1099017048923168 + ], + [ + -0.5183169508154263, + -2.5915847540768904 + ], + [ + -0.5183169508154263, + -2.073267803261464 + ], + [ + -0.5183169508154263, + -1.5549508524460378 + ], + [ + -0.5183169508154263, + -1.0366339016308526 + ], + [ + 0, + -0.5183169508151851 + ], + [ + 0.5183169508154263, + -0.5183169508151851 + ], + [ + 1.0366339016308526, + -0.5183169508151851 + ], + [ + 1.554950852446279, + -0.5183169508151851 + ], + [ + 1.554950852446279, + -1.0366339016308526 + ], + [ + 1.0366339016308526, + -1.0366339016308526 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 332, + "versionNonce": 906810699, + "index": "b1W", + "isDeleted": false, + "id": "DJPhr21pzK_fIqc9WFQoG", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1799.5151911485816, + "y": 1970.0949899612303, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 60.10995723885139, + "height": 48.38118509468541, + "seed": 909887813, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919431443, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.48869883934025893 + ], + [ + 0, + 1.4660965180207768 + ], + [ + 0, + 1.9547953573610357 + ], + [ + 0, + 2.4434941967012946 + ], + [ + 0, + 3.4208918753818125 + ], + [ + 0, + 4.39828955406233 + ], + [ + -0.48869883934025893, + 5.864386072083107 + ], + [ + -0.48869883934025893, + 6.353084911423366 + ], + [ + -0.48869883934025893, + 7.819181429444143 + ], + [ + -0.48869883934025893, + 8.307880268784402 + ], + [ + -0.48869883934025893, + 9.773976786805179 + ], + [ + -0.48869883934025893, + 10.262675626145437 + ], + [ + -0.48869883934025893, + 11.728772144166214 + ], + [ + 0, + 12.217470983506246 + ], + [ + 0, + 13.68356750152725 + ], + [ + 0.9773976786805179, + 14.660965180207768 + ], + [ + 0.9773976786805179, + 15.1496640195478 + ], + [ + 1.4660965180207768, + 15.638362858888286 + ], + [ + 1.9547953573610357, + 15.638362858888286 + ], + [ + 2.4434941967012946, + 15.638362858888286 + ], + [ + 2.9321930360415536, + 15.638362858888286 + ], + [ + 3.4208918753818125, + 15.638362858888286 + ], + [ + 3.9095907147220714, + 15.1496640195478 + ], + [ + 4.886988393402589, + 15.1496640195478 + ], + [ + 5.375687232742848, + 15.1496640195478 + ], + [ + 6.353084911423366, + 15.638362858888286 + ], + [ + 7.330482590103884, + 15.638362858888286 + ], + [ + 8.796579108124433, + 15.638362858888286 + ], + [ + 9.28527794746492, + 15.638362858888286 + ], + [ + 11.240073304825955, + 16.127061698228317 + ], + [ + 12.217470983506473, + 16.127061698228317 + ], + [ + 14.66096518020754, + 16.127061698228317 + ], + [ + 15.638362858888058, + 16.127061698228317 + ], + [ + 17.593158216249094, + 16.127061698228317 + ], + [ + 19.54795357361013, + 16.127061698228317 + ], + [ + 20.525351252290648, + 16.127061698228317 + ], + [ + 22.480146609651683, + 16.127061698228317 + ], + [ + 23.4575442883322, + 15.638362858888286 + ], + [ + 24.923640806352978, + 15.638362858888286 + ], + [ + 25.901038485033496, + 15.638362858888286 + ], + [ + 27.367135003054273, + 15.1496640195478 + ], + [ + 28.34453268173479, + 15.1496640195478 + ], + [ + 29.32193036041531, + 14.660965180207768 + ], + [ + 30.299328039095826, + 14.660965180207768 + ], + [ + 31.276725717776344, + 14.172266340867282 + ], + [ + 31.765424557116603, + 14.172266340867282 + ], + [ + 32.25412339645686, + 14.172266340867282 + ], + [ + 32.25412339645686, + 13.68356750152725 + ], + [ + 32.74282223579712, + 13.194868662186764 + ], + [ + 32.74282223579712, + 12.706169822846732 + ], + [ + 32.74282223579712, + 12.217470983506246 + ], + [ + 33.23152107513738, + 11.728772144166214 + ], + [ + 33.23152107513738, + 11.240073304825955 + ], + [ + 33.72021991447764, + 10.262675626145437 + ], + [ + 33.72021991447764, + 9.28527794746492 + ], + [ + 34.2089187538179, + 8.307880268784402 + ], + [ + 34.2089187538179, + 7.330482590103884 + ], + [ + 35.186316432498415, + 5.375687232742848 + ], + [ + 35.186316432498415, + 4.39828955406233 + ], + [ + 35.675015271838674, + 2.9321930360415536 + ], + [ + 35.675015271838674, + 1.4660965180207768 + ], + [ + 36.16371411117893, + 0 + ], + [ + 36.16371411117893, + -0.9773976786805179 + ], + [ + 36.16371411117893, + -1.9547953573610357 + ], + [ + 36.16371411117893, + -2.9321930360415536 + ], + [ + 36.16371411117893, + -3.9095907147220714 + ], + [ + 36.16371411117893, + -4.39828955406233 + ], + [ + 36.16371411117893, + -5.375687232742848 + ], + [ + 36.16371411117893, + -5.864386072083107 + ], + [ + 36.16371411117893, + -6.353084911423366 + ], + [ + 35.675015271838674, + -6.353084911423366 + ], + [ + 35.186316432498415, + -6.353084911423366 + ], + [ + 34.2089187538179, + -6.353084911423366 + ], + [ + 33.72021991447764, + -6.353084911423366 + ], + [ + 33.23152107513738, + -6.353084911423366 + ], + [ + 31.765424557116603, + -6.353084911423366 + ], + [ + 30.299328039095826, + -5.864386072083107 + ], + [ + 29.810629199755567, + -5.864386072083107 + ], + [ + 27.85583384239453, + -5.864386072083107 + ], + [ + 26.878436163714014, + -5.864386072083107 + ], + [ + 24.43494196701272, + -5.864386072083107 + ], + [ + 23.4575442883322, + -5.864386072083107 + ], + [ + 21.014050091630907, + -5.375687232742848 + ], + [ + 20.03665241295039, + -5.375687232742848 + ], + [ + 17.593158216249094, + -5.375687232742848 + ], + [ + 16.615760537568576, + -5.375687232742848 + ], + [ + 14.172266340867509, + -4.886988393402589 + ], + [ + 12.706169822846505, + -4.886988393402589 + ], + [ + 10.262675626145437, + -4.886988393402589 + ], + [ + 9.28527794746492, + -4.39828955406233 + ], + [ + 7.330482590103884, + -4.39828955406233 + ], + [ + 6.353084911423366, + -4.39828955406233 + ], + [ + 4.39828955406233, + -3.9095907147220714 + ], + [ + 3.9095907147220714, + -3.9095907147220714 + ], + [ + 2.4434941967012946, + -3.9095907147220714 + ], + [ + 1.9547953573610357, + -3.9095907147220714 + ], + [ + 0.48869883934025893, + -3.9095907147220714 + ], + [ + 0, + -3.4208918753818125 + ], + [ + -0.48869883934025893, + -3.4208918753818125 + ], + [ + -0.9773976786805179, + -3.4208918753818125 + ], + [ + -0.9773976786805179, + -3.9095907147220714 + ], + [ + -0.9773976786805179, + -4.39828955406233 + ], + [ + -0.9773976786805179, + -4.886988393402589 + ], + [ + -0.48869883934025893, + -5.375687232742848 + ], + [ + 0, + -8.307880268784402 + ], + [ + 0.48869883934025893, + -8.79657910812466 + ], + [ + 0.48869883934025893, + -9.773976786805179 + ], + [ + 0.9773976786805179, + -10.262675626145437 + ], + [ + 1.4660965180207768, + -11.240073304825955 + ], + [ + 1.9547953573610357, + -12.706169822846732 + ], + [ + 2.4434941967012946, + -13.194868662186991 + ], + [ + 2.4434941967012946, + -13.68356750152725 + ], + [ + 3.4208918753818125, + -14.660965180207768 + ], + [ + 3.4208918753818125, + -15.149664019548027 + ], + [ + 3.9095907147220714, + -16.127061698228545 + ], + [ + 4.39828955406233, + -16.615760537568804 + ], + [ + 4.886988393402589, + -17.59315821624932 + ], + [ + 5.375687232742848, + -18.57055589492984 + ], + [ + 5.864386072083107, + -19.059254734270098 + ], + [ + 6.353084911423366, + -19.547953573610357 + ], + [ + 6.353084911423366, + -20.036652412950616 + ], + [ + 6.841783750763625, + -20.525351252290875 + ], + [ + 7.330482590103884, + -21.014050091631134 + ], + [ + 7.819181429444143, + -21.502748930971393 + ], + [ + 7.819181429444143, + -21.99144777031165 + ], + [ + 8.307880268784402, + -22.48014660965191 + ], + [ + 8.796579108124433, + -22.96884544899217 + ], + [ + 9.28527794746492, + -23.45754428833243 + ], + [ + 9.28527794746492, + -23.946243127672687 + ], + [ + 10.262675626145437, + -24.434941967012946 + ], + [ + 10.262675626145437, + -24.923640806353205 + ], + [ + 10.751374465485469, + -24.923640806353205 + ], + [ + 10.751374465485469, + -25.412339645693464 + ], + [ + 11.240073304825955, + -25.412339645693464 + ], + [ + 11.240073304825955, + -25.901038485033723 + ], + [ + 11.728772144165987, + -26.389737324373982 + ], + [ + 11.728772144165987, + -26.87843616371424 + ], + [ + 12.217470983506473, + -26.87843616371424 + ], + [ + 12.217470983506473, + -27.3671350030545 + ], + [ + 12.706169822846505, + -27.85583384239476 + ], + [ + 13.194868662186991, + -28.344532681735018 + ], + [ + 13.683567501527023, + -28.833231521075277 + ], + [ + 14.172266340867509, + -28.833231521075277 + ], + [ + 14.172266340867509, + -29.321930360415536 + ], + [ + 14.66096518020754, + -29.321930360415536 + ], + [ + 14.66096518020754, + -29.810629199755567 + ], + [ + 15.149664019548027, + -30.299328039096054 + ], + [ + 15.149664019548027, + -30.788026878436085 + ], + [ + 15.638362858888058, + -30.788026878436085 + ], + [ + 15.638362858888058, + -31.27672571777657 + ], + [ + 16.127061698228545, + -31.765424557116603 + ], + [ + 16.127061698228545, + -32.25412339645709 + ], + [ + 16.615760537568576, + -32.25412339645709 + ], + [ + 17.104459376909062, + -31.765424557116603 + ], + [ + 17.593158216249094, + -31.765424557116603 + ], + [ + 18.08185705558958, + -31.27672571777657 + ], + [ + 19.059254734270098, + -31.27672571777657 + ], + [ + 19.54795357361013, + -31.27672571777657 + ], + [ + 21.014050091630907, + -30.788026878436085 + ], + [ + 21.502748930971165, + -30.299328039096054 + ], + [ + 22.968845448991942, + -30.299328039096054 + ], + [ + 23.94624312767246, + -29.810629199755567 + ], + [ + 25.412339645693237, + -29.810629199755567 + ], + [ + 25.901038485033496, + -29.321930360415536 + ], + [ + 27.367135003054273, + -29.321930360415536 + ], + [ + 27.85583384239453, + -29.321930360415536 + ], + [ + 29.32193036041531, + -28.833231521075277 + ], + [ + 29.810629199755567, + -28.833231521075277 + ], + [ + 30.788026878436085, + -28.344532681735018 + ], + [ + 31.765424557116603, + -28.344532681735018 + ], + [ + 32.74282223579712, + -27.85583384239476 + ], + [ + 33.23152107513738, + -27.85583384239476 + ], + [ + 34.2089187538179, + -27.3671350030545 + ], + [ + 35.186316432498415, + -27.3671350030545 + ], + [ + 36.16371411117893, + -27.3671350030545 + ], + [ + 36.65241295051919, + -27.3671350030545 + ], + [ + 37.62981062919971, + -26.87843616371424 + ], + [ + 38.11850946853997, + -26.87843616371424 + ], + [ + 39.09590714722049, + -26.87843616371424 + ], + [ + 39.584605986560746, + -26.389737324373982 + ], + [ + 40.562003665241264, + -26.389737324373982 + ], + [ + 41.05070250458152, + -25.901038485033723 + ], + [ + 42.02810018326204, + -25.901038485033723 + ], + [ + 43.00549786194256, + -25.901038485033723 + ], + [ + 43.49419670128282, + -25.412339645693464 + ], + [ + 44.471594379963335, + -25.412339645693464 + ], + [ + 45.44899205864385, + -25.412339645693464 + ], + [ + 46.42638973732437, + -25.412339645693464 + ], + [ + 46.91508857666463, + -25.412339645693464 + ], + [ + 47.40378741600489, + -24.923640806353205 + ], + [ + 47.89248625534515, + -24.923640806353205 + ], + [ + 48.38118509468541, + -24.923640806353205 + ], + [ + 48.869883934025665, + -24.923640806353205 + ], + [ + 49.358582773365924, + -24.923640806353205 + ], + [ + 49.84728161270618, + -24.923640806353205 + ], + [ + 50.8246792913867, + -24.434941967012946 + ], + [ + 51.31337813072696, + -24.434941967012946 + ], + [ + 51.80207697006722, + -24.434941967012946 + ], + [ + 52.29077580940748, + -24.434941967012946 + ], + [ + 53.268173488087996, + -24.434941967012946 + ], + [ + 54.245571166768514, + -23.946243127672687 + ], + [ + 54.73427000610877, + -23.946243127672687 + ], + [ + 55.222968845448804, + -23.946243127672687 + ], + [ + 56.20036652412932, + -23.946243127672687 + ], + [ + 56.68906536346981, + -23.946243127672687 + ], + [ + 57.17776420280984, + -23.946243127672687 + ], + [ + 57.17776420280984, + -23.45754428833243 + ], + [ + 58.15516188149036, + -23.45754428833243 + ], + [ + 58.643860720830844, + -23.45754428833243 + ], + [ + 59.132559560170876, + -23.45754428833243 + ], + [ + 58.643860720830844, + -23.45754428833243 + ], + [ + 58.643860720830844, + -22.96884544899217 + ], + [ + 58.15516188149036, + -22.96884544899217 + ], + [ + 57.666463042150326, + -22.48014660965191 + ], + [ + 57.666463042150326, + -21.99144777031165 + ], + [ + 57.17776420280984, + -21.502748930971393 + ], + [ + 56.68906536346981, + -20.525351252290875 + ], + [ + 56.20036652412932, + -20.525351252290875 + ], + [ + 55.222968845448804, + -19.547953573610357 + ], + [ + 54.73427000610877, + -18.57055589492984 + ], + [ + 54.73427000610877, + -18.08185705558958 + ], + [ + 53.756872327428255, + -17.104459376909062 + ], + [ + 52.77947464874774, + -16.127061698228545 + ], + [ + 52.77947464874774, + -15.638362858888286 + ], + [ + 51.31337813072696, + -14.660965180207768 + ], + [ + 50.33598045204644, + -13.68356750152725 + ], + [ + 49.358582773365924, + -13.194868662186991 + ], + [ + 48.38118509468541, + -12.217470983506473 + ], + [ + 47.89248625534515, + -12.217470983506473 + ], + [ + 47.40378741600489, + -11.728772144166214 + ], + [ + 46.91508857666463, + -11.240073304825955 + ], + [ + 46.42638973732437, + -11.240073304825955 + ], + [ + 45.93769089798411, + -10.751374465485696 + ], + [ + 45.44899205864385, + -10.262675626145437 + ], + [ + 44.960293219303594, + -10.262675626145437 + ], + [ + 44.960293219303594, + -9.773976786805179 + ], + [ + 44.471594379963335, + -9.773976786805179 + ], + [ + 44.471594379963335, + -9.28527794746492 + ], + [ + 43.982895540623076, + -9.28527794746492 + ], + [ + 43.49419670128282, + -8.79657910812466 + ], + [ + 43.00549786194256, + -8.79657910812466 + ], + [ + 42.5167990226023, + -8.307880268784402 + ], + [ + 42.02810018326204, + -8.307880268784402 + ], + [ + 41.05070250458152, + -8.307880268784402 + ], + [ + 41.05070250458152, + -7.819181429444143 + ], + [ + 40.562003665241264, + -7.819181429444143 + ], + [ + 40.073304825901005, + -7.330482590103884 + ], + [ + 39.584605986560746, + -7.330482590103884 + ], + [ + 39.584605986560746, + -7.330482590103884 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "text", + "version": 167, + "versionNonce": 861870437, + "index": "b1X", + "isDeleted": false, + "id": "yrMLMaP8hB6ezngk1Y_H9", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1556.1431691571338, + "y": 1998.629093365462, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 49.282828282828305, + "height": 22.90909090909092, + "seed": 1505590757, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "ldtBH_Mc3-DYjw69pcUaV", + "type": "arrow" + }, + { + "id": "NFrs7hBfcfS6Sh6nfbpdz", + "type": "arrow" + } + ], + "updated": 1726919457020, + "link": null, + "locked": false, + "fontSize": 16.969696969696976, + "fontFamily": 6, + "text": "Phone", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Phone", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "freedraw", + "version": 3, + "versionNonce": 475272267, + "index": "b1Y", + "isDeleted": false, + "id": "XimpoObq-XuIb3zN3NNFU", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1806.8456737386853, + "y": 1973.027182997272, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.0001, + "height": 0.0001, + "seed": 1197354789, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919438747, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0001, + 0.0001 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 3, + "versionNonce": 687068203, + "index": "b1Z", + "isDeleted": false, + "id": "zuRV8Tn6LPPv7A81dIzsq", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1819.0631447221917, + "y": 1972.5384841579316, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.0001, + "height": 0.0001, + "seed": 950134085, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919439008, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0001, + 0.0001 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 3, + "versionNonce": 471408651, + "index": "b1a", + "isDeleted": false, + "id": "mekZs7jnqYFc73V7hkrLW", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1825.9049284729554, + "y": 1973.5158818366122, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.0001, + "height": 0.0001, + "seed": 1716244325, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919439266, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0001, + 0.0001 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 5, + "versionNonce": 152321957, + "index": "b1b", + "isDeleted": false, + "id": "-OetnfO6zwcYatX9RedPR", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1821.9953377582333, + "y": 1976.4480748726537, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.48869883934025893, + "height": 0, + "seed": 270036357, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919439499, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.48869883934025893, + 0 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 3, + "versionNonce": 966510021, + "index": "b1c", + "isDeleted": false, + "id": "QC0MhalhM79LOlnEDdfkD", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1812.221360971428, + "y": 1977.4254725513342, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.0001, + "height": 0.0001, + "seed": 1427266507, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919439722, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0001, + 0.0001 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 6, + "versionNonce": 1003391653, + "index": "b1d", + "isDeleted": false, + "id": "jcb9CJs3vxx1fCi0elsOc", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1807.8230714173658, + "y": 1975.4706771939732, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.48869883934025893, + "height": 0.48869883934025893, + "seed": 1504529323, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919439911, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.48869883934025893 + ], + [ + 0.48869883934025893, + -0.48869883934025893 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 3, + "versionNonce": 97906885, + "index": "b1e", + "isDeleted": false, + "id": "qkYTBxkCkU0SMHVRnt2AI", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1814.1761563287891, + "y": 1972.5384841579316, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.0001, + "height": 0.0001, + "seed": 1450287307, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919440237, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0001, + 0.0001 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 3, + "versionNonce": 614566629, + "index": "b1f", + "isDeleted": false, + "id": "4S-mv7-OXX3OuNH_9Maez", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1817.597048204171, + "y": 1978.891569069355, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.0001, + "height": 0.0001, + "seed": 1060548779, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919440452, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0001, + 0.0001 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 5, + "versionNonce": 322106475, + "index": "b1g", + "isDeleted": false, + "id": "1vZ2rq_zh7EfpffanBaa5", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1826.3936273122956, + "y": 1979.8689667480355, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.48869883934025893, + "height": 0, + "seed": 1388530827, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919440693, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.48869883934025893, + 0 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 3, + "versionNonce": 508635211, + "index": "b1h", + "isDeleted": false, + "id": "ImFxwNd-hSAqvwnty7ADb", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1808.311770256706, + "y": 1978.891569069355, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0.0001, + "height": 0.0001, + "seed": 2110216997, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919441334, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.0001, + 0.0001 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "text", + "version": 54, + "versionNonce": 1133127467, + "index": "b1i", + "isDeleted": false, + "id": "8_s9rHl1BqFvYQf9Y5qMM", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1789.252515522436, + "y": 1994.529931928243, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 51.233333333333334, + "height": 21.6, + "seed": 84578629, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "id": "QM1nGdcAGkuba_LeIPPGs", + "type": "arrow" + }, + { + "id": "qUGaP5OV93rxoqqy7V6Au", + "type": "arrow" + } + ], + "updated": 1726919502507, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Laptop", + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Laptop", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "rectangle", + "version": 978, + "versionNonce": 894425867, + "index": "b1j", + "isDeleted": false, + "id": "IqZ18gef20BdSUYa6vLD0", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1997.130798574223, + "y": 2303.2982152862746, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 778.8793335223208, + "height": 431.6222461467981, + "seed": 1397298187, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "MkMYxixZDt1Vj-hKG-YBB" + } + ], + "updated": 1726919487796, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 774, + "versionNonce": 1280721323, + "index": "b1k", + "isDeleted": false, + "id": "MkMYxixZDt1Vj-hKG-YBB", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2316.5121309847973, + "y": 2308.2982152862746, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 140.11666870117188, + "height": 21.6, + "seed": 683694763, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1726919487796, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Client-specific Task", + "textAlign": "center", + "verticalAlign": "top", + "containerId": "IqZ18gef20BdSUYa6vLD0", + "originalText": "Client-specific Task", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "rectangle", + "version": 798, + "versionNonce": 570726507, + "index": "b1l", + "isDeleted": false, + "id": "NEKEWaqq5lWTHOkiGx6DU", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2005.1483940010205, + "y": 2342.603734708385, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 196.66666666666652, + "height": 95.55555555555566, + "seed": 1758842187, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "0-FTztADtIwv8vL54K2VN" + }, + { + "id": "C0S_ZOuxsKKIkAwQJ4sZv", + "type": "arrow" + }, + { + "id": "JofU-D5K9-E52PJsrbTe3", + "type": "arrow" + }, + { + "id": "cyaBPg8_R5_8xioLhaPje", + "type": "arrow" + }, + { + "id": "AJNR16UTCUbXwxmFRId6d", + "type": "arrow" + }, + { + "id": "QM1nGdcAGkuba_LeIPPGs", + "type": "arrow" + }, + { + "id": "qUGaP5OV93rxoqqy7V6Au", + "type": "arrow" + } + ], + "updated": 1726919502507, "link": null, - "locked": false, - "fontSize": 20, - "fontFamily": 5, - "text": "Goals in Mind:\n\n- Support multiple chats opened at the \nsame time\n- Send the least amount of messages \nrequired without sacrificing UX\n- Do not modify Discord API/WS behaviour", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "Goals in Mind:\n\n- Support multiple chats opened at the same time\n- Send the least amount of messages required without sacrificing UX\n- Do not modify Discord API/WS behaviour", - "autoResize": false, - "lineHeight": 1.25 + "locked": false }, { "type": "text", - "version": 1239, - "versionNonce": 977772244, - "index": "aN", + "version": 764, + "versionNonce": 1567363819, + "index": "b1m", "isDeleted": false, - "id": "KUumgEMrgSNz1yHP1SAwp", + "id": "0-FTztADtIwv8vL54K2VN", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 359, - "y": 987, + "x": 2052.773395272586, + "y": 2379.581512486163, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 420.6236847441232, - "height": 550, - "seed": 1310238883, + "width": 101.41666412353516, + "height": 21.6, + "seed": 1994648555, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726599088418, + "updated": 1726919487796, "link": null, "locked": false, - "fontSize": 20, - "fontFamily": 5, - "text": "Idea:\n- Always send events from the latest \nchannel scope that has been accessed. \nWhen a user clicks on a channel, they will \nsend a get messages request to the \nchannel. We will use that request to \ndetermine which channel the user is \nviewing.\n- Send events from all other channels the \nuser has accessed via HTTP API in the \nlast XY seconds. When the user accesses a\nnew channel, the \"old\" channel is pushed \nback into a sort of queue, where after a \nset amount of time, the user will be \nunsubscribed from these channels' \n\"detailed events\". The unsubscribing can be\nstopped if the user sends a WS payload \nwhich describes the intent to keep \nreceiving events from that channel. This \nway, one can view multiple channels at the \nsame time using custom clients, while \nsingle-view clients are also fully supported.", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "Idea:\n- Always send events from the latest channel scope that has been accessed. When a user clicks on a channel, they will send a get messages request to the channel. We will use that request to determine which channel the user is viewing.\n- Send events from all other channels the user has accessed via HTTP API in the last XY seconds. When the user accesses a new channel, the \"old\" channel is pushed back into a sort of queue, where after a set amount of time, the user will be unsubscribed from these channels' \"detailed events\". The unsubscribing can be stopped if the user sends a WS payload which describes the intent to keep receiving events from that channel. This way, one can view multiple channels at the same time using custom clients, while single-view clients are also fully supported.", - "autoResize": false, - "lineHeight": 1.25 + "fontSize": 16, + "fontFamily": 6, + "text": "Gateway Task", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "NEKEWaqq5lWTHOkiGx6DU", + "originalText": "Gateway Task", + "autoResize": true, + "lineHeight": 1.35 }, { "type": "rectangle", - "version": 124, - "versionNonce": 564583892, - "index": "aO", + "version": 1003, + "versionNonce": 1305206795, + "index": "b1n", "isDeleted": false, - "id": "2sjonxYVzFv7deYkoQoX5", + "id": "NK9bIOXHnIk_mQH6hQ-us", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1259, - "y": 460, + "x": 2535.703949556576, + "y": 2342.603734708385, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 214, - "height": 74, - "seed": 1759587, + "width": 196.66666666666652, + "height": 95.55555555555566, + "seed": 1727346315, "groupIds": [], "frameId": null, - "roundness": null, + "roundness": { + "type": 3 + }, "boundElements": [ { "type": "text", - "id": "fq34t956YSgc0zsG_zZeu" + "id": "bBx085lHdLn5JljxcmBw5" + }, + { + "id": "C0S_ZOuxsKKIkAwQJ4sZv", + "type": "arrow" + }, + { + "id": "JofU-D5K9-E52PJsrbTe3", + "type": "arrow" + }, + { + "id": "Jq97yESttTu4P8U4otwNL", + "type": "arrow" + }, + { + "id": "A8kPyLU06mH9oyBkobdZa", + "type": "arrow" } ], - "updated": 1726598715928, + "updated": 1726919487796, "link": null, "locked": false }, { "type": "text", - "version": 47, - "versionNonce": 1461557612, - "index": "aP", + "version": 992, + "versionNonce": 1628022443, + "index": "b1o", "isDeleted": false, - "id": "fq34t956YSgc0zsG_zZeu", + "id": "bBx085lHdLn5JljxcmBw5", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1291.7583312988281, - "y": 472, + "x": 2578.253949556575, + "y": 2379.581512486163, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 148.48333740234375, - "height": 50, - "seed": 1130921997, + "width": 111.56666666666666, + "height": 21.6, + "seed": 1740909867, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598715928, + "updated": 1726919487796, "link": null, "locked": false, - "fontSize": 20, - "fontFamily": 5, - "text": "Active Channel \nUpdates", + "fontSize": 16, + "fontFamily": 6, + "text": "Heartbeat Task", "textAlign": "center", "verticalAlign": "middle", - "containerId": "2sjonxYVzFv7deYkoQoX5", - "originalText": "Active Channel Updates", + "containerId": "NK9bIOXHnIk_mQH6hQ-us", + "originalText": "Heartbeat Task", "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.35 + }, + { + "type": "arrow", + "version": 2652, + "versionNonce": 1295656517, + "index": "b1p", + "isDeleted": false, + "id": "C0S_ZOuxsKKIkAwQJ4sZv", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2209.5928384454646, + "y": 2371.492623597274, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 317.77777777777806, + "height": 0.7658583367585834, + "seed": 1824214987, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "type": "text", + "id": "-AvJk9PYXKBlq1yd1yAB2" + } + ], + "updated": 1726919488628, + "link": null, + "locked": false, + "startBinding": { + "elementId": "NEKEWaqq5lWTHOkiGx6DU", + "focus": -0.39872362474490314, + "gap": 7.777777777777487, + "fixedPoint": null + }, + "endBinding": { + "elementId": "NK9bIOXHnIk_mQH6hQ-us", + "focus": 0.3720930232558083, + "gap": 8.333333333332803, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 317.77777777777806, + 0.7658583367585834 + ] + ], + "elbowed": false }, { "type": "text", - "version": 421, - "versionNonce": 578730068, - "index": "aQ", + "version": 46, + "versionNonce": 1323229669, + "index": "b1q", "isDeleted": false, - "id": "B14jWAlwXTnH4FLL-5KOB", + "id": "-AvJk9PYXKBlq1yd1yAB2", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1262, - "y": 571, + "x": 1815.8026825699912, + "y": 2146.538194668723, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 208.64414504623892, - "height": 275, - "seed": 2043685229, + "width": 199.60000610351562, + "height": 43.2, + "seed": 401940075, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726600670944, + "updated": 1726919475979, "link": null, "locked": false, - "fontSize": 20, - "fontFamily": 5, - "text": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user \nupdates (See User \nUpdates)\n- New \nThreads/Deleted \nThreads\n- Passive Channel \nUpdates", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "- New messages\n- Message Edits\n- Message Deletions\n- Participant user updates (See User Updates)\n- New Threads/Deleted Threads\n- Passive Channel Updates", - "autoResize": false, - "lineHeight": 1.25 + "fontSize": 16, + "fontFamily": 6, + "text": "forwards heartbeat related \nmessages", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "C0S_ZOuxsKKIkAwQJ4sZv", + "originalText": "forwards heartbeat related messages", + "autoResize": true, + "lineHeight": 1.35 }, { - "type": "rectangle", - "version": 146, - "versionNonce": 1027198060, - "index": "aR", + "type": "arrow", + "version": 2659, + "versionNonce": 1514084613, + "index": "b1r", "isDeleted": false, - "id": "7Nqx9LYOEEzgetXjNa2_b", + "id": "JofU-D5K9-E52PJsrbTe3", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1000, - "y": 900, + "x": 2528.481727334354, + "y": 2400.7941767002935, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 1188.92885863872, - "height": 500, - "seed": 741046727, + "width": 322.2222222222224, + "height": 0.6984468969803856, + "seed": 1681660171, "groupIds": [], "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1726600639440, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "type": "text", + "id": "WCymYLSdk-lSf7sWN6sES" + } + ], + "updated": 1726919488628, "link": null, - "locked": false + "locked": false, + "startBinding": { + "elementId": "NK9bIOXHnIk_mQH6hQ-us", + "focus": -0.21220392709341096, + "gap": 7.22222222222149, + "fixedPoint": null + }, + "endBinding": { + "elementId": "NEKEWaqq5lWTHOkiGx6DU", + "focus": 0.2361673942352932, + "gap": 4.444444444444457, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -322.2222222222224, + 0.6984468969803856 + ] + ], + "elbowed": false }, { "type": "text", - "version": 30, - "versionNonce": 956332244, - "index": "aS", + "version": 135, + "versionNonce": 1859345733, + "index": "b1s", "isDeleted": false, - "id": "E9kU-lMID9gyW-OY4I5Un", + "id": "WCymYLSdk-lSf7sWN6sES", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1260, - "y": 900, + "x": 1825.024907843971, + "y": 2186.6060420518534, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 179.059814453125, - "height": 25, - "seed": 529728071, + "width": 178.93333333333334, + "height": 21.6, + "seed": 252362667, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598715928, + "updated": 1726919475979, "link": null, "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "User Subscriptions", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "User Subscriptions", + "fontSize": 16, + "fontFamily": 6, + "text": "response to send to user", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "JofU-D5K9-E52PJsrbTe3", + "originalText": "response to send to user", "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.35 }, { "type": "rectangle", - "version": 10, - "versionNonce": 805252716, - "index": "aT", + "version": 772, + "versionNonce": 1446176011, + "index": "b1t", "isDeleted": false, - "id": "SFCx_f3SYUvjWr1siydbq", + "id": "a6ufwTaJNWAO4qjJj2EfG", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1020, - "y": 960, + "x": 2304.703949556576, + "y": 2578.603734708385, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 140, - "height": 60, - "seed": 911025735, + "width": 104, + "height": 106, + "seed": 1116443211, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [ { "type": "text", - "id": "qcvyp3WuBaAPIQ-5fqR3H" + "id": "Zi55OvWi4cRaY5F7HZoDI" + }, + { + "id": "cyaBPg8_R5_8xioLhaPje", + "type": "arrow" + }, + { + "id": "AJNR16UTCUbXwxmFRId6d", + "type": "arrow" + }, + { + "id": "Jq97yESttTu4P8U4otwNL", + "type": "arrow" + }, + { + "id": "A8kPyLU06mH9oyBkobdZa", + "type": "arrow" } ], - "updated": 1726598715928, + "updated": 1726919487796, "link": null, "locked": false }, { "type": "text", - "version": 17, - "versionNonce": 899825236, - "index": "aU", + "version": 697, + "versionNonce": 810027947, + "index": "b1u", "isDeleted": false, - "id": "qcvyp3WuBaAPIQ-5fqR3H", + "id": "Zi55OvWi4cRaY5F7HZoDI", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1049.020034790039, - "y": 965, + "x": 2338.2122827627527, + "y": 2610.0037347083853, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 81.95993041992188, - "height": 50, - "seed": 740720969, + "width": 36.983333587646484, + "height": 43.2, + "seed": 2140737771, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726598715928, + "updated": 1726919487796, "link": null, "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "Guild \nUpdates", + "fontSize": 16, + "fontFamily": 6, + "text": "Kill\nSend", "textAlign": "center", "verticalAlign": "middle", - "containerId": "SFCx_f3SYUvjWr1siydbq", - "originalText": "Guild Updates", + "containerId": "a6ufwTaJNWAO4qjJj2EfG", + "originalText": "Kill\nSend", "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.35 }, { - "type": "text", - "version": 34, - "versionNonce": 459702508, - "index": "aV", + "type": "arrow", + "version": 2575, + "versionNonce": 792091589, + "index": "b1v", "isDeleted": false, - "id": "CrENgJMFm5et4g_0hTvHx", + "id": "cyaBPg8_R5_8xioLhaPje", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1021.1800482818668, - "y": 1038.9178098974699, + "x": 2172.703949556576, + "y": 2444.603734708385, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 191.09982299804688, - "height": 25, - "seed": 345871591, + "width": 121, + "height": 158, + "seed": 1203616651, "groupIds": [], "frameId": null, - "roundness": null, + "roundness": { + "type": 2 + }, "boundElements": [], - "updated": 1726598715928, + "updated": 1726919488628, "link": null, "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "- Always subscribed", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "- Always subscribed", - "autoResize": true, - "lineHeight": 1.25 + "startBinding": { + "elementId": "NEKEWaqq5lWTHOkiGx6DU", + "focus": -0.20528510372146347, + "gap": 6.444444444444343, + "fixedPoint": null + }, + "endBinding": { + "elementId": "a6ufwTaJNWAO4qjJj2EfG", + "focus": -0.44056326474810314, + "gap": 11, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 121, + 158 + ] + ], + "elbowed": false }, { - "type": "rectangle", - "version": 13, - "versionNonce": 351228884, - "index": "aW", + "type": "arrow", + "version": 2555, + "versionNonce": 265023109, + "index": "b1w", "isDeleted": false, - "id": "Z9pK_w-Js_hASq2cNWM8Z", + "id": "AJNR16UTCUbXwxmFRId6d", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1240, - "y": 960, + "x": 2331.703949556576, + "y": 2573.603734708385, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 140, - "height": 60, - "seed": 190047209, + "width": 120, + "height": 151, + "seed": 120853035, "groupIds": [], "frameId": null, - "roundness": null, - "boundElements": [ - { - "type": "text", - "id": "IgHGpcaA-ptVAN4_bbFL_" - } - ], - "updated": 1726598715928, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726919488628, "link": null, - "locked": false + "locked": false, + "startBinding": { + "elementId": "a6ufwTaJNWAO4qjJj2EfG", + "focus": 0.22410638896707005, + "gap": 5, + "fixedPoint": null + }, + "endBinding": { + "elementId": "NEKEWaqq5lWTHOkiGx6DU", + "focus": -0.6061165546468001, + "gap": 9.8888888888888, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -120, + -151 + ] + ], + "elbowed": false }, { - "type": "text", - "version": 16, - "versionNonce": 1933542252, - "index": "aX", + "type": "arrow", + "version": 2548, + "versionNonce": 1152110917, + "index": "b1x", "isDeleted": false, - "id": "IgHGpcaA-ptVAN4_bbFL_", + "id": "Jq97yESttTu4P8U4otwNL", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1269.020034790039, - "y": 965, + "x": 2388.703949556576, + "y": 2572.603734708385, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 81.95993041992188, - "height": 50, - "seed": 669337481, + "width": 137, + "height": 138, + "seed": 1055298763, "groupIds": [], "frameId": null, - "roundness": null, + "roundness": { + "type": 2 + }, "boundElements": [], - "updated": 1726598715928, + "updated": 1726919488628, "link": null, "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "User \nUpdates", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "Z9pK_w-Js_hASq2cNWM8Z", - "originalText": "User Updates", - "autoResize": true, - "lineHeight": 1.25 + "startBinding": { + "elementId": "a6ufwTaJNWAO4qjJj2EfG", + "focus": -0.2540001385329362, + "gap": 6, + "fixedPoint": null + }, + "endBinding": { + "elementId": "NK9bIOXHnIk_mQH6hQ-us", + "focus": 0.4420238621299144, + "gap": 9.999999999999773, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 137, + -138 + ] + ], + "elbowed": false }, { - "type": "text", - "version": 230, - "versionNonce": 542704236, - "index": "aY", + "type": "arrow", + "version": 2579, + "versionNonce": 1535247365, + "index": "b1y", "isDeleted": false, - "id": "Zt9bVlywuPXkMqJ81QcUV", + "id": "A8kPyLU06mH9oyBkobdZa", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, "angle": 0, - "x": 1240, - "y": 1040, + "x": 2572.703949556576, + "y": 2445.603734708385, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 198.63333333333333, - "height": 175, - "seed": 1862542215, + "width": 155, + "height": 156, + "seed": 1852818283, "groupIds": [], "frameId": null, - "roundness": null, + "roundness": { + "type": 2 + }, "boundElements": [], - "updated": 1726598746109, + "updated": 1726919488628, "link": null, "locked": false, - "fontSize": 20, - "fontFamily": 1, - "text": "- Always subscribed\nto Friends\n- Subscribed to\nmax. 10 participants\nin a channel(?)\n- Not subscribed to\nDM people", - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "- Always subscribed\nto Friends\n- Subscribed to\nmax. 10 participants\nin a channel(?)\n- Not subscribed to\nDM people", - "autoResize": true, - "lineHeight": 1.25 + "startBinding": { + "elementId": "NK9bIOXHnIk_mQH6hQ-us", + "focus": 0.0443407747545291, + "gap": 7.444444444444343, + "fixedPoint": null + }, + "endBinding": { + "elementId": "a6ufwTaJNWAO4qjJj2EfG", + "focus": 0.29803393152446866, + "gap": 9, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -155, + 156 + ] + ], + "elbowed": false }, { - "id": "A7Hdhub5PVx-5ZA7a6e8x", - "type": "rectangle", - "x": 1483.0463180382053, - "y": 959.1795524862915, - "width": 149.3334749235912, - "height": 63.31852166496879, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "type": "arrow", + "version": 139, + "versionNonce": 1994454411, + "index": "b1z", + "isDeleted": false, + "id": "QM1nGdcAGkuba_LeIPPGs", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "aa", - "roundness": null, - "seed": 1879765588, - "version": 157, - "versionNonce": 324374124, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "a__xxvCZDR7K9FGOwsbI0" - } - ], - "updated": 1726599019432, - "link": null, - "locked": false - }, - { - "id": "a__xxvCZDR7K9FGOwsbI0", - "type": "text", - "x": 1488.313055500001, - "y": 965.8388133187759, - "width": 138.8, - "height": 50, "angle": 0, + "x": 2181.6688937636136, + "y": 2338.656249759536, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, + "width": 327.14398642992774, + "height": 329.47241337960713, + "seed": 1727694533, "groupIds": [], "frameId": null, - "index": "aa2", - "roundness": null, - "seed": 888381268, - "version": 65, - "versionNonce": 82895084, - "isDeleted": false, - "boundElements": null, - "updated": 1726599019432, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726919507750, "link": null, "locked": false, - "text": "Extended \nGuild Updates", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "A7Hdhub5PVx-5ZA7a6e8x", - "originalText": "Extended Guild Updates", - "autoResize": true, - "lineHeight": 1.25 + "startBinding": { + "elementId": "NEKEWaqq5lWTHOkiGx6DU", + "focus": 0.8886863254129974, + "gap": 3.947484948849251, + "fixedPoint": null + }, + "endBinding": { + "elementId": "8_s9rHl1BqFvYQf9Y5qMM", + "focus": -0.9859306547665662, + "gap": 14.039058477916683, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -327.14398642992774, + -329.47241337960713 + ] + ], + "elbowed": false }, { - "id": "3HvfXk7LSKslLUwOPx1g1", - "type": "text", - "x": 1484.2586658148514, - "y": 1039.266071876967, - "width": 142, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "type": "arrow", + "version": 152, + "versionNonce": 44872165, + "index": "b20", + "isDeleted": false, + "id": "qUGaP5OV93rxoqqy7V6Au", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ac", - "roundness": null, - "seed": 371454956, - "version": 40, - "versionNonce": 752428396, - "isDeleted": false, - "boundElements": null, - "updated": 1726598893667, - "link": null, - "locked": false, - "text": "- Subscribed if\neligible", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "- Subscribed if\neligible", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "0s0v0Ph79R3KV2D6b1p89", - "type": "rectangle", - "x": 1664.5650096114834, - "y": 958.5153780293955, - "width": 163.71373547178882, - "height": 63.05191163440509, "angle": 0, + "x": 1841.71855911045, + "y": 2028.975465452202, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, + "width": 303.8597169331358, + "height": 308.51657083249415, + "seed": 793680459, "groupIds": [], "frameId": null, - "index": "ad", - "roundness": null, - "seed": 1300696788, - "version": 118, - "versionNonce": 1920810580, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "X2DYwzvMmzYfm_C8EYUfZ" - } - ], - "updated": 1726599032470, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1726919505404, "link": null, - "locked": false + "locked": false, + "startBinding": { + "elementId": "8_s9rHl1BqFvYQf9Y5qMM", + "focus": -0.09821789880910446, + "gap": 12.84553352395892, + "fixedPoint": null + }, + "endBinding": { + "elementId": "NEKEWaqq5lWTHOkiGx6DU", + "focus": 0.6478281445653695, + "gap": 5.111698423689177, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 303.8597169331358, + 308.51657083249415 + ] + ], + "elbowed": false }, { - "id": "X2DYwzvMmzYfm_C8EYUfZ", "type": "text", - "x": 1670.0552106807113, - "y": 965.041333846598, - "width": 152.73333333333332, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 1112, + "versionNonce": 1688383141, + "index": "b21", + "isDeleted": false, + "id": "Dpc-UdQdcDM_QYYampaa_", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "ae", - "roundness": null, - "seed": 1760833132, - "version": 28, - "versionNonce": 265753708, - "isDeleted": false, - "boundElements": null, - "updated": 1726599036977, - "link": null, - "locked": false, - "text": "Passive Channel\nUpdates", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "0s0v0Ph79R3KV2D6b1p89", - "originalText": "Passive Channel Updates", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "NmUZHPnkX3HyWYoAyEAQb", - "type": "text", - "x": 1666.7773573881293, - "y": 1039.266071876967, - "width": 189.81666564941406, - "height": 25, "angle": 0, + "x": 1988.8550096162564, + "y": 2120.3041725116436, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, + "width": 194.69675842101927, + "height": 43.2, + "seed": 2041976677, "groupIds": [], "frameId": null, - "index": "af", "roundness": null, - "seed": 2127948140, - "version": 184, - "versionNonce": 1680085740, - "isDeleted": false, - "boundElements": null, - "updated": 1726600631188, + "boundElements": [], + "updated": 1726919518099, "link": null, "locked": false, - "text": "- Always subscribed", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "left", + "fontSize": 16, + "fontFamily": 6, + "text": "Send/Receive messages\nto/from client", + "textAlign": "right", "verticalAlign": "top", "containerId": null, - "originalText": "- Always subscribed", - "autoResize": true, - "lineHeight": 1.25 + "originalText": "Send/Receive messages\nto/from client", + "autoResize": false, + "lineHeight": 1.35 }, { - "id": "dXRiLAxAGjQsMZJou8SsK", "type": "rectangle", - "x": 1903.4985694892282, - "y": 957.4092041410731, - "width": 158.18286603017418, - "height": 65.26425941105094, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", + "version": 292, + "versionNonce": 1134546731, + "index": "b22", + "isDeleted": false, + "id": "BNra93QOEfDm6IfLrZPxO", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, + "angle": 0, + "x": 2879.6559358388795, + "y": 2152.9715030399793, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 295.1449965676214, + "height": 118, + "seed": 1137650821, "groupIds": [], "frameId": null, - "index": "ag", "roundness": null, - "seed": 1174667476, - "version": 86, - "versionNonce": 572168940, - "isDeleted": false, "boundElements": [ { "type": "text", - "id": "VWbtks-OQBuNFRwUPRL6D" + "id": "KcE2WjmALQ71_MrVW5VSl" } ], - "updated": 1726600742156, + "updated": 1726919792306, "link": null, "locked": false }, { - "id": "VWbtks-OQBuNFRwUPRL6D", "type": "text", - "x": 1912.3483338031433, - "y": 965.0413338465986, - "width": 140.48333740234375, - "height": 50, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 1, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "agV", - "roundness": null, - "seed": 1823310932, - "version": 25, - "versionNonce": 1364958676, + "version": 480, + "versionNonce": 1355985355, + "index": "b23", "isDeleted": false, - "boundElements": null, - "updated": 1726600746259, - "link": null, - "locked": false, - "text": "Active Channel\nUpdates", - "fontSize": 20, - "fontFamily": 5, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "dXRiLAxAGjQsMZJou8SsK", - "originalText": "Active Channel Updates", - "autoResize": true, - "lineHeight": 1.25 - }, - { - "id": "ElxDUoNGS-gEJhuVxp-Ru", - "type": "text", - "x": 1719.8737040276271, - "y": 443.038346070926, - "width": 471.5, - "height": 100, - "angle": 0, - "strokeColor": "#f08c00", - "backgroundColor": "transparent", + "id": "KcE2WjmALQ71_MrVW5VSl", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", - "roughness": 1, + "roughness": 0, "opacity": 100, + "angle": 0, + "x": 2884.6559358388795, + "y": 2168.7715030399795, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 285.98333333333335, + "height": 86.4, + "seed": 250829605, "groupIds": [], "frameId": null, - "index": "ai", "roundness": null, - "seed": 1068866388, - "version": 168, - "versionNonce": 88154860, - "isDeleted": false, - "boundElements": null, - "updated": 1726601168303, + "boundElements": [], + "updated": 1726919792306, "link": null, "locked": false, - "text": "How does Discord handle the \"unread\nMessages\" indicator in Guilds and Channels?\nDo we just receive all the messages all the time,\nor how does it work?", - "fontSize": 20, - "fontFamily": 5, + "fontSize": 16, + "fontFamily": 6, + "text": "The User-specific task can be iterated \nover to yield a shared or exclusive \nreference to existing client-specific task\nwith each new iteration.", "textAlign": "left", - "verticalAlign": "top", - "containerId": null, - "originalText": "How does Discord handle the \"unread\nMessages\" indicator in Guilds and Channels?\nDo we just receive all the messages all the time,\nor how does it work?", + "verticalAlign": "middle", + "containerId": "BNra93QOEfDm6IfLrZPxO", + "originalText": "The User-specific task can be iterated over to yield a shared or exclusive reference to existing client-specific task with each new iteration.", "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.35 } ], "appState": { From e55cfa1daafb4533e17d91f7090faba263d35536 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 23 Sep 2024 20:31:55 +0200 Subject: [PATCH 128/162] Update whiteboard --- docs/Event Publishing in Symfonia.excalidraw | 7532 +++++++++++++++++- 1 file changed, 7270 insertions(+), 262 deletions(-) diff --git a/docs/Event Publishing in Symfonia.excalidraw b/docs/Event Publishing in Symfonia.excalidraw index f792b6d..468e31b 100644 --- a/docs/Event Publishing in Symfonia.excalidraw +++ b/docs/Event Publishing in Symfonia.excalidraw @@ -5,148 +5,148 @@ "elements": [ { "type": "rectangle", - "version": 930, - "versionNonce": 508696171, - "index": "Zs", + "version": 916, + "versionNonce": 1162684619, + "index": "Zq", "isDeleted": false, - "id": "Yef_zHBLDgJgcHaAYtii3", + "id": "pWnZ5m49e23wgtjp43Cjk", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 992.8531459400365, - "y": 2219.8733700419616, + "x": 1016.5124452887101, + "y": 2329.4044112999004, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 1839.1026870295834, - "height": 565.5943080887822, - "seed": 1583650603, + "width": 1798.6009389590572, + "height": 687.3836809678236, + "seed": 532042917, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [ { "type": "text", - "id": "Y-VdE-5JZCbVRtwiRhZM0" + "id": "d___i3s4lLCSsCIGrwJcj" } ], - "updated": 1726919645376, + "updated": 1727114801280, "link": null, "locked": false }, { "type": "text", - "version": 551, - "versionNonce": 1306663083, - "index": "Zt", + "version": 613, + "versionNonce": 1611586917, + "index": "Zr", "isDeleted": false, - "id": "Y-VdE-5JZCbVRtwiRhZM0", + "id": "d___i3s4lLCSsCIGrwJcj", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 1818.3544864030705, - "y": 2224.8733700419616, + "x": 1849.9379147682387, + "y": 2334.4044112999004, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 188.10000610351562, + "width": 131.75, "height": 21.6, - "seed": 1533350667, + "seed": 940294693, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919381684, + "updated": 1727114801280, "link": null, "locked": false, "fontSize": 16, "fontFamily": 6, - "text": "Server-/Outside boundary", + "text": "User-specific Task", "textAlign": "center", "verticalAlign": "top", - "containerId": "Yef_zHBLDgJgcHaAYtii3", - "originalText": "Server-/Outside boundary", + "containerId": "pWnZ5m49e23wgtjp43Cjk", + "originalText": "User-specific Task", "autoResize": true, "lineHeight": 1.35 }, { "type": "rectangle", - "version": 677, - "versionNonce": 1255271621, - "index": "Zu", + "version": 1023, + "versionNonce": 235161099, + "index": "Zs", "isDeleted": false, - "id": "pWnZ5m49e23wgtjp43Cjk", + "id": "Yef_zHBLDgJgcHaAYtii3", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 1007.4070365241471, - "y": 2265.70346669144, + "x": 991.8565274665173, + "y": 2218.876751568442, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 1798.6009389590572, - "height": 493.0980439136671, - "seed": 532042917, + "width": 1839.1026870295834, + "height": 913.1174092696018, + "seed": 1583650603, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [ { "type": "text", - "id": "d___i3s4lLCSsCIGrwJcj" + "id": "Y-VdE-5JZCbVRtwiRhZM0" } ], - "updated": 1726919642616, + "updated": 1727114918418, "link": null, "locked": false }, { "type": "text", - "version": 374, - "versionNonce": 1946962219, - "index": "Zv", + "version": 567, + "versionNonce": 155536555, + "index": "Zt", "isDeleted": false, - "id": "d___i3s4lLCSsCIGrwJcj", + "id": "Y-VdE-5JZCbVRtwiRhZM0", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 1840.8325060036757, - "y": 2270.70346669144, + "x": 1817.3578679295513, + "y": 2223.876751568442, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 131.75, + "width": 188.10000610351562, "height": 21.6, - "seed": 940294693, + "seed": 1533350667, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919467527, + "updated": 1727114918418, "link": null, "locked": false, "fontSize": 16, "fontFamily": 6, - "text": "User-specific Task", + "text": "Server-/Outside boundary", "textAlign": "center", "verticalAlign": "top", - "containerId": "pWnZ5m49e23wgtjp43Cjk", - "originalText": "User-specific Task", + "containerId": "Yef_zHBLDgJgcHaAYtii3", + "originalText": "Server-/Outside boundary", "autoResize": true, "lineHeight": 1.35 }, { "type": "rectangle", - "version": 746, - "versionNonce": 1014573035, + "version": 867, + "versionNonce": 2093992805, "index": "Zy", "isDeleted": false, "id": "aBcRnfi5WZIFdLks0fcNk", @@ -156,8 +156,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1043.6701333544931, - "y": 2303.0614322921792, + "x": 1052.7755421190561, + "y": 2561.0480139547963, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 778.8793335223208, @@ -176,14 +176,14 @@ "type": "arrow" } ], - "updated": 1726919565141, + "updated": 1727114723600, "link": null, "locked": false }, { "type": "text", - "version": 542, - "versionNonce": 1048769163, + "version": 663, + "versionNonce": 1374361285, "index": "Zz", "isDeleted": false, "id": "oJsFTZtKwwj41ba8qbaAF", @@ -193,8 +193,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1363.0514657650676, - "y": 2308.0614322921792, + "x": 1372.1568745296306, + "y": 2566.0480139547963, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 140.11666870117188, @@ -204,7 +204,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919565141, + "updated": 1727114723600, "link": null, "locked": false, "fontSize": 16, @@ -16194,8 +16194,8 @@ }, { "type": "text", - "version": 355, - "versionNonce": 1225766821, + "version": 356, + "versionNonce": 1248564651, "index": "b0g", "isDeleted": false, "id": "oHiI-diNFXNo4nn79ecEY", @@ -16215,8 +16215,13 @@ "groupIds": [], "frameId": null, "roundness": null, - "boundElements": [], - "updated": 1726918366440, + "boundElements": [ + { + "id": "cq055tV0PJ_kX7ISOHUKW", + "type": "arrow" + } + ], + "updated": 1727115085656, "link": null, "locked": false, "fontSize": 16, @@ -17178,19 +17183,19 @@ }, { "type": "rectangle", - "version": 341, - "versionNonce": 433404133, + "version": 545, + "versionNonce": 882531019, "index": "b11", "isDeleted": false, "id": "YvrAXYERhR-VVUCxxvzwy", "fillStyle": "solid", "strokeWidth": 2, - "strokeStyle": "solid", + "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2576.6672092013887, - "y": 825.8888888888889, + "x": 2389.7700249811346, + "y": 619.9711947701132, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", "width": 408, @@ -17198,23 +17203,21 @@ "seed": 456582347, "groupIds": [], "frameId": null, - "roundness": { - "type": 3 - }, + "roundness": null, "boundElements": [ { "type": "text", "id": "5uIKmQGCHQ4JNzkHQ2WTP" } ], - "updated": 1726918829164, + "updated": 1727115242295, "link": null, "locked": false }, { "type": "text", - "version": 430, - "versionNonce": 1457968197, + "version": 632, + "versionNonce": 1169869163, "index": "b12", "isDeleted": false, "id": "5uIKmQGCHQ4JNzkHQ2WTP", @@ -17224,8 +17227,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2581.6672092013887, - "y": 842.6888888888889, + "x": 2394.7700249811346, + "y": 636.7711947701132, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", "width": 373.8500061035156, @@ -17235,7 +17238,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918829164, + "updated": 1727115242295, "link": null, "locked": false, "fontSize": 16, @@ -17250,8 +17253,8 @@ }, { "type": "rectangle", - "version": 563, - "versionNonce": 788942283, + "version": 685, + "versionNonce": 1715288395, "index": "b13", "isDeleted": false, "id": "2-xZdr1N7h94XCAl94juo", @@ -17261,8 +17264,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1052.8519422561303, - "y": 2342.3669517142903, + "x": 1061.9573510206933, + "y": 2600.3535333769073, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 196.66666666666652, @@ -17301,16 +17304,20 @@ { "id": "HQ69vqNQrY84lbpyc-1HF", "type": "arrow" + }, + { + "id": "yMeEUEU3Lphx3vyL3whwt", + "type": "arrow" } ], - "updated": 1726919381684, + "updated": 1727115132489, "link": null, "locked": false }, { "type": "text", - "version": 530, - "versionNonce": 338713707, + "version": 651, + "versionNonce": 1926097285, "index": "b13V", "isDeleted": false, "id": "t0kacxDg-xYJMZRhr-ORv", @@ -17320,8 +17327,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1100.476943527696, - "y": 2379.344729492068, + "x": 1109.582352292259, + "y": 2637.331311154685, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 101.41666412353516, @@ -17331,7 +17338,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919381684, + "updated": 1727114723600, "link": null, "locked": false, "fontSize": 16, @@ -17346,8 +17353,8 @@ }, { "type": "rectangle", - "version": 770, - "versionNonce": 1741918923, + "version": 891, + "versionNonce": 1694613797, "index": "b15", "isDeleted": false, "id": "ernpMbdN54sSKmYGoM7nC", @@ -17357,8 +17364,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1583.4074978116857, - "y": 2342.36695171429, + "x": 1592.5129065762487, + "y": 2600.353533376907, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 196.66666666666652, @@ -17391,14 +17398,14 @@ "type": "arrow" } ], - "updated": 1726919381684, + "updated": 1727114723600, "link": null, "locked": false }, { "type": "text", - "version": 758, - "versionNonce": 755827051, + "version": 879, + "versionNonce": 1406686341, "index": "b16", "isDeleted": false, "id": "iFTMGnafTCa2cKPMR7lDj", @@ -17408,8 +17415,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1625.9574978116857, - "y": 2379.344729492068, + "x": 1635.0629065762487, + "y": 2637.331311154685, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 111.56666666666666, @@ -17419,7 +17426,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919381684, + "updated": 1727114723600, "link": null, "locked": false, "fontSize": 16, @@ -17434,8 +17441,8 @@ }, { "type": "arrow", - "version": 1809, - "versionNonce": 188264357, + "version": 2212, + "versionNonce": 935757579, "index": "b17", "isDeleted": false, "id": "_FXruw5utykj1QDL3wURd", @@ -17445,8 +17452,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1257.2963867005747, - "y": 2371.255840603179, + "x": 1266.4017954651376, + "y": 2629.242422265796, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 317.77777777777806, @@ -17463,7 +17470,7 @@ "id": "rVjVYzDRISZ3zbtZ2lI7r" } ], - "updated": 1726919383641, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { @@ -17532,8 +17539,8 @@ }, { "type": "arrow", - "version": 1816, - "versionNonce": 947047013, + "version": 2219, + "versionNonce": 1288863819, "index": "b18", "isDeleted": false, "id": "VQweVVOoEtZ8a74fJHKXs", @@ -17543,8 +17550,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1576.1852755894638, - "y": 2400.5573937061986, + "x": 1585.2906843540268, + "y": 2658.5439753688156, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 322.2222222222224, @@ -17561,7 +17568,7 @@ "id": "vH_ahY76bpif3eJ3dl-IZ" } ], - "updated": 1726919383641, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { @@ -19432,8 +19439,8 @@ }, { "type": "arrow", - "version": 1097, - "versionNonce": 266216453, + "version": 1354, + "versionNonce": 1293779333, "index": "b1E", "isDeleted": false, "id": "ldtBH_Mc3-DYjw69pcUaV", @@ -19443,12 +19450,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1102.8466310158012, - "y": 2337.3669517142903, + "x": 1101.6580186716383, + "y": 2595.3535333769073, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 443.89723817272056, - "height": 333.4131343177314, + "width": 445.0858505168835, + "height": 591.3997159803484, "seed": 1145680325, "groupIds": [], "frameId": null, @@ -19456,18 +19463,18 @@ "type": 2 }, "boundElements": [], - "updated": 1726919461234, + "updated": 1727114848645, "link": null, "locked": false, "startBinding": { "elementId": "2-xZdr1N7h94XCAl94juo", - "focus": -0.7323890299850935, + "focus": -0.6139785493447132, "gap": 5, "fixedPoint": null }, "endBinding": { "elementId": "yrMLMaP8hB6ezngk1Y_H9", - "focus": 1.057909466192341, + "focus": 0.7479659987749174, "gap": 9.399299968612127, "fixedPoint": null }, @@ -19480,16 +19487,20 @@ 0 ], [ - 443.89723817272056, - -333.4131343177314 + 39.23300843497145, + -528.0174000634461 + ], + [ + 445.0858505168835, + -591.3997159803484 ] ], "elbowed": false }, { "type": "arrow", - "version": 1260, - "versionNonce": 1152907141, + "version": 1596, + "versionNonce": 1465714917, "index": "b1F", "isDeleted": false, "id": "NFrs7hBfcfS6Sh6nfbpdz", @@ -19499,12 +19510,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1572.2903640815873, + "x": 1572.2903640815875, "y": 2024.2886438786966, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 426.16246819580033, - "height": 314.5440885902324, + "width": 434.60857125128405, + "height": 572.5306702528494, "seed": 1362911237, "groupIds": [], "frameId": null, @@ -19512,18 +19523,18 @@ "type": 2 }, "boundElements": [], - "updated": 1726919457459, + "updated": 1727114848645, "link": null, "locked": false, "startBinding": { "elementId": "yrMLMaP8hB6ezngk1Y_H9", - "focus": -0.267713028894834, + "focus": -0.8550942666127462, "gap": 2.7504596041435434, "fixedPoint": null }, "endBinding": { "elementId": "2-xZdr1N7h94XCAl94juo", - "focus": -0.4573491501323452, + "focus": -0.25116468114773316, "gap": 3.534219245361328, "fixedPoint": null }, @@ -19536,8 +19547,12 @@ 0 ], [ - -426.16246819580033, - 314.5440885902324 + -407.41474551078113, + 60.77523095177935 + ], + [ + -434.60857125128405, + 572.5306702528494 ] ], "elbowed": false @@ -19586,8 +19601,8 @@ }, { "type": "rectangle", - "version": 539, - "versionNonce": 2060753131, + "version": 660, + "versionNonce": 419597349, "index": "b1H", "isDeleted": false, "id": "ylLtdS1Ye_gyVKSaQJs2x", @@ -19597,8 +19612,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1352.407497811686, - "y": 2578.3669517142903, + "x": 1361.512906576249, + "y": 2836.3535333769073, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 104, @@ -19629,14 +19644,14 @@ "type": "arrow" } ], - "updated": 1726919381684, + "updated": 1727114723600, "link": null, "locked": false }, { "type": "text", - "version": 463, - "versionNonce": 1648903051, + "version": 584, + "versionNonce": 59228037, "index": "b1I", "isDeleted": false, "id": "AwYqSeI9CW9aJE0REZarf", @@ -19646,8 +19661,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1385.9158310178627, - "y": 2609.7669517142904, + "x": 1395.0212397824257, + "y": 2867.7535333769074, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 36.983333587646484, @@ -19657,7 +19672,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919381684, + "updated": 1727114723600, "link": null, "locked": false, "fontSize": 16, @@ -19672,8 +19687,8 @@ }, { "type": "arrow", - "version": 1732, - "versionNonce": 2037116901, + "version": 2135, + "versionNonce": 1671506315, "index": "b1J", "isDeleted": false, "id": "kuMt4oieo-H6xuQKoGSWP", @@ -19683,8 +19698,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1220.407497811686, - "y": 2444.3669517142903, + "x": 1229.512906576249, + "y": 2702.3535333769073, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 121, @@ -19696,7 +19711,7 @@ "type": 2 }, "boundElements": [], - "updated": 1726919383641, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { @@ -19728,8 +19743,8 @@ }, { "type": "arrow", - "version": 1712, - "versionNonce": 2122513061, + "version": 2115, + "versionNonce": 256279243, "index": "b1K", "isDeleted": false, "id": "HQ69vqNQrY84lbpyc-1HF", @@ -19739,8 +19754,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1379.407497811686, - "y": 2573.3669517142903, + "x": 1388.512906576249, + "y": 2831.3535333769073, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 120, @@ -19752,7 +19767,7 @@ "type": 2 }, "boundElements": [], - "updated": 1726919383641, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { @@ -19784,8 +19799,8 @@ }, { "type": "arrow", - "version": 1705, - "versionNonce": 1340495205, + "version": 2108, + "versionNonce": 769035275, "index": "b1L", "isDeleted": false, "id": "o1dloWTz2OdDChjfXO3W8", @@ -19795,8 +19810,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1436.407497811686, - "y": 2572.3669517142903, + "x": 1445.512906576249, + "y": 2830.3535333769073, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 137, @@ -19808,7 +19823,7 @@ "type": 2 }, "boundElements": [], - "updated": 1726919383641, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { @@ -19840,8 +19855,8 @@ }, { "type": "arrow", - "version": 1736, - "versionNonce": 1656881189, + "version": 2139, + "versionNonce": 677725515, "index": "b1M", "isDeleted": false, "id": "iPGwpGNDp9qVzkdoEhrce", @@ -19851,8 +19866,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1620.407497811686, - "y": 2445.3669517142903, + "x": 1629.512906576249, + "y": 2703.3535333769073, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 155, @@ -19864,7 +19879,7 @@ "type": 2 }, "boundElements": [], - "updated": 1726919383641, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { @@ -22261,8 +22276,8 @@ }, { "type": "text", - "version": 167, - "versionNonce": 861870437, + "version": 169, + "versionNonce": 704269323, "index": "b1X", "isDeleted": false, "id": "yrMLMaP8hB6ezngk1Y_H9", @@ -22292,7 +22307,7 @@ "type": "arrow" } ], - "updated": 1726919457020, + "updated": 1727114848645, "link": null, "locked": false, "fontSize": 16.969696969696976, @@ -22779,8 +22794,8 @@ }, { "type": "rectangle", - "version": 978, - "versionNonce": 894425867, + "version": 1099, + "versionNonce": 459182565, "index": "b1j", "isDeleted": false, "id": "IqZ18gef20BdSUYa6vLD0", @@ -22790,8 +22805,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1997.130798574223, - "y": 2303.2982152862746, + "x": 2006.236207338786, + "y": 2561.2847969488917, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 778.8793335223208, @@ -22806,14 +22821,14 @@ "id": "MkMYxixZDt1Vj-hKG-YBB" } ], - "updated": 1726919487796, + "updated": 1727114723601, "link": null, "locked": false }, { "type": "text", - "version": 774, - "versionNonce": 1280721323, + "version": 895, + "versionNonce": 1876757829, "index": "b1k", "isDeleted": false, "id": "MkMYxixZDt1Vj-hKG-YBB", @@ -22823,8 +22838,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2316.5121309847973, - "y": 2308.2982152862746, + "x": 2325.6175397493603, + "y": 2566.2847969488917, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 140.11666870117188, @@ -22834,7 +22849,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919487796, + "updated": 1727114723601, "link": null, "locked": false, "fontSize": 16, @@ -22849,8 +22864,8 @@ }, { "type": "rectangle", - "version": 798, - "versionNonce": 570726507, + "version": 920, + "versionNonce": 21130245, "index": "b1l", "isDeleted": false, "id": "NEKEWaqq5lWTHOkiGx6DU", @@ -22860,8 +22875,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2005.1483940010205, - "y": 2342.603734708385, + "x": 2014.2538027655835, + "y": 2600.5903163710022, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 196.66666666666652, @@ -22900,16 +22915,20 @@ { "id": "qUGaP5OV93rxoqqy7V6Au", "type": "arrow" + }, + { + "id": "sneG026N1n_G0mJImR8vA", + "type": "arrow" } ], - "updated": 1726919502507, + "updated": 1727115150280, "link": null, "locked": false }, { "type": "text", - "version": 764, - "versionNonce": 1567363819, + "version": 885, + "versionNonce": 546586629, "index": "b1m", "isDeleted": false, "id": "0-FTztADtIwv8vL54K2VN", @@ -22919,8 +22938,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2052.773395272586, - "y": 2379.581512486163, + "x": 2061.878804037149, + "y": 2637.56809414878, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 101.41666412353516, @@ -22930,7 +22949,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919487796, + "updated": 1727114723601, "link": null, "locked": false, "fontSize": 16, @@ -22945,8 +22964,8 @@ }, { "type": "rectangle", - "version": 1003, - "versionNonce": 1305206795, + "version": 1124, + "versionNonce": 1744988069, "index": "b1n", "isDeleted": false, "id": "NK9bIOXHnIk_mQH6hQ-us", @@ -22956,8 +22975,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2535.703949556576, - "y": 2342.603734708385, + "x": 2544.809358321139, + "y": 2600.5903163710022, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 196.66666666666652, @@ -22990,14 +23009,14 @@ "type": "arrow" } ], - "updated": 1726919487796, + "updated": 1727114723601, "link": null, "locked": false }, { "type": "text", - "version": 992, - "versionNonce": 1628022443, + "version": 1113, + "versionNonce": 754985733, "index": "b1o", "isDeleted": false, "id": "bBx085lHdLn5JljxcmBw5", @@ -23007,8 +23026,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2578.253949556575, - "y": 2379.581512486163, + "x": 2587.359358321138, + "y": 2637.56809414878, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 111.56666666666666, @@ -23018,7 +23037,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919487796, + "updated": 1727114723601, "link": null, "locked": false, "fontSize": 16, @@ -23033,8 +23052,8 @@ }, { "type": "arrow", - "version": 2652, - "versionNonce": 1295656517, + "version": 3055, + "versionNonce": 856075915, "index": "b1p", "isDeleted": false, "id": "C0S_ZOuxsKKIkAwQJ4sZv", @@ -23044,8 +23063,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2209.5928384454646, - "y": 2371.492623597274, + "x": 2218.6982472100276, + "y": 2629.479205259891, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 317.77777777777806, @@ -23062,19 +23081,19 @@ "id": "-AvJk9PYXKBlq1yd1yAB2" } ], - "updated": 1726919488628, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { "elementId": "NEKEWaqq5lWTHOkiGx6DU", "focus": -0.39872362474490314, - "gap": 7.777777777777487, + "gap": 7.777777777777715, "fixedPoint": null }, "endBinding": { "elementId": "NK9bIOXHnIk_mQH6hQ-us", - "focus": 0.3720930232558083, - "gap": 8.333333333332803, + "focus": 0.3720930232558081, + "gap": 8.333333333333712, "fixedPoint": null }, "lastCommittedPoint": null, @@ -23131,8 +23150,8 @@ }, { "type": "arrow", - "version": 2659, - "versionNonce": 1514084613, + "version": 3062, + "versionNonce": 214878155, "index": "b1r", "isDeleted": false, "id": "JofU-D5K9-E52PJsrbTe3", @@ -23142,8 +23161,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2528.481727334354, - "y": 2400.7941767002935, + "x": 2537.587136098917, + "y": 2658.7807583629105, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 322.2222222222224, @@ -23160,19 +23179,19 @@ "id": "WCymYLSdk-lSf7sWN6sES" } ], - "updated": 1726919488628, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { "elementId": "NK9bIOXHnIk_mQH6hQ-us", - "focus": -0.21220392709341096, - "gap": 7.22222222222149, + "focus": -0.21220392709341088, + "gap": 7.222222222221944, "fixedPoint": null }, "endBinding": { "elementId": "NEKEWaqq5lWTHOkiGx6DU", "focus": 0.2361673942352932, - "gap": 4.444444444444457, + "gap": 4.444444444445139, "fixedPoint": null }, "lastCommittedPoint": null, @@ -23229,8 +23248,8 @@ }, { "type": "rectangle", - "version": 772, - "versionNonce": 1446176011, + "version": 893, + "versionNonce": 739553957, "index": "b1t", "isDeleted": false, "id": "a6ufwTaJNWAO4qjJj2EfG", @@ -23240,8 +23259,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2304.703949556576, - "y": 2578.603734708385, + "x": 2313.809358321139, + "y": 2836.5903163710022, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 104, @@ -23272,14 +23291,14 @@ "type": "arrow" } ], - "updated": 1726919487796, + "updated": 1727114723601, "link": null, "locked": false }, { "type": "text", - "version": 697, - "versionNonce": 810027947, + "version": 818, + "versionNonce": 1855002117, "index": "b1u", "isDeleted": false, "id": "Zi55OvWi4cRaY5F7HZoDI", @@ -23289,8 +23308,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2338.2122827627527, - "y": 2610.0037347083853, + "x": 2347.3176915273157, + "y": 2867.9903163710023, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 36.983333587646484, @@ -23300,7 +23319,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919487796, + "updated": 1727114723601, "link": null, "locked": false, "fontSize": 16, @@ -23315,8 +23334,8 @@ }, { "type": "arrow", - "version": 2575, - "versionNonce": 792091589, + "version": 2978, + "versionNonce": 1070698763, "index": "b1v", "isDeleted": false, "id": "cyaBPg8_R5_8xioLhaPje", @@ -23326,8 +23345,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2172.703949556576, - "y": 2444.603734708385, + "x": 2181.809358321139, + "y": 2702.5903163710022, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 121, @@ -23339,7 +23358,7 @@ "type": 2 }, "boundElements": [], - "updated": 1726919488628, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { @@ -23371,8 +23390,8 @@ }, { "type": "arrow", - "version": 2555, - "versionNonce": 265023109, + "version": 2958, + "versionNonce": 1629686347, "index": "b1w", "isDeleted": false, "id": "AJNR16UTCUbXwxmFRId6d", @@ -23382,8 +23401,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2331.703949556576, - "y": 2573.603734708385, + "x": 2340.809358321139, + "y": 2831.5903163710022, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 120, @@ -23395,7 +23414,7 @@ "type": 2 }, "boundElements": [], - "updated": 1726919488628, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { @@ -23407,7 +23426,7 @@ "endBinding": { "elementId": "NEKEWaqq5lWTHOkiGx6DU", "focus": -0.6061165546468001, - "gap": 9.8888888888888, + "gap": 9.888888888889028, "fixedPoint": null }, "lastCommittedPoint": null, @@ -23427,8 +23446,8 @@ }, { "type": "arrow", - "version": 2548, - "versionNonce": 1152110917, + "version": 2951, + "versionNonce": 1356280715, "index": "b1x", "isDeleted": false, "id": "Jq97yESttTu4P8U4otwNL", @@ -23438,8 +23457,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2388.703949556576, - "y": 2572.603734708385, + "x": 2397.809358321139, + "y": 2830.5903163710022, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 137, @@ -23451,7 +23470,7 @@ "type": 2 }, "boundElements": [], - "updated": 1726919488628, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { @@ -23462,8 +23481,8 @@ }, "endBinding": { "elementId": "NK9bIOXHnIk_mQH6hQ-us", - "focus": 0.4420238621299144, - "gap": 9.999999999999773, + "focus": 0.44202386212991757, + "gap": 10.000000000000227, "fixedPoint": null }, "lastCommittedPoint": null, @@ -23483,8 +23502,8 @@ }, { "type": "arrow", - "version": 2579, - "versionNonce": 1535247365, + "version": 2982, + "versionNonce": 2046954699, "index": "b1y", "isDeleted": false, "id": "A8kPyLU06mH9oyBkobdZa", @@ -23494,8 +23513,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2572.703949556576, - "y": 2445.603734708385, + "x": 2581.809358321139, + "y": 2703.5903163710022, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 155, @@ -23507,12 +23526,12 @@ "type": 2 }, "boundElements": [], - "updated": 1726919488628, + "updated": 1727114724747, "link": null, "locked": false, "startBinding": { "elementId": "NK9bIOXHnIk_mQH6hQ-us", - "focus": 0.0443407747545291, + "focus": 0.044340774754532214, "gap": 7.444444444444343, "fixedPoint": null }, @@ -23539,8 +23558,8 @@ }, { "type": "arrow", - "version": 139, - "versionNonce": 1994454411, + "version": 374, + "versionNonce": 912616363, "index": "b1z", "isDeleted": false, "id": "QM1nGdcAGkuba_LeIPPGs", @@ -23550,12 +23569,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2181.6688937636136, - "y": 2338.656249759536, + "x": 2194.6105612898186, + "y": 2596.642831422153, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 327.14398642992774, - "height": 329.47241337960713, + "width": 375.05801058254974, + "height": 587.4589950422242, "seed": 1727694533, "groupIds": [], "frameId": null, @@ -23563,18 +23582,18 @@ "type": 2 }, "boundElements": [], - "updated": 1726919507750, + "updated": 1727114740873, "link": null, "locked": false, "startBinding": { "elementId": "NEKEWaqq5lWTHOkiGx6DU", - "focus": 0.8886863254129974, + "focus": 0.7719037348701197, "gap": 3.947484948849251, "fixedPoint": null }, "endBinding": { "elementId": "8_s9rHl1BqFvYQf9Y5qMM", - "focus": -0.9859306547665662, + "focus": -0.2889051410798594, "gap": 14.039058477916683, "fixedPoint": null }, @@ -23587,16 +23606,20 @@ 0 ], [ - -327.14398642992774, - -329.47241337960713 + 34.97235662641697, + -506.36491496902545 + ], + [ + -340.0856539561328, + -587.4589950422242 ] ], "elbowed": false }, { "type": "arrow", - "version": 152, - "versionNonce": 44872165, + "version": 384, + "versionNonce": 1742976971, "index": "b20", "isDeleted": false, "id": "qUGaP5OV93rxoqqy7V6Au", @@ -23610,8 +23633,8 @@ "y": 2028.975465452202, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 303.8597169331358, - "height": 308.51657083249415, + "width": 367.0081923151795, + "height": 566.5031524951112, "seed": 793680459, "groupIds": [], "frameId": null, @@ -23619,18 +23642,18 @@ "type": 2 }, "boundElements": [], - "updated": 1726919505404, + "updated": 1727114743910, "link": null, "locked": false, "startBinding": { "elementId": "8_s9rHl1BqFvYQf9Y5qMM", - "focus": -0.09821789880910446, + "focus": 1.0759054008449098, "gap": 12.84553352395892, "fixedPoint": null }, "endBinding": { "elementId": "NEKEWaqq5lWTHOkiGx6DU", - "focus": 0.6478281445653695, + "focus": 0.45094548255454076, "gap": 5.111698423689177, "fixedPoint": null }, @@ -23643,8 +23666,12 @@ 0 ], [ - 303.8597169331358, - 308.51657083249415 + 367.0081923151795, + 81.11580916700109 + ], + [ + 322.0984974270384, + 566.5031524951112 ] ], "elbowed": false @@ -23688,14 +23715,14 @@ }, { "type": "rectangle", - "version": 292, - "versionNonce": 1134546731, + "version": 293, + "versionNonce": 1084002411, "index": "b22", "isDeleted": false, "id": "BNra93QOEfDm6IfLrZPxO", "fillStyle": "solid", "strokeWidth": 2, - "strokeStyle": "solid", + "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, @@ -23715,7 +23742,7 @@ "id": "KcE2WjmALQ71_MrVW5VSl" } ], - "updated": 1726919792306, + "updated": 1727114926736, "link": null, "locked": false }, @@ -23755,6 +23782,6987 @@ "originalText": "The User-specific task can be iterated over to yield a shared or exclusive reference to existing client-specific task with each new iteration.", "autoResize": true, "lineHeight": 1.35 + }, + { + "id": "Jy9q4ysrAAbpXdaiX521F", + "type": "rectangle", + "x": 1268.5980407258883, + "y": 2398.328369542565, + "width": 171.02056522296834, + "height": 73.49554175745014, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b24", + "roundness": null, + "seed": 778731787, + "version": 687, + "versionNonce": 2011576645, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "VV756MSKFVMvOq63JiNpf" + }, + { + "id": "cq055tV0PJ_kX7ISOHUKW", + "type": "arrow" + }, + { + "id": "yMeEUEU3Lphx3vyL3whwt", + "type": "arrow" + }, + { + "id": "sneG026N1n_G0mJImR8vA", + "type": "arrow" + } + ], + "updated": 1727115150280, + "link": null, + "locked": false + }, + { + "id": "VV756MSKFVMvOq63JiNpf", + "type": "text", + "x": 1333.849990004039, + "y": 2424.2761404212897, + "width": 40.516666666666666, + "height": 21.6, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b25", + "roundness": null, + "seed": 1079341541, + "version": 84, + "versionNonce": 76190379, + "isDeleted": false, + "boundElements": null, + "updated": 1727114848645, + "link": null, + "locked": false, + "text": "Inbox", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Jy9q4ysrAAbpXdaiX521F", + "originalText": "Inbox", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "KJ0ryEoixn2-Drv3HAq5o", + "type": "rectangle", + "x": 1453.347392849494, + "y": 2373.3518486475355, + "width": 277.05993563837916, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b26", + "roundness": null, + "seed": 599905189, + "version": 213, + "versionNonce": 652084555, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "hyPdPmvErLq9WZ6GdmO_V" + } + ], + "updated": 1727115197785, + "link": null, + "locked": false + }, + { + "id": "hyPdPmvErLq9WZ6GdmO_V", + "type": "text", + "x": 1465.8356965451485, + "y": 2378.4518486475354, + "width": 252.0833282470703, + "height": 64.80000000000001, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b27", + "roundness": null, + "seed": 684949195, + "version": 240, + "versionNonce": 339122309, + "isDeleted": false, + "boundElements": null, + "updated": 1727115209247, + "link": null, + "locked": false, + "text": "The Inbox distributes messages to \nall clients a user is connected to.\nIt is a tokio::mpsc channel.", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "KJ0ryEoixn2-Drv3HAq5o", + "originalText": "The Inbox distributes messages to all clients a user is connected to.\nIt is a tokio::mpsc channel.", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "cq055tV0PJ_kX7ISOHUKW", + "type": "arrow", + "x": 2047.9288620645198, + "y": 1360.4185088538184, + "width": 695.4056643952301, + "height": 1033.1265971039184, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b28", + "roundness": { + "type": 2 + }, + "seed": 1977842827, + "version": 959, + "versionNonce": 1571775429, + "isDeleted": false, + "boundElements": null, + "updated": 1727115121371, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -133.09199318568994, + 86.50979557069854 + ], + [ + -207.9562393526403, + 232.91098807495746 + ], + [ + -281.1568356047701, + 549.004471890971 + ], + [ + -489.1130749574106, + 821.8430579216356 + ], + [ + -673.7782155025554, + 901.6982538330494 + ], + [ + -695.4056643952301, + 1033.1265971039184 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "oHiI-diNFXNo4nn79ecEY", + "focus": -0.6925371638619507, + "gap": 7.129619964929702, + "fixedPoint": null + }, + "endBinding": { + "elementId": "Jy9q4ysrAAbpXdaiX521F", + "focus": -0.09195704538171595, + "gap": 4.7832635848280916, + "fixedPoint": null + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "yMeEUEU3Lphx3vyL3whwt", + "type": "arrow", + "x": 1355.8824862193296, + "y": 2478.5447016544967, + "width": 185.79785193709245, + "height": 120.86849507735587, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b29", + "roundness": { + "type": 2 + }, + "seed": 1538411947, + "version": 242, + "versionNonce": 1974046635, + "isDeleted": false, + "boundElements": null, + "updated": 1727115146103, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -32.964135021097036, + 41.95435366321453 + ], + [ + -164.8206751054854, + 58.9358777649918 + ], + [ + -185.79785193709245, + 120.86849507735587 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "Jy9q4ysrAAbpXdaiX521F", + "focus": -0.314101445009377, + "gap": 6.720790354481778, + "fixedPoint": null + }, + "endBinding": { + "elementId": "2-xZdr1N7h94XCAl94juo", + "focus": -0.05857119821555177, + "gap": 1, + "fixedPoint": null + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "sneG026N1n_G0mJImR8vA", + "type": "arrow", + "x": 1368.868357591277, + "y": 2475.547962107124, + "width": 736.1990154711671, + "height": 123.8652346247286, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2A", + "roundness": { + "type": 2 + }, + "seed": 2123839301, + "version": 602, + "versionNonce": 1688022021, + "isDeleted": false, + "boundElements": null, + "updated": 1727115176112, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 33.963048203554536, + 29.96739547372499 + ], + [ + 180.80328602480495, + 37.95870093338499 + ], + [ + 663.2783531517707, + 61.93261731236407 + ], + [ + 736.1990154711671, + 123.8652346247286 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "Jy9q4ysrAAbpXdaiX521F", + "focus": 0.2446412333379998, + "gap": 3.7240508071090517, + "fixedPoint": null + }, + "endBinding": { + "elementId": "NEKEWaqq5lWTHOkiGx6DU", + "focus": 0.32422142161568906, + "gap": 1.1771196391496233, + "fixedPoint": null + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "Ih6O2CVqHKg5-szfAXDhu", + "type": "diamond", + "x": 3125.2697698305014, + "y": 420.4025642562489, + "width": 247.39095075069528, + "height": 164.6026404601082, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2C", + "roundness": null, + "seed": 1092419845, + "version": 267, + "versionNonce": 306122347, + "isDeleted": false, + "boundElements": null, + "updated": 1727115557614, + "link": null, + "locked": false + }, + { + "id": "yN5E3yZ8SM7NGSXeA-0oP", + "type": "rectangle", + "x": 3127.217730072633, + "y": 502.2168944257702, + "width": 243.49503026643197, + "height": 161.68070009691098, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2D", + "roundness": null, + "seed": 1654909317, + "version": 121, + "versionNonce": 1108382245, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "Yn7n1nNGxCBncgRMYIJUT" + } + ], + "updated": 1727115585644, + "link": null, + "locked": false + }, + { + "id": "Yn7n1nNGxCBncgRMYIJUT", + "type": "text", + "x": 3223.4069127626117, + "y": 572.2572444742257, + "width": 51.11666488647461, + "height": 21.6, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2DG", + "roundness": null, + "seed": 1536952331, + "version": 9, + "versionNonce": 1177236395, + "isDeleted": false, + "boundElements": null, + "updated": 1727115594963, + "link": null, + "locked": false, + "text": "<-- Viv", + "fontSize": 16, + "fontFamily": 6, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "yN5E3yZ8SM7NGSXeA-0oP", + "originalText": "<-- Viv", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "type": "freedraw", + "version": 649, + "versionNonce": 873076363, + "index": "b2F", + "isDeleted": false, + "id": "WBPMPJ76_7bvOYZIcaHuZ", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3187.552457820839, + "y": 568.3286878640658, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 28, + "height": 22, + "seed": 1716601995, + "groupIds": [ + "-DMu6XEZMLghW5wuD-QkJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727115577484, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -1, + 0 + ], + [ + -2, + 1 + ], + [ + -3, + 1 + ], + [ + -4, + 2 + ], + [ + -5, + 2 + ], + [ + -5, + 3 + ], + [ + -6, + 3 + ], + [ + -6, + 4 + ], + [ + -7, + 4 + ], + [ + -7, + 5 + ], + [ + -8, + 5 + ], + [ + -8, + 6 + ], + [ + -9, + 7 + ], + [ + -9, + 8 + ], + [ + -10, + 9 + ], + [ + -10, + 10 + ], + [ + -10, + 11 + ], + [ + -9, + 13 + ], + [ + -9, + 14 + ], + [ + -9, + 16 + ], + [ + -8, + 16 + ], + [ + -8, + 17 + ], + [ + -7, + 17 + ], + [ + -6, + 18 + ], + [ + -5, + 19 + ], + [ + -4, + 19 + ], + [ + -3, + 20 + ], + [ + -2, + 20 + ], + [ + -1, + 21 + ], + [ + 0, + 21 + ], + [ + 1, + 21 + ], + [ + 2, + 21 + ], + [ + 3, + 21 + ], + [ + 4, + 21 + ], + [ + 5, + 21 + ], + [ + 6, + 21 + ], + [ + 8, + 21 + ], + [ + 9, + 21 + ], + [ + 11, + 21 + ], + [ + 12, + 21 + ], + [ + 14, + 21 + ], + [ + 15, + 20 + ], + [ + 16, + 20 + ], + [ + 16, + 19 + ], + [ + 16, + 18 + ], + [ + 17, + 18 + ], + [ + 17, + 17 + ], + [ + 17, + 16 + ], + [ + 17, + 15 + ], + [ + 18, + 14 + ], + [ + 18, + 13 + ], + [ + 18, + 12 + ], + [ + 18, + 11 + ], + [ + 17, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 7 + ], + [ + 16, + 6 + ], + [ + 16, + 5 + ], + [ + 16, + 4 + ], + [ + 15, + 4 + ], + [ + 15, + 3 + ], + [ + 14, + 3 + ], + [ + 13, + 2 + ], + [ + 12, + 2 + ], + [ + 12, + 1 + ], + [ + 11, + 1 + ], + [ + 10, + 1 + ], + [ + 10, + 0 + ], + [ + 9, + 0 + ], + [ + 8, + 0 + ], + [ + 7, + 0 + ], + [ + 5, + 0 + ], + [ + 4, + -1 + ], + [ + 3, + -1 + ], + [ + 2, + -1 + ], + [ + 1, + -1 + ], + [ + 0, + 0 + ], + [ + -1, + 1 + ], + [ + -1, + 2 + ], + [ + -2, + 2 + ], + [ + -2, + 3 + ], + [ + -3, + 3 + ], + [ + -3, + 4 + ], + [ + -3, + 5 + ], + [ + -3, + 6 + ], + [ + -3, + 7 + ], + [ + -3, + 8 + ], + [ + -3, + 9 + ], + [ + -2, + 9 + ], + [ + -2, + 10 + ], + [ + -1, + 10 + ], + [ + -1, + 11 + ], + [ + 0, + 11 + ], + [ + 1, + 11 + ], + [ + 1, + 12 + ], + [ + 2, + 12 + ], + [ + 3, + 12 + ], + [ + 4, + 13 + ], + [ + 5, + 13 + ], + [ + 5, + 12 + ], + [ + 6, + 12 + ], + [ + 6, + 11 + ], + [ + 6, + 10 + ], + [ + 5, + 9 + ], + [ + 5, + 8 + ], + [ + 4, + 8 + ], + [ + 3, + 8 + ], + [ + 2, + 8 + ], + [ + 2, + 9 + ], + [ + 2, + 10 + ], + [ + 2, + 11 + ], + [ + 3, + 12 + ], + [ + 3, + 13 + ], + [ + 4, + 13 + ], + [ + 4, + 14 + ], + [ + 5, + 14 + ], + [ + 6, + 14 + ], + [ + 7, + 14 + ], + [ + 8, + 14 + ], + [ + 9, + 14 + ], + [ + 9, + 13 + ], + [ + 10, + 13 + ], + [ + 10, + 12 + ], + [ + 10, + 11 + ], + [ + 10, + 10 + ], + [ + 9, + 10 + ], + [ + 8, + 10 + ], + [ + 8, + 10 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 824, + "versionNonce": 2134707499, + "index": "b2G", + "isDeleted": false, + "id": "O5Jggv7DlOH4hWWVqYbvz", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3161.552457820839, + "y": 653.3286878640658, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 58, + "height": 42, + "seed": 2087615275, + "groupIds": [ + "-DMu6XEZMLghW5wuD-QkJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727115577484, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -1 + ], + [ + 0, + -2 + ], + [ + -1, + -2 + ], + [ + -1, + -3 + ], + [ + -1, + -4 + ], + [ + -1, + -5 + ], + [ + -1, + -6 + ], + [ + -1, + -7 + ], + [ + -1, + -8 + ], + [ + -1, + -9 + ], + [ + -1, + -10 + ], + [ + -1, + -11 + ], + [ + -1, + -12 + ], + [ + 0, + -12 + ], + [ + 0, + -13 + ], + [ + 0, + -14 + ], + [ + 0, + -15 + ], + [ + 1, + -16 + ], + [ + 1, + -17 + ], + [ + 1, + -18 + ], + [ + 2, + -19 + ], + [ + 2, + -20 + ], + [ + 3, + -21 + ], + [ + 3, + -22 + ], + [ + 4, + -22 + ], + [ + 4, + -23 + ], + [ + 5, + -24 + ], + [ + 6, + -25 + ], + [ + 7, + -26 + ], + [ + 7, + -27 + ], + [ + 9, + -28 + ], + [ + 9, + -29 + ], + [ + 10, + -29 + ], + [ + 11, + -30 + ], + [ + 11, + -31 + ], + [ + 12, + -31 + ], + [ + 13, + -32 + ], + [ + 14, + -33 + ], + [ + 15, + -34 + ], + [ + 16, + -34 + ], + [ + 16, + -35 + ], + [ + 17, + -35 + ], + [ + 18, + -35 + ], + [ + 19, + -36 + ], + [ + 20, + -36 + ], + [ + 21, + -37 + ], + [ + 22, + -37 + ], + [ + 23, + -38 + ], + [ + 24, + -38 + ], + [ + 24, + -39 + ], + [ + 25, + -39 + ], + [ + 26, + -40 + ], + [ + 27, + -40 + ], + [ + 28, + -41 + ], + [ + 29, + -41 + ], + [ + 30, + -41 + ], + [ + 31, + -42 + ], + [ + 32, + -42 + ], + [ + 33, + -42 + ], + [ + 34, + -42 + ], + [ + 35, + -42 + ], + [ + 36, + -42 + ], + [ + 37, + -42 + ], + [ + 38, + -41 + ], + [ + 40, + -41 + ], + [ + 41, + -40 + ], + [ + 42, + -40 + ], + [ + 43, + -39 + ], + [ + 44, + -39 + ], + [ + 45, + -38 + ], + [ + 46, + -38 + ], + [ + 47, + -38 + ], + [ + 47, + -37 + ], + [ + 48, + -36 + ], + [ + 48, + -35 + ], + [ + 49, + -34 + ], + [ + 49, + -33 + ], + [ + 50, + -32 + ], + [ + 50, + -30 + ], + [ + 51, + -30 + ], + [ + 52, + -29 + ], + [ + 52, + -28 + ], + [ + 53, + -27 + ], + [ + 53, + -26 + ], + [ + 53, + -25 + ], + [ + 54, + -25 + ], + [ + 54, + -24 + ], + [ + 54, + -23 + ], + [ + 54, + -22 + ], + [ + 55, + -21 + ], + [ + 55, + -20 + ], + [ + 55, + -19 + ], + [ + 55, + -18 + ], + [ + 55, + -17 + ], + [ + 56, + -17 + ], + [ + 56, + -16 + ], + [ + 56, + -15 + ], + [ + 56, + -14 + ], + [ + 56, + -13 + ], + [ + 56, + -12 + ], + [ + 56, + -11 + ], + [ + 56, + -10 + ], + [ + 56, + -9 + ], + [ + 56, + -8 + ], + [ + 56, + -7 + ], + [ + 56, + -6 + ], + [ + 56, + -5 + ], + [ + 57, + -4 + ], + [ + 57, + -3 + ], + [ + 57, + -2 + ], + [ + 56, + -2 + ], + [ + 55, + -2 + ], + [ + 54, + -2 + ], + [ + 52, + -2 + ], + [ + 51, + -2 + ], + [ + 50, + -2 + ], + [ + 48, + -2 + ], + [ + 47, + -2 + ], + [ + 45, + -2 + ], + [ + 44, + -1 + ], + [ + 42, + -1 + ], + [ + 41, + -1 + ], + [ + 40, + -1 + ], + [ + 39, + 0 + ], + [ + 38, + 0 + ], + [ + 37, + 0 + ], + [ + 36, + 0 + ], + [ + 35, + 0 + ], + [ + 33, + 0 + ], + [ + 32, + 0 + ], + [ + 31, + 0 + ], + [ + 29, + 0 + ], + [ + 28, + 0 + ], + [ + 26, + 0 + ], + [ + 25, + 0 + ], + [ + 24, + 0 + ], + [ + 23, + 0 + ], + [ + 22, + 0 + ], + [ + 21, + 0 + ], + [ + 20, + 0 + ], + [ + 19, + 0 + ], + [ + 18, + 0 + ], + [ + 17, + 0 + ], + [ + 16, + 0 + ], + [ + 14, + 0 + ], + [ + 13, + 0 + ], + [ + 12, + 0 + ], + [ + 11, + 0 + ], + [ + 10, + 0 + ], + [ + 9, + -1 + ], + [ + 7, + -1 + ], + [ + 6, + -1 + ], + [ + 5, + -1 + ], + [ + 4, + -1 + ], + [ + 4, + -2 + ], + [ + 4, + -3 + ], + [ + 4, + -4 + ], + [ + 4, + -5 + ], + [ + 5, + -6 + ], + [ + 5, + -7 + ], + [ + 5, + -8 + ], + [ + 5, + -9 + ], + [ + 5, + -10 + ], + [ + 6, + -11 + ], + [ + 6, + -12 + ], + [ + 7, + -13 + ], + [ + 7, + -14 + ], + [ + 8, + -15 + ], + [ + 9, + -16 + ], + [ + 10, + -17 + ], + [ + 11, + -18 + ], + [ + 11, + -19 + ], + [ + 12, + -19 + ], + [ + 13, + -20 + ], + [ + 14, + -21 + ], + [ + 15, + -22 + ], + [ + 16, + -22 + ], + [ + 17, + -23 + ], + [ + 18, + -24 + ], + [ + 19, + -24 + ], + [ + 19, + -25 + ], + [ + 20, + -25 + ], + [ + 20, + -26 + ], + [ + 21, + -27 + ], + [ + 22, + -27 + ], + [ + 23, + -27 + ], + [ + 23, + -28 + ], + [ + 24, + -28 + ], + [ + 25, + -28 + ], + [ + 26, + -29 + ], + [ + 27, + -29 + ], + [ + 28, + -29 + ], + [ + 29, + -30 + ], + [ + 30, + -30 + ], + [ + 31, + -30 + ], + [ + 32, + -30 + ], + [ + 33, + -30 + ], + [ + 34, + -30 + ], + [ + 35, + -30 + ], + [ + 36, + -29 + ], + [ + 36, + -28 + ], + [ + 37, + -27 + ], + [ + 38, + -26 + ], + [ + 38, + -25 + ], + [ + 39, + -24 + ], + [ + 40, + -23 + ], + [ + 40, + -22 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 42, + -20 + ], + [ + 42, + -19 + ], + [ + 42, + -18 + ], + [ + 42, + -17 + ], + [ + 41, + -17 + ], + [ + 41, + -16 + ], + [ + 40, + -16 + ], + [ + 40, + -15 + ], + [ + 39, + -15 + ], + [ + 38, + -14 + ], + [ + 36, + -14 + ], + [ + 35, + -14 + ], + [ + 33, + -14 + ], + [ + 32, + -14 + ], + [ + 30, + -13 + ], + [ + 29, + -12 + ], + [ + 26, + -12 + ], + [ + 24, + -11 + ], + [ + 23, + -11 + ], + [ + 21, + -11 + ], + [ + 20, + -11 + ], + [ + 19, + -11 + ], + [ + 18, + -11 + ], + [ + 17, + -11 + ], + [ + 16, + -11 + ], + [ + 15, + -11 + ], + [ + 14, + -12 + ], + [ + 14, + -13 + ], + [ + 15, + -14 + ], + [ + 16, + -14 + ], + [ + 17, + -15 + ], + [ + 18, + -15 + ], + [ + 20, + -16 + ], + [ + 21, + -16 + ], + [ + 23, + -16 + ], + [ + 24, + -16 + ], + [ + 26, + -17 + ], + [ + 27, + -17 + ], + [ + 29, + -17 + ], + [ + 30, + -17 + ], + [ + 34, + -17 + ], + [ + 37, + -17 + ], + [ + 40, + -16 + ], + [ + 41, + -15 + ], + [ + 41, + -14 + ], + [ + 41, + -13 + ], + [ + 40, + -13 + ], + [ + 40, + -12 + ], + [ + 39, + -12 + ], + [ + 38, + -12 + ], + [ + 37, + -12 + ], + [ + 36, + -12 + ], + [ + 34, + -13 + ], + [ + 33, + -13 + ], + [ + 32, + -14 + ], + [ + 31, + -14 + ], + [ + 30, + -14 + ], + [ + 29, + -14 + ], + [ + 29, + -15 + ], + [ + 29, + -16 + ], + [ + 29, + -17 + ], + [ + 29, + -18 + ], + [ + 30, + -19 + ], + [ + 31, + -19 + ], + [ + 33, + -20 + ], + [ + 35, + -20 + ], + [ + 37, + -20 + ], + [ + 38, + -20 + ], + [ + 41, + -21 + ], + [ + 42, + -21 + ], + [ + 43, + -21 + ], + [ + 44, + -21 + ], + [ + 45, + -21 + ], + [ + 45, + -20 + ], + [ + 45, + -19 + ], + [ + 45, + -18 + ], + [ + 45, + -17 + ], + [ + 45, + -16 + ], + [ + 45, + -15 + ], + [ + 45, + -14 + ], + [ + 45, + -13 + ], + [ + 45, + -12 + ], + [ + 44, + -12 + ], + [ + 44, + -11 + ], + [ + 44, + -12 + ], + [ + 45, + -12 + ], + [ + 46, + -12 + ], + [ + 47, + -13 + ], + [ + 48, + -13 + ], + [ + 49, + -13 + ], + [ + 50, + -13 + ], + [ + 50, + -12 + ], + [ + 50, + -11 + ], + [ + 50, + -11 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "text", + "version": 253, + "versionNonce": 1373954565, + "index": "b2H", + "isDeleted": false, + "id": "iwJeId6hgp7L3UL4Vq-vc", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3221.943278910431, + "y": 676.3868987599815, + "strokeColor": "#1e1e1e", + "backgroundColor": "#a5d8ff", + "width": 71.28333282470703, + "height": 21.6, + "seed": 1208291851, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727115630886, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Viv house", + "textAlign": "right", + "verticalAlign": "top", + "containerId": null, + "originalText": "Viv house", + "autoResize": true, + "lineHeight": 1.35 + }, + { + "id": "8jJDFpFP1mZtt156CKJsj", + "type": "freedraw", + "x": 3184.7458310397383, + "y": 557.2867080427628, + "width": 6.204837356517146, + "height": 6.313694152245603, + "angle": 0, + "strokeColor": "#9c36b5", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2I", + "roundness": null, + "seed": 985483685, + "version": 173, + "versionNonce": 721495275, + "isDeleted": false, + "boundElements": null, + "updated": 1727116184378, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.10885679572834306 + ], + [ + 0, + 0.2177135914567998 + ], + [ + 0, + 0.32657038718514286 + ], + [ + 0, + 0.4354271829134859 + ], + [ + 0, + 0.544283978641829 + ], + [ + 0, + 0.6531407743702857 + ], + [ + 0, + 0.7619975700986288 + ], + [ + 0, + 0.8708543658269718 + ], + [ + 0, + 1.0885679572837716 + ], + [ + 0, + 1.1974247530121147 + ], + [ + 0, + 1.3062815487404578 + ], + [ + 0.10885679572857043, + 1.4151383444688008 + ], + [ + 0.10885679572857043, + 1.5239951401972576 + ], + [ + 0.10885679572857043, + 1.6328519359256006 + ], + [ + 0.10885679572857043, + 1.7417087316539437 + ], + [ + 0.10885679572857043, + 1.8505655273822867 + ], + [ + 0.10885679572857043, + 1.9594223231107435 + ], + [ + 0.10885679572857043, + 2.0682791188390865 + ], + [ + 0.21771359145668612, + 2.1771359145674296 + ], + [ + 0.21771359145668612, + 2.2859927102957727 + ], + [ + 0.21771359145668612, + 2.3948495060242294 + ], + [ + 0.21771359145668612, + 2.5037063017525725 + ], + [ + 0.21771359145668612, + 2.6125630974809155 + ], + [ + 0.21771359145668612, + 2.7214198932092586 + ], + [ + 0.10885679572857043, + 2.8302766889377153 + ], + [ + 0.10885679572857043, + 2.9391334846660584 + ], + [ + 0.10885679572857043, + 3.0479902803944015 + ], + [ + 0.10885679572857043, + 3.1568470761227445 + ], + [ + 0.10885679572857043, + 3.2657038718512013 + ], + [ + 0.10885679572857043, + 3.3745606675795443 + ], + [ + 0.10885679572857043, + 3.4834174633078874 + ], + [ + 0.10885679572857043, + 3.592274259036344 + ], + [ + 0, + 3.592274259036344 + ], + [ + 0, + 3.8099878504930302 + ], + [ + 0, + 3.9188446462213733 + ], + [ + 0, + 4.136558237678173 + ], + [ + 0, + 4.245415033406516 + ], + [ + 0, + 4.354271829134859 + ], + [ + 0, + 4.463128624863316 + ], + [ + 0, + 4.571985420591659 + ], + [ + 0, + 4.680842216320002 + ], + [ + 0, + 4.789699012048345 + ], + [ + 0, + 4.898555807776802 + ], + [ + 0, + 5.007412603505145 + ], + [ + 0, + 5.116269399233488 + ], + [ + 0, + 5.225126194961831 + ], + [ + 0, + 5.333982990690288 + ], + [ + 0, + 5.442839786418631 + ], + [ + 0, + 5.551696582146974 + ], + [ + 0.10885679572857043, + 5.551696582146974 + ], + [ + 0.10885679572857043, + 5.442839786418631 + ], + [ + 0.21771359145668612, + 5.442839786418631 + ], + [ + 0.32657038718525655, + 5.442839786418631 + ], + [ + 0.43542718291337223, + 5.333982990690288 + ], + [ + 0.5442839786419427, + 5.333982990690288 + ], + [ + 0.6531407743705131, + 5.333982990690288 + ], + [ + 0.7619975700986288, + 5.333982990690288 + ], + [ + 0.9797111615553149, + 5.333982990690288 + ], + [ + 1.197424753012001, + 5.333982990690288 + ], + [ + 1.3062815487405715, + 5.225126194961831 + ], + [ + 1.4151383444691419, + 5.225126194961831 + ], + [ + 1.632851935925828, + 5.225126194961831 + ], + [ + 1.7417087316539437, + 5.225126194961831 + ], + [ + 1.9594223231106298, + 5.225126194961831 + ], + [ + 2.0682791188392002, + 5.225126194961831 + ], + [ + 2.177135914567316, + 5.225126194961831 + ], + [ + 2.2859927102958864, + 5.225126194961831 + ], + [ + 2.394849506024457, + 5.116269399233488 + ], + [ + 2.5037063017525725, + 5.116269399233488 + ], + [ + 2.612563097481143, + 5.116269399233488 + ], + [ + 2.612563097481143, + 5.007412603505145 + ], + [ + 2.7214198932092586, + 5.007412603505145 + ], + [ + 2.830276688937829, + 5.007412603505145 + ], + [ + 2.9391334846659447, + 5.007412603505145 + ], + [ + 3.047990280394515, + 4.898555807776802 + ], + [ + 3.1568470761230856, + 4.898555807776802 + ], + [ + 3.2657038718512013, + 4.789699012048345 + ], + [ + 3.4834174633078874, + 4.789699012048345 + ], + [ + 3.592274259036458, + 4.680842216320002 + ], + [ + 3.7011310547645735, + 4.680842216320002 + ], + [ + 3.809987850493144, + 4.680842216320002 + ], + [ + 3.9188446462212596, + 4.571985420591659 + ], + [ + 4.02770144194983, + 4.571985420591659 + ], + [ + 4.02770144194983, + 4.463128624863316 + ], + [ + 4.1365582376784005, + 4.463128624863316 + ], + [ + 4.245415033406516, + 4.463128624863316 + ], + [ + 4.245415033406516, + 4.354271829134859 + ], + [ + 4.354271829135087, + 4.354271829134859 + ], + [ + 4.463128624863202, + 4.354271829134859 + ], + [ + 4.463128624863202, + 4.245415033406516 + ], + [ + 4.571985420591773, + 4.245415033406516 + ], + [ + 4.680842216319888, + 4.245415033406516 + ], + [ + 4.789699012048459, + 4.245415033406516 + ], + [ + 4.789699012048459, + 4.136558237678173 + ], + [ + 4.789699012048459, + 4.02770144194983 + ], + [ + 4.898555807777029, + 4.02770144194983 + ], + [ + 4.898555807777029, + 3.9188446462213733 + ], + [ + 5.007412603505145, + 3.9188446462213733 + ], + [ + 5.007412603505145, + 3.8099878504930302 + ], + [ + 5.116269399233715, + 3.8099878504930302 + ], + [ + 5.116269399233715, + 3.701131054764687 + ], + [ + 5.116269399233715, + 3.592274259036344 + ], + [ + 5.225126194961831, + 3.592274259036344 + ], + [ + 5.225126194961831, + 3.4834174633078874 + ], + [ + 5.3339829906904015, + 3.4834174633078874 + ], + [ + 5.3339829906904015, + 3.3745606675795443 + ], + [ + 5.442839786418517, + 3.3745606675795443 + ], + [ + 5.551696582147088, + 3.2657038718512013 + ], + [ + 5.660553377875203, + 3.2657038718512013 + ], + [ + 5.660553377875203, + 3.1568470761227445 + ], + [ + 5.769410173603774, + 3.1568470761227445 + ], + [ + 5.769410173603774, + 3.0479902803944015 + ], + [ + 5.878266969332344, + 3.0479902803944015 + ], + [ + 5.98712376506046, + 2.9391334846660584 + ], + [ + 6.09598056078903, + 2.8302766889377153 + ], + [ + 6.204837356517146, + 2.8302766889377153 + ], + [ + 6.204837356517146, + 2.7214198932092586 + ], + [ + 6.204837356517146, + 2.6125630974809155 + ], + [ + 6.09598056078903, + 2.6125630974809155 + ], + [ + 5.98712376506046, + 2.6125630974809155 + ], + [ + 5.98712376506046, + 2.5037063017525725 + ], + [ + 5.878266969332344, + 2.5037063017525725 + ], + [ + 5.878266969332344, + 2.3948495060242294 + ], + [ + 5.769410173603774, + 2.3948495060242294 + ], + [ + 5.660553377875203, + 2.2859927102957727 + ], + [ + 5.551696582147088, + 2.2859927102957727 + ], + [ + 5.442839786418517, + 2.1771359145674296 + ], + [ + 5.3339829906904015, + 2.0682791188390865 + ], + [ + 5.225126194961831, + 2.0682791188390865 + ], + [ + 5.225126194961831, + 1.9594223231107435 + ], + [ + 5.116269399233715, + 1.9594223231107435 + ], + [ + 5.116269399233715, + 1.8505655273822867 + ], + [ + 5.007412603505145, + 1.8505655273822867 + ], + [ + 5.007412603505145, + 1.7417087316539437 + ], + [ + 4.898555807777029, + 1.7417087316539437 + ], + [ + 4.898555807777029, + 1.6328519359256006 + ], + [ + 4.789699012048459, + 1.6328519359256006 + ], + [ + 4.680842216319888, + 1.5239951401972576 + ], + [ + 4.571985420591773, + 1.4151383444688008 + ], + [ + 4.463128624863202, + 1.4151383444688008 + ], + [ + 4.354271829135087, + 1.3062815487404578 + ], + [ + 4.245415033406516, + 1.1974247530121147 + ], + [ + 4.1365582376784005, + 1.0885679572837716 + ], + [ + 4.02770144194983, + 1.0885679572837716 + ], + [ + 4.02770144194983, + 0.9797111615553149 + ], + [ + 3.9188446462212596, + 0.8708543658269718 + ], + [ + 3.809987850493144, + 0.7619975700986288 + ], + [ + 3.592274259036458, + 0.6531407743702857 + ], + [ + 3.592274259036458, + 0.544283978641829 + ], + [ + 3.4834174633078874, + 0.544283978641829 + ], + [ + 3.4834174633078874, + 0.4354271829134859 + ], + [ + 3.3745606675797717, + 0.32657038718514286 + ], + [ + 3.2657038718512013, + 0.32657038718514286 + ], + [ + 3.2657038718512013, + 0.2177135914567998 + ], + [ + 3.1568470761230856, + 0.2177135914567998 + ], + [ + 3.047990280394515, + 0.10885679572834306 + ], + [ + 2.9391334846659447, + 0 + ], + [ + 2.830276688937829, + 0 + ], + [ + 2.830276688937829, + -0.10885679572834306 + ], + [ + 2.7214198932092586, + -0.10885679572834306 + ], + [ + 2.7214198932092586, + -0.21771359145668612 + ], + [ + 2.612563097481143, + -0.21771359145668612 + ], + [ + 2.612563097481143, + -0.32657038718514286 + ], + [ + 2.5037063017525725, + -0.32657038718514286 + ], + [ + 2.394849506024457, + -0.4354271829134859 + ], + [ + 2.2859927102958864, + -0.544283978641829 + ], + [ + 2.177135914567316, + -0.653140774370172 + ], + [ + 2.0682791188392002, + -0.653140774370172 + ], + [ + 1.9594223231106298, + -0.7619975700986288 + ], + [ + 1.8505655273825141, + -0.7619975700986288 + ], + [ + 1.8505655273825141, + -0.7619975700986288 + ] + ], + "pressures": [], + "simulatePressure": true, + "lastCommittedPoint": [ + 1.8505655273825141, + -0.7619975700986288 + ] + }, + { + "id": "PI9kEnqaE5B3YoSCi-sm6", + "type": "freedraw", + "x": 3194.6517994510205, + "y": 559.3549871616019, + "width": 2.5037063017525725, + "height": 2.6125630974809155, + "angle": 0, + "strokeColor": "#9c36b5", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2J", + "roundness": null, + "seed": 1262715557, + "version": 88, + "versionNonce": 2103541541, + "isDeleted": false, + "boundElements": null, + "updated": 1727116186329, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.10885679572857043, + 0 + ], + [ + -0.21771359145714086, + 0 + ], + [ + -0.32657038718525655, + 0 + ], + [ + -0.435427182913827, + 0 + ], + [ + -0.5442839786419427, + 0 + ], + [ + -0.6531407743705131, + 0 + ], + [ + -0.7619975700986288, + 0 + ], + [ + -0.8708543658271992, + 0 + ], + [ + -0.9797111615557696, + 0 + ], + [ + -1.0885679572838853, + 0 + ], + [ + -1.0885679572838853, + 0.10885679572834306 + ], + [ + -1.0885679572838853, + 0.21771359145668612 + ], + [ + -1.1974247530124558, + 0.32657038718514286 + ], + [ + -1.1974247530124558, + 0.4354271829134859 + ], + [ + -1.1974247530124558, + 0.544283978641829 + ], + [ + -1.1974247530124558, + 0.653140774370172 + ], + [ + -1.1974247530124558, + 0.7619975700986288 + ], + [ + -1.1974247530124558, + 0.8708543658269718 + ], + [ + -1.1974247530124558, + 0.9797111615553149 + ], + [ + -1.1974247530124558, + 1.088567957283658 + ], + [ + -1.0885679572838853, + 1.1974247530121147 + ], + [ + -1.0885679572838853, + 1.3062815487404578 + ], + [ + -0.9797111615557696, + 1.3062815487404578 + ], + [ + -0.9797111615557696, + 1.4151383444688008 + ], + [ + -0.8708543658271992, + 1.4151383444688008 + ], + [ + -0.7619975700986288, + 1.5239951401972576 + ], + [ + -0.7619975700986288, + 1.6328519359256006 + ], + [ + -0.6531407743705131, + 1.7417087316539437 + ], + [ + -0.5442839786419427, + 1.7417087316539437 + ], + [ + -0.435427182913827, + 1.8505655273822867 + ], + [ + -0.435427182913827, + 1.9594223231107435 + ], + [ + -0.32657038718525655, + 1.9594223231107435 + ], + [ + -0.21771359145714086, + 2.0682791188390865 + ], + [ + 0, + 2.0682791188390865 + ], + [ + 0, + 2.1771359145674296 + ], + [ + 0.21771359145668612, + 2.1771359145674296 + ], + [ + 0.3265703871848018, + 2.1771359145674296 + ], + [ + 0.5442839786414879, + 2.1771359145674296 + ], + [ + 0.6531407743700584, + 2.1771359145674296 + ], + [ + 0.761997570098174, + 2.1771359145674296 + ], + [ + 0.8708543658267445, + 2.0682791188390865 + ], + [ + 0.9797111615553149, + 2.0682791188390865 + ], + [ + 0.9797111615553149, + 1.9594223231107435 + ], + [ + 1.0885679572834306, + 1.9594223231107435 + ], + [ + 1.0885679572834306, + 1.8505655273822867 + ], + [ + 1.0885679572834306, + 1.7417087316539437 + ], + [ + 1.197424753012001, + 1.5239951401972576 + ], + [ + 1.197424753012001, + 1.4151383444688008 + ], + [ + 1.197424753012001, + 1.3062815487404578 + ], + [ + 1.3062815487401167, + 1.1974247530121147 + ], + [ + 1.3062815487401167, + 1.088567957283658 + ], + [ + 1.3062815487401167, + 0.9797111615553149 + ], + [ + 1.3062815487401167, + 0.8708543658269718 + ], + [ + 1.3062815487401167, + 0.7619975700986288 + ], + [ + 1.3062815487401167, + 0.653140774370172 + ], + [ + 1.3062815487401167, + 0.544283978641829 + ], + [ + 1.3062815487401167, + 0.4354271829134859 + ], + [ + 1.3062815487401167, + 0.32657038718514286 + ], + [ + 1.197424753012001, + 0.21771359145668612 + ], + [ + 1.197424753012001, + 0.10885679572834306 + ], + [ + 1.197424753012001, + 0 + ], + [ + 1.0885679572834306, + 0 + ], + [ + 1.0885679572834306, + -0.10885679572834306 + ], + [ + 0.9797111615553149, + -0.10885679572834306 + ], + [ + 0.9797111615553149, + -0.2177135914567998 + ], + [ + 0.8708543658267445, + -0.2177135914567998 + ], + [ + 0.761997570098174, + -0.32657038718514286 + ], + [ + 0.6531407743700584, + -0.32657038718514286 + ], + [ + 0.5442839786414879, + -0.4354271829134859 + ], + [ + 0.3265703871848018, + -0.4354271829134859 + ], + [ + 0.21771359145668612, + -0.4354271829134859 + ], + [ + 0.10885679572811569, + -0.4354271829134859 + ], + [ + 0, + -0.4354271829134859 + ], + [ + -0.10885679572857043, + -0.4354271829134859 + ], + [ + -0.21771359145714086, + -0.4354271829134859 + ], + [ + -0.32657038718525655, + -0.4354271829134859 + ], + [ + -0.435427182913827, + -0.4354271829134859 + ], + [ + -0.5442839786419427, + -0.4354271829134859 + ], + [ + -0.6531407743705131, + -0.4354271829134859 + ], + [ + -0.7619975700986288, + -0.4354271829134859 + ], + [ + -0.7619975700986288, + -0.32657038718514286 + ], + [ + -0.8708543658271992, + -0.32657038718514286 + ], + [ + -0.8708543658271992, + -0.2177135914567998 + ], + [ + -0.8708543658271992, + -0.10885679572834306 + ], + [ + -0.8708543658271992, + 0 + ], + [ + -0.8708543658271992, + 0 + ] + ], + "pressures": [], + "simulatePressure": true, + "lastCommittedPoint": [ + -0.8708543658271992, + 0 + ] + }, + { + "id": "AVC9tB7fxXyBXB1cMYTrp", + "type": "freedraw", + "x": 3197.5909329356864, + "y": 559.4638439573303, + "width": 6.966834926615775, + "height": 9.688254819825147, + "angle": 0, + "strokeColor": "#9c36b5", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2K", + "roundness": null, + "seed": 406307915, + "version": 181, + "versionNonce": 23559589, + "isDeleted": false, + "boundElements": null, + "updated": 1727116188694, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.10885679572811569, + -0.10885679572834306 + ], + [ + 0.21771359145668612, + -0.10885679572834306 + ], + [ + 0.21771359145668612, + -0.21771359145668612 + ], + [ + 0.3265703871848018, + -0.21771359145668612 + ], + [ + 0.3265703871848018, + -0.32657038718514286 + ], + [ + 0.43542718291337223, + -0.32657038718514286 + ], + [ + 0.43542718291337223, + -0.4354271829134859 + ], + [ + 0.5442839786419427, + -0.544283978641829 + ], + [ + 0.5442839786419427, + -0.653140774370172 + ], + [ + 0.6531407743700584, + -0.653140774370172 + ], + [ + 0.7619975700986288, + -0.7619975700986288 + ], + [ + 0.8708543658267445, + -0.8708543658269718 + ], + [ + 0.9797111615553149, + -0.9797111615553149 + ], + [ + 1.0885679572834306, + -1.088567957283658 + ], + [ + 1.197424753012001, + -1.088567957283658 + ], + [ + 1.4151383444686871, + -1.1974247530121147 + ], + [ + 1.5239951401972576, + -1.3062815487404578 + ], + [ + 1.6328519359253733, + -1.4151383444688008 + ], + [ + 1.7417087316539437, + -1.4151383444688008 + ], + [ + 1.9594223231106298, + -1.6328519359256006 + ], + [ + 2.0682791188387455, + -1.6328519359256006 + ], + [ + 2.177135914567316, + -1.7417087316539437 + ], + [ + 2.2859927102958864, + -1.8505655273822867 + ], + [ + 2.394849506024002, + -1.9594223231106298 + ], + [ + 2.5037063017525725, + -1.9594223231106298 + ], + [ + 2.5037063017525725, + -2.0682791188390865 + ], + [ + 2.612563097480688, + -2.0682791188390865 + ], + [ + 2.7214198932092586, + -2.1771359145674296 + ], + [ + 2.8302766889373743, + -2.2859927102957727 + ], + [ + 2.9391334846659447, + -2.2859927102957727 + ], + [ + 3.047990280394515, + -2.2859927102957727 + ], + [ + 3.156847076122631, + -2.3948495060241157 + ], + [ + 3.2657038718512013, + -2.3948495060241157 + ], + [ + 3.374560667579317, + -2.3948495060241157 + ], + [ + 3.4834174633078874, + -2.3948495060241157 + ], + [ + 3.592274259036003, + -2.5037063017525725 + ], + [ + 3.7011310547645735, + -2.5037063017525725 + ], + [ + 3.809987850492689, + -2.6125630974809155 + ], + [ + 3.9188446462212596, + -2.6125630974809155 + ], + [ + 4.02770144194983, + -2.6125630974809155 + ], + [ + 4.136558237677946, + -2.7214198932092586 + ], + [ + 4.245415033406516, + -2.8302766889376016 + ], + [ + 4.354271829134632, + -2.9391334846660584 + ], + [ + 4.463128624863202, + -2.9391334846660584 + ], + [ + 4.680842216319888, + -3.0479902803944015 + ], + [ + 4.789699012048459, + -3.1568470761227445 + ], + [ + 5.007412603505145, + -3.2657038718512013 + ], + [ + 5.116269399233261, + -3.3745606675795443 + ], + [ + 5.225126194961831, + -3.4834174633078874 + ], + [ + 5.333982990689947, + -3.4834174633078874 + ], + [ + 5.551696582147088, + -3.5922742590362304 + ], + [ + 5.769410173603774, + -3.701131054764687 + ], + [ + 5.878266969331889, + -3.8099878504930302 + ], + [ + 5.98712376506046, + -3.9188446462213733 + ], + [ + 6.0959805607885755, + -3.9188446462213733 + ], + [ + 6.204837356517146, + -4.027701441949716 + ], + [ + 6.313694152245262, + -4.027701441949716 + ], + [ + 6.313694152245262, + -4.136558237678173 + ], + [ + 6.422550947973832, + -4.136558237678173 + ], + [ + 6.422550947973832, + -4.245415033406516 + ], + [ + 6.5314077437024025, + -4.245415033406516 + ], + [ + 6.640264539430518, + -4.354271829134859 + ], + [ + 6.749121335159089, + -4.463128624863202 + ], + [ + 6.857978130887204, + -4.463128624863202 + ], + [ + 6.857978130887204, + -4.571985420591659 + ], + [ + 6.966834926615775, + -4.571985420591659 + ], + [ + 6.966834926615775, + -4.463128624863202 + ], + [ + 6.966834926615775, + -4.354271829134859 + ], + [ + 6.966834926615775, + -4.136558237678173 + ], + [ + 6.966834926615775, + -4.027701441949716 + ], + [ + 6.857978130887204, + -3.8099878504930302 + ], + [ + 6.749121335159089, + -3.701131054764687 + ], + [ + 6.749121335159089, + -3.5922742590362304 + ], + [ + 6.749121335159089, + -3.3745606675795443 + ], + [ + 6.640264539430518, + -3.2657038718512013 + ], + [ + 6.5314077437024025, + -2.9391334846660584 + ], + [ + 6.5314077437024025, + -2.8302766889376016 + ], + [ + 6.422550947973832, + -2.6125630974809155 + ], + [ + 6.313694152245262, + -2.3948495060241157 + ], + [ + 6.313694152245262, + -2.2859927102957727 + ], + [ + 6.204837356517146, + -2.1771359145674296 + ], + [ + 6.0959805607885755, + -1.8505655273822867 + ], + [ + 6.0959805607885755, + -1.5239951401971439 + ], + [ + 5.98712376506046, + -1.4151383444688008 + ], + [ + 5.98712376506046, + -1.1974247530121147 + ], + [ + 5.98712376506046, + -1.088567957283658 + ], + [ + 5.878266969331889, + -0.7619975700986288 + ], + [ + 5.878266969331889, + -0.653140774370172 + ], + [ + 5.878266969331889, + -0.4354271829134859 + ], + [ + 5.878266969331889, + -0.32657038718514286 + ], + [ + 5.878266969331889, + -0.10885679572834306 + ], + [ + 5.769410173603774, + 0 + ], + [ + 5.769410173603774, + 0.32657038718514286 + ], + [ + 5.769410173603774, + 0.6531407743702857 + ], + [ + 5.769410173603774, + 0.8708543658269718 + ], + [ + 5.769410173603774, + 0.9797111615553149 + ], + [ + 5.769410173603774, + 1.1974247530121147 + ], + [ + 5.769410173603774, + 1.3062815487404578 + ], + [ + 5.660553377875203, + 1.4151383444689145 + ], + [ + 5.660553377875203, + 1.5239951401972576 + ], + [ + 5.660553377875203, + 1.6328519359256006 + ], + [ + 5.660553377875203, + 1.7417087316539437 + ], + [ + 5.660553377875203, + 1.8505655273824004 + ], + [ + 5.660553377875203, + 1.9594223231107435 + ], + [ + 5.660553377875203, + 2.0682791188390865 + ], + [ + 5.660553377875203, + 2.1771359145674296 + ], + [ + 5.660553377875203, + 2.2859927102958864 + ], + [ + 5.660553377875203, + 2.3948495060242294 + ], + [ + 5.660553377875203, + 2.5037063017525725 + ], + [ + 5.660553377875203, + 2.6125630974809155 + ], + [ + 5.660553377875203, + 2.7214198932093723 + ], + [ + 5.660553377875203, + 2.8302766889377153 + ], + [ + 5.551696582147088, + 2.9391334846660584 + ], + [ + 5.551696582147088, + 3.0479902803944015 + ], + [ + 5.551696582147088, + 3.156847076122858 + ], + [ + 5.551696582147088, + 3.2657038718512013 + ], + [ + 5.551696582147088, + 3.3745606675795443 + ], + [ + 5.551696582147088, + 3.4834174633078874 + ], + [ + 5.551696582147088, + 3.592274259036344 + ], + [ + 5.551696582147088, + 3.701131054764687 + ], + [ + 5.551696582147088, + 3.8099878504930302 + ], + [ + 5.442839786418517, + 3.8099878504930302 + ], + [ + 5.442839786418517, + 3.9188446462213733 + ], + [ + 5.442839786418517, + 4.02770144194983 + ], + [ + 5.442839786418517, + 4.136558237678173 + ], + [ + 5.442839786418517, + 4.245415033406516 + ], + [ + 5.442839786418517, + 4.354271829134859 + ], + [ + 5.333982990689947, + 4.463128624863316 + ], + [ + 5.333982990689947, + 4.571985420591659 + ], + [ + 5.333982990689947, + 4.680842216320002 + ], + [ + 5.333982990689947, + 4.789699012048459 + ], + [ + 5.333982990689947, + 4.898555807776802 + ], + [ + 5.333982990689947, + 5.007412603505145 + ], + [ + 5.333982990689947, + 5.116269399233488 + ], + [ + 5.225126194961831, + 5.116269399233488 + ], + [ + 5.116269399233261, + 5.007412603505145 + ], + [ + 5.007412603505145, + 4.898555807776802 + ], + [ + 4.8985558077765745, + 4.789699012048459 + ], + [ + 4.8985558077765745, + 4.680842216320002 + ], + [ + 4.789699012048459, + 4.680842216320002 + ], + [ + 4.680842216319888, + 4.571985420591659 + ], + [ + 4.571985420591318, + 4.463128624863316 + ], + [ + 4.463128624863202, + 4.354271829134859 + ], + [ + 4.463128624863202, + 4.245415033406516 + ], + [ + 4.354271829134632, + 4.136558237678173 + ], + [ + 4.245415033406516, + 4.02770144194983 + ], + [ + 4.245415033406516, + 3.9188446462213733 + ], + [ + 4.02770144194983, + 3.8099878504930302 + ], + [ + 4.02770144194983, + 3.701131054764687 + ], + [ + 3.9188446462212596, + 3.592274259036344 + ], + [ + 3.809987850492689, + 3.4834174633078874 + ], + [ + 3.7011310547645735, + 3.3745606675795443 + ], + [ + 3.4834174633078874, + 3.2657038718512013 + ], + [ + 3.4834174633078874, + 3.156847076122858 + ], + [ + 3.2657038718512013, + 3.0479902803944015 + ], + [ + 3.156847076122631, + 2.9391334846660584 + ], + [ + 3.047990280394515, + 2.8302766889377153 + ], + [ + 2.8302766889373743, + 2.7214198932093723 + ], + [ + 2.7214198932092586, + 2.6125630974809155 + ], + [ + 2.7214198932092586, + 2.5037063017525725 + ], + [ + 2.612563097480688, + 2.5037063017525725 + ], + [ + 2.5037063017525725, + 2.5037063017525725 + ], + [ + 2.5037063017525725, + 2.3948495060242294 + ], + [ + 2.394849506024002, + 2.3948495060242294 + ], + [ + 2.394849506024002, + 2.2859927102958864 + ], + [ + 2.2859927102958864, + 2.2859927102958864 + ], + [ + 2.177135914567316, + 2.1771359145674296 + ], + [ + 2.0682791188387455, + 2.1771359145674296 + ], + [ + 1.9594223231106298, + 2.0682791188390865 + ], + [ + 1.8505655273820594, + 1.9594223231107435 + ], + [ + 1.7417087316539437, + 1.9594223231107435 + ], + [ + 1.7417087316539437, + 1.8505655273824004 + ], + [ + 1.6328519359253733, + 1.8505655273824004 + ], + [ + 1.6328519359253733, + 1.7417087316539437 + ], + [ + 1.5239951401972576, + 1.7417087316539437 + ], + [ + 1.5239951401972576, + 1.6328519359256006 + ], + [ + 1.4151383444686871, + 1.6328519359256006 + ], + [ + 1.4151383444686871, + 1.5239951401972576 + ], + [ + 1.4151383444686871, + 1.5239951401972576 + ] + ], + "pressures": [], + "simulatePressure": true, + "lastCommittedPoint": [ + 1.4151383444686871, + 1.5239951401972576 + ] + }, + { + "id": "Joxwi2r20qJApxbNkc90w", + "type": "freedraw", + "x": 3172.134224946585, + "y": 573.9214285172549, + "width": 2.8732383277924782, + "height": 5.890138571974717, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2L", + "roundness": null, + "seed": 264640613, + "version": 132, + "versionNonce": 575626021, + "isDeleted": false, + "boundElements": null, + "updated": 1727116205603, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.14366191638953296 + ], + [ + -0.14366191638964665, + 0.14366191638953296 + ], + [ + -0.14366191638964665, + 0.2873238327791796 + ], + [ + -0.14366191638964665, + 0.43098574916882626 + ], + [ + -0.14366191638964665, + 0.5746476655584729 + ], + [ + -0.14366191638964665, + 0.7183095819481196 + ], + [ + -0.14366191638964665, + 0.8619714983377662 + ], + [ + -0.2873238327792933, + 0.8619714983377662 + ], + [ + -0.2873238327792933, + 1.0056334147274129 + ], + [ + -0.43098574916893995, + 1.1492953311169458 + ], + [ + -0.43098574916893995, + 1.2929572475065925 + ], + [ + -0.5746476655585866, + 1.4366191638962391 + ], + [ + -0.5746476655585866, + 1.5802810802858858 + ], + [ + -0.7183095819482332, + 1.7239429966755324 + ], + [ + -0.8619714983378799, + 1.8676049130650654 + ], + [ + -0.8619714983378799, + 2.011266829454712 + ], + [ + -1.0056334147275265, + 2.1549287458443587 + ], + [ + -1.1492953311171732, + 2.2985906622340053 + ], + [ + -1.1492953311171732, + 2.442252578623652 + ], + [ + -1.1492953311171732, + 2.5859144950132986 + ], + [ + -1.2929572475068198, + 2.5859144950132986 + ], + [ + -1.2929572475068198, + 2.7295764114028316 + ], + [ + -1.4366191638964665, + 2.8732383277924782 + ], + [ + -1.4366191638964665, + 3.016900244182125 + ], + [ + -1.5802810802856584, + 3.1605621605717715 + ], + [ + -1.5802810802856584, + 3.304224076961418 + ], + [ + -1.5802810802856584, + 3.447885993350951 + ], + [ + -1.5802810802856584, + 3.591547909740598 + ], + [ + -1.5802810802856584, + 3.878871742519891 + ], + [ + -1.5802810802856584, + 4.022533658909538 + ], + [ + -1.5802810802856584, + 4.166195575299184 + ], + [ + -1.723942996675305, + 4.453519408078364 + ], + [ + -1.723942996675305, + 4.597181324468011 + ], + [ + -1.723942996675305, + 4.740843240857657 + ], + [ + -1.723942996675305, + 4.884505157247304 + ], + [ + -1.5802810802856584, + 5.028167073636951 + ], + [ + -1.5802810802856584, + 5.1718289900264836 + ], + [ + -1.4366191638964665, + 5.31549090641613 + ], + [ + -1.2929572475068198, + 5.459152822805777 + ], + [ + -1.1492953311171732, + 5.459152822805777 + ], + [ + -1.0056334147275265, + 5.459152822805777 + ], + [ + -1.0056334147275265, + 5.31549090641613 + ], + [ + -0.8619714983378799, + 5.31549090641613 + ], + [ + -0.7183095819482332, + 5.1718289900264836 + ], + [ + -0.7183095819482332, + 5.028167073636951 + ], + [ + -0.5746476655585866, + 4.884505157247304 + ], + [ + -0.43098574916893995, + 4.740843240857657 + ], + [ + -0.43098574916893995, + 4.597181324468011 + ], + [ + -0.2873238327792933, + 4.453519408078364 + ], + [ + -0.2873238327792933, + 4.309857491688831 + ], + [ + -0.2873238327792933, + 4.166195575299184 + ], + [ + -0.14366191638964665, + 4.022533658909538 + ], + [ + -0.14366191638964665, + 3.878871742519891 + ], + [ + -0.14366191638964665, + 3.7352098261302444 + ], + [ + 0, + 3.591547909740598 + ], + [ + 0, + 3.447885993350951 + ], + [ + 0, + 3.1605621605717715 + ], + [ + 0, + 3.016900244182125 + ], + [ + 0, + 2.8732383277924782 + ], + [ + 0, + 2.5859144950132986 + ], + [ + 0, + 2.442252578623652 + ], + [ + 0, + 2.2985906622340053 + ], + [ + 0, + 2.1549287458443587 + ], + [ + 0, + 2.011266829454712 + ], + [ + 0, + 1.8676049130650654 + ], + [ + 0, + 1.7239429966755324 + ], + [ + 0, + 1.5802810802858858 + ], + [ + 0, + 1.4366191638962391 + ], + [ + 0, + 1.2929572475065925 + ], + [ + 0, + 1.1492953311169458 + ], + [ + -0.14366191638964665, + 1.0056334147274129 + ], + [ + -0.14366191638964665, + 0.8619714983377662 + ], + [ + -0.14366191638964665, + 0.7183095819481196 + ], + [ + -0.14366191638964665, + 0.5746476655584729 + ], + [ + -0.14366191638964665, + 0.43098574916882626 + ], + [ + -0.14366191638964665, + 0.2873238327791796 + ], + [ + -0.2873238327792933, + 0.14366191638953296 + ], + [ + -0.2873238327792933, + 0 + ], + [ + -0.43098574916893995, + 0 + ], + [ + -0.43098574916893995, + -0.14366191638964665 + ], + [ + -0.5746476655585866, + -0.14366191638964665 + ], + [ + -0.7183095819482332, + -0.2873238327792933 + ], + [ + -0.8619714983378799, + -0.2873238327792933 + ], + [ + -0.8619714983378799, + -0.43098574916893995 + ], + [ + -1.0056334147275265, + -0.43098574916893995 + ], + [ + -1.1492953311171732, + -0.43098574916893995 + ], + [ + -1.2929572475068198, + -0.43098574916893995 + ], + [ + -1.2929572475068198, + -0.2873238327792933 + ], + [ + -1.4366191638964665, + -0.2873238327792933 + ], + [ + -1.4366191638964665, + -0.14366191638964665 + ], + [ + -1.4366191638964665, + 0 + ], + [ + -1.5802810802856584, + 0.14366191638953296 + ], + [ + -1.5802810802856584, + 0.2873238327791796 + ], + [ + -1.723942996675305, + 0.43098574916882626 + ], + [ + -1.723942996675305, + 0.5746476655584729 + ], + [ + -1.8676049130649517, + 0.7183095819481196 + ], + [ + -1.8676049130649517, + 0.8619714983377662 + ], + [ + -2.0112668294545983, + 1.0056334147274129 + ], + [ + -2.0112668294545983, + 1.1492953311169458 + ], + [ + -2.154928745844245, + 1.1492953311169458 + ], + [ + -2.154928745844245, + 1.4366191638962391 + ], + [ + -2.2985906622338916, + 1.4366191638962391 + ], + [ + -2.2985906622338916, + 1.5802810802858858 + ], + [ + -2.2985906622338916, + 1.7239429966755324 + ], + [ + -2.4422525786235383, + 1.8676049130650654 + ], + [ + -2.4422525786235383, + 2.011266829454712 + ], + [ + -2.585914495013185, + 2.1549287458443587 + ], + [ + -2.585914495013185, + 2.2985906622340053 + ], + [ + -2.7295764114028316, + 2.442252578623652 + ], + [ + -2.7295764114028316, + 2.5859144950132986 + ], + [ + -2.7295764114028316, + 2.7295764114028316 + ], + [ + -2.8732383277924782, + 2.8732383277924782 + ], + [ + -2.8732383277924782, + 3.016900244182125 + ], + [ + -2.8732383277924782, + 3.1605621605717715 + ], + [ + -2.8732383277924782, + 3.304224076961418 + ], + [ + -2.8732383277924782, + 3.447885993350951 + ], + [ + -2.8732383277924782, + 3.591547909740598 + ], + [ + -2.8732383277924782, + 3.7352098261302444 + ], + [ + -2.8732383277924782, + 3.878871742519891 + ], + [ + -2.8732383277924782, + 4.022533658909538 + ], + [ + -2.8732383277924782, + 4.166195575299184 + ], + [ + -2.8732383277924782, + 4.309857491688831 + ], + [ + -2.7295764114028316, + 4.453519408078364 + ], + [ + -2.7295764114028316, + 4.597181324468011 + ], + [ + -2.585914495013185, + 4.740843240857657 + ], + [ + -2.585914495013185, + 4.884505157247304 + ], + [ + -2.585914495013185, + 5.028167073636951 + ], + [ + -2.4422525786235383, + 5.1718289900264836 + ], + [ + -2.2985906622338916, + 5.028167073636951 + ], + [ + -2.2985906622338916, + 5.028167073636951 + ] + ], + "pressures": [], + "simulatePressure": true, + "lastCommittedPoint": [ + -2.2985906622338916, + 5.028167073636951 + ] + }, + { + "id": "P2X2FiQisUdlR-CM3SiDW", + "type": "freedraw", + "x": 3213.652518783187, + "y": 575.9326953467096, + "width": 2.873238327792933, + "height": 8.332391150598369, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2M", + "roundness": null, + "seed": 659105867, + "version": 125, + "versionNonce": 1276228389, + "isDeleted": false, + "boundElements": null, + "updated": 1727116207564, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.14366191638964665, + 0.14366191638964665 + ], + [ + -0.14366191638964665, + 0.5746476655585866 + ], + [ + -0.14366191638964665, + 1.0056334147274129 + ], + [ + -0.14366191638964665, + 1.2929572475067062 + ], + [ + -0.2873238327792933, + 1.5802810802858858 + ], + [ + -0.2873238327792933, + 1.7239429966755324 + ], + [ + -0.2873238327792933, + 2.0112668294548257 + ], + [ + -0.2873238327792933, + 2.1549287458444724 + ], + [ + -0.2873238327792933, + 2.298590662234119 + ], + [ + -0.2873238327792933, + 2.442252578623652 + ], + [ + -0.2873238327792933, + 2.7295764114029453 + ], + [ + -0.2873238327792933, + 2.873238327792592 + ], + [ + -0.43098574916893995, + 3.1605621605717715 + ], + [ + -0.43098574916893995, + 3.304224076961418 + ], + [ + -0.43098574916893995, + 3.5915479097407115 + ], + [ + -0.43098574916893995, + 3.735209826130358 + ], + [ + -0.43098574916893995, + 3.8788717425200048 + ], + [ + -0.43098574916893995, + 4.166195575299184 + ], + [ + -0.43098574916893995, + 4.453519408078478 + ], + [ + -0.43098574916893995, + 4.740843240857657 + ], + [ + -0.43098574916893995, + 4.884505157247304 + ], + [ + -0.43098574916893995, + 5.171828990026597 + ], + [ + -0.43098574916893995, + 5.315490906416244 + ], + [ + -0.43098574916893995, + 5.6028147391954235 + ], + [ + -0.43098574916893995, + 5.74647665558507 + ], + [ + -0.43098574916893995, + 5.890138571974717 + ], + [ + -0.43098574916893995, + 6.0338004883643634 + ], + [ + -0.43098574916893995, + 6.17746240475401 + ], + [ + -0.43098574916893995, + 6.321124321143543 + ], + [ + -0.43098574916893995, + 6.46478623753319 + ], + [ + -0.43098574916893995, + 6.608448153922836 + ], + [ + -0.43098574916893995, + 6.752110070312483 + ], + [ + -0.43098574916893995, + 6.89577198670213 + ], + [ + -0.43098574916893995, + 7.039433903091776 + ], + [ + -0.2873238327792933, + 7.039433903091776 + ], + [ + -0.2873238327792933, + 7.183095819481423 + ], + [ + -0.2873238327792933, + 7.326757735870956 + ], + [ + -0.14366191638964665, + 7.326757735870956 + ], + [ + -0.14366191638964665, + 7.470419652260603 + ], + [ + 0, + 7.614081568650249 + ], + [ + 0.14366191638964665, + 7.614081568650249 + ], + [ + 0.2873238327792933, + 7.614081568650249 + ], + [ + 0.2873238327792933, + 7.470419652260603 + ], + [ + 0.43098574916893995, + 7.326757735870956 + ], + [ + 0.43098574916893995, + 7.183095819481423 + ], + [ + 0.5746476655585866, + 7.039433903091776 + ], + [ + 0.7183095819482332, + 6.752110070312483 + ], + [ + 0.8619714983378799, + 6.608448153922836 + ], + [ + 1.0056334147275265, + 6.46478623753319 + ], + [ + 1.1492953311171732, + 6.17746240475401 + ], + [ + 1.2929572475068198, + 6.17746240475401 + ], + [ + 1.4366191638964665, + 5.890138571974717 + ], + [ + 1.5802810802861131, + 5.74647665558507 + ], + [ + 1.7239429966757598, + 5.6028147391954235 + ], + [ + 1.8676049130654064, + 5.4591528228058905 + ], + [ + 2.011266829455053, + 5.315490906416244 + ], + [ + 2.011266829455053, + 5.171828990026597 + ], + [ + 2.2985906622343464, + 4.884505157247304 + ], + [ + 2.2985906622343464, + 4.597181324468124 + ], + [ + 2.442252578623993, + 4.309857491688831 + ], + [ + 2.442252578623993, + 4.166195575299184 + ], + [ + 2.442252578623993, + 4.022533658909538 + ], + [ + 2.442252578623993, + 3.735209826130358 + ], + [ + 2.442252578623993, + 3.447885993351065 + ], + [ + 2.442252578623993, + 3.304224076961418 + ], + [ + 2.442252578623993, + 3.1605621605717715 + ], + [ + 2.442252578623993, + 3.0169002441822386 + ], + [ + 2.442252578623993, + 2.7295764114029453 + ], + [ + 2.442252578623993, + 2.5859144950132986 + ], + [ + 2.2985906622343464, + 2.442252578623652 + ], + [ + 2.2985906622343464, + 2.298590662234119 + ], + [ + 2.2985906622343464, + 2.0112668294548257 + ], + [ + 2.1549287458446997, + 2.0112668294548257 + ], + [ + 2.1549287458446997, + 1.7239429966755324 + ], + [ + 2.011266829455053, + 1.7239429966755324 + ], + [ + 2.011266829455053, + 1.5802810802858858 + ], + [ + 2.011266829455053, + 1.4366191638962391 + ], + [ + 2.011266829455053, + 1.2929572475067062 + ], + [ + 2.011266829455053, + 1.1492953311170595 + ], + [ + 1.8676049130654064, + 1.0056334147274129 + ], + [ + 1.8676049130654064, + 0.8619714983377662 + ], + [ + 1.8676049130654064, + 0.7183095819481196 + ], + [ + 1.8676049130654064, + 0.5746476655585866 + ], + [ + 1.8676049130654064, + 0.43098574916893995 + ], + [ + 1.7239429966757598, + 0.2873238327792933 + ], + [ + 1.7239429966757598, + 0.14366191638964665 + ], + [ + 1.7239429966757598, + 0 + ], + [ + 1.5802810802861131, + -0.14366191638964665 + ], + [ + 1.5802810802861131, + -0.2873238327791796 + ], + [ + 1.4366191638964665, + -0.2873238327791796 + ], + [ + 1.4366191638964665, + -0.43098574916882626 + ], + [ + 1.2929572475068198, + -0.43098574916882626 + ], + [ + 1.1492953311171732, + -0.5746476655584729 + ], + [ + 1.1492953311171732, + -0.7183095819481196 + ], + [ + 1.0056334147275265, + -0.7183095819481196 + ], + [ + 0.8619714983378799, + -0.7183095819481196 + ], + [ + 0.8619714983378799, + -0.5746476655584729 + ], + [ + 0.7183095819482332, + -0.5746476655584729 + ], + [ + 0.7183095819482332, + -0.43098574916882626 + ], + [ + 0.5746476655585866, + -0.43098574916882626 + ], + [ + 0.5746476655585866, + -0.2873238327791796 + ], + [ + 0.43098574916893995, + -0.2873238327791796 + ], + [ + 0.43098574916893995, + -0.14366191638964665 + ], + [ + 0.43098574916893995, + 0 + ], + [ + 0.43098574916893995, + 0.14366191638964665 + ], + [ + 0.2873238327792933, + 0.14366191638964665 + ], + [ + 0.2873238327792933, + 0.2873238327792933 + ], + [ + 0.2873238327792933, + 0.43098574916893995 + ], + [ + 0.2873238327792933, + 0.5746476655585866 + ], + [ + 0.2873238327792933, + 0.7183095819481196 + ], + [ + 0.2873238327792933, + 0.8619714983377662 + ], + [ + 0.14366191638964665, + 1.0056334147274129 + ], + [ + 0.14366191638964665, + 1.1492953311170595 + ], + [ + 0.14366191638964665, + 1.2929572475067062 + ], + [ + 0.14366191638964665, + 1.4366191638962391 + ], + [ + 0.14366191638964665, + 1.5802810802858858 + ], + [ + 0.14366191638964665, + 1.7239429966755324 + ], + [ + 0.14366191638964665, + 1.867604913065179 + ], + [ + 0.14366191638964665, + 2.0112668294548257 + ], + [ + 0.2873238327792933, + 2.0112668294548257 + ], + [ + 0.2873238327792933, + 2.1549287458444724 + ], + [ + 0.2873238327792933, + 2.298590662234119 + ], + [ + 0.2873238327792933, + 2.298590662234119 + ] + ], + "pressures": [], + "simulatePressure": true, + "lastCommittedPoint": [ + 0.2873238327792933, + 2.298590662234119 + ] + }, + { + "id": "s_4M7ZNcqVVT6upuP7mEb", + "type": "freedraw", + "x": 3216.238433278201, + "y": 573.6341046844756, + "width": 8.045067317819303, + "height": 10.918305645611667, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2N", + "roundness": null, + "seed": 861098059, + "version": 87, + "versionNonce": 2055582469, + "isDeleted": false, + "boundElements": null, + "updated": 1727116209674, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.2873238327791796 + ], + [ + 0, + -0.8619714983377662 + ], + [ + 0, + -1.2929572475065925 + ], + [ + -0.14366191638964665, + -1.4366191638962391 + ], + [ + -0.2873238327792933, + -1.7239429966755324 + ], + [ + -0.2873238327792933, + -2.011266829454712 + ], + [ + -0.43098574916893995, + -2.2985906622340053 + ], + [ + -0.5746476655585866, + -2.442252578623652 + ], + [ + -0.7183095819482332, + -2.7295764114029453 + ], + [ + -0.8619714983378799, + -2.8732383277924782 + ], + [ + -0.8619714983378799, + -3.304224076961418 + ], + [ + -1.0056334147275265, + -3.447885993351065 + ], + [ + -1.0056334147275265, + -3.591547909740598 + ], + [ + -1.1492953311171732, + -3.7352098261302444 + ], + [ + -1.1492953311171732, + -4.022533658909538 + ], + [ + -1.2929572475068198, + -4.166195575299184 + ], + [ + -1.4366191638964665, + -4.453519408078478 + ], + [ + -1.5802810802861131, + -4.597181324468011 + ], + [ + -1.7239429966757598, + -4.884505157247304 + ], + [ + -1.7239429966757598, + -5.028167073636951 + ], + [ + -1.8676049130654064, + -5.31549090641613 + ], + [ + -2.011266829455053, + -5.31549090641613 + ], + [ + -2.1549287458446997, + -5.6028147391954235 + ], + [ + -2.1549287458446997, + -5.74647665558507 + ], + [ + -2.442252578623993, + -5.890138571974717 + ], + [ + -2.442252578623993, + -6.0338004883643634 + ], + [ + -2.5859144950136397, + -6.177462404753896 + ], + [ + -2.5859144950136397, + -6.321124321143543 + ], + [ + -2.7295764114032863, + -6.46478623753319 + ], + [ + -2.873238327792933, + -6.46478623753319 + ], + [ + -2.873238327792933, + -6.608448153922836 + ], + [ + -3.0169002441825796, + -6.752110070312483 + ], + [ + -3.0169002441825796, + -6.895771986702016 + ], + [ + -3.1605621605717715, + -6.895771986702016 + ], + [ + -3.304224076961418, + -7.039433903091663 + ], + [ + -3.304224076961418, + -7.183095819481309 + ], + [ + -3.447885993351065, + -7.326757735870956 + ], + [ + -3.5915479097407115, + -7.470419652260603 + ], + [ + -3.735209826130358, + -7.614081568650249 + ], + [ + -3.735209826130358, + -7.757743485039782 + ], + [ + -3.8788717425200048, + -7.901405401429429 + ], + [ + -4.022533658909651, + -8.045067317819075 + ], + [ + -4.166195575299298, + -8.188729234208722 + ], + [ + -4.166195575299298, + -8.332391150598369 + ], + [ + -4.309857491688945, + -8.332391150598369 + ], + [ + -4.309857491688945, + -8.476053066987902 + ], + [ + -4.453519408078591, + -8.619714983377548 + ], + [ + -4.453519408078591, + -8.763376899767195 + ], + [ + -4.597181324468238, + -8.763376899767195 + ], + [ + -4.597181324468238, + -8.907038816156842 + ], + [ + -4.740843240857885, + -8.907038816156842 + ], + [ + -4.740843240857885, + -9.050700732546488 + ], + [ + -4.884505157247531, + -9.050700732546488 + ], + [ + -4.884505157247531, + -9.194362648936135 + ], + [ + -5.028167073637178, + -9.194362648936135 + ], + [ + -5.028167073637178, + -9.338024565325782 + ], + [ + -5.171828990026825, + -9.481686481715315 + ], + [ + -5.315490906416471, + -9.481686481715315 + ], + [ + -5.459152822806118, + -9.625348398104961 + ], + [ + -5.459152822806118, + -9.769010314494608 + ], + [ + -5.602814739195765, + -9.769010314494608 + ], + [ + -5.602814739195765, + -9.912672230884255 + ], + [ + -5.746476655585411, + -9.912672230884255 + ], + [ + -5.746476655585411, + -10.056334147273901 + ], + [ + -5.890138571975058, + -10.056334147273901 + ], + [ + -5.890138571975058, + -10.199996063663434 + ], + [ + -6.0338004883647045, + -10.199996063663434 + ], + [ + -6.0338004883647045, + -10.34365798005308 + ], + [ + -6.177462404754351, + -10.34365798005308 + ], + [ + -6.321124321143998, + -10.34365798005308 + ], + [ + -6.321124321143998, + -10.487319896442727 + ], + [ + -6.4647862375336445, + -10.487319896442727 + ], + [ + -6.608448153922836, + -10.630981812832374 + ], + [ + -6.752110070312483, + -10.630981812832374 + ], + [ + -6.89577198670213, + -10.630981812832374 + ], + [ + -7.039433903091776, + -10.630981812832374 + ], + [ + -7.039433903091776, + -10.77464372922202 + ], + [ + -7.183095819481423, + -10.77464372922202 + ], + [ + -7.32675773587107, + -10.77464372922202 + ], + [ + -7.470419652260716, + -10.77464372922202 + ], + [ + -7.614081568650363, + -10.918305645611667 + ], + [ + -7.7577434850400095, + -10.918305645611667 + ], + [ + -7.901405401429656, + -10.918305645611667 + ], + [ + -8.045067317819303, + -10.918305645611667 + ], + [ + -8.045067317819303, + -10.918305645611667 + ] + ], + "pressures": [], + "simulatePressure": true, + "lastCommittedPoint": [ + -8.045067317819303, + -10.918305645611667 + ] + }, + { + "id": "EwHCnpxC1ZMSpGU0_AxOV", + "type": "freedraw", + "x": 3182.0468971774694, + "y": 560.4172083766299, + "width": 10.630981812832488, + "height": 10.918305645611667, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2O", + "roundness": null, + "seed": 2106915435, + "version": 124, + "versionNonce": 1547579691, + "isDeleted": false, + "boundElements": null, + "updated": 1727116213337, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.14366191638964665, + 0 + ], + [ + -0.2873238327792933, + 0 + ], + [ + -0.43098574916893995, + 0 + ], + [ + -0.7183095819482332, + 0.14366191638964665 + ], + [ + -0.8619714983378799, + 0.14366191638964665 + ], + [ + -1.0056334147275265, + 0.14366191638964665 + ], + [ + -1.1492953311171732, + 0.14366191638964665 + ], + [ + -1.4366191638964665, + 0.2873238327792933 + ], + [ + -1.5802810802856584, + 0.2873238327792933 + ], + [ + -1.723942996675305, + 0.2873238327792933 + ], + [ + -1.8676049130649517, + 0.2873238327792933 + ], + [ + -2.0112668294545983, + 0.43098574916893995 + ], + [ + -2.154928745844245, + 0.43098574916893995 + ], + [ + -2.2985906622338916, + 0.43098574916893995 + ], + [ + -2.4422525786235383, + 0.43098574916893995 + ], + [ + -2.7295764114028316, + 0.5746476655585866 + ], + [ + -3.016900244182125, + 0.7183095819481196 + ], + [ + -3.1605621605717715, + 0.7183095819481196 + ], + [ + -3.304224076961418, + 0.8619714983377662 + ], + [ + -3.447885993351065, + 0.8619714983377662 + ], + [ + -3.5915479097407115, + 1.0056334147274129 + ], + [ + -3.735209826130358, + 1.0056334147274129 + ], + [ + -3.8788717425200048, + 1.0056334147274129 + ], + [ + -4.022533658909651, + 1.1492953311170595 + ], + [ + -4.166195575299298, + 1.1492953311170595 + ], + [ + -4.309857491688945, + 1.2929572475067062 + ], + [ + -4.453519408078591, + 1.4366191638963528 + ], + [ + -4.597181324468238, + 1.5802810802858858 + ], + [ + -4.740843240857885, + 1.5802810802858858 + ], + [ + -4.884505157247531, + 1.7239429966755324 + ], + [ + -5.028167073636723, + 1.7239429966755324 + ], + [ + -5.17182899002637, + 1.867604913065179 + ], + [ + -5.3154909064160165, + 1.867604913065179 + ], + [ + -5.459152822805663, + 2.0112668294548257 + ], + [ + -5.60281473919531, + 2.0112668294548257 + ], + [ + -5.60281473919531, + 2.1549287458444724 + ], + [ + -5.7464766555849565, + 2.1549287458444724 + ], + [ + -5.890138571974603, + 2.1549287458444724 + ], + [ + -5.890138571974603, + 2.2985906622340053 + ], + [ + -6.03380048836425, + 2.2985906622340053 + ], + [ + -6.03380048836425, + 2.442252578623652 + ], + [ + -6.177462404753896, + 2.442252578623652 + ], + [ + -6.321124321143543, + 2.442252578623652 + ], + [ + -6.321124321143543, + 2.5859144950132986 + ], + [ + -6.46478623753319, + 2.5859144950132986 + ], + [ + -6.608448153922836, + 2.5859144950132986 + ], + [ + -6.608448153922836, + 2.7295764114029453 + ], + [ + -6.752110070312483, + 2.7295764114029453 + ], + [ + -6.89577198670213, + 2.873238327792592 + ], + [ + -7.039433903091776, + 2.873238327792592 + ], + [ + -7.039433903091776, + 3.0169002441822386 + ], + [ + -7.183095819481423, + 3.0169002441822386 + ], + [ + -7.183095819481423, + 3.1605621605717715 + ], + [ + -7.32675773587107, + 3.304224076961418 + ], + [ + -7.470419652260716, + 3.447885993351065 + ], + [ + -7.614081568650363, + 3.5915479097407115 + ], + [ + -7.614081568650363, + 3.735209826130358 + ], + [ + -7.7577434850400095, + 3.735209826130358 + ], + [ + -7.7577434850400095, + 3.878871742519891 + ], + [ + -7.901405401429656, + 4.022533658909538 + ], + [ + -8.045067317819303, + 4.166195575299184 + ], + [ + -8.045067317819303, + 4.309857491688831 + ], + [ + -8.188729234208495, + 4.309857491688831 + ], + [ + -8.188729234208495, + 4.453519408078478 + ], + [ + -8.332391150598141, + 4.453519408078478 + ], + [ + -8.332391150598141, + 4.597181324468124 + ], + [ + -8.476053066987788, + 4.740843240857771 + ], + [ + -8.476053066987788, + 4.884505157247304 + ], + [ + -8.619714983377435, + 5.028167073636951 + ], + [ + -8.763376899767081, + 5.171828990026597 + ], + [ + -8.763376899767081, + 5.315490906416244 + ], + [ + -8.907038816156728, + 5.315490906416244 + ], + [ + -8.907038816156728, + 5.4591528228058905 + ], + [ + -9.050700732546375, + 5.6028147391954235 + ], + [ + -9.050700732546375, + 5.74647665558507 + ], + [ + -9.194362648936021, + 5.74647665558507 + ], + [ + -9.194362648936021, + 5.890138571974717 + ], + [ + -9.194362648936021, + 6.0338004883643634 + ], + [ + -9.338024565325668, + 6.0338004883643634 + ], + [ + -9.338024565325668, + 6.17746240475401 + ], + [ + -9.338024565325668, + 6.321124321143657 + ], + [ + -9.338024565325668, + 6.46478623753319 + ], + [ + -9.338024565325668, + 6.608448153922836 + ], + [ + -9.481686481715315, + 6.752110070312483 + ], + [ + -9.481686481715315, + 6.89577198670213 + ], + [ + -9.481686481715315, + 7.039433903091776 + ], + [ + -9.481686481715315, + 7.183095819481309 + ], + [ + -9.481686481715315, + 7.326757735870956 + ], + [ + -9.481686481715315, + 7.470419652260603 + ], + [ + -9.481686481715315, + 7.614081568650249 + ], + [ + -9.481686481715315, + 7.757743485039896 + ], + [ + -9.481686481715315, + 7.9014054014295425 + ], + [ + -9.481686481715315, + 8.045067317819075 + ], + [ + -9.481686481715315, + 8.188729234208722 + ], + [ + -9.481686481715315, + 8.332391150598369 + ], + [ + -9.481686481715315, + 8.476053066988015 + ], + [ + -9.481686481715315, + 8.619714983377662 + ], + [ + -9.481686481715315, + 8.763376899767195 + ], + [ + -9.481686481715315, + 8.907038816156842 + ], + [ + -9.481686481715315, + 9.050700732546488 + ], + [ + -9.481686481715315, + 9.194362648936135 + ], + [ + -9.481686481715315, + 9.338024565325782 + ], + [ + -9.481686481715315, + 9.481686481715428 + ], + [ + -9.625348398104961, + 9.481686481715428 + ], + [ + -9.625348398104961, + 9.625348398105075 + ], + [ + -9.625348398104961, + 9.769010314494608 + ], + [ + -9.625348398104961, + 9.912672230884255 + ], + [ + -9.625348398104961, + 10.056334147273901 + ], + [ + -9.625348398104961, + 10.199996063663548 + ], + [ + -9.769010314494608, + 10.199996063663548 + ], + [ + -9.769010314494608, + 10.343657980053194 + ], + [ + -9.912672230884255, + 10.343657980053194 + ], + [ + -9.912672230884255, + 10.487319896442727 + ], + [ + -10.056334147273901, + 10.487319896442727 + ], + [ + -10.199996063663548, + 10.487319896442727 + ], + [ + -10.199996063663548, + 10.630981812832374 + ], + [ + -10.343657980053194, + 10.630981812832374 + ], + [ + -10.343657980053194, + 10.77464372922202 + ], + [ + -10.487319896442841, + 10.77464372922202 + ], + [ + -10.487319896442841, + 10.918305645611667 + ], + [ + -10.630981812832488, + 10.918305645611667 + ], + [ + -10.630981812832488, + 10.918305645611667 + ] + ], + "pressures": [], + "simulatePressure": true, + "lastCommittedPoint": [ + -10.630981812832488, + 10.918305645611667 + ] + }, + { + "id": "8pgTqOFi7Msi_nNuwmVV5", + "type": "freedraw", + "x": 3169.2609866187927, + "y": 570.6172044402934, + "width": 5.3154909064160165, + "height": 10.056334147273787, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2P", + "roundness": null, + "seed": 1609261669, + "version": 116, + "versionNonce": 650271269, + "isDeleted": false, + "boundElements": null, + "updated": 1727116215693, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.14366191638964665, + 0 + ], + [ + -0.2873238327792933, + 0.14366191638964665 + ], + [ + -0.43098574916893995, + 0.14366191638964665 + ], + [ + -0.43098574916893995, + 0.2873238327791796 + ], + [ + -0.5746476655585866, + 0.2873238327791796 + ], + [ + -0.5746476655585866, + 0.43098574916882626 + ], + [ + -0.7183095819482332, + 0.5746476655584729 + ], + [ + -0.8619714983378799, + 0.8619714983377662 + ], + [ + -0.8619714983378799, + 1.0056334147274129 + ], + [ + -1.0056334147275265, + 1.0056334147274129 + ], + [ + -1.1492953311171732, + 1.1492953311169458 + ], + [ + -1.1492953311171732, + 1.2929572475065925 + ], + [ + -1.2929572475068198, + 1.4366191638962391 + ], + [ + -1.4366191638964665, + 1.5802810802858858 + ], + [ + -1.5802810802861131, + 1.7239429966755324 + ], + [ + -1.5802810802861131, + 1.8676049130650654 + ], + [ + -1.7239429966757598, + 2.011266829454712 + ], + [ + -1.8676049130649517, + 2.1549287458443587 + ], + [ + -1.8676049130649517, + 2.2985906622340053 + ], + [ + -2.0112668294545983, + 2.442252578623652 + ], + [ + -2.0112668294545983, + 2.5859144950132986 + ], + [ + -2.154928745844245, + 2.7295764114029453 + ], + [ + -2.154928745844245, + 2.8732383277924782 + ], + [ + -2.154928745844245, + 3.016900244182125 + ], + [ + -2.154928745844245, + 3.1605621605717715 + ], + [ + -2.154928745844245, + 3.304224076961418 + ], + [ + -2.154928745844245, + 3.447885993350951 + ], + [ + -2.154928745844245, + 3.591547909740598 + ], + [ + -2.2985906622338916, + 3.7352098261302444 + ], + [ + -2.2985906622338916, + 4.022533658909538 + ], + [ + -2.2985906622338916, + 4.166195575299184 + ], + [ + -2.2985906622338916, + 4.453519408078364 + ], + [ + -2.4422525786235383, + 4.597181324468011 + ], + [ + -2.4422525786235383, + 4.884505157247304 + ], + [ + -2.4422525786235383, + 5.028167073636951 + ], + [ + -2.4422525786235383, + 5.31549090641613 + ], + [ + -2.585914495013185, + 5.459152822805777 + ], + [ + -2.585914495013185, + 5.74647665558507 + ], + [ + -2.585914495013185, + 5.890138571974717 + ], + [ + -2.585914495013185, + 6.177462404753896 + ], + [ + -2.585914495013185, + 6.321124321143543 + ], + [ + -2.585914495013185, + 6.608448153922836 + ], + [ + -2.585914495013185, + 6.752110070312369 + ], + [ + -2.585914495013185, + 7.039433903091663 + ], + [ + -2.4422525786235383, + 7.183095819481309 + ], + [ + -2.4422525786235383, + 7.326757735870956 + ], + [ + -2.4422525786235383, + 7.470419652260603 + ], + [ + -2.4422525786235383, + 7.614081568650249 + ], + [ + -2.4422525786235383, + 7.757743485039782 + ], + [ + -2.2985906622338916, + 7.901405401429429 + ], + [ + -2.2985906622338916, + 8.045067317819075 + ], + [ + -2.2985906622338916, + 8.188729234208722 + ], + [ + -2.154928745844245, + 8.332391150598369 + ], + [ + -2.154928745844245, + 8.476053066987902 + ], + [ + -2.0112668294545983, + 8.619714983377548 + ], + [ + -2.0112668294545983, + 8.763376899767195 + ], + [ + -1.8676049130649517, + 8.907038816156842 + ], + [ + -1.8676049130649517, + 9.050700732546488 + ], + [ + -1.7239429966757598, + 9.050700732546488 + ], + [ + -1.7239429966757598, + 9.194362648936135 + ], + [ + -1.5802810802861131, + 9.194362648936135 + ], + [ + -1.4366191638964665, + 9.194362648936135 + ], + [ + -1.4366191638964665, + 9.338024565325668 + ], + [ + -1.2929572475068198, + 9.481686481715315 + ], + [ + -1.1492953311171732, + 9.625348398104961 + ], + [ + -1.0056334147275265, + 9.625348398104961 + ], + [ + -0.8619714983378799, + 9.769010314494608 + ], + [ + -0.7183095819482332, + 9.912672230884255 + ], + [ + -0.5746476655585866, + 9.912672230884255 + ], + [ + -0.43098574916893995, + 9.912672230884255 + ], + [ + -0.2873238327792933, + 9.912672230884255 + ], + [ + -0.14366191638964665, + 9.912672230884255 + ], + [ + 0, + 9.912672230884255 + ], + [ + 0.14366191638964665, + 9.912672230884255 + ], + [ + 0.2873238327792933, + 9.912672230884255 + ], + [ + 0.43098574916893995, + 9.912672230884255 + ], + [ + 0.5746476655585866, + 9.912672230884255 + ], + [ + 0.8619714983378799, + 9.912672230884255 + ], + [ + 0.8619714983378799, + 10.056334147273787 + ], + [ + 1.1492953311171732, + 10.056334147273787 + ], + [ + 1.2929572475068198, + 10.056334147273787 + ], + [ + 1.4366191638960117, + 10.056334147273787 + ], + [ + 1.5802810802856584, + 10.056334147273787 + ], + [ + 1.723942996675305, + 10.056334147273787 + ], + [ + 1.723942996675305, + 9.912672230884255 + ], + [ + 1.723942996675305, + 9.769010314494608 + ], + [ + 1.723942996675305, + 9.625348398104961 + ], + [ + 1.8676049130649517, + 9.625348398104961 + ], + [ + 1.8676049130649517, + 9.481686481715315 + ], + [ + 1.8676049130649517, + 9.338024565325668 + ], + [ + 1.8676049130649517, + 9.194362648936135 + ], + [ + 2.0112668294545983, + 9.194362648936135 + ], + [ + 2.0112668294545983, + 9.050700732546488 + ], + [ + 2.154928745844245, + 8.907038816156842 + ], + [ + 2.2985906622338916, + 8.763376899767195 + ], + [ + 2.2985906622338916, + 8.619714983377548 + ], + [ + 2.2985906622338916, + 8.476053066987902 + ], + [ + 2.4422525786235383, + 8.476053066987902 + ], + [ + 2.4422525786235383, + 8.332391150598369 + ], + [ + 2.4422525786235383, + 8.188729234208722 + ], + [ + 2.585914495013185, + 7.901405401429429 + ], + [ + 2.585914495013185, + 7.614081568650249 + ], + [ + 2.7295764114028316, + 7.470419652260603 + ], + [ + 2.7295764114028316, + 7.183095819481309 + ], + [ + 2.7295764114028316, + 7.039433903091663 + ], + [ + 2.7295764114028316, + 6.895771986702016 + ], + [ + 2.7295764114028316, + 6.752110070312369 + ], + [ + 2.7295764114028316, + 6.46478623753319 + ], + [ + 2.7295764114028316, + 6.321124321143543 + ], + [ + 2.7295764114028316, + 6.177462404753896 + ], + [ + 2.7295764114028316, + 6.03380048836425 + ], + [ + 2.7295764114028316, + 5.890138571974717 + ], + [ + 2.7295764114028316, + 5.74647665558507 + ], + [ + 2.7295764114028316, + 5.74647665558507 + ] + ], + "pressures": [], + "simulatePressure": true, + "lastCommittedPoint": [ + 2.7295764114028316, + 5.74647665558507 + ] + }, + { + "id": "mxaGNWNFDtpqougV0fQYG", + "type": "freedraw", + "x": 3217.3877286093175, + "y": 576.2200191794889, + "width": 4.740843240857885, + "height": 8.619714983377548, + "angle": 0, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "b2Q", + "roundness": null, + "seed": 261323083, + "version": 73, + "versionNonce": 75490917, + "isDeleted": false, + "boundElements": null, + "updated": 1727116217187, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.14366191638964665, + 0.14366191638964665 + ], + [ + 0.2873238327792933, + 0.5746476655584729 + ], + [ + 0.2873238327792933, + 0.7183095819481196 + ], + [ + 0.43098574916893995, + 0.7183095819481196 + ], + [ + 0.43098574916893995, + 0.8619714983377662 + ], + [ + 0.43098574916893995, + 1.0056334147274129 + ], + [ + 0.5746476655585866, + 1.2929572475065925 + ], + [ + 0.5746476655585866, + 1.4366191638962391 + ], + [ + 0.5746476655585866, + 1.7239429966755324 + ], + [ + 0.5746476655585866, + 1.867604913065179 + ], + [ + 0.7183095819482332, + 2.2985906622340053 + ], + [ + 0.7183095819482332, + 2.442252578623652 + ], + [ + 0.8619714983378799, + 2.8732383277924782 + ], + [ + 0.8619714983378799, + 3.016900244182125 + ], + [ + 1.0056334147275265, + 3.447885993351065 + ], + [ + 1.0056334147275265, + 3.5915479097407115 + ], + [ + 1.1492953311171732, + 3.878871742519891 + ], + [ + 1.1492953311171732, + 4.022533658909538 + ], + [ + 1.1492953311171732, + 4.309857491688831 + ], + [ + 1.2929572475068198, + 4.309857491688831 + ], + [ + 1.2929572475068198, + 4.597181324468011 + ], + [ + 1.2929572475068198, + 4.740843240857657 + ], + [ + 1.2929572475068198, + 4.884505157247304 + ], + [ + 1.2929572475068198, + 5.028167073636951 + ], + [ + 1.2929572475068198, + 5.171828990026597 + ], + [ + 1.2929572475068198, + 5.31549090641613 + ], + [ + 1.1492953311171732, + 5.31549090641613 + ], + [ + 1.1492953311171732, + 5.459152822805777 + ], + [ + 1.1492953311171732, + 5.6028147391954235 + ], + [ + 1.0056334147275265, + 5.74647665558507 + ], + [ + 1.0056334147275265, + 5.890138571974717 + ], + [ + 0.8619714983378799, + 6.03380048836425 + ], + [ + 0.8619714983378799, + 6.177462404753896 + ], + [ + 0.8619714983378799, + 6.321124321143543 + ], + [ + 0.7183095819482332, + 6.46478623753319 + ], + [ + 0.7183095819482332, + 6.752110070312483 + ], + [ + 0.5746476655585866, + 6.89577198670213 + ], + [ + 0.5746476655585866, + 7.039433903091663 + ], + [ + 0.43098574916893995, + 7.326757735870956 + ], + [ + 0.2873238327792933, + 7.470419652260603 + ], + [ + 0.2873238327792933, + 7.614081568650249 + ], + [ + 0.14366191638964665, + 7.757743485039782 + ], + [ + 0, + 7.901405401429429 + ], + [ + -0.14366191638964665, + 7.901405401429429 + ], + [ + -0.2873238327792933, + 7.901405401429429 + ], + [ + -0.43098574916893995, + 8.045067317819075 + ], + [ + -0.5746476655585866, + 8.045067317819075 + ], + [ + -0.8619714983378799, + 8.188729234208722 + ], + [ + -1.1492953311167184, + 8.332391150598369 + ], + [ + -1.292957247506365, + 8.476053066988015 + ], + [ + -1.5802810802856584, + 8.476053066988015 + ], + [ + -1.8676049130649517, + 8.619714983377548 + ], + [ + -2.0112668294545983, + 8.619714983377548 + ], + [ + -2.154928745844245, + 8.619714983377548 + ], + [ + -2.2985906622338916, + 8.619714983377548 + ], + [ + -2.4422525786235383, + 8.619714983377548 + ], + [ + -2.4422525786235383, + 8.476053066988015 + ], + [ + -2.585914495013185, + 8.476053066988015 + ], + [ + -2.585914495013185, + 8.332391150598369 + ], + [ + -2.7295764114028316, + 8.332391150598369 + ], + [ + -2.8732383277924782, + 8.188729234208722 + ], + [ + -3.016900244182125, + 8.188729234208722 + ], + [ + -3.1605621605717715, + 8.188729234208722 + ], + [ + -3.1605621605717715, + 8.045067317819075 + ], + [ + -3.1605621605717715, + 7.901405401429429 + ], + [ + -3.304224076961418, + 7.757743485039782 + ], + [ + -3.447885993351065, + 7.614081568650249 + ], + [ + -3.447885993351065, + 7.470419652260603 + ], + [ + -3.304224076961418, + 7.470419652260603 + ], + [ + -3.304224076961418, + 7.326757735870956 + ], + [ + -3.304224076961418, + 7.326757735870956 + ] + ], + "pressures": [], + "simulatePressure": true, + "lastCommittedPoint": [ + -3.304224076961418, + 7.326757735870956 + ] } ], "appState": { From e3385051b1fde86a07ed08ad5ced67cfce7b013c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Mon, 23 Sep 2024 23:41:51 +0200 Subject: [PATCH 129/162] change resumeable clients store and gateway user store to ConnectedUsers structure --- src/api/mod.rs | 5 +- src/gateway/establish_connection.rs | 33 +----- src/gateway/mod.rs | 155 +++++++++++++++++++++++----- src/main.rs | 12 ++- 4 files changed, 144 insertions(+), 61 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index ee82b9a..45abd17 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -13,6 +13,7 @@ use poem::{ use serde_json::json; use sqlx::PgPool; +use crate::gateway::ConnectedUsers; use crate::SharedEventPublisherMap; use crate::{ api::{ @@ -30,7 +31,7 @@ mod routes; pub async fn start_api( db: PgPool, - publisher_map: SharedEventPublisherMap, + connected_users: ConnectedUsers, config: Config, ) -> Result<(), Error> { log::info!(target: "symfonia::api::cfg", "Loading configuration"); @@ -79,7 +80,7 @@ pub async fn start_api( .nest("/api/v9", routes) .data(db) .data(config) - .data(publisher_map) + .data(connected_users) .with(NormalizePath::new(TrailingSlash::Trim)) .catch_all_error(custom_error); diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 8cc7588..ceef1de 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -29,15 +29,14 @@ use crate::{ util::token::check_token, }; -use super::{Connection, GatewayClient, GatewayUsersStore, NewConnection, ResumableClientsStore}; +use super::{ConnectedUsers, Connection, GatewayClient, NewConnection, ResumableClientsStore}; /// Internal use only state struct to pass around data to the `finish_connecting` function. struct State { connection: Arc>, db: PgPool, config: Config, - gateway_users_store: GatewayUsersStore, - resumeable_clients_store: ResumableClientsStore, + connected_users: ConnectedUsers, sequence_number: Arc>, kill_send: Sender<()>, kill_receive: tokio::sync::broadcast::Receiver<()>, @@ -57,8 +56,7 @@ pub(super) async fn establish_connection( stream: TcpStream, db: PgPool, config: Config, - gateway_users_store: GatewayUsersStore, - resumeable_clients_store: ResumableClientsStore, + connected_users: ConnectedUsers, ) -> Result { trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Beginning process to establish connection (handshake)"); // Accept the connection and split it into its sender and receiver halves. @@ -99,8 +97,7 @@ pub(super) async fn establish_connection( connection: connection.clone(), db: db.clone(), config: config.clone(), - gateway_users_store: gateway_users_store.clone(), - resumeable_clients_store: resumeable_clients_store.clone(), + connected_users: connected_users.clone(), sequence_number: sequence_number.clone(), kill_send: kill_send.clone(), kill_receive: kill_receive.resubscribe(), @@ -135,28 +132,6 @@ pub(super) async fn establish_connection( } } -/// `get_or_new_gateway_user` is a helper function that retrieves a [GatewayUser] from the store if it exists, -/// or creates a new user, stores it in the store and then returns it, if it does not exist. -// TODO: Refactor this function according to the new `ResumeableClientsStore` definition. -async fn get_or_new_gateway_user( - user_id: Snowflake, - store: GatewayUsersStore, - resumeable_clients_store: ResumableClientsStore, -) -> Arc> { - let mut store = store.lock().await; - if let Some(user) = store.get(&user_id) { - return user.clone(); - } - let user = Arc::new(Mutex::new(GatewayUser { - id: user_id, - clients: HashMap::new(), - subscriptions: Vec::new(), - resumeable_clients_store: Arc::downgrade(&resumeable_clients_store), - })); - store.insert(user_id, user.clone()); - user -} - /// `finish_connecting` is the second part of the connection establishment process. It picks up after /// the initial `Hello` message has been sent to the client. It then waits on the next message from /// the client, which should be either a `Heartbeat`, `Identify` or `Resume` message, handling each diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index ccd3b1a..1d46acc 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -11,6 +11,9 @@ mod gateway_task; mod heartbeat; mod types; +use std::collections::HashSet; +use std::hash::Hash; +use std::ops::{Deref, DerefMut}; use std::{ collections::{BTreeMap, HashMap}, sync::{Arc, Weak}, @@ -66,14 +69,102 @@ Handling a connection involves the following steps: Handling disconnects and session resumes is for later and not considered at this exact moment. -From there on, run a task that takes ownership of the GatewayClient struct. This task will be what -is sending the events that the (to be implemented) Subscribers receive from the Publishers that the -GatewayUser is subscribed to +From there on, run a task that takes ownership of the Ga + +It is important to make a distinction between the user and the client. A user can potentially +be connected with many devices at once. They are still just one user. Respecting this fact +will likely save a lot of computational power. + +Handling a connection involves the following steps: + +1. Accepting the connection +2. Sending a hello event back +3. Receiving a Heartbeat event +4. Returning a Heartbeat ACK event +5. Receiving an Identify payload <- "GatewayUser" and/or "GatewayClient" are instantiated here. +6. Responding with a Ready event + +Handling disconnects and session resumes is for late */ +#[derive(Default, Clone)] +pub struct ConnectedUsers { + store: Arc>, +} + +/// A mapping of Snowflake IDs to the "inbox" of a [GatewayUser]. +/// +/// An "inbox" is a [tokio::sync::mpsc::Sender] that can be used to send [Event]s to all connected +/// clients of a [GatewayUser]. +#[derive(Default)] +pub struct ConnectedUsersInner { + pub inboxes: HashMap>, + pub users: HashMap>>, + pub resumeable_clients_store: ResumableClientsStore, +} + +impl ConnectedUsers { + /// Create a new, empty [ConnectedUsers] instance. + pub fn new() -> Self { + Self::default() + } + + pub fn inner(&self) -> Arc> { + self.store.clone() + } + + /// Register a new [GatewayUser] with the [ConnectedUsers] instance. + async fn register(&self, user: GatewayUser) -> Arc> { + self.store + .lock() + .await + .inboxes + .insert(user.id, user.outbox.clone()); + let id = user.id; + let arc = Arc::new(Mutex::new(user)); + self.store.lock().await.users.insert(id, arc.clone()); + arc + } + + /// Deregister a [GatewayUser] from the [ConnectedUsers] instance. + pub async fn deregister(&self, user: &GatewayUser) { + self.store.lock().await.inboxes.remove(&user.id); + self.store.lock().await.users.remove(&user.id); + } + + /// Get the "inbox" of a [GatewayUser] by its Snowflake ID. + pub async fn inbox(&self, id: Snowflake) -> Option> { + self.store.lock().await.inboxes.get(&id).cloned() + } + + pub async fn new_user( + &self, + clients: HashMap>>, + id: Snowflake, + subscriptions: Vec>>, + ) -> Arc> { + let channel = tokio::sync::broadcast::channel(20); + let user = GatewayUser { + inbox: channel.1, + outbox: channel.0.clone(), + clients, + id, + subscriptions, + connected_users: self.clone(), + }; + self.register(user).await + } +} + /// A single identifiable User connected to the Gateway - possibly using many clients at the same /// time. struct GatewayUser { + /// The "inbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Receiver]. Events sent to + /// this inbox will be sent to all connected clients of this user. + pub inbox: tokio::sync::broadcast::Receiver, + /// The "outbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Sender]. From this outbox, + /// more inboxes can be created. + pub(super) outbox: tokio::sync::broadcast::Sender, /// Sessions a User is connected with. HashMap of SessionToken -> GatewayClient clients: HashMap>>, /// The Snowflake ID of the User. @@ -83,10 +174,24 @@ struct GatewayUser { /// A GatewayUser may have many [GatewayClients](GatewayClient), but he only gets subscribed to /// all relevant [Publishers](pubserve::Publisher) *once* to save resources. subscriptions: Vec>>, - /// [Weak] reference to the [ResumableClientsStore]. - resumeable_clients_store: Weak>>, + /// [Weak] reference to the [ConnectedUsers] store. + connected_users: ConnectedUsers, +} + +impl Hash for GatewayUser { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } } +impl PartialEq for GatewayUser { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for GatewayUser {} + /// A concrete session, that a [GatewayUser] is connected to the Gateway with. struct GatewayClient { connection: Arc>, @@ -132,6 +237,7 @@ struct Connection { receiver: WebSocketReceive, } +#[derive(Clone)] struct DisconnectInfo { /// session token that was used for this connection session_token: String, @@ -163,16 +269,15 @@ struct NewConnection { client: Arc>, } -/// A thread-shareable map of resumable clients. The key is the session token used +/// A map of resumable clients. The key is the session token used /// for the connection. The value is a [GatewayClient] that can be resumed. // TODO: this is stupid. it should be a map of string and DisconnectInfo. there is no need to store // the whole GatewayClient, nor would it make sense to do so. -type ResumableClientsStore = Arc>>; -type GatewayUsersStore = Arc>>>>; +type ResumableClientsStore = HashMap; pub async fn start_gateway( db: PgPool, - publisher_map: SharedEventPublisherMap, + connected_users: ConnectedUsers, config: Config, ) -> Result<(), Error> { // TODO(bitfl0wer): Add log messages throughout the method for debugging the gateway @@ -184,10 +289,9 @@ pub async fn start_gateway( info!(target: "symfonia::gateway", "Gateway server listening on port {bind}"); - let gateway_users: GatewayUsersStore = Arc::new(Mutex::new(BTreeMap::new())); - let resumeable_clients: ResumableClientsStore = Arc::new(Mutex::new(BTreeMap::new())); - let resumeable_clients_clone = resumeable_clients.clone(); - tokio::task::spawn(async { purge_expired_disconnects(resumeable_clients_clone).await }); + let resumeable_clients: ResumableClientsStore = HashMap::new(); + let connected_users = ConnectedUsers::new(); + tokio::task::spawn(async { purge_expired_disconnects(connected_users.clone()).await }); while let Ok((stream, _)) = listener.accept().await { log::trace!(target: "symfonia::gateway", "New connection received"); let connection_result = match tokio::task::spawn( @@ -195,8 +299,7 @@ pub async fn start_gateway( stream, db.clone(), config.clone(), - gateway_users.clone(), - resumeable_clients.clone(), + connected_users.clone(), ), ) .await @@ -209,7 +312,7 @@ pub async fn start_gateway( }; match connection_result { Ok(new_connection) => { - checked_add_new_connection(gateway_users.clone(), new_connection).await + checked_add_new_connection(connected_users.clone(), new_connection).await } Err(e) => { log::debug!(target: "symfonia::gateway::establish_connection", "User gateway connection could not be established: {e}"); @@ -223,7 +326,7 @@ pub async fn start_gateway( /// A disconnected, resumable session can only be resumed within `RESUME_RECONNECT_WINDOW_SECONDS` /// seconds after a disconnect occurs. Sessions that can be resumed are stored in a `Map`. The /// purpose of this method is to periodically throw out expired sessions from that map. -async fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { +async fn purge_expired_disconnects(connected_users: ConnectedUsers) { let mut minutely_log_timer = 0; let mut removed_elements_last_minute: u128 = 0; loop { @@ -234,8 +337,10 @@ async fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { .expect("Check the clock/time settings on the host machine") .as_secs(); let mut to_remove = Vec::new(); - let mut resumeable_clients_lock = resumeable_clients.lock().await; - for (disconnected_session_id, disconnected_session) in resumeable_clients_lock.iter() { + let mut _inner = connected_users.inner(); + let mut lock = _inner.lock().await; + for (disconnected_session_id, disconnected_session) in lock.resumeable_clients_store.iter() + { // TODO(bitfl0wer): What are we calculating here? At least, this should be commented if current_unix_timestamp - disconnected_session.disconnected_at_sequence > RESUME_RECONNECT_WINDOW_SECONDS as u64 @@ -248,9 +353,9 @@ async fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { .checked_add(len as u128) .unwrap_or(u128::MAX); for session_id in to_remove.iter() { - resumeable_clients_lock.remove(session_id); + lock.resumeable_clients_store.remove(session_id); } - drop(resumeable_clients_lock); + drop(lock); minutely_log_timer += 1; if minutely_log_timer == 12 { log::debug!(target: "symfonia::gateway::purge_expired_disconnects", "Removed {} stale sessions in the last 60 seconds", removed_elements_last_minute); @@ -269,7 +374,7 @@ async fn purge_expired_disconnects(resumeable_clients: ResumableClientsStore) { /// /// Else, add the [new GatewayUser] and the new [GatewayClient] into `gateway_users` as-is. async fn checked_add_new_connection( - gateway_users: Arc>>>>, + connected_users: ConnectedUsers, new_connection: NewConnection, ) { // Make `new_connection` mutable @@ -278,12 +383,12 @@ async fn checked_add_new_connection( // of the way through this method let new_connection_user = new_connection.user.lock().await; let new_connection_token = new_connection.client.lock().await.session_token.clone(); - let mut locked_map = gateway_users.lock().await; + let mut lock = connected_users.inner().lock().await; // If our map contains the user from `new_connection` already, modify the `parent` of the `client` // of `new_connection` to point to the user already in our map, then insert that `client` into // the `clients` field of our existing user. - if locked_map.contains_key(&new_connection_user.id) { - let existing_user = locked_map.get(&new_connection_user.id).unwrap(); + if lock.inner.contains_key(&new_connection_user.id) { + let existing_user = lock.inner.get(&new_connection_user.id).unwrap(); new_connection.client.lock().await.parent = Arc::downgrade(existing_user); existing_user .lock() diff --git a/src/main.rs b/src/main.rs index 0496d01..38ad5ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,13 +6,13 @@ #![allow(unused)] // TODO: Remove, I just want to clean up my build output -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use chorus::types::Snowflake; use clap::Parser; -use gateway::Event; +use gateway::{ConnectedUsers, Event}; use log::LevelFilter; use log4rs::{ append::{ @@ -31,6 +31,7 @@ use log4rs::{ }; use parking_lot::RwLock; use pubserve::Publisher; +use tokio::sync::Mutex; mod api; mod cdn; @@ -217,16 +218,17 @@ async fn main() { .await .unwrap_or_default(); - let shared_publisher_map = Arc::new(RwLock::new(HashMap::new())); + let connected_users = ConnectedUsers::default(); + let mut tasks = [ tokio::spawn(api::start_api( db.clone(), - shared_publisher_map.clone(), + connected_users.clone(), symfonia_config.clone(), )), tokio::spawn(gateway::start_gateway( db.clone(), - shared_publisher_map.clone(), + connected_users.clone(), symfonia_config.clone(), )), ]; From 7eda68e474b0ea1876fc27f6d4d6ce7788b4a207 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 24 Sep 2024 00:20:09 +0200 Subject: [PATCH 130/162] fix die function --- src/gateway/mod.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 1d46acc..b4c2bab 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -211,7 +211,7 @@ struct GatewayClient { } impl GatewayClient { - pub async fn die(mut self, resumeable_clients: ResumableClientsStore) { + pub async fn die(mut self, connected_users: ConnectedUsers) { self.kill_send.send(()).unwrap(); let disconnect_info = DisconnectInfo { session_token: self.session_token.clone(), @@ -225,9 +225,14 @@ impl GatewayClient { .await .clients .remove(&self.session_token); - resumeable_clients + connected_users + .deregister(self.parent.upgrade().unwrap().lock().await.deref()) + .await; + connected_users + .store .lock() .await + .resumeable_clients_store .insert(self.session_token.clone(), disconnect_info); } } From bc65539629f33b89de10f8d9e29bffa7e9c3cbcd Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 24 Sep 2024 11:08:03 +0200 Subject: [PATCH 131/162] Add missing fixes for migration to ConnectedUsers store --- src/gateway/establish_connection.rs | 7 +------ src/gateway/mod.rs | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index ceef1de..40988c0 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -204,12 +204,7 @@ async fn finish_connecting( return Err(crate::errors::UserError::InvalidToken.into()); } }; - let mut gateway_user = get_or_new_gateway_user( - claims.id, - state.gateway_users_store.clone(), - state.resumeable_clients_store.clone(), - ) - .await; + let mut gateway_user = state.connected_users.get_user_or_new(claims.id).await; let gateway_client = GatewayClient { parent: Arc::downgrade(&gateway_user), connection: state.connection.clone(), diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index b4c2bab..775b556 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -32,6 +32,7 @@ use log::info; use pubserve::Subscriber; use serde_json::{from_str, json}; use sqlx::PgPool; +use tokio::sync::MutexGuard; use tokio::{ net::{TcpListener, TcpStream}, sync::Mutex, @@ -109,6 +110,16 @@ impl ConnectedUsers { Self::default() } + pub async fn get_user_or_new(&self, id: Snowflake) -> Arc> { + let inner = self.store.clone(); + let mut lock = inner.lock().await; + if let Some(user) = lock.users.get(&id) { + user.clone() + } else { + self.new_user(HashMap::new(), id, Vec::new()).await + } + } + pub fn inner(&self) -> Arc> { self.store.clone() } @@ -296,7 +307,8 @@ pub async fn start_gateway( let resumeable_clients: ResumableClientsStore = HashMap::new(); let connected_users = ConnectedUsers::new(); - tokio::task::spawn(async { purge_expired_disconnects(connected_users.clone()).await }); + let connected_users_clone = connected_users.clone(); + tokio::task::spawn(async { purge_expired_disconnects(connected_users_clone).await }); while let Ok((stream, _)) = listener.accept().await { log::trace!(target: "symfonia::gateway", "New connection received"); let connection_result = match tokio::task::spawn( @@ -388,12 +400,13 @@ async fn checked_add_new_connection( // of the way through this method let new_connection_user = new_connection.user.lock().await; let new_connection_token = new_connection.client.lock().await.session_token.clone(); - let mut lock = connected_users.inner().lock().await; + let inner = connected_users.inner(); + let mut lock = inner.lock().await; // If our map contains the user from `new_connection` already, modify the `parent` of the `client` // of `new_connection` to point to the user already in our map, then insert that `client` into // the `clients` field of our existing user. - if lock.inner.contains_key(&new_connection_user.id) { - let existing_user = lock.inner.get(&new_connection_user.id).unwrap(); + if lock.users.contains_key(&new_connection_user.id) { + let existing_user = lock.users.get(&new_connection_user.id).unwrap(); new_connection.client.lock().await.parent = Arc::downgrade(existing_user); existing_user .lock() @@ -405,6 +418,6 @@ async fn checked_add_new_connection( // locked. Just bind the id we need to a new variable, then drop the lock. let id = new_connection_user.id; drop(new_connection_user); - locked_map.insert(id, new_connection.user); + lock.users.insert(id, new_connection.user); } } From df514836c51776136379158fd945d474615850e8 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 24 Sep 2024 11:11:23 +0200 Subject: [PATCH 132/162] Modify visibilities --- src/gateway/mod.rs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 775b556..aac75ab 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -169,7 +169,7 @@ impl ConnectedUsers { /// A single identifiable User connected to the Gateway - possibly using many clients at the same /// time. -struct GatewayUser { +pub struct GatewayUser { /// The "inbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Receiver]. Events sent to /// this inbox will be sent to all connected clients of this user. pub inbox: tokio::sync::broadcast::Receiver, @@ -204,18 +204,18 @@ impl PartialEq for GatewayUser { impl Eq for GatewayUser {} /// A concrete session, that a [GatewayUser] is connected to the Gateway with. -struct GatewayClient { +pub struct GatewayClient { connection: Arc>, /// A [Weak] reference to the [GatewayUser] this client belongs to. - parent: Weak>, + pub parent: Weak>, // Handle to the main Gateway task for this client main_task_handle: tokio::task::JoinHandle<()>, // Handle to the heartbeat task for this client heartbeat_task_handle: tokio::task::JoinHandle<()>, // Kill switch to disconnect the client - kill_send: tokio::sync::broadcast::Sender<()>, + pub kill_send: tokio::sync::broadcast::Sender<()>, /// Token of the session token used for this connection - session_token: String, + pub session_token: String, /// The last sequence number received from the client. Shared between the main task, heartbeat /// task, and this struct. last_sequence: Arc>, @@ -254,11 +254,11 @@ struct Connection { } #[derive(Clone)] -struct DisconnectInfo { +pub struct DisconnectInfo { /// session token that was used for this connection - session_token: String, - disconnected_at_sequence: u64, - parent: Weak>, + pub session_token: String, + pub disconnected_at_sequence: u64, + pub parent: Weak>, } impl @@ -289,7 +289,7 @@ struct NewConnection { /// for the connection. The value is a [GatewayClient] that can be resumed. // TODO: this is stupid. it should be a map of string and DisconnectInfo. there is no need to store // the whole GatewayClient, nor would it make sense to do so. -type ResumableClientsStore = HashMap; +pub type ResumableClientsStore = HashMap; pub async fn start_gateway( db: PgPool, @@ -306,7 +306,6 @@ pub async fn start_gateway( info!(target: "symfonia::gateway", "Gateway server listening on port {bind}"); let resumeable_clients: ResumableClientsStore = HashMap::new(); - let connected_users = ConnectedUsers::new(); let connected_users_clone = connected_users.clone(); tokio::task::spawn(async { purge_expired_disconnects(connected_users_clone).await }); while let Ok((stream, _)) = listener.accept().await { From a75836669997f02b846e7318c3f50f8937f85916 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 24 Sep 2024 15:05:38 +0200 Subject: [PATCH 133/162] add roleusermap and init function for roleusermap --- src/gateway/mod.rs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index aac75ab..d0d9d9c 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -32,6 +32,7 @@ use log::info; use pubserve::Subscriber; use serde_json::{from_str, json}; use sqlx::PgPool; +use sqlx_pg_uint::PgU64; use tokio::sync::MutexGuard; use tokio::{ net::{TcpListener, TcpStream}, @@ -88,9 +89,45 @@ Handling a connection involves the following steps: Handling disconnects and session resumes is for late */ +/// Represents all Roles that exist on the Discord server and the Users that have them. +#[derive(Default)] +pub struct RoleUserMap { + /// Map Role Snowflake ID to a list of User Snowflake IDs + map: HashMap>, +} + +impl RoleUserMap { + /// Initialize the RoleUserMap with data from the database. + pub async fn init(&mut self, db: &PgPool) -> Result<(), Error> { + // First, get all role ids from the roles table and insert them into the map + let all_role_ids: Vec = sqlx::query_as("SELECT id FROM roles") + .fetch_all(db) + .await + .map_err(Error::SQLX)?; + for role_id in all_role_ids.iter() { + self.map + .insert(Snowflake::from(role_id.to_uint()), HashSet::new()); + } + // Then, query member_roles and insert the user ids into the map + let all_member_roles: Vec<(PgU64, PgU64)> = + sqlx::query_as("SELECT index, role_id FROM roles") + .fetch_all(db) + .await + .map_err(Error::SQLX)?; + for (user_id, role_id) in all_member_roles.iter() { + // Unwrapping is fine here, as the member_roles table has a foreign key constraint + // which states that role_id must be a valid id in the roles table. + let users_for_role_id = self.map.get_mut(&role_id.to_uint().into()).unwrap(); + users_for_role_id.insert(user_id.to_uint().into()); + } + Ok(()) + } +} + #[derive(Default, Clone)] pub struct ConnectedUsers { - store: Arc>, + pub store: Arc>, + role_user_map: Arc>, } /// A mapping of Snowflake IDs to the "inbox" of a [GatewayUser]. From f4f68b535a3ed4910960be6fe58e4f049fede2c7 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 24 Sep 2024 15:36:45 +0200 Subject: [PATCH 134/162] Init role-user map on server start --- src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main.rs b/src/main.rs index 38ad5ff..3b1c4d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -219,6 +219,12 @@ async fn main() { .unwrap_or_default(); let connected_users = ConnectedUsers::default(); + log::debug!(target: "symfonia", "Initializing Role->User map..."); + connected_users + .init_role_user_map(&db) + .await + .expect("Failed to init role user map"); + log::trace!(target: "symfonia", "Role->User map initialized with {} entries", connected_users.role_user_map.lock().await.len()); let mut tasks = [ tokio::spawn(api::start_api( From 4e89e76d19d7f92d5cb3c7efea22bc477023e10b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 24 Sep 2024 15:36:57 +0200 Subject: [PATCH 135/162] change dep chorus to point to dev branch --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b7fa818..b707c25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,7 @@ sentry = { version = "0.34.0", default-features = false, features = [ clap = { version = "4.5.4", features = ["derive"] } chorus = { features = [ "backend", -], default-features = false, path = "../chorus" } +], default-features = false, git = "https://github.com/polyphony-chat/chorus", branch = "dev" } serde_path_to_error = "0.1.16" percent-encoding = "2.3.1" hex = "0.4.3" From fcbee3ed178648ea062b7314f31bfe8fcce69c7b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Tue, 24 Sep 2024 15:37:17 +0200 Subject: [PATCH 136/162] impl deref, deref mut for roleusermap, fix init() referring to wrong table --- src/gateway/mod.rs | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index d0d9d9c..89eba66 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -89,15 +89,36 @@ Handling a connection involves the following steps: Handling disconnects and session resumes is for late */ -/// Represents all Roles that exist on the Discord server and the Users that have them. #[derive(Default)] +/// Represents all existing roles on the server and the users that have these roles. pub struct RoleUserMap { /// Map Role Snowflake ID to a list of User Snowflake IDs map: HashMap>, } +impl Deref for RoleUserMap { + type Target = HashMap>; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for RoleUserMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + impl RoleUserMap { - /// Initialize the RoleUserMap with data from the database. + /// Initialize the [RoleUserMap] with data from the database. + /// + /// This method will query the database for all roles and all users that have these roles. + /// The data will then populate the map. + /// + /// Due to the possibly large number of roles and users returned by the database, this method + /// should only be executed once. The [RoleUserMap] should be kept synchronized with the database + /// through means that do not involve this method. pub async fn init(&mut self, db: &PgPool) -> Result<(), Error> { // First, get all role ids from the roles table and insert them into the map let all_role_ids: Vec = sqlx::query_as("SELECT id FROM roles") @@ -110,7 +131,7 @@ impl RoleUserMap { } // Then, query member_roles and insert the user ids into the map let all_member_roles: Vec<(PgU64, PgU64)> = - sqlx::query_as("SELECT index, role_id FROM roles") + sqlx::query_as("SELECT index, role_id FROM member_roles") .fetch_all(db) .await .map_err(Error::SQLX)?; @@ -127,7 +148,7 @@ impl RoleUserMap { #[derive(Default, Clone)] pub struct ConnectedUsers { pub store: Arc>, - role_user_map: Arc>, + pub role_user_map: Arc>, } /// A mapping of Snowflake IDs to the "inbox" of a [GatewayUser]. @@ -147,6 +168,10 @@ impl ConnectedUsers { Self::default() } + pub async fn init_role_user_map(&self, db: &PgPool) -> Result<(), Error> { + self.role_user_map.lock().await.init(db).await + } + pub async fn get_user_or_new(&self, id: Snowflake) -> Arc> { let inner = self.store.clone(); let mut lock = inner.lock().await; From c96c288e7169f31415b06c0c197e26845204c76c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 00:24:24 +0200 Subject: [PATCH 137/162] rename Role.to_inner to .into_inner and remove duplicate definition --- src/api/routes/guilds/id/mod.rs | 2 +- src/database/entities/guild.rs | 2 +- src/database/entities/role.rs | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/api/routes/guilds/id/mod.rs b/src/api/routes/guilds/id/mod.rs index 8a095e4..256f412 100644 --- a/src/api/routes/guilds/id/mod.rs +++ b/src/api/routes/guilds/id/mod.rs @@ -55,7 +55,7 @@ pub async fn get_guild( let roles = Role::get_by_guild(db, guild_id).await?; - guild.roles = roles.into_iter().map(|r| r.to_inner()).collect(); + guild.roles = roles.into_iter().map(|r| r.into_inner()).collect(); let member = GuildMember::get_by_id(db, claims.id, guild_id) .await? diff --git a/src/database/entities/guild.rs b/src/database/entities/guild.rs index 53685cf..93af444 100644 --- a/src/database/entities/guild.rs +++ b/src/database/entities/guild.rs @@ -140,7 +140,7 @@ impl Guild { user.add_to_guild(db, guild.id).await?; guild.owner = Some(true); - guild.roles = vec![everyone.to_inner()]; + guild.roles = vec![everyone.into_inner()]; let channels = if channels.is_empty() { vec![ diff --git a/src/database/entities/role.rs b/src/database/entities/role.rs index 2e19229..d281d27 100644 --- a/src/database/entities/role.rs +++ b/src/database/entities/role.rs @@ -48,10 +48,6 @@ impl DerefMut for Role { } impl Role { - pub fn to_inner(self) -> chorus::types::RoleObject { - self.inner - } - pub async fn create( db: &PgPool, shared_event_publisher_map: SharedEventPublisherMap, From ce88bc78d21ee9a97ad26c49ab6b544c6a2c6005 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 01:00:39 +0200 Subject: [PATCH 138/162] add BulkMessageBuilder --- src/gateway/mod.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 89eba66..3f1b114 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -145,6 +145,60 @@ impl RoleUserMap { } } +#[derive(Default, Clone)] +pub struct BulkMessageBuilder { + users: Vec, + roles: Vec, + message: Option, +} + +impl BulkMessageBuilder { + /// Add the given list of user snowflake IDs to the list of recipients. + pub async fn add_user_recipients(&mut self, users: &[Snowflake]) { + self.users.extend_from_slice(users); + } + + /// Add all members which have the given role snowflake IDs to the list of recipients. + pub async fn add_role_recipients(&mut self, roles: &[Snowflake]) { + self.roles.extend_from_slice(roles); + } + + /// Set the message to be sent to the recipients. + pub async fn set_message(&mut self, message: Event) { + self.message = Some(message); + } + + /// Send the message to all recipients. + pub async fn send(self, connected_users: ConnectedUsers) -> Result<(), Error> { + if self.message.is_none() { + return Err(Error::Custom("No message to send".to_string())); + } + let mut recipients = HashSet::new(); + let lock = connected_users.role_user_map.lock().await; + for role in self.roles.iter() { + if let Some(users) = lock.get(role) { + for user in users.iter() { + recipients.insert(*user); + } + } + for user in self.users.iter() { + recipients.insert(*user); + } + } + if recipients.is_empty() { + return Ok(()); + } + for recipient in recipients.iter() { + if let Some(inbox) = connected_users.inbox(*recipient).await { + inbox + .send(self.message.clone().unwrap()) + .map_err(|e| Error::Custom(format!("tokio broadcast error: {}", e)))?; + } + } + Ok(()) + } +} + #[derive(Default, Clone)] pub struct ConnectedUsers { pub store: Arc>, @@ -168,6 +222,18 @@ impl ConnectedUsers { Self::default() } + pub fn bulk_message_builder(&self) -> BulkMessageBuilder { + BulkMessageBuilder::default() + } + + /// Initialize the [RoleUserMap] with data from the database. + /// + /// This method will query the database for all roles and all users that have these roles. + /// The data will then populate the map. + /// + /// Due to the possibly large number of roles and users returned by the database, this method + /// should only be executed once. The [RoleUserMap] should be kept synchronized with the database + /// through means that do not involve this method. pub async fn init_role_user_map(&self, db: &PgPool) -> Result<(), Error> { self.role_user_map.lock().await.init(db).await } From 7e50b032ba055138244b792913c79ffb25289ff6 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 01:00:51 +0200 Subject: [PATCH 139/162] Add Custom error type --- src/errors.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index 1b89634..31d8f31 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,6 +5,7 @@ */ use std::error::Error as StdError; +use std::fmt::Display; use chorus::types::{APIError, AuthError, Rights}; use poem::{error::ResponseError, http::StatusCode, Response}; @@ -61,6 +62,9 @@ pub enum Error { #[error(transparent)] SqlxPgUint(#[from] sqlx_pg_uint::Error), + + #[error("{0}")] + Custom(String), } #[derive(Debug, thiserror::Error)] @@ -241,6 +245,7 @@ impl ResponseError for Error { GatewayError::Closed => StatusCode::BAD_REQUEST, }, Error::SqlxPgUint(_) => StatusCode::BAD_REQUEST, + Error::Custom(_) => StatusCode::BAD_REQUEST, } } From 2fe804ed0392dce75b7867ffc08bd3b9a6ae6b9a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 17:57:45 +0200 Subject: [PATCH 140/162] Replace manual constructions of GatewayUser and -Client with new_user and new_client methods --- src/gateway/establish_connection.rs | 59 ++++++++++++++--------------- src/gateway/mod.rs | 38 ++++++++++++++++++- 2 files changed, 64 insertions(+), 33 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 40988c0..c9185ae 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -205,40 +205,37 @@ async fn finish_connecting( } }; let mut gateway_user = state.connected_users.get_user_or_new(claims.id).await; - let gateway_client = GatewayClient { - parent: Arc::downgrade(&gateway_user), - connection: state.connection.clone(), - main_task_handle: tokio::spawn(gateway_task::gateway_task( + let gateway_client = state + .connected_users + .new_client( + gateway_user.clone(), state.connection.clone(), - )), - heartbeat_task_handle: match heartbeat_handler_handle { - Some(handle) => handle, - None => tokio::spawn({ - let mut heartbeat_handler = HeartbeatHandler::new( - state.connection.clone(), - state.kill_receive.resubscribe(), - state.kill_send.clone(), - state.heartbeat_receive.resubscribe(), - state.sequence_number.clone(), - state.session_id_receive.resubscribe(), - ); - async move { - heartbeat_handler.run().await; - } - }), - }, - kill_send: state.kill_send.clone(), - session_token: identify.event_data.token, - last_sequence: state.sequence_number.clone(), - }; - let gateway_client_arc_mutex = Arc::new(Mutex::new(gateway_client)); - gateway_user.lock().await.clients.insert( - gateway_client_arc_mutex.lock().await.session_token.clone(), - gateway_client_arc_mutex.clone(), - ); + tokio::spawn(gateway_task::gateway_task(state.connection.clone())), + match heartbeat_handler_handle { + Some(handle) => handle, + None => tokio::spawn({ + let mut heartbeat_handler = HeartbeatHandler::new( + state.connection.clone(), + state.kill_receive.resubscribe(), + state.kill_send.clone(), + state.heartbeat_receive.resubscribe(), + state.sequence_number.clone(), + state.session_id_receive.resubscribe(), + ); + async move { + heartbeat_handler.run().await; + } + }), + }, + state.kill_send.clone(), + &identify.event_data.token, + state.sequence_number.clone(), + ) + .await; + return Ok(NewConnection { user: gateway_user, - client: gateway_client_arc_mutex.clone(), + client: gateway_client.clone(), }); } else if let Ok(resume) = from_str::(&raw_message.to_string()) { log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Received resume payload"); diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 3f1b114..9b55e16 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -238,6 +238,8 @@ impl ConnectedUsers { self.role_user_map.lock().await.init(db).await } + /// Get a [GatewayUser] by its Snowflake ID if it already exists in the store, or create a new + /// [GatewayUser] if it does not exist using [ConnectedUsers::new_user]. pub async fn get_user_or_new(&self, id: Snowflake) -> Arc> { let inner = self.store.clone(); let mut lock = inner.lock().await; @@ -276,6 +278,8 @@ impl ConnectedUsers { self.store.lock().await.inboxes.get(&id).cloned() } + /// Create a new [GatewayUser] with the given Snowflake ID, [GatewayClient]s, and subscriptions. + /// Registers the new [GatewayUser] with the [ConnectedUsers] instance. pub async fn new_user( &self, clients: HashMap>>, @@ -293,6 +297,36 @@ impl ConnectedUsers { }; self.register(user).await } + + /// Create a new [GatewayClient] with the given [GatewayUser], [Connection], and other data. + /// Also handles appending the new [GatewayClient] to the [GatewayUser]'s list of clients. + #[allow(clippy::too_many_arguments)] + pub async fn new_client( + &self, + user: Arc>, + connection: Arc>, + main_task_handle: tokio::task::JoinHandle<()>, + heartbeat_task_handle: tokio::task::JoinHandle<()>, + kill_send: tokio::sync::broadcast::Sender<()>, + session_token: &str, + last_sequence: Arc>, + ) -> Arc> { + let client = GatewayClient { + connection, + parent: Arc::downgrade(&user), + main_task_handle, + heartbeat_task_handle, + kill_send, + session_token: session_token.to_string(), + last_sequence, + }; + let arc = Arc::new(Mutex::new(client)); + user.lock() + .await + .clients + .insert(session_token.to_string(), arc.clone()); + arc + } } /// A single identifiable User connected to the Gateway - possibly using many clients at the same @@ -303,7 +337,7 @@ pub struct GatewayUser { pub inbox: tokio::sync::broadcast::Receiver, /// The "outbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Sender]. From this outbox, /// more inboxes can be created. - pub(super) outbox: tokio::sync::broadcast::Sender, + outbox: tokio::sync::broadcast::Sender, /// Sessions a User is connected with. HashMap of SessionToken -> GatewayClient clients: HashMap>>, /// The Snowflake ID of the User. @@ -376,7 +410,7 @@ impl GatewayClient { } } -struct Connection { +pub struct Connection { sender: WebSocketSend, receiver: WebSocketReceive, } From 8c29c5a5068a26481bde2acc58e6c89edea8a51b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 18:18:40 +0200 Subject: [PATCH 141/162] Move a lot of structs into types module, clean up dumb methods/functions --- src/gateway/mod.rs | 311 +------------------------------------------ src/gateway/types.rs | 276 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+), 308 deletions(-) diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 9b55e16..5c911ef 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -145,271 +145,6 @@ impl RoleUserMap { } } -#[derive(Default, Clone)] -pub struct BulkMessageBuilder { - users: Vec, - roles: Vec, - message: Option, -} - -impl BulkMessageBuilder { - /// Add the given list of user snowflake IDs to the list of recipients. - pub async fn add_user_recipients(&mut self, users: &[Snowflake]) { - self.users.extend_from_slice(users); - } - - /// Add all members which have the given role snowflake IDs to the list of recipients. - pub async fn add_role_recipients(&mut self, roles: &[Snowflake]) { - self.roles.extend_from_slice(roles); - } - - /// Set the message to be sent to the recipients. - pub async fn set_message(&mut self, message: Event) { - self.message = Some(message); - } - - /// Send the message to all recipients. - pub async fn send(self, connected_users: ConnectedUsers) -> Result<(), Error> { - if self.message.is_none() { - return Err(Error::Custom("No message to send".to_string())); - } - let mut recipients = HashSet::new(); - let lock = connected_users.role_user_map.lock().await; - for role in self.roles.iter() { - if let Some(users) = lock.get(role) { - for user in users.iter() { - recipients.insert(*user); - } - } - for user in self.users.iter() { - recipients.insert(*user); - } - } - if recipients.is_empty() { - return Ok(()); - } - for recipient in recipients.iter() { - if let Some(inbox) = connected_users.inbox(*recipient).await { - inbox - .send(self.message.clone().unwrap()) - .map_err(|e| Error::Custom(format!("tokio broadcast error: {}", e)))?; - } - } - Ok(()) - } -} - -#[derive(Default, Clone)] -pub struct ConnectedUsers { - pub store: Arc>, - pub role_user_map: Arc>, -} - -/// A mapping of Snowflake IDs to the "inbox" of a [GatewayUser]. -/// -/// An "inbox" is a [tokio::sync::mpsc::Sender] that can be used to send [Event]s to all connected -/// clients of a [GatewayUser]. -#[derive(Default)] -pub struct ConnectedUsersInner { - pub inboxes: HashMap>, - pub users: HashMap>>, - pub resumeable_clients_store: ResumableClientsStore, -} - -impl ConnectedUsers { - /// Create a new, empty [ConnectedUsers] instance. - pub fn new() -> Self { - Self::default() - } - - pub fn bulk_message_builder(&self) -> BulkMessageBuilder { - BulkMessageBuilder::default() - } - - /// Initialize the [RoleUserMap] with data from the database. - /// - /// This method will query the database for all roles and all users that have these roles. - /// The data will then populate the map. - /// - /// Due to the possibly large number of roles and users returned by the database, this method - /// should only be executed once. The [RoleUserMap] should be kept synchronized with the database - /// through means that do not involve this method. - pub async fn init_role_user_map(&self, db: &PgPool) -> Result<(), Error> { - self.role_user_map.lock().await.init(db).await - } - - /// Get a [GatewayUser] by its Snowflake ID if it already exists in the store, or create a new - /// [GatewayUser] if it does not exist using [ConnectedUsers::new_user]. - pub async fn get_user_or_new(&self, id: Snowflake) -> Arc> { - let inner = self.store.clone(); - let mut lock = inner.lock().await; - if let Some(user) = lock.users.get(&id) { - user.clone() - } else { - self.new_user(HashMap::new(), id, Vec::new()).await - } - } - - pub fn inner(&self) -> Arc> { - self.store.clone() - } - - /// Register a new [GatewayUser] with the [ConnectedUsers] instance. - async fn register(&self, user: GatewayUser) -> Arc> { - self.store - .lock() - .await - .inboxes - .insert(user.id, user.outbox.clone()); - let id = user.id; - let arc = Arc::new(Mutex::new(user)); - self.store.lock().await.users.insert(id, arc.clone()); - arc - } - - /// Deregister a [GatewayUser] from the [ConnectedUsers] instance. - pub async fn deregister(&self, user: &GatewayUser) { - self.store.lock().await.inboxes.remove(&user.id); - self.store.lock().await.users.remove(&user.id); - } - - /// Get the "inbox" of a [GatewayUser] by its Snowflake ID. - pub async fn inbox(&self, id: Snowflake) -> Option> { - self.store.lock().await.inboxes.get(&id).cloned() - } - - /// Create a new [GatewayUser] with the given Snowflake ID, [GatewayClient]s, and subscriptions. - /// Registers the new [GatewayUser] with the [ConnectedUsers] instance. - pub async fn new_user( - &self, - clients: HashMap>>, - id: Snowflake, - subscriptions: Vec>>, - ) -> Arc> { - let channel = tokio::sync::broadcast::channel(20); - let user = GatewayUser { - inbox: channel.1, - outbox: channel.0.clone(), - clients, - id, - subscriptions, - connected_users: self.clone(), - }; - self.register(user).await - } - - /// Create a new [GatewayClient] with the given [GatewayUser], [Connection], and other data. - /// Also handles appending the new [GatewayClient] to the [GatewayUser]'s list of clients. - #[allow(clippy::too_many_arguments)] - pub async fn new_client( - &self, - user: Arc>, - connection: Arc>, - main_task_handle: tokio::task::JoinHandle<()>, - heartbeat_task_handle: tokio::task::JoinHandle<()>, - kill_send: tokio::sync::broadcast::Sender<()>, - session_token: &str, - last_sequence: Arc>, - ) -> Arc> { - let client = GatewayClient { - connection, - parent: Arc::downgrade(&user), - main_task_handle, - heartbeat_task_handle, - kill_send, - session_token: session_token.to_string(), - last_sequence, - }; - let arc = Arc::new(Mutex::new(client)); - user.lock() - .await - .clients - .insert(session_token.to_string(), arc.clone()); - arc - } -} - -/// A single identifiable User connected to the Gateway - possibly using many clients at the same -/// time. -pub struct GatewayUser { - /// The "inbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Receiver]. Events sent to - /// this inbox will be sent to all connected clients of this user. - pub inbox: tokio::sync::broadcast::Receiver, - /// The "outbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Sender]. From this outbox, - /// more inboxes can be created. - outbox: tokio::sync::broadcast::Sender, - /// Sessions a User is connected with. HashMap of SessionToken -> GatewayClient - clients: HashMap>>, - /// The Snowflake ID of the User. - id: Snowflake, - /// A collection of [Subscribers](Subscriber) to [Event] [Publishers](pubserve::Publisher). - /// - /// A GatewayUser may have many [GatewayClients](GatewayClient), but he only gets subscribed to - /// all relevant [Publishers](pubserve::Publisher) *once* to save resources. - subscriptions: Vec>>, - /// [Weak] reference to the [ConnectedUsers] store. - connected_users: ConnectedUsers, -} - -impl Hash for GatewayUser { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - -impl PartialEq for GatewayUser { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Eq for GatewayUser {} - -/// A concrete session, that a [GatewayUser] is connected to the Gateway with. -pub struct GatewayClient { - connection: Arc>, - /// A [Weak] reference to the [GatewayUser] this client belongs to. - pub parent: Weak>, - // Handle to the main Gateway task for this client - main_task_handle: tokio::task::JoinHandle<()>, - // Handle to the heartbeat task for this client - heartbeat_task_handle: tokio::task::JoinHandle<()>, - // Kill switch to disconnect the client - pub kill_send: tokio::sync::broadcast::Sender<()>, - /// Token of the session token used for this connection - pub session_token: String, - /// The last sequence number received from the client. Shared between the main task, heartbeat - /// task, and this struct. - last_sequence: Arc>, -} - -impl GatewayClient { - pub async fn die(mut self, connected_users: ConnectedUsers) { - self.kill_send.send(()).unwrap(); - let disconnect_info = DisconnectInfo { - session_token: self.session_token.clone(), - disconnected_at_sequence: *self.last_sequence.lock().await, - parent: self.parent.clone(), - }; - self.parent - .upgrade() - .unwrap() - .lock() - .await - .clients - .remove(&self.session_token); - connected_users - .deregister(self.parent.upgrade().unwrap().lock().await.deref()) - .await; - connected_users - .store - .lock() - .await - .resumeable_clients_store - .insert(self.session_token.clone(), disconnect_info); - } -} - pub struct Connection { sender: WebSocketSend, receiver: WebSocketReceive, @@ -442,6 +177,8 @@ impl } } +/// Represents a new successful connection to the gateway. The user is already part of the [ConnectedUsers] +/// and the client is already registered with the [GatewayClient] "clients" map. struct NewConnection { user: Arc>, client: Arc>, @@ -489,9 +226,7 @@ pub async fn start_gateway( } }; match connection_result { - Ok(new_connection) => { - checked_add_new_connection(connected_users.clone(), new_connection).await - } + Ok(_) => (), Err(e) => { log::debug!(target: "symfonia::gateway::establish_connection", "User gateway connection could not be established: {e}"); continue; @@ -542,43 +277,3 @@ async fn purge_expired_disconnects(connected_users: ConnectedUsers) { } } } - -/// Adds the contents of a [NewConnection] struct to a `gateway_users` map in a "checked" manner. -/// -/// If the `NewConnection` contains a [GatewayUser] which is already in `gateway_users`, then -/// change the `parent` of the `NewConnection` [GatewayClient] to the User -/// from our `gateway_users` and push the client to the `clients` field of the User in our -/// `gateway_users``. -/// -/// Else, add the [new GatewayUser] and the new [GatewayClient] into `gateway_users` as-is. -async fn checked_add_new_connection( - connected_users: ConnectedUsers, - new_connection: NewConnection, -) { - // Make `new_connection` mutable - let mut new_connection = new_connection; - // To avoid having to get the lock a lot of times, lock once here and hold this lock for most - // of the way through this method - let new_connection_user = new_connection.user.lock().await; - let new_connection_token = new_connection.client.lock().await.session_token.clone(); - let inner = connected_users.inner(); - let mut lock = inner.lock().await; - // If our map contains the user from `new_connection` already, modify the `parent` of the `client` - // of `new_connection` to point to the user already in our map, then insert that `client` into - // the `clients` field of our existing user. - if lock.users.contains_key(&new_connection_user.id) { - let existing_user = lock.users.get(&new_connection_user.id).unwrap(); - new_connection.client.lock().await.parent = Arc::downgrade(existing_user); - existing_user - .lock() - .await - .clients - .insert(new_connection_token, new_connection.client); - } else { - // We cannot do `locked_map.insert(id, new_connection.user)` if new_connection is still - // locked. Just bind the id we need to a new variable, then drop the lock. - let id = new_connection_user.id; - drop(new_connection_user); - lock.users.insert(id, new_connection.user); - } -} diff --git a/src/gateway/types.rs b/src/gateway/types.rs index 2ca6f18..33670da 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -2,9 +2,18 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use std::collections::{HashMap, HashSet}; +use std::ops::Deref; +use std::sync::{Arc, Weak}; + use ::serde::de::DeserializeOwned; use ::serde::{Deserialize, Serialize}; use chorus::types::*; +use pubserve::Subscriber; +use sqlx::PgPool; +use tokio::sync::Mutex; + +use super::{Connection, DisconnectInfo, ResumableClientsStore, RoleUserMap}; #[derive( Debug, @@ -173,3 +182,270 @@ impl<'de, T: DeserializeOwned + Serialize> Deserialize<'de> for GatewayPayload>, + pub role_user_map: Arc>, +} + +/// A mapping of Snowflake IDs to the "inbox" of a [GatewayUser]. +/// +/// An "inbox" is a [tokio::sync::mpsc::Sender] that can be used to send [Event]s to all connected +/// clients of a [GatewayUser]. +#[derive(Default)] +pub struct ConnectedUsersInner { + pub inboxes: HashMap>, + pub users: HashMap>>, + pub resumeable_clients_store: ResumableClientsStore, +} + +/// A single identifiable User connected to the Gateway - possibly using many clients at the same +/// time. +pub struct GatewayUser { + /// The "inbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Receiver]. Events sent to + /// this inbox will be sent to all connected clients of this user. + pub inbox: tokio::sync::broadcast::Receiver, + /// The "outbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Sender]. From this outbox, + /// more inboxes can be created. + outbox: tokio::sync::broadcast::Sender, + /// Sessions a User is connected with. HashMap of SessionToken -> GatewayClient + clients: HashMap>>, + /// The Snowflake ID of the User. + pub id: Snowflake, + /// A collection of [Subscribers](Subscriber) to [Event] [Publishers](pubserve::Publisher). + /// + /// A GatewayUser may have many [GatewayClients](GatewayClient), but he only gets subscribed to + /// all relevant [Publishers](pubserve::Publisher) *once* to save resources. + subscriptions: Vec>>, + /// [Weak] reference to the [ConnectedUsers] store. + connected_users: ConnectedUsers, +} + +/// A concrete session, that a [GatewayUser] is connected to the Gateway with. +pub struct GatewayClient { + connection: Arc>, + /// A [Weak] reference to the [GatewayUser] this client belongs to. + pub parent: Weak>, + // Handle to the main Gateway task for this client + main_task_handle: tokio::task::JoinHandle<()>, + // Handle to the heartbeat task for this client + heartbeat_task_handle: tokio::task::JoinHandle<()>, + // Kill switch to disconnect the client + pub kill_send: tokio::sync::broadcast::Sender<()>, + /// Token of the session token used for this connection + pub session_token: String, + /// The last sequence number received from the client. Shared between the main task, heartbeat + /// task, and this struct. + last_sequence: Arc>, +} + +impl ConnectedUsers { + /// Create a new, empty [ConnectedUsers] instance. + pub fn new() -> Self { + Self::default() + } + + pub fn bulk_message_builder(&self) -> BulkMessageBuilder { + BulkMessageBuilder::default() + } + + /// Initialize the [RoleUserMap] with data from the database. + /// + /// This method will query the database for all roles and all users that have these roles. + /// The data will then populate the map. + /// + /// Due to the possibly large number of roles and users returned by the database, this method + /// should only be executed once. The [RoleUserMap] should be kept synchronized with the database + /// through means that do not involve this method. + pub async fn init_role_user_map(&self, db: &PgPool) -> Result<(), crate::errors::Error> { + self.role_user_map.lock().await.init(db).await + } + + /// Get a [GatewayUser] by its Snowflake ID if it already exists in the store, or create a new + /// [GatewayUser] if it does not exist using [ConnectedUsers::new_user]. + pub async fn get_user_or_new(&self, id: Snowflake) -> Arc> { + let inner = self.store.clone(); + let mut lock = inner.lock().await; + if let Some(user) = lock.users.get(&id) { + user.clone() + } else { + self.new_user(HashMap::new(), id, Vec::new()).await + } + } + + pub fn inner(&self) -> Arc> { + self.store.clone() + } + + /// Register a new [GatewayUser] with the [ConnectedUsers] instance. + async fn register(&self, user: GatewayUser) -> Arc> { + self.store + .lock() + .await + .inboxes + .insert(user.id, user.outbox.clone()); + let id = user.id; + let arc = Arc::new(Mutex::new(user)); + self.store.lock().await.users.insert(id, arc.clone()); + arc + } + + /// Deregister a [GatewayUser] from the [ConnectedUsers] instance. + pub async fn deregister(&self, user: &GatewayUser) { + self.store.lock().await.inboxes.remove(&user.id); + self.store.lock().await.users.remove(&user.id); + } + + /// Get the "inbox" of a [GatewayUser] by its Snowflake ID. + pub async fn inbox(&self, id: Snowflake) -> Option> { + self.store.lock().await.inboxes.get(&id).cloned() + } + + /// Create a new [GatewayUser] with the given Snowflake ID, [GatewayClient]s, and subscriptions. + /// Registers the new [GatewayUser] with the [ConnectedUsers] instance. + pub async fn new_user( + &self, + clients: HashMap>>, + id: Snowflake, + subscriptions: Vec>>, + ) -> Arc> { + let channel = tokio::sync::broadcast::channel(20); + let user = GatewayUser { + inbox: channel.1, + outbox: channel.0.clone(), + clients, + id, + subscriptions, + connected_users: self.clone(), + }; + self.register(user).await + } + + /// Create a new [GatewayClient] with the given [GatewayUser], [Connection], and other data. + /// Also handles appending the new [GatewayClient] to the [GatewayUser]'s list of clients. + #[allow(clippy::too_many_arguments)] + pub async fn new_client( + &self, + user: Arc>, + connection: Arc>, + main_task_handle: tokio::task::JoinHandle<()>, + heartbeat_task_handle: tokio::task::JoinHandle<()>, + kill_send: tokio::sync::broadcast::Sender<()>, + session_token: &str, + last_sequence: Arc>, + ) -> Arc> { + let client = GatewayClient { + connection, + parent: Arc::downgrade(&user), + main_task_handle, + heartbeat_task_handle, + kill_send, + session_token: session_token.to_string(), + last_sequence, + }; + let arc = Arc::new(Mutex::new(client)); + user.lock() + .await + .clients + .insert(session_token.to_string(), arc.clone()); + arc + } +} + +impl std::hash::Hash for GatewayUser { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl PartialEq for GatewayUser { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for GatewayUser {} + +impl GatewayClient { + pub async fn die(mut self, connected_users: ConnectedUsers) { + self.kill_send.send(()).unwrap(); + let disconnect_info = DisconnectInfo { + session_token: self.session_token.clone(), + disconnected_at_sequence: *self.last_sequence.lock().await, + parent: self.parent.clone(), + }; + self.parent + .upgrade() + .unwrap() + .lock() + .await + .clients + .remove(&self.session_token); + connected_users + .deregister(self.parent.upgrade().unwrap().lock().await.deref()) + .await; + connected_users + .store + .lock() + .await + .resumeable_clients_store + .insert(self.session_token.clone(), disconnect_info); + } +} + +#[derive(Default, Clone)] +pub struct BulkMessageBuilder { + users: Vec, + roles: Vec, + message: Option, +} + +impl BulkMessageBuilder { + /// Add the given list of user snowflake IDs to the list of recipients. + pub async fn add_user_recipients(&mut self, users: &[Snowflake]) { + self.users.extend_from_slice(users); + } + + /// Add all members which have the given role snowflake IDs to the list of recipients. + pub async fn add_role_recipients(&mut self, roles: &[Snowflake]) { + self.roles.extend_from_slice(roles); + } + + /// Set the message to be sent to the recipients. + pub async fn set_message(&mut self, message: Event) { + self.message = Some(message); + } + + /// Send the message to all recipients. + pub async fn send(self, connected_users: ConnectedUsers) -> Result<(), crate::errors::Error> { + if self.message.is_none() { + return Err(crate::errors::Error::Custom( + "No message to send".to_string(), + )); + } + let mut recipients = HashSet::new(); + let lock = connected_users.role_user_map.lock().await; + for role in self.roles.iter() { + if let Some(users) = lock.get(role) { + for user in users.iter() { + recipients.insert(*user); + } + } + for user in self.users.iter() { + recipients.insert(*user); + } + } + if recipients.is_empty() { + return Ok(()); + } + for recipient in recipients.iter() { + if let Some(inbox) = connected_users.inbox(*recipient).await { + inbox.send(self.message.clone().unwrap()).map_err(|e| { + crate::errors::Error::Custom(format!("tokio broadcast error: {}", e)) + })?; + } + } + Ok(()) + } +} From bfc7f530eb1ccb5f15586a1c223e9cd921643fb2 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 18:18:40 +0200 Subject: [PATCH 142/162] Move a lot of structs into types module, clean up dumb methods/functions --- src/gateway/establish_connection.rs | 8 +- src/gateway/gateway_task.rs | 2 +- src/gateway/heartbeat.rs | 6 +- src/gateway/mod.rs | 402 +--------------------------- src/gateway/types.rs | 389 ++++++++++++++++++++++++++- 5 files changed, 398 insertions(+), 409 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index c9185ae..94c418c 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -29,11 +29,13 @@ use crate::{ util::token::check_token, }; -use super::{ConnectedUsers, Connection, GatewayClient, NewConnection, ResumableClientsStore}; +use super::{ + ConnectedUsers, GatewayClient, NewConnection, ResumableClientsStore, WebSocketConnection, +}; /// Internal use only state struct to pass around data to the `finish_connecting` function. struct State { - connection: Arc>, + connection: Arc>, db: PgPool, config: Config, connected_users: ConnectedUsers, @@ -61,7 +63,7 @@ pub(super) async fn establish_connection( trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Beginning process to establish connection (handshake)"); // Accept the connection and split it into its sender and receiver halves. let ws_stream = accept_async(stream).await?; - let mut connection: Connection = ws_stream.split().into(); + let mut connection: WebSocketConnection = ws_stream.split().into(); trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Sending hello message"); // Hello message connection diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs index a1e166d..c7982b2 100644 --- a/src/gateway/gateway_task.rs +++ b/src/gateway/gateway_task.rs @@ -5,7 +5,7 @@ use tokio::sync::Mutex; use super::GatewayClient; /// Handles all messages a client sends to the gateway post-handshake. -pub(super) async fn gateway_task(connection: Arc>) { +pub(super) async fn gateway_task(connection: Arc>) { // TODO todo!() } diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index 7078cc3..435970c 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -13,13 +13,13 @@ use tokio_tungstenite::tungstenite::{ use crate::gateway::DisconnectInfo; -use super::{Connection, GatewayClient}; +use super::{WebSocketConnection, GatewayClient}; static HEARTBEAT_INTERVAL: std::time::Duration = std::time::Duration::from_secs(45); static LATENCY_BUFFER: std::time::Duration = std::time::Duration::from_secs(5); pub(super) struct HeartbeatHandler { - connection: Arc>, + connection: Arc>, kill_receive: tokio::sync::broadcast::Receiver<()>, kill_send: tokio::sync::broadcast::Sender<()>, message_receive: tokio::sync::broadcast::Receiver, @@ -63,7 +63,7 @@ impl HeartbeatHandler { /// let heartbeat_handler = HeartbeatHandler::new(connection, kill_receive, kill_send, message_receive).await; /// ``` pub(super) fn new( - connection: Arc>, + connection: Arc>, kill_receive: tokio::sync::broadcast::Receiver<()>, kill_send: tokio::sync::broadcast::Sender<()>, message_receive: tokio::sync::broadcast::Receiver, diff --git a/src/gateway/mod.rs b/src/gateway/mod.rs index 9b55e16..c6cb320 100644 --- a/src/gateway/mod.rs +++ b/src/gateway/mod.rs @@ -89,364 +89,6 @@ Handling a connection involves the following steps: Handling disconnects and session resumes is for late */ -#[derive(Default)] -/// Represents all existing roles on the server and the users that have these roles. -pub struct RoleUserMap { - /// Map Role Snowflake ID to a list of User Snowflake IDs - map: HashMap>, -} - -impl Deref for RoleUserMap { - type Target = HashMap>; - - fn deref(&self) -> &Self::Target { - &self.map - } -} - -impl DerefMut for RoleUserMap { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.map - } -} - -impl RoleUserMap { - /// Initialize the [RoleUserMap] with data from the database. - /// - /// This method will query the database for all roles and all users that have these roles. - /// The data will then populate the map. - /// - /// Due to the possibly large number of roles and users returned by the database, this method - /// should only be executed once. The [RoleUserMap] should be kept synchronized with the database - /// through means that do not involve this method. - pub async fn init(&mut self, db: &PgPool) -> Result<(), Error> { - // First, get all role ids from the roles table and insert them into the map - let all_role_ids: Vec = sqlx::query_as("SELECT id FROM roles") - .fetch_all(db) - .await - .map_err(Error::SQLX)?; - for role_id in all_role_ids.iter() { - self.map - .insert(Snowflake::from(role_id.to_uint()), HashSet::new()); - } - // Then, query member_roles and insert the user ids into the map - let all_member_roles: Vec<(PgU64, PgU64)> = - sqlx::query_as("SELECT index, role_id FROM member_roles") - .fetch_all(db) - .await - .map_err(Error::SQLX)?; - for (user_id, role_id) in all_member_roles.iter() { - // Unwrapping is fine here, as the member_roles table has a foreign key constraint - // which states that role_id must be a valid id in the roles table. - let users_for_role_id = self.map.get_mut(&role_id.to_uint().into()).unwrap(); - users_for_role_id.insert(user_id.to_uint().into()); - } - Ok(()) - } -} - -#[derive(Default, Clone)] -pub struct BulkMessageBuilder { - users: Vec, - roles: Vec, - message: Option, -} - -impl BulkMessageBuilder { - /// Add the given list of user snowflake IDs to the list of recipients. - pub async fn add_user_recipients(&mut self, users: &[Snowflake]) { - self.users.extend_from_slice(users); - } - - /// Add all members which have the given role snowflake IDs to the list of recipients. - pub async fn add_role_recipients(&mut self, roles: &[Snowflake]) { - self.roles.extend_from_slice(roles); - } - - /// Set the message to be sent to the recipients. - pub async fn set_message(&mut self, message: Event) { - self.message = Some(message); - } - - /// Send the message to all recipients. - pub async fn send(self, connected_users: ConnectedUsers) -> Result<(), Error> { - if self.message.is_none() { - return Err(Error::Custom("No message to send".to_string())); - } - let mut recipients = HashSet::new(); - let lock = connected_users.role_user_map.lock().await; - for role in self.roles.iter() { - if let Some(users) = lock.get(role) { - for user in users.iter() { - recipients.insert(*user); - } - } - for user in self.users.iter() { - recipients.insert(*user); - } - } - if recipients.is_empty() { - return Ok(()); - } - for recipient in recipients.iter() { - if let Some(inbox) = connected_users.inbox(*recipient).await { - inbox - .send(self.message.clone().unwrap()) - .map_err(|e| Error::Custom(format!("tokio broadcast error: {}", e)))?; - } - } - Ok(()) - } -} - -#[derive(Default, Clone)] -pub struct ConnectedUsers { - pub store: Arc>, - pub role_user_map: Arc>, -} - -/// A mapping of Snowflake IDs to the "inbox" of a [GatewayUser]. -/// -/// An "inbox" is a [tokio::sync::mpsc::Sender] that can be used to send [Event]s to all connected -/// clients of a [GatewayUser]. -#[derive(Default)] -pub struct ConnectedUsersInner { - pub inboxes: HashMap>, - pub users: HashMap>>, - pub resumeable_clients_store: ResumableClientsStore, -} - -impl ConnectedUsers { - /// Create a new, empty [ConnectedUsers] instance. - pub fn new() -> Self { - Self::default() - } - - pub fn bulk_message_builder(&self) -> BulkMessageBuilder { - BulkMessageBuilder::default() - } - - /// Initialize the [RoleUserMap] with data from the database. - /// - /// This method will query the database for all roles and all users that have these roles. - /// The data will then populate the map. - /// - /// Due to the possibly large number of roles and users returned by the database, this method - /// should only be executed once. The [RoleUserMap] should be kept synchronized with the database - /// through means that do not involve this method. - pub async fn init_role_user_map(&self, db: &PgPool) -> Result<(), Error> { - self.role_user_map.lock().await.init(db).await - } - - /// Get a [GatewayUser] by its Snowflake ID if it already exists in the store, or create a new - /// [GatewayUser] if it does not exist using [ConnectedUsers::new_user]. - pub async fn get_user_or_new(&self, id: Snowflake) -> Arc> { - let inner = self.store.clone(); - let mut lock = inner.lock().await; - if let Some(user) = lock.users.get(&id) { - user.clone() - } else { - self.new_user(HashMap::new(), id, Vec::new()).await - } - } - - pub fn inner(&self) -> Arc> { - self.store.clone() - } - - /// Register a new [GatewayUser] with the [ConnectedUsers] instance. - async fn register(&self, user: GatewayUser) -> Arc> { - self.store - .lock() - .await - .inboxes - .insert(user.id, user.outbox.clone()); - let id = user.id; - let arc = Arc::new(Mutex::new(user)); - self.store.lock().await.users.insert(id, arc.clone()); - arc - } - - /// Deregister a [GatewayUser] from the [ConnectedUsers] instance. - pub async fn deregister(&self, user: &GatewayUser) { - self.store.lock().await.inboxes.remove(&user.id); - self.store.lock().await.users.remove(&user.id); - } - - /// Get the "inbox" of a [GatewayUser] by its Snowflake ID. - pub async fn inbox(&self, id: Snowflake) -> Option> { - self.store.lock().await.inboxes.get(&id).cloned() - } - - /// Create a new [GatewayUser] with the given Snowflake ID, [GatewayClient]s, and subscriptions. - /// Registers the new [GatewayUser] with the [ConnectedUsers] instance. - pub async fn new_user( - &self, - clients: HashMap>>, - id: Snowflake, - subscriptions: Vec>>, - ) -> Arc> { - let channel = tokio::sync::broadcast::channel(20); - let user = GatewayUser { - inbox: channel.1, - outbox: channel.0.clone(), - clients, - id, - subscriptions, - connected_users: self.clone(), - }; - self.register(user).await - } - - /// Create a new [GatewayClient] with the given [GatewayUser], [Connection], and other data. - /// Also handles appending the new [GatewayClient] to the [GatewayUser]'s list of clients. - #[allow(clippy::too_many_arguments)] - pub async fn new_client( - &self, - user: Arc>, - connection: Arc>, - main_task_handle: tokio::task::JoinHandle<()>, - heartbeat_task_handle: tokio::task::JoinHandle<()>, - kill_send: tokio::sync::broadcast::Sender<()>, - session_token: &str, - last_sequence: Arc>, - ) -> Arc> { - let client = GatewayClient { - connection, - parent: Arc::downgrade(&user), - main_task_handle, - heartbeat_task_handle, - kill_send, - session_token: session_token.to_string(), - last_sequence, - }; - let arc = Arc::new(Mutex::new(client)); - user.lock() - .await - .clients - .insert(session_token.to_string(), arc.clone()); - arc - } -} - -/// A single identifiable User connected to the Gateway - possibly using many clients at the same -/// time. -pub struct GatewayUser { - /// The "inbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Receiver]. Events sent to - /// this inbox will be sent to all connected clients of this user. - pub inbox: tokio::sync::broadcast::Receiver, - /// The "outbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Sender]. From this outbox, - /// more inboxes can be created. - outbox: tokio::sync::broadcast::Sender, - /// Sessions a User is connected with. HashMap of SessionToken -> GatewayClient - clients: HashMap>>, - /// The Snowflake ID of the User. - id: Snowflake, - /// A collection of [Subscribers](Subscriber) to [Event] [Publishers](pubserve::Publisher). - /// - /// A GatewayUser may have many [GatewayClients](GatewayClient), but he only gets subscribed to - /// all relevant [Publishers](pubserve::Publisher) *once* to save resources. - subscriptions: Vec>>, - /// [Weak] reference to the [ConnectedUsers] store. - connected_users: ConnectedUsers, -} - -impl Hash for GatewayUser { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - -impl PartialEq for GatewayUser { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Eq for GatewayUser {} - -/// A concrete session, that a [GatewayUser] is connected to the Gateway with. -pub struct GatewayClient { - connection: Arc>, - /// A [Weak] reference to the [GatewayUser] this client belongs to. - pub parent: Weak>, - // Handle to the main Gateway task for this client - main_task_handle: tokio::task::JoinHandle<()>, - // Handle to the heartbeat task for this client - heartbeat_task_handle: tokio::task::JoinHandle<()>, - // Kill switch to disconnect the client - pub kill_send: tokio::sync::broadcast::Sender<()>, - /// Token of the session token used for this connection - pub session_token: String, - /// The last sequence number received from the client. Shared between the main task, heartbeat - /// task, and this struct. - last_sequence: Arc>, -} - -impl GatewayClient { - pub async fn die(mut self, connected_users: ConnectedUsers) { - self.kill_send.send(()).unwrap(); - let disconnect_info = DisconnectInfo { - session_token: self.session_token.clone(), - disconnected_at_sequence: *self.last_sequence.lock().await, - parent: self.parent.clone(), - }; - self.parent - .upgrade() - .unwrap() - .lock() - .await - .clients - .remove(&self.session_token); - connected_users - .deregister(self.parent.upgrade().unwrap().lock().await.deref()) - .await; - connected_users - .store - .lock() - .await - .resumeable_clients_store - .insert(self.session_token.clone(), disconnect_info); - } -} - -pub struct Connection { - sender: WebSocketSend, - receiver: WebSocketReceive, -} - -#[derive(Clone)] -pub struct DisconnectInfo { - /// session token that was used for this connection - pub session_token: String, - pub disconnected_at_sequence: u64, - pub parent: Weak>, -} - -impl - From<( - SplitSink, Message>, - SplitStream>, - )> for Connection -{ - fn from( - value: ( - SplitSink, Message>, - SplitStream>, - ), - ) -> Self { - Self { - sender: value.0, - receiver: value.1, - } - } -} - -struct NewConnection { - user: Arc>, - client: Arc>, -} - /// A map of resumable clients. The key is the session token used /// for the connection. The value is a [GatewayClient] that can be resumed. // TODO: this is stupid. it should be a map of string and DisconnectInfo. there is no need to store @@ -489,9 +131,7 @@ pub async fn start_gateway( } }; match connection_result { - Ok(new_connection) => { - checked_add_new_connection(connected_users.clone(), new_connection).await - } + Ok(_) => (), Err(e) => { log::debug!(target: "symfonia::gateway::establish_connection", "User gateway connection could not be established: {e}"); continue; @@ -542,43 +182,3 @@ async fn purge_expired_disconnects(connected_users: ConnectedUsers) { } } } - -/// Adds the contents of a [NewConnection] struct to a `gateway_users` map in a "checked" manner. -/// -/// If the `NewConnection` contains a [GatewayUser] which is already in `gateway_users`, then -/// change the `parent` of the `NewConnection` [GatewayClient] to the User -/// from our `gateway_users` and push the client to the `clients` field of the User in our -/// `gateway_users``. -/// -/// Else, add the [new GatewayUser] and the new [GatewayClient] into `gateway_users` as-is. -async fn checked_add_new_connection( - connected_users: ConnectedUsers, - new_connection: NewConnection, -) { - // Make `new_connection` mutable - let mut new_connection = new_connection; - // To avoid having to get the lock a lot of times, lock once here and hold this lock for most - // of the way through this method - let new_connection_user = new_connection.user.lock().await; - let new_connection_token = new_connection.client.lock().await.session_token.clone(); - let inner = connected_users.inner(); - let mut lock = inner.lock().await; - // If our map contains the user from `new_connection` already, modify the `parent` of the `client` - // of `new_connection` to point to the user already in our map, then insert that `client` into - // the `clients` field of our existing user. - if lock.users.contains_key(&new_connection_user.id) { - let existing_user = lock.users.get(&new_connection_user.id).unwrap(); - new_connection.client.lock().await.parent = Arc::downgrade(existing_user); - existing_user - .lock() - .await - .clients - .insert(new_connection_token, new_connection.client); - } else { - // We cannot do `locked_map.insert(id, new_connection.user)` if new_connection is still - // locked. Just bind the id we need to a new variable, then drop the lock. - let id = new_connection_user.id; - drop(new_connection_user); - lock.users.insert(id, new_connection.user); - } -} diff --git a/src/gateway/types.rs b/src/gateway/types.rs index 2ca6f18..dd39d47 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -2,9 +2,34 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use std::collections::{HashMap, HashSet}; +use std::ops::{Deref, DerefMut}; +use std::sync::{Arc, Weak}; + use ::serde::de::DeserializeOwned; use ::serde::{Deserialize, Serialize}; -use chorus::types::*; +use chorus::types::{ + ChannelCreate, ChannelDelete, ChannelUpdate, GatewayHello, GatewayInvalidSession, GatewayReady, + GatewayResume, GuildBanAdd, GuildBanRemove, GuildCreate, GuildDelete, GuildEmojisUpdate, + GuildIntegrationsUpdate, GuildMemberAdd, GuildMemberRemove, GuildMemberUpdate, + GuildMembersChunk, GuildUpdate, InteractionCreate, InviteCreate, InviteDelete, MessageCreate, + MessageDelete, MessageDeleteBulk, MessageReactionAdd, MessageReactionRemove, + MessageReactionRemoveAll, MessageReactionRemoveEmoji, MessageUpdate, PresenceUpdate, Snowflake, + StageInstanceCreate, StageInstanceDelete, StageInstanceUpdate, ThreadCreate, ThreadDelete, + ThreadListSync, ThreadMemberUpdate, ThreadMembersUpdate, ThreadUpdate, TypingStartEvent, + UserUpdate, VoiceServerUpdate, VoiceStateUpdate, WebhooksUpdate, +}; +use futures::stream::{SplitSink, SplitStream}; +use pubserve::Subscriber; +use sqlx::PgPool; +use sqlx_pg_uint::PgU64; +use tokio::net::TcpStream; +use tokio::sync::Mutex; +use tokio_tungstenite::WebSocketStream; + +use crate::{WebSocketReceive, WebSocketSend}; + +use super::ResumableClientsStore; #[derive( Debug, @@ -173,3 +198,365 @@ impl<'de, T: DeserializeOwned + Serialize> Deserialize<'de> for GatewayPayload>, + pub role_user_map: Arc>, +} + +/// A mapping of Snowflake IDs to the "inbox" of a [GatewayUser]. +/// +/// An "inbox" is a [tokio::sync::mpsc::Sender] that can be used to send [Event]s to all connected +/// clients of a [GatewayUser]. +#[derive(Default)] +pub struct ConnectedUsersInner { + pub inboxes: HashMap>, + pub users: HashMap>>, + pub resumeable_clients_store: ResumableClientsStore, +} + +/// A single identifiable User connected to the Gateway - possibly using many clients at the same +/// time. +pub struct GatewayUser { + /// The "inbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Receiver]. Events sent to + /// this inbox will be sent to all connected clients of this user. + pub inbox: tokio::sync::broadcast::Receiver, + /// The "outbox" of a [GatewayUser]. This is a [tokio::sync::mpsc::Sender]. From this outbox, + /// more inboxes can be created. + outbox: tokio::sync::broadcast::Sender, + /// Sessions a User is connected with. HashMap of SessionToken -> GatewayClient + clients: HashMap>>, + /// The Snowflake ID of the User. + pub id: Snowflake, + /// A collection of [Subscribers](Subscriber) to [Event] [Publishers](pubserve::Publisher). + /// + /// A GatewayUser may have many [GatewayClients](GatewayClient), but he only gets subscribed to + /// all relevant [Publishers](pubserve::Publisher) *once* to save resources. + subscriptions: Vec>>, + /// [Weak] reference to the [ConnectedUsers] store. + connected_users: ConnectedUsers, +} + +/// A concrete session, that a [GatewayUser] is connected to the Gateway with. +pub struct GatewayClient { + connection: Arc>, + /// A [Weak] reference to the [GatewayUser] this client belongs to. + pub parent: Weak>, + // Handle to the main Gateway task for this client + main_task_handle: tokio::task::JoinHandle<()>, + // Handle to the heartbeat task for this client + heartbeat_task_handle: tokio::task::JoinHandle<()>, + // Kill switch to disconnect the client + pub kill_send: tokio::sync::broadcast::Sender<()>, + /// Token of the session token used for this connection + pub session_token: String, + /// The last sequence number received from the client. Shared between the main task, heartbeat + /// task, and this struct. + last_sequence: Arc>, +} + +impl ConnectedUsers { + /// Create a new, empty [ConnectedUsers] instance. + pub fn new() -> Self { + Self::default() + } + + pub fn bulk_message_builder(&self) -> BulkMessageBuilder { + BulkMessageBuilder::default() + } + + /// Initialize the [RoleUserMap] with data from the database. + /// + /// This method will query the database for all roles and all users that have these roles. + /// The data will then populate the map. + /// + /// Due to the possibly large number of roles and users returned by the database, this method + /// should only be executed once. The [RoleUserMap] should be kept synchronized with the database + /// through means that do not involve this method. + pub async fn init_role_user_map(&self, db: &PgPool) -> Result<(), crate::errors::Error> { + self.role_user_map.lock().await.init(db).await + } + + /// Get a [GatewayUser] by its Snowflake ID if it already exists in the store, or create a new + /// [GatewayUser] if it does not exist using [ConnectedUsers::new_user]. + pub async fn get_user_or_new(&self, id: Snowflake) -> Arc> { + let inner = self.store.clone(); + let mut lock = inner.lock().await; + if let Some(user) = lock.users.get(&id) { + user.clone() + } else { + self.new_user(HashMap::new(), id, Vec::new()).await + } + } + + pub fn inner(&self) -> Arc> { + self.store.clone() + } + + /// Register a new [GatewayUser] with the [ConnectedUsers] instance. + async fn register(&self, user: GatewayUser) -> Arc> { + self.store + .lock() + .await + .inboxes + .insert(user.id, user.outbox.clone()); + let id = user.id; + let arc = Arc::new(Mutex::new(user)); + self.store.lock().await.users.insert(id, arc.clone()); + arc + } + + /// Deregister a [GatewayUser] from the [ConnectedUsers] instance. + pub async fn deregister(&self, user: &GatewayUser) { + self.store.lock().await.inboxes.remove(&user.id); + self.store.lock().await.users.remove(&user.id); + } + + /// Get the "inbox" of a [GatewayUser] by its Snowflake ID. + pub async fn inbox(&self, id: Snowflake) -> Option> { + self.store.lock().await.inboxes.get(&id).cloned() + } + + /// Create a new [GatewayUser] with the given Snowflake ID, [GatewayClient]s, and subscriptions. + /// Registers the new [GatewayUser] with the [ConnectedUsers] instance. + pub async fn new_user( + &self, + clients: HashMap>>, + id: Snowflake, + subscriptions: Vec>>, + ) -> Arc> { + let channel = tokio::sync::broadcast::channel(20); + let user = GatewayUser { + inbox: channel.1, + outbox: channel.0.clone(), + clients, + id, + subscriptions, + connected_users: self.clone(), + }; + self.register(user).await + } + + /// Create a new [GatewayClient] with the given [GatewayUser], [Connection], and other data. + /// Also handles appending the new [GatewayClient] to the [GatewayUser]'s list of clients. + #[allow(clippy::too_many_arguments)] + pub async fn new_client( + &self, + user: Arc>, + connection: Arc>, + main_task_handle: tokio::task::JoinHandle<()>, + heartbeat_task_handle: tokio::task::JoinHandle<()>, + kill_send: tokio::sync::broadcast::Sender<()>, + session_token: &str, + last_sequence: Arc>, + ) -> Arc> { + let client = GatewayClient { + connection, + parent: Arc::downgrade(&user), + main_task_handle, + heartbeat_task_handle, + kill_send, + session_token: session_token.to_string(), + last_sequence, + }; + let arc = Arc::new(Mutex::new(client)); + user.lock() + .await + .clients + .insert(session_token.to_string(), arc.clone()); + arc + } +} + +impl std::hash::Hash for GatewayUser { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl PartialEq for GatewayUser { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for GatewayUser {} + +impl GatewayClient { + pub async fn die(mut self, connected_users: ConnectedUsers) { + self.kill_send.send(()).unwrap(); + let disconnect_info = DisconnectInfo { + session_token: self.session_token.clone(), + disconnected_at_sequence: *self.last_sequence.lock().await, + parent: self.parent.clone(), + }; + self.parent + .upgrade() + .unwrap() + .lock() + .await + .clients + .remove(&self.session_token); + connected_users + .deregister(self.parent.upgrade().unwrap().lock().await.deref()) + .await; + connected_users + .store + .lock() + .await + .resumeable_clients_store + .insert(self.session_token.clone(), disconnect_info); + } +} + +#[derive(Default, Clone)] +pub struct BulkMessageBuilder { + users: Vec, + roles: Vec, + message: Option, +} + +impl BulkMessageBuilder { + /// Add the given list of user snowflake IDs to the list of recipients. + pub async fn add_user_recipients(&mut self, users: &[Snowflake]) { + self.users.extend_from_slice(users); + } + + /// Add all members which have the given role snowflake IDs to the list of recipients. + pub async fn add_role_recipients(&mut self, roles: &[Snowflake]) { + self.roles.extend_from_slice(roles); + } + + /// Set the message to be sent to the recipients. + pub async fn set_message(&mut self, message: Event) { + self.message = Some(message); + } + + /// Send the message to all recipients. + pub async fn send(self, connected_users: ConnectedUsers) -> Result<(), crate::errors::Error> { + if self.message.is_none() { + return Err(crate::errors::Error::Custom( + "No message to send".to_string(), + )); + } + let mut recipients = HashSet::new(); + let lock = connected_users.role_user_map.lock().await; + for role in self.roles.iter() { + if let Some(users) = lock.get(role) { + for user in users.iter() { + recipients.insert(*user); + } + } + for user in self.users.iter() { + recipients.insert(*user); + } + } + if recipients.is_empty() { + return Ok(()); + } + for recipient in recipients.iter() { + if let Some(inbox) = connected_users.inbox(*recipient).await { + inbox.send(self.message.clone().unwrap()).map_err(|e| { + crate::errors::Error::Custom(format!("tokio broadcast error: {}", e)) + })?; + } + } + Ok(()) + } +} + +#[derive(Default)] +/// Represents all existing roles on the server and the users that have these roles. +pub struct RoleUserMap { + /// Map Role Snowflake ID to a list of User Snowflake IDs + map: HashMap>, +} + +impl Deref for RoleUserMap { + type Target = HashMap>; + + fn deref(&self) -> &Self::Target { + &self.map + } +} + +impl DerefMut for RoleUserMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.map + } +} + +impl RoleUserMap { + /// Initialize the [RoleUserMap] with data from the database. + /// + /// This method will query the database for all roles and all users that have these roles. + /// The data will then populate the map. + /// + /// Due to the possibly large number of roles and users returned by the database, this method + /// should only be executed once. The [RoleUserMap] should be kept synchronized with the database + /// through means that do not involve this method. + pub async fn init(&mut self, db: &PgPool) -> Result<(), crate::errors::Error> { + // First, get all role ids from the roles table and insert them into the map + let all_role_ids: Vec = sqlx::query_as("SELECT id FROM roles") + .fetch_all(db) + .await + .map_err(crate::errors::Error::SQLX)?; + for role_id in all_role_ids.iter() { + self.map + .insert(Snowflake::from(role_id.to_uint()), HashSet::new()); + } + // Then, query member_roles and insert the user ids into the map + let all_member_roles: Vec<(PgU64, PgU64)> = + sqlx::query_as("SELECT index, role_id FROM member_roles") + .fetch_all(db) + .await + .map_err(crate::errors::Error::SQLX)?; + for (user_id, role_id) in all_member_roles.iter() { + // Unwrapping is fine here, as the member_roles table has a foreign key constraint + // which states that role_id must be a valid id in the roles table. + let users_for_role_id = self.map.get_mut(&role_id.to_uint().into()).unwrap(); + users_for_role_id.insert(user_id.to_uint().into()); + } + Ok(()) + } +} + +pub struct WebSocketConnection { + pub sender: WebSocketSend, + pub receiver: WebSocketReceive, +} + +#[derive(Clone)] +pub struct DisconnectInfo { + /// session token that was used for this connection + pub session_token: String, + pub disconnected_at_sequence: u64, + pub parent: Weak>, +} + +impl + From<( + SplitSink, tokio_tungstenite::tungstenite::Message>, + SplitStream>, + )> for WebSocketConnection +{ + fn from( + value: ( + SplitSink, tokio_tungstenite::tungstenite::Message>, + SplitStream>, + ), + ) -> Self { + Self { + sender: value.0, + receiver: value.1, + } + } +} + +/// Represents a new successful connection to the gateway. The user is already part of the [ConnectedUsers] +/// and the client is already registered with the [GatewayClient] "clients" map. +pub struct NewConnection { + pub user: Arc>, + pub client: Arc>, +} From 75ee5fac8eaefa1a7f9a49a9bed4c5cc55341fde Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 18:38:17 +0200 Subject: [PATCH 143/162] rename NewConnection to NewWebSocketConnection --- src/gateway/establish_connection.rs | 9 +++++---- src/gateway/types.rs | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 94c418c..86ccba1 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -30,7 +30,8 @@ use crate::{ }; use super::{ - ConnectedUsers, GatewayClient, NewConnection, ResumableClientsStore, WebSocketConnection, + ConnectedUsers, GatewayClient, NewWebSocketConnection, ResumableClientsStore, + WebSocketConnection, }; /// Internal use only state struct to pass around data to the `finish_connecting` function. @@ -59,7 +60,7 @@ pub(super) async fn establish_connection( db: PgPool, config: Config, connected_users: ConnectedUsers, -) -> Result { +) -> Result { trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Beginning process to establish connection (handshake)"); // Accept the connection and split it into its sender and receiver halves. let ws_stream = accept_async(stream).await?; @@ -142,7 +143,7 @@ pub(super) async fn establish_connection( async fn finish_connecting( mut heartbeat_handler_handle: Option>, state: State, -) -> Result { +) -> Result { loop { trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Waiting for next message..."); let raw_message = match state.connection.lock().await.receiver.next().await { @@ -235,7 +236,7 @@ async fn finish_connecting( ) .await; - return Ok(NewConnection { + return Ok(NewWebSocketConnection { user: gateway_user, client: gateway_client.clone(), }); diff --git a/src/gateway/types.rs b/src/gateway/types.rs index dd39d47..7a66bf6 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -556,7 +556,7 @@ impl /// Represents a new successful connection to the gateway. The user is already part of the [ConnectedUsers] /// and the client is already registered with the [GatewayClient] "clients" map. -pub struct NewConnection { +pub struct NewWebSocketConnection { pub user: Arc>, pub client: Arc>, } From 4cebc4153f9dc765bda10faa3caf601430fa591a Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 20:16:17 +0200 Subject: [PATCH 144/162] add "Internal" error for Gateway Errors --- src/errors.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index 31d8f31..fd041ce 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -75,6 +75,8 @@ pub enum GatewayError { Timeout, #[error("CLOSED")] Closed, + #[error("INTERNAL_SERVER_ERROR")] + Internal, } #[derive(Debug, thiserror::Error)] @@ -243,6 +245,7 @@ impl ResponseError for Error { GatewayError::UnexpectedMessage => StatusCode::BAD_REQUEST, GatewayError::Timeout => StatusCode::BAD_REQUEST, GatewayError::Closed => StatusCode::BAD_REQUEST, + GatewayError::Internal => StatusCode::INTERNAL_SERVER_ERROR, }, Error::SqlxPgUint(_) => StatusCode::BAD_REQUEST, Error::Custom(_) => StatusCode::BAD_REQUEST, From 0b95bd0eb61eeb7011307bbbe5cd6e51f5ec52a9 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 20:17:04 +0200 Subject: [PATCH 145/162] Send session id to heartbeat task when received, add missing args to gateway task spawn --- src/gateway/establish_connection.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 86ccba1..9dc1778 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -47,6 +47,7 @@ struct State { heartbeat_receive: tokio::sync::broadcast::Receiver, /// Sender for heartbeat messages. The main gateway task will send messages to this channel for the `HeartbeatHandler` to receive and handle. heartbeat_send: tokio::sync::broadcast::Sender, + session_id_send: tokio::sync::broadcast::Sender, session_id_receive: tokio::sync::broadcast::Receiver, } @@ -106,6 +107,7 @@ pub(super) async fn establish_connection( kill_receive: kill_receive.resubscribe(), heartbeat_receive: message_receive.resubscribe(), heartbeat_send: message_send.clone(), + session_id_send: session_id_send.clone(), session_id_receive: session_id_receive.resubscribe(), }; @@ -213,7 +215,13 @@ async fn finish_connecting( .new_client( gateway_user.clone(), state.connection.clone(), - tokio::spawn(gateway_task::gateway_task(state.connection.clone())), + tokio::spawn(gateway_task::gateway_task( + state.connection.clone(), + gateway_user.lock().await.inbox.resubscribe(), + state.kill_receive.resubscribe(), + state.kill_send.clone(), + state.sequence_number.clone(), + )), match heartbeat_handler_handle { Some(handle) => handle, None => tokio::spawn({ @@ -235,7 +243,17 @@ async fn finish_connecting( state.sequence_number.clone(), ) .await; - + match state.session_id_send.send(identify.event_data.token) { + Ok(_) => (), + Err(_) => { + log::error!(target: "symfonia::gateway::establish_connection::finish_connecting", "Failed to send session_id to heartbeat handler"); + state + .kill_send + .send(()) + .expect("Failed to send kill signal"); + return Err(GatewayError::Internal.into()); + } + } return Ok(NewWebSocketConnection { user: gateway_user, client: gateway_client.clone(), From 146609ed113aa48a260fcb230405501280071933 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 20:17:15 +0200 Subject: [PATCH 146/162] add some missing arguments to gateway task spawn --- src/gateway/gateway_task.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs index c7982b2..6867ddf 100644 --- a/src/gateway/gateway_task.rs +++ b/src/gateway/gateway_task.rs @@ -2,10 +2,21 @@ use std::sync::Arc; use tokio::sync::Mutex; -use super::GatewayClient; +use super::{Event, GatewayClient}; /// Handles all messages a client sends to the gateway post-handshake. -pub(super) async fn gateway_task(connection: Arc>) { +pub(super) async fn gateway_task( + connection: Arc>, + inbox: tokio::sync::broadcast::Receiver, + kill_receive: tokio::sync::broadcast::Receiver<()>, + kill_send: tokio::sync::broadcast::Sender<()>, + last_sequence_number: Arc>, +) { + // TODO + todo!() +} + +async fn handle_event(event: Event, connection: Arc>) { // TODO todo!() } From 1b9f0acc1b6687822631c952364411fe7dc234a5 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 20:18:00 +0200 Subject: [PATCH 147/162] rename SQLX error variant to Sqlx --- src/database/entities/application.rs | 4 ++-- src/database/entities/audit_log.rs | 2 +- src/database/entities/channel.rs | 6 +++--- src/database/entities/config.rs | 4 ++-- src/database/entities/emoji.rs | 12 ++++++------ src/database/entities/guild.rs | 24 ++++++++++++------------ src/database/entities/guild_template.rs | 2 +- src/database/entities/invite.rs | 14 +++++++------- src/database/entities/message.rs | 22 +++++++++++----------- src/database/entities/read_state.rs | 4 ++-- src/database/entities/role.rs | 10 +++++----- src/database/entities/sticker.rs | 8 ++++---- src/database/entities/user.rs | 10 +++++----- src/database/entities/user_settings.rs | 2 +- src/database/entities/webhook.rs | 6 +++--- src/errors.rs | 4 ++-- src/gateway/types.rs | 4 ++-- 17 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/database/entities/application.rs b/src/database/entities/application.rs index 0365108..74c9a07 100644 --- a/src/database/entities/application.rs +++ b/src/database/entities/application.rs @@ -95,7 +95,7 @@ impl Application { .bind(id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_owner(db: &PgPool, owner_id: &Snowflake) -> Result, Error> { @@ -103,7 +103,7 @@ impl Application { .bind(owner_id) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_owner(&self, db: &PgPool) -> Result { diff --git a/src/database/entities/audit_log.rs b/src/database/entities/audit_log.rs index 05266b4..6678a0b 100644 --- a/src/database/entities/audit_log.rs +++ b/src/database/entities/audit_log.rs @@ -91,7 +91,7 @@ impl AuditLogEntry { .bind(guild_id) .fetch_all(db) .await - .map_err(Error::SQLX)?; + .map_err(Error::Sqlx)?; Ok(r.into_iter() .flat_map(|r| AuditLogEntry::from_row(&r)) diff --git a/src/database/entities/channel.rs b/src/database/entities/channel.rs index bea950d..bd663f7 100644 --- a/src/database/entities/channel.rs +++ b/src/database/entities/channel.rs @@ -183,7 +183,7 @@ impl Channel { .bind(id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_guild_id(db: &PgPool, guild_id: Snowflake) -> Result, Error> { @@ -191,7 +191,7 @@ impl Channel { .bind(guild_id) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_invites(&self, db: &PgPool) -> Result, Error> { @@ -361,7 +361,7 @@ impl Channel { .bind(self.id) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn add_follower_webhook( diff --git a/src/database/entities/config.rs b/src/database/entities/config.rs index 6153e70..d7fc676 100644 --- a/src/database/entities/config.rs +++ b/src/database/entities/config.rs @@ -137,7 +137,7 @@ impl ConfigEntity { .fetch_one(db) .await .map(Self) - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn collect_entities(db: &PgPool) -> Result, Error> { @@ -145,7 +145,7 @@ impl ConfigEntity { .fetch_all(db) .await .map(|res| res.into_iter().map(Self).collect()) - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } } diff --git a/src/database/entities/emoji.rs b/src/database/entities/emoji.rs index 6979872..c90437a 100644 --- a/src/database/entities/emoji.rs +++ b/src/database/entities/emoji.rs @@ -65,7 +65,7 @@ impl Emoji { .bind(require_colons) .execute(db) .await - .map_err(Error::SQLX)?; + .map_err(Error::Sqlx)?; Ok(Self { inner: chorus::types::Emoji { @@ -88,7 +88,7 @@ impl Emoji { .bind(id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_guild(db: &PgPool, guild_id: Snowflake) -> Result, Error> { @@ -96,7 +96,7 @@ impl Emoji { .bind(guild_id) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn count(db: &PgPool, guild_id: Snowflake) -> Result { @@ -104,7 +104,7 @@ impl Emoji { .bind(guild_id) .fetch_one(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) .map(|r| r.get::(0)) } @@ -119,7 +119,7 @@ impl Emoji { .bind(self.guild_id) .execute(db) .await - .map_err(Error::SQLX)?; + .map_err(Error::Sqlx)?; Ok(()) } @@ -129,7 +129,7 @@ impl Emoji { .bind(self.id) .execute(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) .map(|_| ()) } diff --git a/src/database/entities/guild.rs b/src/database/entities/guild.rs index 93af444..d0d8f2b 100644 --- a/src/database/entities/guild.rs +++ b/src/database/entities/guild.rs @@ -216,7 +216,7 @@ impl Guild { .bind(id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } // Helper functions start @@ -242,7 +242,7 @@ impl Guild { .bind(user_id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) .map(|r: Option| r.is_some()) } @@ -282,7 +282,7 @@ impl Guild { sqlx::query("SELECT COUNT(*) FROM guilds") .fetch_one(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) .map(|r| r.get::(0)) } @@ -338,7 +338,7 @@ impl Guild { .bind(highest_role) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } else { let mut builder = QueryBuilder::new("SELECT gm.* FROM members gm JOIN member_roles mr ON gm.index = mr.index JOIN roles r ON r.id = mr.role_id WHERE gm.guild_id = ? AND (gm.last_message_id < ? OR gm.last_message_id IS NULL) AND r.position < ? AND mr.role_id IN ("); @@ -387,7 +387,7 @@ impl Guild { .execute(db) .await .map(|_| ()) - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn delete(self, db: &PgPool) -> Result<(), Error> { @@ -395,7 +395,7 @@ impl Guild { .bind(self.id) .execute(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) .map(|_| ()) } @@ -463,7 +463,7 @@ impl GuildBan { .bind(&reason) .execute(db) .await - .map_err(Error::SQLX)?; + .map_err(Error::Sqlx)?; Ok(Self { inner: chorus::types::GuildBan { @@ -541,7 +541,7 @@ impl GuildBan { .bind(id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_guild( @@ -561,7 +561,7 @@ impl GuildBan { .bind(limit) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_user( @@ -574,7 +574,7 @@ impl GuildBan { .bind(guild_id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn find_by_username( @@ -589,7 +589,7 @@ impl GuildBan { .bind(limit) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn delete(self, db: &PgPool) -> Result<(), Error> { @@ -597,7 +597,7 @@ impl GuildBan { .bind(self.id) .execute(db) .await - .map_err(Error::SQLX)?; + .map_err(Error::Sqlx)?; Ok(()) } diff --git a/src/database/entities/guild_template.rs b/src/database/entities/guild_template.rs index 74989bb..cf5cc3e 100644 --- a/src/database/entities/guild_template.rs +++ b/src/database/entities/guild_template.rs @@ -37,7 +37,7 @@ impl GuildTemplate { .bind(code) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub fn into_inner(self) -> chorus::types::GuildTemplate { diff --git a/src/database/entities/invite.rs b/src/database/entities/invite.rs index 5fd55b1..3cb3468 100644 --- a/src/database/entities/invite.rs +++ b/src/database/entities/invite.rs @@ -174,7 +174,7 @@ impl Invite { .bind(code) .fetch_optional(db) .await - .map_err(Error::SQLX)?; + .map_err(Error::Sqlx)?; Ok(invite) } @@ -188,7 +188,7 @@ impl Invite { .bind(guild_id) .fetch_all(db) .await - .map_err(Error::SQLX)?; + .map_err(Error::Sqlx)?; invites .iter_mut() @@ -205,7 +205,7 @@ impl Invite { .bind(guild_id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_channel(db: &PgPool, channel_id: Snowflake) -> Result, Error> { @@ -213,7 +213,7 @@ impl Invite { .bind(channel_id) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn delete(&self, db: &PgPool) -> Result<(), Error> { @@ -222,7 +222,7 @@ impl Invite { .execute(db) .await .map(|_| ()) - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn join(&mut self, db: &PgPool, user: &User) -> Result<(), Error> { @@ -264,7 +264,7 @@ impl Invite { .execute(db) .await .map(|_| ()) - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn populate_relations(&mut self, db: &PgPool) -> Result<(), Error> { @@ -309,7 +309,7 @@ impl Invite { .execute(db) .await .map(|_| ()) - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub fn into_inner(self) -> chorus::types::Invite { diff --git a/src/database/entities/message.rs b/src/database/entities/message.rs index 21200af..88fac67 100644 --- a/src/database/entities/message.rs +++ b/src/database/entities/message.rs @@ -142,7 +142,7 @@ impl Message { .bind(nonce) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_id( @@ -155,7 +155,7 @@ impl Message { .bind(channel_id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_channel_id( @@ -172,7 +172,7 @@ impl Message { .bind(limit) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } ChannelMessagesAnchor::Around(around_id) => { let limit = limit / 2; @@ -183,7 +183,7 @@ impl Message { .bind(limit) .fetch_all(db) .await - .map_err(Error::SQLX)?; + .map_err(Error::Sqlx)?; let mut lower = sqlx::query_as("SELECT * FROM `messages` WHERE `channel_id` = ? AND `id` < ? ORDER BY `timestamp` DESC LIMIT ?") .bind(channel_id) @@ -191,7 +191,7 @@ impl Message { .bind(limit) .fetch_all(db) .await - .map_err(Error::SQLX)?; + .map_err(Error::Sqlx)?; upper.append(&mut lower); upper.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); @@ -208,7 +208,7 @@ impl Message { .bind(limit) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } } } @@ -218,7 +218,7 @@ impl Message { .bind(channel_id) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn count_by_user_in_window( @@ -293,7 +293,7 @@ impl Message { .bind(self.id) .execute(db) .await - .map_err(Error::SQLX)?; + .map_err(Error::Sqlx)?; Ok(()) } @@ -332,7 +332,7 @@ impl Message { .execute(db) .await .map(|_| ()) - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn delete(&self, db: &PgPool) -> Result<(), Error> { @@ -341,7 +341,7 @@ impl Message { .execute(db) .await .map(|_| ()) - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn bulk_delete(db: &PgPool, ids: Vec) -> Result<(), Error> { @@ -460,7 +460,7 @@ impl Message { let query = query_builder.build(); - let res = query.fetch_all(db).await.map_err(Error::SQLX)?; + let res = query.fetch_all(db).await.map_err(Error::Sqlx)?; Ok(res .into_iter() diff --git a/src/database/entities/read_state.rs b/src/database/entities/read_state.rs index 55f9b51..b2f239f 100644 --- a/src/database/entities/read_state.rs +++ b/src/database/entities/read_state.rs @@ -68,7 +68,7 @@ impl ReadState { .bind(user_id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn populate_relations(&mut self, db: &PgPool) -> Result<(), Error> { @@ -89,7 +89,7 @@ impl ReadState { .bind(self.manual) .execute(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) .map(|_| ()) } } diff --git a/src/database/entities/role.rs b/src/database/entities/role.rs index d281d27..5a8c8e1 100644 --- a/src/database/entities/role.rs +++ b/src/database/entities/role.rs @@ -106,7 +106,7 @@ impl Role { .bind(id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_guild(db: &PgPool, guild_id: Snowflake) -> Result, Error> { @@ -114,7 +114,7 @@ impl Role { .bind(guild_id) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn count_by_guild(db: &PgPool, guild_id: Snowflake) -> Result { @@ -123,7 +123,7 @@ impl Role { .fetch_one(db) .await .map(|res| res.get::(0)) - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn save(&self, db: &PgPool) -> Result<(), Error> { @@ -141,7 +141,7 @@ impl Role { .execute(db) .await .map(|_| ()) - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn delete(&self, db: &PgPool) -> Result<(), Error> { @@ -150,7 +150,7 @@ impl Role { .execute(db) .await .map(|_| ()) - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub fn into_inner(self) -> chorus::types::RoleObject { diff --git a/src/database/entities/sticker.rs b/src/database/entities/sticker.rs index d310ba0..80a568d 100644 --- a/src/database/entities/sticker.rs +++ b/src/database/entities/sticker.rs @@ -92,7 +92,7 @@ impl Sticker { .bind(id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_guild(db: &PgPool, guild_id: Snowflake) -> Result, Error> { @@ -100,7 +100,7 @@ impl Sticker { .bind(guild_id) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn count_by_guild(db: &PgPool, guild_id: Snowflake) -> Result { @@ -108,7 +108,7 @@ impl Sticker { .bind(guild_id) .fetch_one(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) .map(|r| r.get::(0)) } @@ -129,7 +129,7 @@ impl Sticker { .bind(self.id) .execute(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) .map(|_| ()) } diff --git a/src/database/entities/user.rs b/src/database/entities/user.rs index 9dc7947..1a016a5 100644 --- a/src/database/entities/user.rs +++ b/src/database/entities/user.rs @@ -130,7 +130,7 @@ impl User { .bind(id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_id_list( @@ -155,7 +155,7 @@ impl User { let query = query_builder.build(); - let r = query.fetch_all(db).await.map_err(Error::SQLX)?; + let r = query.fetch_all(db).await.map_err(Error::Sqlx)?; let users = r .iter() .map(User::from_row) @@ -175,7 +175,7 @@ impl User { .bind(discrim) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_user_by_email_or_phone( @@ -188,7 +188,7 @@ impl User { .bind(phone) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn add_to_guild( @@ -221,7 +221,7 @@ impl User { sqlx::query("SELECT COUNT(*) FROM users") .fetch_one(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) .map(|r| r.get::(0)) } diff --git a/src/database/entities/user_settings.rs b/src/database/entities/user_settings.rs index fd71c0f..c97d785 100644 --- a/src/database/entities/user_settings.rs +++ b/src/database/entities/user_settings.rs @@ -80,6 +80,6 @@ impl UserSettings { .bind(index) .fetch_one(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } } diff --git a/src/database/entities/webhook.rs b/src/database/entities/webhook.rs index f88c4b3..1e56710 100644 --- a/src/database/entities/webhook.rs +++ b/src/database/entities/webhook.rs @@ -89,7 +89,7 @@ impl Webhook { .bind(id) .fetch_optional(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn get_by_channel_id(db: &PgPool, channel_id: Snowflake) -> Result, Error> { @@ -97,7 +97,7 @@ impl Webhook { .bind(channel_id) .fetch_all(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) } pub async fn count_by_channel(db: &PgPool, channel_id: Snowflake) -> Result { @@ -105,7 +105,7 @@ impl Webhook { .bind(channel_id) .fetch_one(db) .await - .map_err(Error::SQLX) + .map_err(Error::Sqlx) .map(|row| row.get::(0)) } } diff --git a/src/errors.rs b/src/errors.rs index fd041ce..2a39597 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -31,7 +31,7 @@ pub enum Error { Reaction(#[from] ReactionError), #[error("SQLX error: {0}")] - SQLX(#[from] sqlx::Error), + Sqlx(#[from] sqlx::Error), #[error("Migration error: {0}")] SQLXMigration(#[from] sqlx::migrate::MigrateError), @@ -226,7 +226,7 @@ impl ResponseError for Error { ReactionError::AlreadyExists => StatusCode::BAD_REQUEST, ReactionError::NotFound => StatusCode::NOT_FOUND, }, - Error::SQLX(_) => StatusCode::INTERNAL_SERVER_ERROR, + Error::Sqlx(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::SQLXMigration(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::Serde(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::IO(_) => StatusCode::INTERNAL_SERVER_ERROR, diff --git a/src/gateway/types.rs b/src/gateway/types.rs index 7a66bf6..0028042 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -501,7 +501,7 @@ impl RoleUserMap { let all_role_ids: Vec = sqlx::query_as("SELECT id FROM roles") .fetch_all(db) .await - .map_err(crate::errors::Error::SQLX)?; + .map_err(crate::errors::Error::Sqlx)?; for role_id in all_role_ids.iter() { self.map .insert(Snowflake::from(role_id.to_uint()), HashSet::new()); @@ -511,7 +511,7 @@ impl RoleUserMap { sqlx::query_as("SELECT index, role_id FROM member_roles") .fetch_all(db) .await - .map_err(crate::errors::Error::SQLX)?; + .map_err(crate::errors::Error::Sqlx)?; for (user_id, role_id) in all_member_roles.iter() { // Unwrapping is fine here, as the member_roles table has a foreign key constraint // which states that role_id must be a valid id in the roles table. From d57d7c0f208a61138a68bbde75efc890043bc2f3 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 20:20:56 +0200 Subject: [PATCH 148/162] Lay out basics for gateway task --- src/gateway/gateway_task.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs index 6867ddf..a4a099a 100644 --- a/src/gateway/gateway_task.rs +++ b/src/gateway/gateway_task.rs @@ -2,6 +2,8 @@ use std::sync::Arc; use tokio::sync::Mutex; +use crate::errors::Error; + use super::{Event, GatewayClient}; /// Handles all messages a client sends to the gateway post-handshake. @@ -20,3 +22,25 @@ async fn handle_event(event: Event, connection: Arc, + mut kill_receive: tokio::sync::broadcast::Receiver<()>, +) -> Result<(), Error> { + tokio::select! { + _ = kill_receive.recv() => { + Ok(()) + } + event = inbox.recv() => { + match event { + Ok(event) => { + // TODO + todo!() + } + Err(_) => { + Ok(()) + } + } + } + } +} From 62277af374279443f2386bff4108658f702fae17 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 20:23:08 +0200 Subject: [PATCH 149/162] simplify process_inbox a little --- src/gateway/gateway_task.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs index a4a099a..acd5474 100644 --- a/src/gateway/gateway_task.rs +++ b/src/gateway/gateway_task.rs @@ -24,21 +24,23 @@ async fn handle_event(event: Event, connection: Arc>, mut inbox: tokio::sync::broadcast::Receiver, mut kill_receive: tokio::sync::broadcast::Receiver<()>, -) -> Result<(), Error> { - tokio::select! { - _ = kill_receive.recv() => { - Ok(()) - } - event = inbox.recv() => { - match event { - Ok(event) => { - // TODO - todo!() - } - Err(_) => { - Ok(()) +) { + loop { + tokio::select! { + _ = kill_receive.recv() => { + return; + } + event = inbox.recv() => { + match event { + Ok(event) => { + handle_event(event, connection.clone()).await; + } + Err(_) => { + return; + } } } } From c191cf571d0f0236d79a613f3ec71302753b654c Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Wed, 25 Sep 2024 20:47:00 +0200 Subject: [PATCH 150/162] Update whiteboard --- docs/Event Publishing in Symfonia.excalidraw | 17477 ++++++++++++++--- 1 file changed, 14970 insertions(+), 2507 deletions(-) diff --git a/docs/Event Publishing in Symfonia.excalidraw b/docs/Event Publishing in Symfonia.excalidraw index 468e31b..1229c9e 100644 --- a/docs/Event Publishing in Symfonia.excalidraw +++ b/docs/Event Publishing in Symfonia.excalidraw @@ -1,13 +1,12 @@ { "type": "excalidraw", "version": 2, - "source": "https://excalidraw.com", + "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", "elements": [ { "type": "rectangle", "version": 916, "versionNonce": 1162684619, - "index": "Zq", "isDeleted": false, "id": "pWnZ5m49e23wgtjp43Cjk", "fillStyle": "solid", @@ -40,7 +39,6 @@ "type": "text", "version": 613, "versionNonce": 1611586917, - "index": "Zr", "isDeleted": false, "id": "d___i3s4lLCSsCIGrwJcj", "fillStyle": "solid", @@ -70,14 +68,13 @@ "verticalAlign": "top", "containerId": "pWnZ5m49e23wgtjp43Cjk", "originalText": "User-specific Task", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "rectangle", - "version": 1023, - "versionNonce": 235161099, - "index": "Zs", + "version": 1031, + "versionNonce": 1422291908, "isDeleted": false, "id": "Yef_zHBLDgJgcHaAYtii3", "fillStyle": "solid", @@ -87,7 +84,7 @@ "opacity": 100, "angle": 0, "x": 991.8565274665173, - "y": 2218.876751568442, + "y": 2217.5076781162825, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 1839.1026870295834, @@ -102,15 +99,14 @@ "id": "Y-VdE-5JZCbVRtwiRhZM0" } ], - "updated": 1727114918418, + "updated": 1727281332846, "link": null, "locked": false }, { "type": "text", - "version": 567, - "versionNonce": 155536555, - "index": "Zt", + "version": 576, + "versionNonce": 1321950020, "isDeleted": false, "id": "Y-VdE-5JZCbVRtwiRhZM0", "fillStyle": "solid", @@ -119,8 +115,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1817.3578679295513, - "y": 2223.876751568442, + "x": 1817.357867929551, + "y": 2222.5076781162825, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 188.10000610351562, @@ -130,7 +126,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1727114918418, + "updated": 1727281332846, "link": null, "locked": false, "fontSize": 16, @@ -140,14 +136,13 @@ "verticalAlign": "top", "containerId": "Yef_zHBLDgJgcHaAYtii3", "originalText": "Server-/Outside boundary", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "rectangle", - "version": 867, - "versionNonce": 2093992805, - "index": "Zy", + "version": 868, + "versionNonce": 1863234172, "isDeleted": false, "id": "aBcRnfi5WZIFdLks0fcNk", "fillStyle": "solid", @@ -156,7 +151,7 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1052.7755421190561, + "x": 1054.1446155712158, "y": 2561.0480139547963, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -176,15 +171,14 @@ "type": "arrow" } ], - "updated": 1727114723600, + "updated": 1727281271165, "link": null, "locked": false }, { "type": "text", - "version": 663, - "versionNonce": 1374361285, - "index": "Zz", + "version": 664, + "versionNonce": 67830524, "isDeleted": false, "id": "oJsFTZtKwwj41ba8qbaAF", "fillStyle": "solid", @@ -193,7 +187,7 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1372.1568745296306, + "x": 1373.5259479817903, "y": 2566.0480139547963, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", @@ -204,7 +198,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1727114723600, + "updated": 1727281271165, "link": null, "locked": false, "fontSize": 16, @@ -214,14 +208,13 @@ "verticalAlign": "top", "containerId": "aBcRnfi5WZIFdLks0fcNk", "originalText": "Client-specific Task", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "rectangle", "version": 155, "versionNonce": 2135653332, - "index": "a0", "isDeleted": false, "id": "2MVXKRrbf5jvhORRI9URX", "fillStyle": "solid", @@ -256,9 +249,8 @@ }, { "type": "text", - "version": 49, - "versionNonce": 882960718, - "index": "a1", + "version": 50, + "versionNonce": 89908233, "isDeleted": false, "id": "OTBzzVqwEY6os024JScwh", "fillStyle": "solid", @@ -271,14 +263,14 @@ "y": 179, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 132.3333282470703, + "width": 110.556640625, "height": 25, "seed": 2120332483, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726872241417, + "updated": 1727290012230, "link": null, "locked": false, "fontSize": 20, @@ -288,14 +280,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "API Endpoint", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { "type": "rectangle", "version": 189, "versionNonce": 563405140, - "index": "a2", "isDeleted": false, "id": "4Sf2ujjlCt5_fuSqXkiLB", "fillStyle": "solid", @@ -336,7 +327,6 @@ "type": "text", "version": 160, "versionNonce": 1254865810, - "index": "a3", "isDeleted": false, "id": "FZtE7MMsPYRo6B5BjG9_E", "fillStyle": "solid", @@ -366,14 +356,13 @@ "verticalAlign": "middle", "containerId": "4Sf2ujjlCt5_fuSqXkiLB", "originalText": "tokio channel sender", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { "type": "ellipse", "version": 574, "versionNonce": 1351980756, - "index": "a4", "isDeleted": false, "id": "1JFUtbSSfm7QWM0KjMPE2", "fillStyle": "solid", @@ -410,7 +399,6 @@ "type": "text", "version": 540, "versionNonce": 1962865486, - "index": "a5", "isDeleted": false, "id": "_Pw8i03cpH5wSwGx3Ri9P", "fillStyle": "solid", @@ -440,14 +428,13 @@ "verticalAlign": "middle", "containerId": "1JFUtbSSfm7QWM0KjMPE2", "originalText": "Receiver", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { "type": "ellipse", "version": 591, "versionNonce": 1037629390, - "index": "a6", "isDeleted": false, "id": "A4UGJKAYTwb60t4OudBqR", "fillStyle": "solid", @@ -484,7 +471,6 @@ "type": "text", "version": 557, "versionNonce": 165177618, - "index": "a7", "isDeleted": false, "id": "3TPexlcGphxPOeYq8__TX", "fillStyle": "solid", @@ -514,14 +500,13 @@ "verticalAlign": "middle", "containerId": "A4UGJKAYTwb60t4OudBqR", "originalText": "Receiver", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { "type": "arrow", "version": 95, "versionNonce": 72675796, - "index": "a8", "isDeleted": false, "id": "hvS4AEabLaqrr1-YIMYjQ", "fillStyle": "solid", @@ -576,7 +561,6 @@ "type": "arrow", "version": 140, "versionNonce": 173792334, - "index": "a9", "isDeleted": false, "id": "0xjS1TWpsg8zIrQa57fCf", "fillStyle": "solid", @@ -629,9 +613,8 @@ }, { "type": "text", - "version": 491, - "versionNonce": 2109563220, - "index": "aM", + "version": 492, + "versionNonce": 2043346183, "isDeleted": false, "id": "dgmOCsrSsYfsKwLF33TK1", "fillStyle": "solid", @@ -644,14 +627,14 @@ "y": 753, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 425.52325827390416, + "width": 355.5078125, "height": 175, "seed": 1967997485, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726599083888, + "updated": 1727290012231, "link": null, "locked": false, "fontSize": 20, @@ -661,14 +644,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "Goals in Mind:\n\n- Support multiple chats opened at the same time\n- Send the least amount of messages required without sacrificing UX\n- Do not modify Discord API/WS behaviour", - "autoResize": false, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 169 }, { "type": "text", - "version": 1239, - "versionNonce": 977772244, - "index": "aN", + "version": 1240, + "versionNonce": 439646953, "isDeleted": false, "id": "KUumgEMrgSNz1yHP1SAwp", "fillStyle": "solid", @@ -681,14 +663,14 @@ "y": 987, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 420.6236847441232, + "width": 350.6640625, "height": 550, "seed": 1310238883, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726599088418, + "updated": 1727290012231, "link": null, "locked": false, "fontSize": 20, @@ -698,14 +680,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "Idea:\n- Always send events from the latest channel scope that has been accessed. When a user clicks on a channel, they will send a get messages request to the channel. We will use that request to determine which channel the user is viewing.\n- Send events from all other channels the user has accessed via HTTP API in the last XY seconds. When the user accesses a new channel, the \"old\" channel is pushed back into a sort of queue, where after a set amount of time, the user will be unsubscribed from these channels' \"detailed events\". The unsubscribing can be stopped if the user sends a WS payload which describes the intent to keep receiving events from that channel. This way, one can view multiple channels at the same time using custom clients, while single-view clients are also fully supported.", - "autoResize": false, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 544 }, { "type": "rectangle", "version": 80, "versionNonce": 336456782, - "index": "aj", "isDeleted": false, "id": "uctAc53nuACE9HNLZ4vY5", "fillStyle": "solid", @@ -740,7 +721,6 @@ "type": "text", "version": 15, "versionNonce": 888406542, - "index": "aj4", "isDeleted": false, "id": "2DuWtSihpY-GZ0JNUaQjX", "fillStyle": "solid", @@ -770,14 +750,13 @@ "verticalAlign": "middle", "containerId": "uctAc53nuACE9HNLZ4vY5", "originalText": "Role based", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { "type": "rectangle", "version": 157, "versionNonce": 1760617102, - "index": "al", "isDeleted": false, "id": "ieZZu2zGIwLcqyTV5a4CE", "fillStyle": "solid", @@ -812,7 +791,6 @@ "type": "text", "version": 113, "versionNonce": 638673102, - "index": "am", "isDeleted": false, "id": "MvOCYSEPCLzw-s5kiK43w", "fillStyle": "solid", @@ -842,14 +820,13 @@ "verticalAlign": "middle", "containerId": "ieZZu2zGIwLcqyTV5a4CE", "originalText": "User ID based", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { "type": "text", - "version": 152, - "versionNonce": 784276302, - "index": "an", + "version": 153, + "versionNonce": 764658727, "isDeleted": false, "id": "jf2ngP537Oa8kowc55JAi", "fillStyle": "solid", @@ -862,14 +839,14 @@ "y": 326, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 312.9, + "width": 266.015625, "height": 75, "seed": 743310670, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726871176942, + "updated": 1727290012231, "link": null, "locked": false, "fontSize": 20, @@ -879,14 +856,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "- Guild updates: @everyone\n- New channel: Send to everyone\nwho can view the channel", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 69 }, { "type": "text", - "version": 62, - "versionNonce": 1518014862, - "index": "ao", + "version": 63, + "versionNonce": 2039952841, "isDeleted": false, "id": "ZQ68mgcM38iWZKi5gb2F3", "fillStyle": "solid", @@ -899,14 +875,14 @@ "y": 331, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 144.96666666666667, + "width": 120.546875, "height": 25, "seed": 1403985870, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726871235792, + "updated": 1727290012232, "link": null, "locked": false, "fontSize": 20, @@ -916,14 +892,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "- Relationships", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 19 }, { "type": "text", - "version": 315, - "versionNonce": 110446162, - "index": "ap", + "version": 316, + "versionNonce": 1995876167, "isDeleted": false, "id": "3PN2ZjYb9D5Pj-SKax1Xc", "fillStyle": "solid", @@ -936,14 +911,14 @@ "y": 429.75, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 812.6333618164062, + "width": 665.83984375, "height": 75, "seed": 1889819986, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726871290983, + "updated": 1727290012232, "link": null, "locked": false, "fontSize": 20, @@ -953,14 +928,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "If multiple roles receive the same event, we need a Mathematical Set-like construct\nwhere each recipient can only exist once in the set, to not send the event multiple\ntimes.", - "autoResize": true, - "lineHeight": 1.25 + "lineHeight": 1.25, + "baseline": 69 }, { "type": "text", - "version": 385, - "versionNonce": 1779127122, - "index": "b04", + "version": 386, + "versionNonce": 379557033, "isDeleted": false, "id": "YUxJv_gEUatv301rCZRUy", "fillStyle": "solid", @@ -973,14 +947,14 @@ "y": 544.8888888888889, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 346.3833333333333, + "width": 311.9609375, "height": 129.60000000000002, "seed": 1317886862, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726871949681, + "updated": 1727290012232, "link": null, "locked": false, "fontSize": 16, @@ -990,14 +964,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "What do we *want*?\n- Do not query database often\n- Re-/storeable state from/to Database\n- Minimum amount of comparisons/computation\n- Deduplication: Only send event once, even if\nrecipient qualifies multiple times", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 124 }, { "type": "freedraw", "version": 242, "versionNonce": 1014662859, - "index": "b05", "isDeleted": false, "id": "CHpg-ObEveeR8Mt-gucHW", "fillStyle": "solid", @@ -1548,7 +1521,6 @@ "type": "freedraw", "version": 417, "versionNonce": 951416171, - "index": "b06", "isDeleted": false, "id": "Sj7f5Exu4Ok9tG5tDK7M3", "fillStyle": "solid", @@ -2799,7 +2771,6 @@ "type": "freedraw", "version": 333, "versionNonce": 1490653195, - "index": "b08", "isDeleted": false, "id": "T-4BfE8nTqvjRm4Bpj2hL", "fillStyle": "solid", @@ -3350,7 +3321,6 @@ "type": "freedraw", "version": 508, "versionNonce": 518084267, - "index": "b09", "isDeleted": false, "id": "FReSJkhy6BcPPAKErFY7N", "fillStyle": "solid", @@ -4601,7 +4571,6 @@ "type": "freedraw", "version": 327, "versionNonce": 1769425227, - "index": "b0A", "isDeleted": false, "id": "Bcw6QA7doNzI8yQ81Zq7U", "fillStyle": "solid", @@ -5152,7 +5121,6 @@ "type": "freedraw", "version": 502, "versionNonce": 1576396779, - "index": "b0B", "isDeleted": false, "id": "5oqix7IsPdeANxSpXRorI", "fillStyle": "solid", @@ -6403,7 +6371,6 @@ "type": "freedraw", "version": 296, "versionNonce": 308785803, - "index": "b0C", "isDeleted": false, "id": "bGO7CwBudzshOJXqkwenw", "fillStyle": "solid", @@ -6954,7 +6921,6 @@ "type": "freedraw", "version": 471, "versionNonce": 2005346603, - "index": "b0D", "isDeleted": false, "id": "xeZeu1Xtow0ELNDfxV7BK", "fillStyle": "solid", @@ -8203,9 +8169,8 @@ }, { "type": "text", - "version": 172, - "versionNonce": 46058443, - "index": "b0E", + "version": 173, + "versionNonce": 1414538855, "isDeleted": false, "id": "8EzYrC-dXIrmkiBP6umfl", "fillStyle": "solid", @@ -8218,14 +8183,14 @@ "y": 1311.888888888889, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 81.43333435058594, + "width": 73.3671875, "height": 64.80000000000001, "seed": 176266507, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918175611, + "updated": 1727290012232, "link": null, "locked": false, "fontSize": 16, @@ -8235,14 +8200,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "@everyone\nrole1\nrole2", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 59 }, { "type": "text", - "version": 228, - "versionNonce": 405346923, - "index": "b0F", + "version": 229, + "versionNonce": 1962481545, "isDeleted": false, "id": "xk4XNQq63sET4n4cVfEiZ", "fillStyle": "solid", @@ -8255,14 +8219,14 @@ "y": 1319.0888888888892, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 81.43333435058594, + "width": 73.3671875, "height": 43.2, "seed": 1674891019, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918175611, + "updated": 1727290012232, "link": null, "locked": false, "fontSize": 16, @@ -8272,14 +8236,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "@everyone\nrole1", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 37 }, { "type": "text", - "version": 283, - "versionNonce": 881890571, - "index": "b0G", + "version": 284, + "versionNonce": 1147978119, "isDeleted": false, "id": "JUEgPZtV5KL2V38fpjHz4", "fillStyle": "solid", @@ -8292,14 +8255,14 @@ "y": 1324.0888888888887, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 81.43333435058594, + "width": 73.3671875, "height": 43.2, "seed": 627094373, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918175611, + "updated": 1727290012233, "link": null, "locked": false, "fontSize": 16, @@ -8309,14 +8272,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "@everyone\nrole2", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 37 }, { "type": "text", - "version": 338, - "versionNonce": 1466940331, - "index": "b0H", + "version": 339, + "versionNonce": 383729257, "isDeleted": false, "id": "CPiCG41jzAmrPZzI14zP7", "fillStyle": "solid", @@ -8329,14 +8291,14 @@ "y": 1323.0888888888892, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 81.43333435058594, + "width": 73.3671875, "height": 21.6, "seed": 2122129163, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918175611, + "updated": 1727290012233, "link": null, "locked": false, "fontSize": 16, @@ -8346,14 +8308,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "@everyone", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "ellipse", "version": 187, "versionNonce": 863988491, - "index": "b0J", "isDeleted": false, "id": "lTLUoLMALkBdsgu2m-6Lc", "fillStyle": "solid", @@ -8404,7 +8365,6 @@ "type": "text", "version": 159, "versionNonce": 331224267, - "index": "b0K", "isDeleted": false, "id": "X4wb25xwsRKeAYW0d9Vm1", "fillStyle": "solid", @@ -8434,14 +8394,13 @@ "verticalAlign": "middle", "containerId": "lTLUoLMALkBdsgu2m-6Lc", "originalText": "channel update\nevent for\nrole1, role2", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 59 }, { "type": "arrow", "version": 105, "versionNonce": 780049707, - "index": "b0L", "isDeleted": false, "id": "e4Nz2Zcd-PFg1RzFl7Rkb", "fillStyle": "solid", @@ -8488,14 +8447,12 @@ -33.03122005200089, 266.131322440957 ] - ], - "elbowed": false + ] }, { "type": "text", "version": 8, "versionNonce": 292886693, - "index": "b0M", "isDeleted": false, "id": "8eFl_bz4o1x_Gi_OrjgzY", "fillStyle": "solid", @@ -8525,14 +8482,13 @@ "verticalAlign": "middle", "containerId": "e4Nz2Zcd-PFg1RzFl7Rkb", "originalText": "role1", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "arrow", "version": 162, "versionNonce": 2064364165, - "index": "b0N", "isDeleted": false, "id": "zZ5CdQAjnna-l8gWxCqca", "fillStyle": "solid", @@ -8579,14 +8535,12 @@ 81.00029648577288, 278.00101756845515 ] - ], - "elbowed": false + ] }, { "type": "text", "version": 8, "versionNonce": 782494795, - "index": "b0O", "isDeleted": false, "id": "8OUPBM2su9Op99_hGVMNT", "fillStyle": "solid", @@ -8616,14 +8570,13 @@ "verticalAlign": "middle", "containerId": "zZ5CdQAjnna-l8gWxCqca", "originalText": "role1", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "arrow", "version": 189, "versionNonce": 1666546891, - "index": "b0P", "isDeleted": false, "id": "NjFBOI_LqII2FGV8XIwS8", "fillStyle": "solid", @@ -8670,14 +8623,12 @@ 167.03483504441488, 307.0727763985226 ] - ], - "elbowed": false + ] }, { "type": "text", "version": 11, "versionNonce": 1596781675, - "index": "b0Q", "isDeleted": false, "id": "SKaYSFFXuptpGH-z-KnGI", "fillStyle": "solid", @@ -8707,14 +8658,13 @@ "verticalAlign": "middle", "containerId": "NjFBOI_LqII2FGV8XIwS8", "originalText": "role2", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "arrow", "version": 117, "versionNonce": 457943589, - "index": "b0R", "isDeleted": false, "id": "Bi7VjwGIRtHm94qnuHwDw", "fillStyle": "solid", @@ -8761,14 +8711,12 @@ -30.043709300758564, 286.41669533389813 ] - ], - "elbowed": false + ] }, { "type": "text", "version": 9, "versionNonce": 1911717733, - "index": "b0S", "isDeleted": false, "id": "GUT5oAc7DPQFATEjGMFrR", "fillStyle": "solid", @@ -8798,14 +8746,13 @@ "verticalAlign": "middle", "containerId": "Bi7VjwGIRtHm94qnuHwDw", "originalText": "role2", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "arrow", "version": 182, "versionNonce": 1317884075, - "index": "b0T", "isDeleted": false, "id": "oL3Rhg7nAySLp3xks-Exz", "fillStyle": "solid", @@ -8848,14 +8795,12 @@ 76, 111 ] - ], - "elbowed": false + ] }, { "type": "text", - "version": 50, - "versionNonce": 1434019403, - "index": "b0V", + "version": 51, + "versionNonce": 948500647, "isDeleted": false, "id": "CkAKDa8507tqAyhE5X430", "fillStyle": "solid", @@ -8868,14 +8813,14 @@ "y": 925.1888888888889, "strokeColor": "#f08c00", "backgroundColor": "#a5d8ff", - "width": 104.75, + "width": 96.640625, "height": 27, "seed": 381538405, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918286078, + "updated": 1727290012233, "link": null, "locked": false, "fontSize": 20, @@ -8885,14 +8830,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "duplication!", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 20 }, { "type": "rectangle", "version": 262, "versionNonce": 6611333, - "index": "b0W", "isDeleted": false, "id": "qwvPWBT-AMLng4YbFJgE5", "fillStyle": "solid", @@ -8951,7 +8895,6 @@ "type": "text", "version": 233, "versionNonce": 288101, - "index": "b0X", "isDeleted": false, "id": "tM0soXn7oP_i2OGsLUJAq", "fillStyle": "solid", @@ -8981,14 +8924,13 @@ "verticalAlign": "middle", "containerId": "qwvPWBT-AMLng4YbFJgE5", "originalText": "Mathematical Set\nContaining User IDs", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 47 }, { "type": "freedraw", "version": 425, "versionNonce": 1762815141, - "index": "b0Y", "isDeleted": false, "id": "I3rz4sapMk720uV--byCf", "fillStyle": "solid", @@ -9539,7 +9481,6 @@ "type": "freedraw", "version": 600, "versionNonce": 2009567237, - "index": "b0Z", "isDeleted": false, "id": "c6iR_MiueBJ5rn0R-g8Bz", "fillStyle": "solid", @@ -10790,7 +10731,6 @@ "type": "freedraw", "version": 516, "versionNonce": 1858160485, - "index": "b0a", "isDeleted": false, "id": "hfeHrTcskEALtNTDWGVrO", "fillStyle": "solid", @@ -11341,7 +11281,6 @@ "type": "freedraw", "version": 691, "versionNonce": 5051077, - "index": "b0b", "isDeleted": false, "id": "uG6Z89OJ6WxSXpR7-p7q1", "fillStyle": "solid", @@ -12592,7 +12531,6 @@ "type": "freedraw", "version": 510, "versionNonce": 956854821, - "index": "b0c", "isDeleted": false, "id": "glksEOJMoJWHSB1RdDsyn", "fillStyle": "solid", @@ -13143,7 +13081,6 @@ "type": "freedraw", "version": 685, "versionNonce": 903931269, - "index": "b0d", "isDeleted": false, "id": "XupIdN9gDEZBHd-_2F24j", "fillStyle": "solid", @@ -14394,7 +14331,6 @@ "type": "freedraw", "version": 479, "versionNonce": 437513445, - "index": "b0e", "isDeleted": false, "id": "UDvKr0RxgawP5asj6VCaM", "fillStyle": "solid", @@ -14945,7 +14881,6 @@ "type": "freedraw", "version": 654, "versionNonce": 1160710213, - "index": "b0f", "isDeleted": false, "id": "EQVH9YKKVPGd4zatw84ha", "fillStyle": "solid", @@ -16194,9 +16129,8 @@ }, { "type": "text", - "version": 356, - "versionNonce": 1248564651, - "index": "b0g", + "version": 357, + "versionNonce": 723355977, "isDeleted": false, "id": "oHiI-diNFXNo4nn79ecEY", "fillStyle": "solid", @@ -16209,7 +16143,7 @@ "y": 1288.4888888888888, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 81.43333435058594, + "width": 73.3671875, "height": 64.80000000000001, "seed": 1325577733, "groupIds": [], @@ -16221,7 +16155,7 @@ "type": "arrow" } ], - "updated": 1727115085656, + "updated": 1727290012233, "link": null, "locked": false, "fontSize": 16, @@ -16231,14 +16165,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "@everyone\nrole1\nrole2", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 59 }, { "type": "text", - "version": 411, - "versionNonce": 535001861, - "index": "b0h", + "version": 412, + "versionNonce": 1179732935, "isDeleted": false, "id": "xdFtorJIquQrPdTl0BGlU", "fillStyle": "solid", @@ -16251,14 +16184,14 @@ "y": 1295.6888888888896, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 81.43333435058594, + "width": 73.3671875, "height": 43.2, "seed": 1067953509, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918366440, + "updated": 1727290012233, "link": null, "locked": false, "fontSize": 16, @@ -16268,14 +16201,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "@everyone\nrole1", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 37 }, { "type": "text", - "version": 466, - "versionNonce": 573035109, - "index": "b0i", + "version": 467, + "versionNonce": 2064611369, "isDeleted": false, "id": "7emNqD2oswIWvyucmQPgq", "fillStyle": "solid", @@ -16288,14 +16220,14 @@ "y": 1300.6888888888886, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 81.43333435058594, + "width": 73.3671875, "height": 43.2, "seed": 1937160389, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918366440, + "updated": 1727290012233, "link": null, "locked": false, "fontSize": 16, @@ -16305,14 +16237,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "@everyone\nrole2", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 37 }, { "type": "text", - "version": 521, - "versionNonce": 1017789893, - "index": "b0j", + "version": 522, + "versionNonce": 434256615, "isDeleted": false, "id": "QPBL862S6bVU8ck8DKkbi", "fillStyle": "solid", @@ -16325,14 +16256,14 @@ "y": 1299.6888888888896, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 81.43333435058594, + "width": 73.3671875, "height": 21.6, "seed": 604854309, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918366440, + "updated": 1727290012233, "link": null, "locked": false, "fontSize": 16, @@ -16342,14 +16273,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "@everyone", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "ellipse", "version": 355, "versionNonce": 1420342277, - "index": "b0k", "isDeleted": false, "id": "3U99Cb2Kfg8Tlwif9RVQc", "fillStyle": "solid", @@ -16392,7 +16322,6 @@ "type": "text", "version": 326, "versionNonce": 1383070725, - "index": "b0l", "isDeleted": false, "id": "xuUnGiT59o-8cz8a1F7LJ", "fillStyle": "solid", @@ -16422,14 +16351,13 @@ "verticalAlign": "middle", "containerId": "3U99Cb2Kfg8Tlwif9RVQc", "originalText": "channel update\nevent for\nrole1, role2", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 59 }, { "type": "rectangle", "version": 135, "versionNonce": 1154041867, - "index": "b0m", "isDeleted": false, "id": "pX5ccEr5swnPSJx6hZORI", "fillStyle": "solid", @@ -16480,7 +16408,6 @@ "type": "text", "version": 143, "versionNonce": 53590181, - "index": "b0n", "isDeleted": false, "id": "kUmeJCdbzqrKDcbWWED27", "fillStyle": "solid", @@ -16510,14 +16437,13 @@ "verticalAlign": "middle", "containerId": "pX5ccEr5swnPSJx6hZORI", "originalText": "Guild Information:\nRole ID->User ID\nmapping", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 59 }, { "type": "arrow", "version": 62, "versionNonce": 46705701, - "index": "b0o", "isDeleted": false, "id": "sXyxzxgCu764FCGAZM_lx", "fillStyle": "solid", @@ -16571,14 +16497,12 @@ -1, 124 ] - ], - "elbowed": false + ] }, { "type": "text", "version": 9, "versionNonce": 270463851, - "index": "b0oV", "isDeleted": false, "id": "GDegdKUZ9_XlmQsn3cjzY", "fillStyle": "solid", @@ -16608,14 +16532,13 @@ "verticalAlign": "middle", "containerId": "sXyxzxgCu764FCGAZM_lx", "originalText": "role1", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "arrow", "version": 62, "versionNonce": 95890757, - "index": "b0p", "isDeleted": false, "id": "C4nv0UsyABV7pyFF4MjvB", "fillStyle": "solid", @@ -16669,14 +16592,12 @@ -1, 115 ] - ], - "elbowed": false + ] }, { "type": "text", "version": 8, "versionNonce": 1292192843, - "index": "b0q", "isDeleted": false, "id": "ZXByhtNsQIoD3boJwK_1U", "fillStyle": "solid", @@ -16706,14 +16627,13 @@ "verticalAlign": "middle", "containerId": "C4nv0UsyABV7pyFF4MjvB", "originalText": "role2", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "arrow", "version": 286, "versionNonce": 610867813, - "index": "b0r", "isDeleted": false, "id": "d2KqmWXSWVpK1rau0p4Ly", "fillStyle": "solid", @@ -16766,14 +16686,12 @@ 0, 181 ] - ], - "elbowed": false + ] }, { "type": "text", - "version": 172, - "versionNonce": 1515014469, - "index": "b0s", + "version": 173, + "versionNonce": 413174537, "isDeleted": false, "id": "lNZOuEpmA2RQahhdkRqrN", "fillStyle": "solid", @@ -16786,14 +16704,14 @@ "y": 764.8888888888889, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 204.86666666666667, + "width": 175.5078125, "height": 43.2, "seed": 226967749, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918493942, + "updated": 1727290012234, "link": null, "locked": false, "fontSize": 16, @@ -16803,14 +16721,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "Add everyone from Role 1 as recipient", - "autoResize": false, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 37 }, { "type": "arrow", "version": 365, "versionNonce": 309439365, - "index": "b0t", "isDeleted": false, "id": "VSp2nxUoFQQvjUVEjNcqU", "fillStyle": "solid", @@ -16863,14 +16780,12 @@ 11, 166 ] - ], - "elbowed": false + ] }, { "type": "text", - "version": 97, - "versionNonce": 556104709, - "index": "b0u", + "version": 98, + "versionNonce": 667436551, "isDeleted": false, "id": "tmkdW5jNX7uZWaMNJsZRf", "fillStyle": "solid", @@ -16879,18 +16794,18 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1893.667209201389, + "x": 1911.976062350803, "y": 774.8888888888889, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 189.81666564941406, + "width": 171.5078125, "height": 43.2, "seed": 1174821861, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918495409, + "updated": 1727290012234, "link": null, "locked": false, "fontSize": 16, @@ -16900,14 +16815,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "Add everyone from Role 2\nas recipient", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 37 }, { "type": "arrow", "version": 39, "versionNonce": 1947291973, - "index": "b0v", "isDeleted": false, "id": "suRrGqAz0cEyuwlqJcpHg", "fillStyle": "solid", @@ -16951,14 +16865,12 @@ 87, 1 ] - ], - "elbowed": false + ] }, { "type": "text", - "version": 64, - "versionNonce": 360055147, - "index": "b0w", + "version": 65, + "versionNonce": 1280066025, "isDeleted": false, "id": "DkbZYT7aReffbg0UmTWGj", "fillStyle": "solid", @@ -16971,14 +16883,14 @@ "y": 978.8888888888889, "strokeColor": "#2f9e44", "backgroundColor": "#a5d8ff", - "width": 92.51666666666667, + "width": 79.96875, "height": 21.6, "seed": 832989899, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918525184, + "updated": 1727290012234, "link": null, "locked": false, "fontSize": 16, @@ -16988,14 +16900,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "deduplicates", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "arrow", "version": 121, "versionNonce": 227194565, - "index": "b0x", "isDeleted": false, "id": "321eNbFnBxl8j-kWXkY8s", "fillStyle": "solid", @@ -17039,14 +16950,12 @@ -200, 118 ] - ], - "elbowed": false + ] }, { "type": "arrow", "version": 43, "versionNonce": 860129739, - "index": "b0y", "isDeleted": false, "id": "39o_Q6zDovd5a5GljOMno", "fillStyle": "solid", @@ -17090,14 +16999,12 @@ -48, 128 ] - ], - "elbowed": false + ] }, { "type": "arrow", "version": 80, "versionNonce": 593227301, - "index": "b0z", "isDeleted": false, "id": "x0GvUbEbHaO3epjVckQko", "fillStyle": "solid", @@ -17141,14 +17048,12 @@ 35, 128 ] - ], - "elbowed": false + ] }, { "type": "text", - "version": 195, - "versionNonce": 1275942021, - "index": "b10", + "version": 236, + "versionNonce": 520088871, "isDeleted": false, "id": "ZFK4unSk7yiswsQLXlRE0", "fillStyle": "solid", @@ -17161,31 +17066,30 @@ "y": 1103.888888888889, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 209.68333333333334, + "width": 369.671875, "height": 21.6, "seed": 1606759333, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918578911, + "updated": 1727290012234, "link": null, "locked": false, "fontSize": 16, "fontFamily": 6, - "text": "broadcast event to recipients", + "text": "broadcast event to recipients using ConnectedUsers struct", "textAlign": "left", "verticalAlign": "top", "containerId": null, - "originalText": "broadcast event to recipients", - "autoResize": true, - "lineHeight": 1.35 + "originalText": "broadcast event to recipients using ConnectedUsers struct", + "lineHeight": 1.35, + "baseline": 16 }, { "type": "rectangle", - "version": 545, - "versionNonce": 882531019, - "index": "b11", + "version": 546, + "versionNonce": 2115102501, "isDeleted": false, "id": "YvrAXYERhR-VVUCxxvzwy", "fillStyle": "solid", @@ -17194,7 +17098,7 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2389.7700249811346, + "x": 2388.9628208456666, "y": 619.9711947701132, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", @@ -17210,15 +17114,14 @@ "id": "5uIKmQGCHQ4JNzkHQ2WTP" } ], - "updated": 1727115242295, + "updated": 1727130230159, "link": null, "locked": false }, { "type": "text", - "version": 632, - "versionNonce": 1169869163, - "index": "b12", + "version": 633, + "versionNonce": 1676917381, "isDeleted": false, "id": "5uIKmQGCHQ4JNzkHQ2WTP", "fillStyle": "solid", @@ -17227,7 +17130,7 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 2394.7700249811346, + "x": 2393.9628208456666, "y": 636.7711947701132, "strokeColor": "#1e1e1e", "backgroundColor": "#ffec99", @@ -17238,7 +17141,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1727115242295, + "updated": 1727130230159, "link": null, "locked": false, "fontSize": 16, @@ -17248,14 +17151,13 @@ "verticalAlign": "middle", "containerId": "YvrAXYERhR-VVUCxxvzwy", "originalText": "What we need is a sort of omniscient sender struct which knows of every user id connected to the gateway and which holds a channel sender to send messages to that user.", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 81 }, { "type": "rectangle", - "version": 685, - "versionNonce": 1715288395, - "index": "b13", + "version": 813, + "versionNonce": 484249084, "isDeleted": false, "id": "2-xZdr1N7h94XCAl94juo", "fillStyle": "solid", @@ -17264,8 +17166,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1061.9573510206933, - "y": 2600.3535333769073, + "x": 1056.481057212054, + "y": 2841.310460957029, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 196.66666666666652, @@ -17308,17 +17210,24 @@ { "id": "yMeEUEU3Lphx3vyL3whwt", "type": "arrow" + }, + { + "id": "gmFBoiLPBB3mYCMjdlIJg", + "type": "arrow" + }, + { + "id": "bgmRPtR4-gNH9lKHCrEsm", + "type": "arrow" } ], - "updated": 1727115132489, + "updated": 1727281263503, "link": null, "locked": false }, { "type": "text", - "version": 651, - "versionNonce": 1926097285, - "index": "b13V", + "version": 777, + "versionNonce": 1949438844, "isDeleted": false, "id": "t0kacxDg-xYJMZRhr-ORv", "fillStyle": "solid", @@ -17327,8 +17236,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1109.582352292259, - "y": 2637.331311154685, + "x": 1104.1060584836198, + "y": 2878.288238734807, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 101.41666412353516, @@ -17338,7 +17247,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1727114723600, + "updated": 1727281168829, "link": null, "locked": false, "fontSize": 16, @@ -17348,14 +17257,13 @@ "verticalAlign": "middle", "containerId": "2-xZdr1N7h94XCAl94juo", "originalText": "Gateway Task", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "rectangle", - "version": 891, - "versionNonce": 1694613797, - "index": "b15", + "version": 942, + "versionNonce": 496801404, "isDeleted": false, "id": "ernpMbdN54sSKmYGoM7nC", "fillStyle": "solid", @@ -17364,8 +17272,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1592.5129065762487, - "y": 2600.353533376907, + "x": 1587.0366127676095, + "y": 2598.984459924747, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 196.66666666666652, @@ -17398,15 +17306,14 @@ "type": "arrow" } ], - "updated": 1727114723600, + "updated": 1727281173330, "link": null, "locked": false }, { "type": "text", - "version": 879, - "versionNonce": 1406686341, - "index": "b16", + "version": 930, + "versionNonce": 1866229500, "isDeleted": false, "id": "iFTMGnafTCa2cKPMR7lDj", "fillStyle": "solid", @@ -17415,8 +17322,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1635.0629065762487, - "y": 2637.331311154685, + "x": 1629.5866127676095, + "y": 2635.9622377025253, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 111.56666666666666, @@ -17426,7 +17333,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1727114723600, + "updated": 1727281173330, "link": null, "locked": false, "fontSize": 16, @@ -17436,14 +17343,13 @@ "verticalAlign": "middle", "containerId": "ernpMbdN54sSKmYGoM7nC", "originalText": "Heartbeat Task", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "arrow", - "version": 2212, - "versionNonce": 935757579, - "index": "b17", + "version": 2465, + "versionNonce": 728508740, "isDeleted": false, "id": "_FXruw5utykj1QDL3wURd", "fillStyle": "solid", @@ -17452,12 +17358,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1266.4017954651376, - "y": 2629.242422265796, + "x": 1186.8704500014996, + "y": 2833.5326831792513, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 317.77777777777806, - "height": 0.7658583367585834, + "width": 390.46375598061695, + "height": 129.70537456127022, "seed": 394883525, "groupIds": [], "frameId": null, @@ -17470,19 +17376,19 @@ "id": "rVjVYzDRISZ3zbtZ2lI7r" } ], - "updated": 1727114724747, + "updated": 1727281227791, "link": null, "locked": false, "startBinding": { "elementId": "2-xZdr1N7h94XCAl94juo", - "focus": -0.39872362474490314, + "focus": -0.558251196738502, "gap": 7.777777777777828, "fixedPoint": null }, "endBinding": { "elementId": "ernpMbdN54sSKmYGoM7nC", - "focus": 0.3720930232557988, - "gap": 8.33333333333303, + "focus": -0.2632621663656723, + "gap": 9.70240678549294, "fixedPoint": null }, "lastCommittedPoint": null, @@ -17494,17 +17400,15 @@ 0 ], [ - 317.77777777777806, - 0.7658583367585834 + 390.46375598061695, + -129.70537456127022 ] - ], - "elbowed": false + ] }, { "type": "text", - "version": 42, - "versionNonce": 1087064805, - "index": "b17V", + "version": 45, + "versionNonce": 1700153980, "isDeleted": false, "id": "rVjVYzDRISZ3zbtZ2lI7r", "fillStyle": "solid", @@ -17517,31 +17421,30 @@ "y": 1805.4973968540703, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 199.60000610351562, - "height": 43.2, + "width": 271.9166666666667, + "height": 21.6, "seed": 740383013, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726918885299, + "updated": 1727281173167, "link": null, "locked": false, "fontSize": 16, "fontFamily": 6, - "text": "forwards heartbeat related \nmessages", + "text": "forwards heartbeat related messages", "textAlign": "center", "verticalAlign": "middle", "containerId": "_FXruw5utykj1QDL3wURd", "originalText": "forwards heartbeat related messages", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "arrow", - "version": 2219, - "versionNonce": 1288863819, - "index": "b18", + "version": 2538, + "versionNonce": 921039996, "isDeleted": false, "id": "VQweVVOoEtZ8a74fJHKXs", "fillStyle": "solid", @@ -17550,12 +17453,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1585.2906843540268, - "y": 2658.5439753688156, + "x": 1573.5502503189919, + "y": 2685.333356276607, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 322.2222222222224, - "height": 0.6984468969803856, + "width": 336.49418377822326, + "height": 6.073617383433884, "seed": 1426645291, "groupIds": [], "frameId": null, @@ -17568,19 +17471,19 @@ "id": "vH_ahY76bpif3eJ3dl-IZ" } ], - "updated": 1727114724747, + "updated": 1727281289803, "link": null, "locked": false, "startBinding": { "elementId": "ernpMbdN54sSKmYGoM7nC", - "focus": -0.21220392709342042, - "gap": 7.222222222221944, + "focus": -0.8191170026051215, + "gap": 13.486362448617683, "fixedPoint": null }, "endBinding": { - "elementId": "2-xZdr1N7h94XCAl94juo", - "focus": 0.2361673942352932, - "gap": 4.444444444444571, + "elementId": "pVxFYg6-5Tv4jl8TsKj0I", + "focus": 0.7794593358170911, + "gap": 1, "fixedPoint": null }, "lastCommittedPoint": null, @@ -17592,17 +17495,15 @@ 0 ], [ - -322.2222222222224, - 0.6984468969803856 + -336.49418377822326, + -6.073617383433884 ] - ], - "elbowed": false + ] }, { "type": "text", "version": 131, "versionNonce": 1376495019, - "index": "b19", "isDeleted": false, "id": "vH_ahY76bpif3eJ3dl-IZ", "fillStyle": "solid", @@ -17632,14 +17533,13 @@ "verticalAlign": "middle", "containerId": "VQweVVOoEtZ8a74fJHKXs", "originalText": "response to send to user", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "freedraw", "version": 1165, "versionNonce": 1606044613, - "index": "b1A", "isDeleted": false, "id": "DrB3Y-Z4QjXjSwPrDuRu6", "fillStyle": "solid", @@ -18190,7 +18090,6 @@ "type": "freedraw", "version": 1340, "versionNonce": 140938021, - "index": "b1B", "isDeleted": false, "id": "e9cm8wo2OWga0vSbbT22r", "fillStyle": "solid", @@ -19439,9 +19338,8 @@ }, { "type": "arrow", - "version": 1354, - "versionNonce": 1293779333, - "index": "b1E", + "version": 1533, + "versionNonce": 1733182916, "isDeleted": false, "id": "ldtBH_Mc3-DYjw69pcUaV", "fillStyle": "solid", @@ -19450,12 +19348,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1101.6580186716383, - "y": 2595.3535333769073, + "x": 1095.8140667426383, + "y": 2587.1390926639488, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 445.0858505168835, - "height": 591.3997159803484, + "width": 450.9298024458835, + "height": 583.1852752673899, "seed": 1145680325, "groupIds": [], "frameId": null, @@ -19463,13 +19361,13 @@ "type": 2 }, "boundElements": [], - "updated": 1727114848645, + "updated": 1727281212693, "link": null, "locked": false, "startBinding": { - "elementId": "2-xZdr1N7h94XCAl94juo", - "focus": -0.6139785493447132, - "gap": 5, + "elementId": "pVxFYg6-5Tv4jl8TsKj0I", + "focus": -0.811284423223443, + "gap": 5.544597277310231, "fixedPoint": null }, "endBinding": { @@ -19487,21 +19385,19 @@ 0 ], [ - 39.23300843497145, - -528.0174000634461 + 45.076960363971466, + -519.8029593504875 ], [ - 445.0858505168835, - -591.3997159803484 + 450.9298024458835, + -583.1852752673899 ] - ], - "elbowed": false + ] }, { "type": "arrow", - "version": 1596, - "versionNonce": 1465714917, - "index": "b1F", + "version": 1798, + "versionNonce": 735591108, "isDeleted": false, "id": "NFrs7hBfcfS6Sh6nfbpdz", "fillStyle": "solid", @@ -19514,8 +19410,8 @@ "y": 2024.2886438786966, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 434.60857125128405, - "height": 572.5306702528494, + "width": 436.3413557153074, + "height": 562.9471560877312, "seed": 1362911237, "groupIds": [], "frameId": null, @@ -19523,19 +19419,19 @@ "type": 2 }, "boundElements": [], - "updated": 1727114848645, + "updated": 1727281206637, "link": null, "locked": false, "startBinding": { "elementId": "yrMLMaP8hB6ezngk1Y_H9", - "focus": -0.8550942666127462, + "focus": -0.8550942666127483, "gap": 2.7504596041435434, "fixedPoint": null }, "endBinding": { - "elementId": "2-xZdr1N7h94XCAl94juo", - "focus": -0.25116468114773316, - "gap": 3.534219245361328, + "elementId": "pVxFYg6-5Tv4jl8TsKj0I", + "focus": -0.3132460772624905, + "gap": 5.4478899748312415, "fixedPoint": null }, "lastCommittedPoint": null, @@ -19551,17 +19447,15 @@ 60.77523095177935 ], [ - -434.60857125128405, - 572.5306702528494 + -436.3413557153074, + 562.9471560877312 ] - ], - "elbowed": false + ] }, { "type": "text", - "version": 940, - "versionNonce": 546839115, - "index": "b1G", + "version": 941, + "versionNonce": 1126046921, "isDeleted": false, "id": "Dn-LVSEKgJ-knjwmjqVvi", "fillStyle": "solid", @@ -19574,7 +19468,7 @@ "y": 2110.5413661943376, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 194.69675842101927, + "width": 152.3984375, "height": 43.2, "seed": 956358117, "groupIds": [], @@ -19586,7 +19480,7 @@ "type": "arrow" } ], - "updated": 1726919381684, + "updated": 1727290012234, "link": null, "locked": false, "fontSize": 16, @@ -19596,14 +19490,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "Send/Receive messages\nto/from client", - "autoResize": false, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 37 }, { "type": "rectangle", - "version": 660, - "versionNonce": 419597349, - "index": "b1H", + "version": 711, + "versionNonce": 957071868, "isDeleted": false, "id": "ylLtdS1Ye_gyVKSaQJs2x", "fillStyle": "solid", @@ -19612,8 +19505,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1361.512906576249, - "y": 2836.3535333769073, + "x": 1631.220376651726, + "y": 2828.1390926639483, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 104, @@ -19644,15 +19537,14 @@ "type": "arrow" } ], - "updated": 1727114723600, + "updated": 1727281171110, "link": null, "locked": false }, { "type": "text", - "version": 584, - "versionNonce": 59228037, - "index": "b1I", + "version": 635, + "versionNonce": 985277052, "isDeleted": false, "id": "AwYqSeI9CW9aJE0REZarf", "fillStyle": "solid", @@ -19661,8 +19553,8 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1395.0212397824257, - "y": 2867.7535333769074, + "x": 1664.7287098579027, + "y": 2859.5390926639484, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", "width": 36.983333587646484, @@ -19672,7 +19564,7 @@ "frameId": null, "roundness": null, "boundElements": [], - "updated": 1727114723600, + "updated": 1727281171110, "link": null, "locked": false, "fontSize": 16, @@ -19682,14 +19574,13 @@ "verticalAlign": "middle", "containerId": "ylLtdS1Ye_gyVKSaQJs2x", "originalText": "Kill\nSend", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 37 }, { "type": "arrow", - "version": 2135, - "versionNonce": 1671506315, - "index": "b1J", + "version": 2327, + "versionNonce": 684225276, "isDeleted": false, "id": "kuMt4oieo-H6xuQKoGSWP", "fillStyle": "solid", @@ -19698,12 +19589,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1229.512906576249, - "y": 2702.3535333769073, + "x": 1259.592168323165, + "y": 2868.324559326621, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 121, - "height": 158, + "width": 360.62820832856096, + "height": 32.54624939685527, "seed": 2107898277, "groupIds": [], "frameId": null, @@ -19711,7 +19602,7 @@ "type": 2 }, "boundElements": [], - "updated": 1727114724747, + "updated": 1727281171110, "link": null, "locked": false, "startBinding": { @@ -19735,17 +19626,15 @@ 0 ], [ - 121, - 158 + 360.62820832856096, + 32.54624939685527 ] - ], - "elbowed": false + ] }, { "type": "arrow", - "version": 2115, - "versionNonce": 256279243, - "index": "b1K", + "version": 2307, + "versionNonce": 1371059068, "isDeleted": false, "id": "HQ69vqNQrY84lbpyc-1HF", "fillStyle": "solid", @@ -19754,12 +19643,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1388.512906576249, - "y": 2831.3535333769073, + "x": 1626.220376651726, + "y": 2865.1919801902395, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 120, - "height": 151, + "width": 363.18376388411616, + "height": 21.527906649725992, "seed": 76404517, "groupIds": [], "frameId": null, @@ -19767,7 +19656,7 @@ "type": 2 }, "boundElements": [], - "updated": 1727114724747, + "updated": 1727281171110, "link": null, "locked": false, "startBinding": { @@ -19791,17 +19680,15 @@ 0 ], [ - -120, - -151 + -363.18376388411616, + -21.527906649725992 ] - ], - "elbowed": false + ] }, { "type": "arrow", - "version": 2108, - "versionNonce": 769035275, - "index": "b1L", + "version": 2226, + "versionNonce": 453073532, "isDeleted": false, "id": "o1dloWTz2OdDChjfXO3W8", "fillStyle": "solid", @@ -19810,12 +19697,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1445.512906576249, - "y": 2830.3535333769073, + "x": 1638.662737772701, + "y": 2822.1390926639483, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 137, - "height": 138, + "width": 1.93999474353609, + "height": 117.59907718364548, "seed": 1651958763, "groupIds": [], "frameId": null, @@ -19823,7 +19710,7 @@ "type": 2 }, "boundElements": [], - "updated": 1727114724747, + "updated": 1727281173497, "link": null, "locked": false, "startBinding": { @@ -19847,17 +19734,15 @@ 0 ], [ - 137, - -138 + 1.93999474353609, + -117.59907718364548 ] - ], - "elbowed": false + ] }, { "type": "arrow", - "version": 2139, - "versionNonce": 677725515, - "index": "b1M", + "version": 2257, + "versionNonce": 1802021628, "isDeleted": false, "id": "iPGwpGNDp9qVzkdoEhrce", "fillStyle": "solid", @@ -19866,12 +19751,12 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 1629.512906576249, - "y": 2703.3535333769073, + "x": 1680.3423721766594, + "y": 2701.9844599247476, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 155, - "height": 156, + "width": 1.363581222544326, + "height": 117.15463273920068, "seed": 1647368325, "groupIds": [], "frameId": null, @@ -19879,7 +19764,7 @@ "type": 2 }, "boundElements": [], - "updated": 1727114724747, + "updated": 1727281173497, "link": null, "locked": false, "startBinding": { @@ -19903,17 +19788,15 @@ 0 ], [ - -155, - 156 + -1.363581222544326, + 117.15463273920068 ] - ], - "elbowed": false + ] }, { "type": "text", - "version": 72, - "versionNonce": 102530213, - "index": "b1S", + "version": 73, + "versionNonce": 452309063, "isDeleted": false, "id": "wYibwBpVWhy536011GXFm", "fillStyle": "solid", @@ -19926,14 +19809,14 @@ "y": 1878.0368903977403, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 82.21666666666667, + "width": 70.2109375, "height": 21.6, "seed": 195156773, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919402510, + "updated": 1727290012234, "link": null, "locked": false, "fontSize": 16, @@ -19943,14 +19826,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "User15278", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { "type": "freedraw", "version": 330, "versionNonce": 137244139, - "index": "b1T", "isDeleted": false, "id": "xr7MX00xNHXe0CkTbirrn", "fillStyle": "solid", @@ -20847,7 +20729,6 @@ "type": "freedraw", "version": 130, "versionNonce": 430389387, - "index": "b1U", "isDeleted": false, "id": "FUyJxHFcU72z5lvGW0Fr8", "fillStyle": "solid", @@ -20944,7 +20825,6 @@ "type": "freedraw", "version": 182, "versionNonce": 954593067, - "index": "b1V", "isDeleted": false, "id": "DMCE9Mbx4X6Tft1CScGCp", "fillStyle": "solid", @@ -21245,7 +21125,6 @@ "type": "freedraw", "version": 332, "versionNonce": 906810699, - "index": "b1W", "isDeleted": false, "id": "DJPhr21pzK_fIqc9WFQoG", "fillStyle": "solid", @@ -22276,9 +22155,8 @@ }, { "type": "text", - "version": 169, - "versionNonce": 704269323, - "index": "b1X", + "version": 170, + "versionNonce": 2092014505, "isDeleted": false, "id": "yrMLMaP8hB6ezngk1Y_H9", "fillStyle": "solid", @@ -22291,7 +22169,7 @@ "y": 1998.629093365462, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 49.282828282828305, + "width": 42.38282775878906, "height": 22.90909090909092, "seed": 1505590757, "groupIds": [], @@ -22307,7 +22185,7 @@ "type": "arrow" } ], - "updated": 1727114848645, + "updated": 1727290012235, "link": null, "locked": false, "fontSize": 16.969696969696976, @@ -22317,14 +22195,13 @@ "verticalAlign": "top", "containerId": null, "originalText": "Phone", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 17 }, { "type": "freedraw", "version": 3, "versionNonce": 475272267, - "index": "b1Y", "isDeleted": false, "id": "XimpoObq-XuIb3zN3NNFU", "fillStyle": "solid", @@ -22365,7 +22242,6 @@ "type": "freedraw", "version": 3, "versionNonce": 687068203, - "index": "b1Z", "isDeleted": false, "id": "zuRV8Tn6LPPv7A81dIzsq", "fillStyle": "solid", @@ -22406,7 +22282,6 @@ "type": "freedraw", "version": 3, "versionNonce": 471408651, - "index": "b1a", "isDeleted": false, "id": "mekZs7jnqYFc73V7hkrLW", "fillStyle": "solid", @@ -22447,7 +22322,6 @@ "type": "freedraw", "version": 5, "versionNonce": 152321957, - "index": "b1b", "isDeleted": false, "id": "-OetnfO6zwcYatX9RedPR", "fillStyle": "solid", @@ -22492,7 +22366,6 @@ "type": "freedraw", "version": 3, "versionNonce": 966510021, - "index": "b1c", "isDeleted": false, "id": "QC0MhalhM79LOlnEDdfkD", "fillStyle": "solid", @@ -22533,7 +22406,6 @@ "type": "freedraw", "version": 6, "versionNonce": 1003391653, - "index": "b1d", "isDeleted": false, "id": "jcb9CJs3vxx1fCi0elsOc", "fillStyle": "solid", @@ -22582,7 +22454,6 @@ "type": "freedraw", "version": 3, "versionNonce": 97906885, - "index": "b1e", "isDeleted": false, "id": "qkYTBxkCkU0SMHVRnt2AI", "fillStyle": "solid", @@ -22623,7 +22494,6 @@ "type": "freedraw", "version": 3, "versionNonce": 614566629, - "index": "b1f", "isDeleted": false, "id": "4S-mv7-OXX3OuNH_9Maez", "fillStyle": "solid", @@ -22664,7 +22534,6 @@ "type": "freedraw", "version": 5, "versionNonce": 322106475, - "index": "b1g", "isDeleted": false, "id": "1vZ2rq_zh7EfpffanBaa5", "fillStyle": "solid", @@ -22709,7 +22578,6 @@ "type": "freedraw", "version": 3, "versionNonce": 508635211, - "index": "b1h", "isDeleted": false, "id": "ImFxwNd-hSAqvwnty7ADb", "fillStyle": "solid", @@ -22748,9 +22616,8 @@ }, { "type": "text", - "version": 54, - "versionNonce": 1133127467, - "index": "b1i", + "version": 55, + "versionNonce": 361107303, "isDeleted": false, "id": "8_s9rHl1BqFvYQf9Y5qMM", "fillStyle": "solid", @@ -22763,7 +22630,7 @@ "y": 1994.529931928243, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 51.233333333333334, + "width": 45.3203125, "height": 21.6, "seed": 84578629, "groupIds": [], @@ -22779,7 +22646,7 @@ "type": "arrow" } ], - "updated": 1726919502507, + "updated": 1727290012235, "link": null, "locked": false, "fontSize": 16, @@ -22789,131 +22656,257 @@ "verticalAlign": "top", "containerId": null, "originalText": "Laptop", - "autoResize": true, - "lineHeight": 1.35 + "lineHeight": 1.35, + "baseline": 16 }, { - "type": "rectangle", - "version": 1099, - "versionNonce": 459182565, - "index": "b1j", + "type": "arrow", + "version": 375, + "versionNonce": 1595689924, "isDeleted": false, - "id": "IqZ18gef20BdSUYa6vLD0", + "id": "QM1nGdcAGkuba_LeIPPGs", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2006.236207338786, - "y": 2561.2847969488917, + "x": 2194.6105612898186, + "y": 2596.642831422153, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 778.8793335223208, - "height": 431.6222461467981, - "seed": 1397298187, + "width": 375.05801058254974, + "height": 587.4589950422242, + "seed": 1727694533, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1727281341030, + "link": null, + "locked": false, + "startBinding": null, + "endBinding": { + "elementId": "8_s9rHl1BqFvYQf9Y5qMM", + "focus": -0.2889051410798594, + "gap": 14.039058477916683, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 34.97235662641697, + -506.36491496902545 + ], + [ + -340.0856539561328, + -587.4589950422242 + ] + ] + }, + { + "type": "arrow", + "version": 413, + "versionNonce": 623449852, + "isDeleted": false, + "id": "qUGaP5OV93rxoqqy7V6Au", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1841.71855911045, + "y": 2028.975465452202, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 367.0081923151795, + "height": 566.5031524951112, + "seed": 793680459, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1727281356492, + "link": null, + "locked": false, + "startBinding": { + "elementId": "8_s9rHl1BqFvYQf9Y5qMM", + "focus": 1.0759054008449098, + "gap": 12.84553352395892, + "fixedPoint": null + }, + "endBinding": { + "elementId": "-E9W5yBlGNCtWEpcR2_2M", + "focus": 0.44065746296374986, + "gap": 12.914348922336785, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 367.0081923151795, + 81.11580916700109 + ], + [ + 302.9314690968015, + 566.5031524951112 + ] + ] + }, + { + "type": "text", + "version": 1113, + "versionNonce": 2033335945, + "isDeleted": false, + "id": "Dpc-UdQdcDM_QYYampaa_", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2031.1533305372761, + "y": 2120.3041725116436, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 152.3984375, + "height": 43.2, + "seed": 2041976677, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727290012235, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Send/Receive messages\nto/from client", + "textAlign": "right", + "verticalAlign": "top", + "containerId": null, + "originalText": "Send/Receive messages\nto/from client", + "lineHeight": 1.35, + "baseline": 37 + }, + { + "type": "rectangle", + "version": 293, + "versionNonce": 1084002411, + "isDeleted": false, + "id": "BNra93QOEfDm6IfLrZPxO", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "dashed", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2879.6559358388795, + "y": 2152.9715030399793, + "strokeColor": "#1e1e1e", + "backgroundColor": "#ffec99", + "width": 295.1449965676214, + "height": 118, + "seed": 1137650821, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [ { "type": "text", - "id": "MkMYxixZDt1Vj-hKG-YBB" + "id": "KcE2WjmALQ71_MrVW5VSl" } ], - "updated": 1727114723601, + "updated": 1727114926736, "link": null, "locked": false }, { "type": "text", - "version": 895, - "versionNonce": 1876757829, - "index": "b1k", + "version": 480, + "versionNonce": 1355985355, "isDeleted": false, - "id": "MkMYxixZDt1Vj-hKG-YBB", + "id": "KcE2WjmALQ71_MrVW5VSl", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2325.6175397493603, - "y": 2566.2847969488917, + "x": 2884.6559358388795, + "y": 2168.7715030399795, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 140.11666870117188, - "height": 21.6, - "seed": 683694763, + "backgroundColor": "#ffec99", + "width": 285.98333333333335, + "height": 86.4, + "seed": 250829605, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1727114723601, + "updated": 1726919792306, "link": null, "locked": false, "fontSize": 16, "fontFamily": 6, - "text": "Client-specific Task", - "textAlign": "center", - "verticalAlign": "top", - "containerId": "IqZ18gef20BdSUYa6vLD0", - "originalText": "Client-specific Task", - "autoResize": true, - "lineHeight": 1.35 + "text": "The User-specific task can be iterated \nover to yield a shared or exclusive \nreference to existing client-specific task\nwith each new iteration.", + "textAlign": "left", + "verticalAlign": "middle", + "containerId": "BNra93QOEfDm6IfLrZPxO", + "originalText": "The User-specific task can be iterated over to yield a shared or exclusive reference to existing client-specific task with each new iteration.", + "lineHeight": 1.35, + "baseline": 81 }, { "type": "rectangle", - "version": 920, - "versionNonce": 21130245, - "index": "b1l", + "version": 687, + "versionNonce": 2011576645, "isDeleted": false, - "id": "NEKEWaqq5lWTHOkiGx6DU", + "id": "Jy9q4ysrAAbpXdaiX521F", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2014.2538027655835, - "y": 2600.5903163710022, + "x": 1268.5980407258883, + "y": 2398.328369542565, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 196.66666666666652, - "height": 95.55555555555566, - "seed": 1758842187, + "width": 171.02056522296834, + "height": 73.49554175745014, + "seed": 778731787, "groupIds": [], "frameId": null, - "roundness": { - "type": 3 - }, + "roundness": null, "boundElements": [ { "type": "text", - "id": "0-FTztADtIwv8vL54K2VN" - }, - { - "id": "C0S_ZOuxsKKIkAwQJ4sZv", - "type": "arrow" - }, - { - "id": "JofU-D5K9-E52PJsrbTe3", - "type": "arrow" - }, - { - "id": "cyaBPg8_R5_8xioLhaPje", - "type": "arrow" - }, - { - "id": "AJNR16UTCUbXwxmFRId6d", - "type": "arrow" + "id": "VV756MSKFVMvOq63JiNpf" }, { - "id": "QM1nGdcAGkuba_LeIPPGs", + "id": "cq055tV0PJ_kX7ISOHUKW", "type": "arrow" }, { - "id": "qUGaP5OV93rxoqqy7V6Au", + "id": "yMeEUEU3Lphx3vyL3whwt", "type": "arrow" }, { @@ -22927,173 +22920,146 @@ }, { "type": "text", - "version": 885, - "versionNonce": 546586629, - "index": "b1m", + "version": 84, + "versionNonce": 76190379, "isDeleted": false, - "id": "0-FTztADtIwv8vL54K2VN", + "id": "VV756MSKFVMvOq63JiNpf", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2061.878804037149, - "y": 2637.56809414878, + "x": 1333.849990004039, + "y": 2424.2761404212897, "strokeColor": "#1e1e1e", "backgroundColor": "transparent", - "width": 101.41666412353516, + "width": 40.516666666666666, "height": 21.6, - "seed": 1994648555, + "seed": 1079341541, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1727114723601, + "updated": 1727114848645, "link": null, "locked": false, "fontSize": 16, "fontFamily": 6, - "text": "Gateway Task", + "text": "Inbox", "textAlign": "center", "verticalAlign": "middle", - "containerId": "NEKEWaqq5lWTHOkiGx6DU", - "originalText": "Gateway Task", - "autoResize": true, - "lineHeight": 1.35 + "containerId": "Jy9q4ysrAAbpXdaiX521F", + "originalText": "Inbox", + "lineHeight": 1.35, + "baseline": 16 }, { "type": "rectangle", - "version": 1124, - "versionNonce": 1744988069, - "index": "b1n", + "version": 213, + "versionNonce": 652084555, "isDeleted": false, - "id": "NK9bIOXHnIk_mQH6hQ-us", + "id": "KJ0ryEoixn2-Drv3HAq5o", "fillStyle": "solid", "strokeWidth": 2, - "strokeStyle": "solid", + "strokeStyle": "dashed", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2544.809358321139, - "y": 2600.5903163710022, + "x": 1453.347392849494, + "y": 2373.3518486475355, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 196.66666666666652, - "height": 95.55555555555566, - "seed": 1727346315, + "backgroundColor": "#ffec99", + "width": 277.05993563837916, + "height": 75, + "seed": 599905189, "groupIds": [], "frameId": null, - "roundness": { - "type": 3 - }, + "roundness": null, "boundElements": [ { "type": "text", - "id": "bBx085lHdLn5JljxcmBw5" - }, - { - "id": "C0S_ZOuxsKKIkAwQJ4sZv", - "type": "arrow" - }, - { - "id": "JofU-D5K9-E52PJsrbTe3", - "type": "arrow" - }, - { - "id": "Jq97yESttTu4P8U4otwNL", - "type": "arrow" - }, - { - "id": "A8kPyLU06mH9oyBkobdZa", - "type": "arrow" + "id": "hyPdPmvErLq9WZ6GdmO_V" } ], - "updated": 1727114723601, + "updated": 1727115197785, "link": null, "locked": false }, { "type": "text", - "version": 1113, - "versionNonce": 754985733, - "index": "b1o", + "version": 240, + "versionNonce": 339122309, "isDeleted": false, - "id": "bBx085lHdLn5JljxcmBw5", + "id": "hyPdPmvErLq9WZ6GdmO_V", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2587.359358321138, - "y": 2637.56809414878, + "x": 1465.8356965451485, + "y": 2378.4518486475354, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 111.56666666666666, - "height": 21.6, - "seed": 1740909867, + "backgroundColor": "#ffec99", + "width": 252.0833282470703, + "height": 64.80000000000001, + "seed": 684949195, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1727114723601, + "updated": 1727115209247, "link": null, "locked": false, "fontSize": 16, "fontFamily": 6, - "text": "Heartbeat Task", + "text": "The Inbox distributes messages to \nall clients a user is connected to.\nIt is a tokio::mpsc channel.", "textAlign": "center", "verticalAlign": "middle", - "containerId": "NK9bIOXHnIk_mQH6hQ-us", - "originalText": "Heartbeat Task", - "autoResize": true, - "lineHeight": 1.35 + "containerId": "KJ0ryEoixn2-Drv3HAq5o", + "originalText": "The Inbox distributes messages to all clients a user is connected to.\nIt is a tokio::mpsc channel.", + "lineHeight": 1.35, + "baseline": 59 }, { "type": "arrow", - "version": 3055, - "versionNonce": 856075915, - "index": "b1p", + "version": 959, + "versionNonce": 1571775429, "isDeleted": false, - "id": "C0S_ZOuxsKKIkAwQJ4sZv", + "id": "cq055tV0PJ_kX7ISOHUKW", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2218.6982472100276, - "y": 2629.479205259891, + "x": 2047.9288620645198, + "y": 1360.4185088538184, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 317.77777777777806, - "height": 0.7658583367585834, - "seed": 1824214987, + "backgroundColor": "#ffec99", + "width": 695.4056643952301, + "height": 1033.1265971039184, + "seed": 1977842827, "groupIds": [], "frameId": null, "roundness": { "type": 2 }, - "boundElements": [ - { - "type": "text", - "id": "-AvJk9PYXKBlq1yd1yAB2" - } - ], - "updated": 1727114724747, + "boundElements": [], + "updated": 1727115121371, "link": null, "locked": false, "startBinding": { - "elementId": "NEKEWaqq5lWTHOkiGx6DU", - "focus": -0.39872362474490314, - "gap": 7.777777777777715, + "elementId": "oHiI-diNFXNo4nn79ecEY", + "focus": -0.6925371638619507, + "gap": 7.129619964929702, "fixedPoint": null }, "endBinding": { - "elementId": "NK9bIOXHnIk_mQH6hQ-us", - "focus": 0.3720930232558081, - "gap": 8.333333333333712, + "elementId": "Jy9q4ysrAAbpXdaiX521F", + "focus": -0.09195704538171595, + "gap": 4.7832635848280916, "fixedPoint": null }, "lastCommittedPoint": null, @@ -23105,93 +23071,131 @@ 0 ], [ - 317.77777777777806, - 0.7658583367585834 + -133.09199318568994, + 86.50979557069854 + ], + [ + -207.9562393526403, + 232.91098807495746 + ], + [ + -281.1568356047701, + 549.004471890971 + ], + [ + -489.1130749574106, + 821.8430579216356 + ], + [ + -673.7782155025554, + 901.6982538330494 + ], + [ + -695.4056643952301, + 1033.1265971039184 ] - ], - "elbowed": false + ] }, { - "type": "text", - "version": 46, - "versionNonce": 1323229669, - "index": "b1q", + "type": "arrow", + "version": 543, + "versionNonce": 1175774716, "isDeleted": false, - "id": "-AvJk9PYXKBlq1yd1yAB2", + "id": "yMeEUEU3Lphx3vyL3whwt", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 1815.8026825699912, - "y": 2146.538194668723, + "x": 1355.8824862193296, + "y": 2478.5447016544967, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 199.60000610351562, - "height": 43.2, - "seed": 401940075, + "backgroundColor": "#ffec99", + "width": 201.29032392396516, + "height": 361.7657593025324, + "seed": 1538411947, "groupIds": [], "frameId": null, - "roundness": null, + "roundness": { + "type": 2 + }, "boundElements": [], - "updated": 1726919475979, + "updated": 1727281283497, "link": null, "locked": false, - "fontSize": 16, - "fontFamily": 6, - "text": "forwards heartbeat related \nmessages", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "C0S_ZOuxsKKIkAwQJ4sZv", - "originalText": "forwards heartbeat related messages", - "autoResize": true, - "lineHeight": 1.35 + "startBinding": { + "elementId": "Jy9q4ysrAAbpXdaiX521F", + "focus": -0.3028171273561128, + "gap": 6.720790354481778, + "fixedPoint": null + }, + "endBinding": { + "elementId": "2-xZdr1N7h94XCAl94juo", + "focus": -0.275103642538984, + "gap": 1, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -98.67966072476656, + 132.31320150575993 + ], + [ + -115.5340308277332, + 247.8680141630416 + ], + [ + -201.29032392396516, + 361.7657593025324 + ] + ] }, { "type": "arrow", - "version": 3062, - "versionNonce": 214878155, - "index": "b1r", + "version": 947, + "versionNonce": 770431100, "isDeleted": false, - "id": "JofU-D5K9-E52PJsrbTe3", + "id": "sneG026N1n_G0mJImR8vA", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2537.587136098917, - "y": 2658.7807583629105, + "x": 1368.868357591277, + "y": 2475.547962107124, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 322.2222222222224, - "height": 0.6984468969803856, - "seed": 1681660171, + "backgroundColor": "#ffec99", + "width": 636.300937519411, + "height": 378.8962659949648, + "seed": 2123839301, "groupIds": [], "frameId": null, "roundness": { "type": 2 }, - "boundElements": [ - { - "type": "text", - "id": "WCymYLSdk-lSf7sWN6sES" - } - ], - "updated": 1727114724747, + "boundElements": [], + "updated": 1727281395383, "link": null, "locked": false, "startBinding": { - "elementId": "NK9bIOXHnIk_mQH6hQ-us", - "focus": -0.21220392709341088, - "gap": 7.222222222221944, + "elementId": "Jy9q4ysrAAbpXdaiX521F", + "focus": 0.37123847346610966, + "gap": 3.7240508071090517, "fixedPoint": null }, "endBinding": { - "elementId": "NEKEWaqq5lWTHOkiGx6DU", - "focus": 0.2361673942352932, - "gap": 4.444444444445139, + "elementId": "7a7GmGAWuf8jSu-sYYfTp", + "focus": -0.36645011720675946, + "gap": 2.5755097833307445, "fixedPoint": null }, "lastCommittedPoint": null, @@ -23203,1461 +23207,534 @@ 0 ], [ - -322.2222222222224, - 0.6984468969803856 + 70.92803141186869, + 40.91998309100336 + ], + [ + 470.17390936624633, + 73.77930381298438 + ], + [ + 574.9835728432095, + 302.48689733443325 + ], + [ + 636.300937519411, + 378.8962659949648 ] - ], - "elbowed": false + ] }, { - "type": "text", - "version": 135, - "versionNonce": 1859345733, - "index": "b1s", + "type": "diamond", + "version": 267, + "versionNonce": 306122347, "isDeleted": false, - "id": "WCymYLSdk-lSf7sWN6sES", + "id": "Ih6O2CVqHKg5-szfAXDhu", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 1825.024907843971, - "y": 2186.6060420518534, + "x": 3125.2697698305014, + "y": 420.4025642562489, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 178.93333333333334, - "height": 21.6, - "seed": 252362667, + "backgroundColor": "#ffec99", + "width": 247.39095075069528, + "height": 164.6026404601082, + "seed": 1092419845, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1726919475979, + "updated": 1727115557614, "link": null, - "locked": false, - "fontSize": 16, - "fontFamily": 6, - "text": "response to send to user", - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "JofU-D5K9-E52PJsrbTe3", - "originalText": "response to send to user", - "autoResize": true, - "lineHeight": 1.35 + "locked": false }, { "type": "rectangle", - "version": 893, - "versionNonce": 739553957, - "index": "b1t", + "version": 121, + "versionNonce": 1108382245, "isDeleted": false, - "id": "a6ufwTaJNWAO4qjJj2EfG", + "id": "yN5E3yZ8SM7NGSXeA-0oP", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2313.809358321139, - "y": 2836.5903163710022, + "x": 3127.217730072633, + "y": 502.2168944257702, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 104, - "height": 106, - "seed": 1116443211, + "backgroundColor": "#ffec99", + "width": 243.49503026643197, + "height": 161.68070009691098, + "seed": 1654909317, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [ { "type": "text", - "id": "Zi55OvWi4cRaY5F7HZoDI" - }, - { - "id": "cyaBPg8_R5_8xioLhaPje", - "type": "arrow" - }, - { - "id": "AJNR16UTCUbXwxmFRId6d", - "type": "arrow" - }, - { - "id": "Jq97yESttTu4P8U4otwNL", - "type": "arrow" - }, - { - "id": "A8kPyLU06mH9oyBkobdZa", - "type": "arrow" + "id": "Yn7n1nNGxCBncgRMYIJUT" } ], - "updated": 1727114723601, + "updated": 1727115585644, "link": null, "locked": false }, { "type": "text", - "version": 818, - "versionNonce": 1855002117, - "index": "b1u", + "version": 9, + "versionNonce": 1177236395, "isDeleted": false, - "id": "Zi55OvWi4cRaY5F7HZoDI", + "id": "Yn7n1nNGxCBncgRMYIJUT", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2347.3176915273157, - "y": 2867.9903163710023, + "x": 3223.4069127626117, + "y": 572.2572444742257, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 36.983333587646484, - "height": 43.2, - "seed": 2140737771, + "backgroundColor": "#ffec99", + "width": 51.11666488647461, + "height": 21.6, + "seed": 1536952331, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1727114723601, + "updated": 1727115594963, "link": null, "locked": false, "fontSize": 16, "fontFamily": 6, - "text": "Kill\nSend", + "text": "<-- Viv", "textAlign": "center", "verticalAlign": "middle", - "containerId": "a6ufwTaJNWAO4qjJj2EfG", - "originalText": "Kill\nSend", - "autoResize": true, - "lineHeight": 1.35 + "containerId": "yN5E3yZ8SM7NGSXeA-0oP", + "originalText": "<-- Viv", + "lineHeight": 1.35, + "baseline": 16 }, { - "type": "arrow", - "version": 2978, - "versionNonce": 1070698763, - "index": "b1v", + "type": "freedraw", + "version": 649, + "versionNonce": 873076363, "isDeleted": false, - "id": "cyaBPg8_R5_8xioLhaPje", + "id": "WBPMPJ76_7bvOYZIcaHuZ", "fillStyle": "solid", "strokeWidth": 2, "strokeStyle": "solid", "roughness": 0, "opacity": 100, "angle": 0, - "x": 2181.809358321139, - "y": 2702.5903163710022, + "x": 3187.552457820839, + "y": 568.3286878640658, "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 121, - "height": 158, - "seed": 1203616651, - "groupIds": [], + "backgroundColor": "#a5d8ff", + "width": 28, + "height": 22, + "seed": 1716601995, + "groupIds": [ + "-DMu6XEZMLghW5wuD-QkJ" + ], "frameId": null, - "roundness": { - "type": 2 - }, + "roundness": null, "boundElements": [], - "updated": 1727114724747, + "updated": 1727115577484, "link": null, "locked": false, - "startBinding": { - "elementId": "NEKEWaqq5lWTHOkiGx6DU", - "focus": -0.20528510372146347, - "gap": 6.444444444444343, - "fixedPoint": null - }, - "endBinding": { - "elementId": "a6ufwTaJNWAO4qjJj2EfG", - "focus": -0.44056326474810314, - "gap": 11, - "fixedPoint": null - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ - 121, - 158 - ] - ], - "elbowed": false - }, - { - "type": "arrow", - "version": 2958, - "versionNonce": 1629686347, - "index": "b1w", - "isDeleted": false, - "id": "AJNR16UTCUbXwxmFRId6d", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "angle": 0, - "x": 2340.809358321139, - "y": 2831.5903163710022, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 120, - "height": 151, - "seed": 120853035, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1727114724747, - "link": null, - "locked": false, - "startBinding": { - "elementId": "a6ufwTaJNWAO4qjJj2EfG", - "focus": 0.22410638896707005, - "gap": 5, - "fixedPoint": null - }, - "endBinding": { - "elementId": "NEKEWaqq5lWTHOkiGx6DU", - "focus": -0.6061165546468001, - "gap": 9.888888888889028, - "fixedPoint": null - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ - [ - 0, + -1, 0 ], [ - -120, - -151 - ] - ], - "elbowed": false - }, - { - "type": "arrow", - "version": 2951, - "versionNonce": 1356280715, - "index": "b1x", - "isDeleted": false, - "id": "Jq97yESttTu4P8U4otwNL", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "angle": 0, - "x": 2397.809358321139, - "y": 2830.5903163710022, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 137, - "height": 138, - "seed": 1055298763, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1727114724747, - "link": null, - "locked": false, - "startBinding": { - "elementId": "a6ufwTaJNWAO4qjJj2EfG", - "focus": -0.2540001385329362, - "gap": 6, - "fixedPoint": null - }, - "endBinding": { - "elementId": "NK9bIOXHnIk_mQH6hQ-us", - "focus": 0.44202386212991757, - "gap": 10.000000000000227, - "fixedPoint": null - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ + -2, + 1 + ], [ - 0, - 0 + -3, + 1 ], [ - 137, - -138 - ] - ], - "elbowed": false - }, - { - "type": "arrow", - "version": 2982, - "versionNonce": 2046954699, - "index": "b1y", - "isDeleted": false, - "id": "A8kPyLU06mH9oyBkobdZa", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "angle": 0, - "x": 2581.809358321139, - "y": 2703.5903163710022, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 155, - "height": 156, - "seed": 1852818283, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1727114724747, - "link": null, - "locked": false, - "startBinding": { - "elementId": "NK9bIOXHnIk_mQH6hQ-us", - "focus": 0.044340774754532214, - "gap": 7.444444444444343, - "fixedPoint": null - }, - "endBinding": { - "elementId": "a6ufwTaJNWAO4qjJj2EfG", - "focus": 0.29803393152446866, - "gap": 9, - "fixedPoint": null - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ + -4, + 2 + ], [ - 0, - 0 + -5, + 2 ], [ - -155, - 156 - ] - ], - "elbowed": false - }, - { - "type": "arrow", - "version": 374, - "versionNonce": 912616363, - "index": "b1z", - "isDeleted": false, - "id": "QM1nGdcAGkuba_LeIPPGs", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "angle": 0, - "x": 2194.6105612898186, - "y": 2596.642831422153, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 375.05801058254974, - "height": 587.4589950422242, - "seed": 1727694533, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1727114740873, - "link": null, - "locked": false, - "startBinding": { - "elementId": "NEKEWaqq5lWTHOkiGx6DU", - "focus": 0.7719037348701197, - "gap": 3.947484948849251, - "fixedPoint": null - }, - "endBinding": { - "elementId": "8_s9rHl1BqFvYQf9Y5qMM", - "focus": -0.2889051410798594, - "gap": 14.039058477916683, - "fixedPoint": null - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ + -5, + 3 + ], + [ + -6, + 3 + ], + [ + -6, + 4 + ], + [ + -7, + 4 + ], + [ + -7, + 5 + ], + [ + -8, + 5 + ], + [ + -8, + 6 + ], + [ + -9, + 7 + ], + [ + -9, + 8 + ], + [ + -10, + 9 + ], + [ + -10, + 10 + ], + [ + -10, + 11 + ], + [ + -9, + 13 + ], + [ + -9, + 14 + ], + [ + -9, + 16 + ], + [ + -8, + 16 + ], + [ + -8, + 17 + ], + [ + -7, + 17 + ], + [ + -6, + 18 + ], + [ + -5, + 19 + ], + [ + -4, + 19 + ], + [ + -3, + 20 + ], + [ + -2, + 20 + ], + [ + -1, + 21 + ], [ 0, + 21 + ], + [ + 1, + 21 + ], + [ + 2, + 21 + ], + [ + 3, + 21 + ], + [ + 4, + 21 + ], + [ + 5, + 21 + ], + [ + 6, + 21 + ], + [ + 8, + 21 + ], + [ + 9, + 21 + ], + [ + 11, + 21 + ], + [ + 12, + 21 + ], + [ + 14, + 21 + ], + [ + 15, + 20 + ], + [ + 16, + 20 + ], + [ + 16, + 19 + ], + [ + 16, + 18 + ], + [ + 17, + 18 + ], + [ + 17, + 17 + ], + [ + 17, + 16 + ], + [ + 17, + 15 + ], + [ + 18, + 14 + ], + [ + 18, + 13 + ], + [ + 18, + 12 + ], + [ + 18, + 11 + ], + [ + 17, + 9 + ], + [ + 17, + 8 + ], + [ + 17, + 7 + ], + [ + 16, + 6 + ], + [ + 16, + 5 + ], + [ + 16, + 4 + ], + [ + 15, + 4 + ], + [ + 15, + 3 + ], + [ + 14, + 3 + ], + [ + 13, + 2 + ], + [ + 12, + 2 + ], + [ + 12, + 1 + ], + [ + 11, + 1 + ], + [ + 10, + 1 + ], + [ + 10, 0 ], [ - 34.97235662641697, - -506.36491496902545 + 9, + 0 ], [ - -340.0856539561328, - -587.4589950422242 - ] - ], - "elbowed": false - }, - { - "type": "arrow", - "version": 384, - "versionNonce": 1742976971, - "index": "b20", - "isDeleted": false, - "id": "qUGaP5OV93rxoqqy7V6Au", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "angle": 0, - "x": 1841.71855911045, - "y": 2028.975465452202, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 367.0081923151795, - "height": 566.5031524951112, - "seed": 793680459, - "groupIds": [], - "frameId": null, - "roundness": { - "type": 2 - }, - "boundElements": [], - "updated": 1727114743910, - "link": null, - "locked": false, - "startBinding": { - "elementId": "8_s9rHl1BqFvYQf9Y5qMM", - "focus": 1.0759054008449098, - "gap": 12.84553352395892, - "fixedPoint": null - }, - "endBinding": { - "elementId": "NEKEWaqq5lWTHOkiGx6DU", - "focus": 0.45094548255454076, - "gap": 5.111698423689177, - "fixedPoint": null - }, - "lastCommittedPoint": null, - "startArrowhead": null, - "endArrowhead": "arrow", - "points": [ + 8, + 0 + ], [ - 0, + 7, 0 ], [ - 367.0081923151795, - 81.11580916700109 + 5, + 0 ], [ - 322.0984974270384, - 566.5031524951112 - ] - ], - "elbowed": false - }, - { - "type": "text", - "version": 1112, - "versionNonce": 1688383141, - "index": "b21", - "isDeleted": false, - "id": "Dpc-UdQdcDM_QYYampaa_", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "angle": 0, - "x": 1988.8550096162564, - "y": 2120.3041725116436, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "width": 194.69675842101927, - "height": 43.2, - "seed": 2041976677, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1726919518099, - "link": null, - "locked": false, - "fontSize": 16, - "fontFamily": 6, - "text": "Send/Receive messages\nto/from client", - "textAlign": "right", - "verticalAlign": "top", - "containerId": null, - "originalText": "Send/Receive messages\nto/from client", - "autoResize": false, - "lineHeight": 1.35 - }, - { - "type": "rectangle", - "version": 293, - "versionNonce": 1084002411, - "index": "b22", - "isDeleted": false, - "id": "BNra93QOEfDm6IfLrZPxO", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dashed", - "roughness": 0, - "opacity": 100, - "angle": 0, - "x": 2879.6559358388795, - "y": 2152.9715030399793, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffec99", - "width": 295.1449965676214, - "height": 118, - "seed": 1137650821, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [ - { - "type": "text", - "id": "KcE2WjmALQ71_MrVW5VSl" - } - ], - "updated": 1727114926736, - "link": null, - "locked": false - }, - { - "type": "text", - "version": 480, - "versionNonce": 1355985355, - "index": "b23", - "isDeleted": false, - "id": "KcE2WjmALQ71_MrVW5VSl", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "angle": 0, - "x": 2884.6559358388795, - "y": 2168.7715030399795, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffec99", - "width": 285.98333333333335, - "height": 86.4, - "seed": 250829605, - "groupIds": [], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1726919792306, - "link": null, - "locked": false, - "fontSize": 16, - "fontFamily": 6, - "text": "The User-specific task can be iterated \nover to yield a shared or exclusive \nreference to existing client-specific task\nwith each new iteration.", - "textAlign": "left", - "verticalAlign": "middle", - "containerId": "BNra93QOEfDm6IfLrZPxO", - "originalText": "The User-specific task can be iterated over to yield a shared or exclusive reference to existing client-specific task with each new iteration.", - "autoResize": true, - "lineHeight": 1.35 - }, - { - "id": "Jy9q4ysrAAbpXdaiX521F", - "type": "rectangle", - "x": 1268.5980407258883, - "y": 2398.328369542565, - "width": 171.02056522296834, - "height": 73.49554175745014, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b24", - "roundness": null, - "seed": 778731787, - "version": 687, - "versionNonce": 2011576645, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "VV756MSKFVMvOq63JiNpf" - }, - { - "id": "cq055tV0PJ_kX7ISOHUKW", - "type": "arrow" - }, - { - "id": "yMeEUEU3Lphx3vyL3whwt", - "type": "arrow" - }, - { - "id": "sneG026N1n_G0mJImR8vA", - "type": "arrow" - } - ], - "updated": 1727115150280, - "link": null, - "locked": false - }, - { - "id": "VV756MSKFVMvOq63JiNpf", - "type": "text", - "x": 1333.849990004039, - "y": 2424.2761404212897, - "width": 40.516666666666666, - "height": 21.6, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "transparent", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b25", - "roundness": null, - "seed": 1079341541, - "version": 84, - "versionNonce": 76190379, - "isDeleted": false, - "boundElements": null, - "updated": 1727114848645, - "link": null, - "locked": false, - "text": "Inbox", - "fontSize": 16, - "fontFamily": 6, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "Jy9q4ysrAAbpXdaiX521F", - "originalText": "Inbox", - "autoResize": true, - "lineHeight": 1.35 - }, - { - "id": "KJ0ryEoixn2-Drv3HAq5o", - "type": "rectangle", - "x": 1453.347392849494, - "y": 2373.3518486475355, - "width": 277.05993563837916, - "height": 75, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffec99", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "dashed", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b26", - "roundness": null, - "seed": 599905189, - "version": 213, - "versionNonce": 652084555, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "hyPdPmvErLq9WZ6GdmO_V" - } - ], - "updated": 1727115197785, - "link": null, - "locked": false - }, - { - "id": "hyPdPmvErLq9WZ6GdmO_V", - "type": "text", - "x": 1465.8356965451485, - "y": 2378.4518486475354, - "width": 252.0833282470703, - "height": 64.80000000000001, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffec99", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b27", - "roundness": null, - "seed": 684949195, - "version": 240, - "versionNonce": 339122309, - "isDeleted": false, - "boundElements": null, - "updated": 1727115209247, - "link": null, - "locked": false, - "text": "The Inbox distributes messages to \nall clients a user is connected to.\nIt is a tokio::mpsc channel.", - "fontSize": 16, - "fontFamily": 6, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "KJ0ryEoixn2-Drv3HAq5o", - "originalText": "The Inbox distributes messages to all clients a user is connected to.\nIt is a tokio::mpsc channel.", - "autoResize": true, - "lineHeight": 1.35 - }, - { - "id": "cq055tV0PJ_kX7ISOHUKW", - "type": "arrow", - "x": 2047.9288620645198, - "y": 1360.4185088538184, - "width": 695.4056643952301, - "height": 1033.1265971039184, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffec99", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b28", - "roundness": { - "type": 2 - }, - "seed": 1977842827, - "version": 959, - "versionNonce": 1571775429, - "isDeleted": false, - "boundElements": null, - "updated": 1727115121371, - "link": null, - "locked": false, - "points": [ + 4, + -1 + ], + [ + 3, + -1 + ], + [ + 2, + -1 + ], + [ + 1, + -1 + ], [ 0, 0 ], [ - -133.09199318568994, - 86.50979557069854 + -1, + 1 ], [ - -207.9562393526403, - 232.91098807495746 + -1, + 2 ], [ - -281.1568356047701, - 549.004471890971 + -2, + 2 ], [ - -489.1130749574106, - 821.8430579216356 + -2, + 3 ], [ - -673.7782155025554, - 901.6982538330494 + -3, + 3 ], [ - -695.4056643952301, - 1033.1265971039184 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "oHiI-diNFXNo4nn79ecEY", - "focus": -0.6925371638619507, - "gap": 7.129619964929702, - "fixedPoint": null - }, - "endBinding": { - "elementId": "Jy9q4ysrAAbpXdaiX521F", - "focus": -0.09195704538171595, - "gap": 4.7832635848280916, - "fixedPoint": null - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": false - }, - { - "id": "yMeEUEU3Lphx3vyL3whwt", - "type": "arrow", - "x": 1355.8824862193296, - "y": 2478.5447016544967, - "width": 185.79785193709245, - "height": 120.86849507735587, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffec99", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b29", - "roundness": { - "type": 2 - }, - "seed": 1538411947, - "version": 242, - "versionNonce": 1974046635, - "isDeleted": false, - "boundElements": null, - "updated": 1727115146103, - "link": null, - "locked": false, - "points": [ + -3, + 4 + ], [ - 0, - 0 + -3, + 5 ], [ - -32.964135021097036, - 41.95435366321453 + -3, + 6 ], [ - -164.8206751054854, - 58.9358777649918 + -3, + 7 ], [ - -185.79785193709245, - 120.86849507735587 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "Jy9q4ysrAAbpXdaiX521F", - "focus": -0.314101445009377, - "gap": 6.720790354481778, - "fixedPoint": null - }, - "endBinding": { - "elementId": "2-xZdr1N7h94XCAl94juo", - "focus": -0.05857119821555177, - "gap": 1, - "fixedPoint": null - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": false - }, - { - "id": "sneG026N1n_G0mJImR8vA", - "type": "arrow", - "x": 1368.868357591277, - "y": 2475.547962107124, - "width": 736.1990154711671, - "height": 123.8652346247286, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffec99", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b2A", - "roundness": { - "type": 2 - }, - "seed": 2123839301, - "version": 602, - "versionNonce": 1688022021, - "isDeleted": false, - "boundElements": null, - "updated": 1727115176112, - "link": null, - "locked": false, - "points": [ + -3, + 8 + ], + [ + -3, + 9 + ], + [ + -2, + 9 + ], + [ + -2, + 10 + ], + [ + -1, + 10 + ], + [ + -1, + 11 + ], [ 0, - 0 + 11 ], [ - 33.963048203554536, - 29.96739547372499 + 1, + 11 ], [ - 180.80328602480495, - 37.95870093338499 + 1, + 12 ], [ - 663.2783531517707, - 61.93261731236407 - ], - [ - 736.1990154711671, - 123.8652346247286 - ] - ], - "lastCommittedPoint": null, - "startBinding": { - "elementId": "Jy9q4ysrAAbpXdaiX521F", - "focus": 0.2446412333379998, - "gap": 3.7240508071090517, - "fixedPoint": null - }, - "endBinding": { - "elementId": "NEKEWaqq5lWTHOkiGx6DU", - "focus": 0.32422142161568906, - "gap": 1.1771196391496233, - "fixedPoint": null - }, - "startArrowhead": null, - "endArrowhead": "arrow", - "elbowed": false - }, - { - "id": "Ih6O2CVqHKg5-szfAXDhu", - "type": "diamond", - "x": 3125.2697698305014, - "y": 420.4025642562489, - "width": 247.39095075069528, - "height": 164.6026404601082, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffec99", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b2C", - "roundness": null, - "seed": 1092419845, - "version": 267, - "versionNonce": 306122347, - "isDeleted": false, - "boundElements": null, - "updated": 1727115557614, - "link": null, - "locked": false - }, - { - "id": "yN5E3yZ8SM7NGSXeA-0oP", - "type": "rectangle", - "x": 3127.217730072633, - "y": 502.2168944257702, - "width": 243.49503026643197, - "height": 161.68070009691098, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffec99", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b2D", - "roundness": null, - "seed": 1654909317, - "version": 121, - "versionNonce": 1108382245, - "isDeleted": false, - "boundElements": [ - { - "type": "text", - "id": "Yn7n1nNGxCBncgRMYIJUT" - } - ], - "updated": 1727115585644, - "link": null, - "locked": false - }, - { - "id": "Yn7n1nNGxCBncgRMYIJUT", - "type": "text", - "x": 3223.4069127626117, - "y": 572.2572444742257, - "width": 51.11666488647461, - "height": 21.6, - "angle": 0, - "strokeColor": "#1e1e1e", - "backgroundColor": "#ffec99", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "groupIds": [], - "frameId": null, - "index": "b2DG", - "roundness": null, - "seed": 1536952331, - "version": 9, - "versionNonce": 1177236395, - "isDeleted": false, - "boundElements": null, - "updated": 1727115594963, - "link": null, - "locked": false, - "text": "<-- Viv", - "fontSize": 16, - "fontFamily": 6, - "textAlign": "center", - "verticalAlign": "middle", - "containerId": "yN5E3yZ8SM7NGSXeA-0oP", - "originalText": "<-- Viv", - "autoResize": true, - "lineHeight": 1.35 - }, - { - "type": "freedraw", - "version": 649, - "versionNonce": 873076363, - "index": "b2F", - "isDeleted": false, - "id": "WBPMPJ76_7bvOYZIcaHuZ", - "fillStyle": "solid", - "strokeWidth": 2, - "strokeStyle": "solid", - "roughness": 0, - "opacity": 100, - "angle": 0, - "x": 3187.552457820839, - "y": 568.3286878640658, - "strokeColor": "#1e1e1e", - "backgroundColor": "#a5d8ff", - "width": 28, - "height": 22, - "seed": 1716601995, - "groupIds": [ - "-DMu6XEZMLghW5wuD-QkJ" - ], - "frameId": null, - "roundness": null, - "boundElements": [], - "updated": 1727115577484, - "link": null, - "locked": false, - "points": [ - [ - 0, - 0 - ], - [ - -1, - 0 - ], - [ - -2, - 1 - ], - [ - -3, - 1 - ], - [ - -4, - 2 - ], - [ - -5, - 2 - ], - [ - -5, - 3 - ], - [ - -6, - 3 - ], - [ - -6, - 4 - ], - [ - -7, - 4 - ], - [ - -7, - 5 - ], - [ - -8, - 5 - ], - [ - -8, - 6 - ], - [ - -9, - 7 - ], - [ - -9, - 8 - ], - [ - -10, - 9 - ], - [ - -10, - 10 - ], - [ - -10, - 11 - ], - [ - -9, - 13 - ], - [ - -9, - 14 - ], - [ - -9, - 16 - ], - [ - -8, - 16 - ], - [ - -8, - 17 - ], - [ - -7, - 17 - ], - [ - -6, - 18 - ], - [ - -5, - 19 - ], - [ - -4, - 19 - ], - [ - -3, - 20 - ], - [ - -2, - 20 - ], - [ - -1, - 21 - ], - [ - 0, - 21 - ], - [ - 1, - 21 - ], - [ - 2, - 21 - ], - [ - 3, - 21 - ], - [ - 4, - 21 - ], - [ - 5, - 21 - ], - [ - 6, - 21 - ], - [ - 8, - 21 - ], - [ - 9, - 21 - ], - [ - 11, - 21 - ], - [ - 12, - 21 - ], - [ - 14, - 21 - ], - [ - 15, - 20 - ], - [ - 16, - 20 - ], - [ - 16, - 19 - ], - [ - 16, - 18 - ], - [ - 17, - 18 - ], - [ - 17, - 17 - ], - [ - 17, - 16 - ], - [ - 17, - 15 - ], - [ - 18, - 14 - ], - [ - 18, - 13 - ], - [ - 18, - 12 - ], - [ - 18, - 11 - ], - [ - 17, - 9 - ], - [ - 17, - 8 - ], - [ - 17, - 7 - ], - [ - 16, - 6 - ], - [ - 16, - 5 - ], - [ - 16, - 4 - ], - [ - 15, - 4 - ], - [ - 15, - 3 - ], - [ - 14, - 3 - ], - [ - 13, - 2 - ], - [ - 12, - 2 - ], - [ - 12, - 1 - ], - [ - 11, - 1 - ], - [ - 10, - 1 - ], - [ - 10, - 0 - ], - [ - 9, - 0 - ], - [ - 8, - 0 - ], - [ - 7, - 0 - ], - [ - 5, - 0 - ], - [ - 4, - -1 - ], - [ - 3, - -1 - ], - [ - 2, - -1 - ], - [ - 1, - -1 - ], - [ - 0, - 0 - ], - [ - -1, - 1 - ], - [ - -1, - 2 - ], - [ - -2, - 2 - ], - [ - -2, - 3 - ], - [ - -3, - 3 - ], - [ - -3, - 4 - ], - [ - -3, - 5 - ], - [ - -3, - 6 - ], - [ - -3, - 7 - ], - [ - -3, - 8 - ], - [ - -3, - 9 - ], - [ - -2, - 9 - ], - [ - -2, - 10 - ], - [ - -1, - 10 - ], - [ - -1, - 11 - ], - [ - 0, - 11 - ], - [ - 1, - 11 - ], - [ - 1, - 12 - ], - [ - 2, - 12 + 2, + 12 ], [ 3, @@ -24796,7 +23873,6 @@ "type": "freedraw", "version": 824, "versionNonce": 2134707499, - "index": "b2G", "isDeleted": false, "id": "O5Jggv7DlOH4hWWVqYbvz", "fillStyle": "solid", @@ -26045,9 +25121,8 @@ }, { "type": "text", - "version": 253, - "versionNonce": 1373954565, - "index": "b2H", + "version": 341, + "versionNonce": 419269255, "isDeleted": false, "id": "iwJeId6hgp7L3UL4Vq-vc", "fillStyle": "solid", @@ -26056,54 +25131,53 @@ "roughness": 0, "opacity": 100, "angle": 0, - "x": 3221.943278910431, - "y": 676.3868987599815, + "x": 3163.0787554368135, + "y": 668.7342084567053, "strokeColor": "#1e1e1e", "backgroundColor": "#a5d8ff", - "width": 71.28333282470703, - "height": 21.6, + "width": 206.140625, + "height": 43.2, "seed": 1208291851, "groupIds": [], "frameId": null, "roundness": null, "boundElements": [], - "updated": 1727115630886, + "updated": 1727290012235, "link": null, "locked": false, "fontSize": 16, "fontFamily": 6, - "text": "Viv house", - "textAlign": "right", + "text": "Viv house\n(Bug infested) (they are friends)", + "textAlign": "center", "verticalAlign": "top", "containerId": null, - "originalText": "Viv house", - "autoResize": true, - "lineHeight": 1.35 + "originalText": "Viv house\n(Bug infested) (they are friends)", + "lineHeight": 1.35, + "baseline": 37 }, { - "id": "8jJDFpFP1mZtt156CKJsj", "type": "freedraw", - "x": 3184.7458310397383, - "y": 557.2867080427628, - "width": 6.204837356517146, - "height": 6.313694152245603, - "angle": 0, - "strokeColor": "#9c36b5", - "backgroundColor": "#ffec99", + "version": 173, + "versionNonce": 721495275, + "isDeleted": false, + "id": "8jJDFpFP1mZtt156CKJsj", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, + "angle": 0, + "x": 3184.7458310397383, + "y": 557.2867080427628, + "strokeColor": "#9c36b5", + "backgroundColor": "#ffec99", + "width": 6.204837356517146, + "height": 6.313694152245603, + "seed": 985483685, "groupIds": [], "frameId": null, - "index": "b2I", "roundness": null, - "seed": 985483685, - "version": 173, - "versionNonce": 721495275, - "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1727116184378, "link": null, "locked": false, @@ -26797,37 +25871,33 @@ -0.7619975700986288 ] ], - "pressures": [], + "lastCommittedPoint": null, "simulatePressure": true, - "lastCommittedPoint": [ - 1.8505655273825141, - -0.7619975700986288 - ] + "pressures": [] }, { - "id": "PI9kEnqaE5B3YoSCi-sm6", "type": "freedraw", - "x": 3194.6517994510205, - "y": 559.3549871616019, - "width": 2.5037063017525725, - "height": 2.6125630974809155, - "angle": 0, - "strokeColor": "#9c36b5", - "backgroundColor": "#ffec99", + "version": 88, + "versionNonce": 2103541541, + "isDeleted": false, + "id": "PI9kEnqaE5B3YoSCi-sm6", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, + "angle": 0, + "x": 3194.6517994510205, + "y": 559.3549871616019, + "strokeColor": "#9c36b5", + "backgroundColor": "#ffec99", + "width": 2.5037063017525725, + "height": 2.6125630974809155, + "seed": 1262715557, "groupIds": [], "frameId": null, - "index": "b2J", "roundness": null, - "seed": 1262715557, - "version": 88, - "versionNonce": 2103541541, - "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1727116186329, "link": null, "locked": false, @@ -27181,37 +26251,33 @@ 0 ] ], - "pressures": [], + "lastCommittedPoint": null, "simulatePressure": true, - "lastCommittedPoint": [ - -0.8708543658271992, - 0 - ] + "pressures": [] }, { - "id": "AVC9tB7fxXyBXB1cMYTrp", "type": "freedraw", - "x": 3197.5909329356864, - "y": 559.4638439573303, - "width": 6.966834926615775, - "height": 9.688254819825147, - "angle": 0, - "strokeColor": "#9c36b5", - "backgroundColor": "#ffec99", + "version": 181, + "versionNonce": 23559589, + "isDeleted": false, + "id": "AVC9tB7fxXyBXB1cMYTrp", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, + "angle": 0, + "x": 3197.5909329356864, + "y": 559.4638439573303, + "strokeColor": "#9c36b5", + "backgroundColor": "#ffec99", + "width": 6.966834926615775, + "height": 9.688254819825147, + "seed": 406307915, "groupIds": [], "frameId": null, - "index": "b2K", "roundness": null, - "seed": 406307915, - "version": 181, - "versionNonce": 23559589, - "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1727116188694, "link": null, "locked": false, @@ -27937,37 +27003,33 @@ 1.5239951401972576 ] ], - "pressures": [], + "lastCommittedPoint": null, "simulatePressure": true, - "lastCommittedPoint": [ - 1.4151383444686871, - 1.5239951401972576 - ] + "pressures": [] }, { - "id": "Joxwi2r20qJApxbNkc90w", "type": "freedraw", - "x": 3172.134224946585, - "y": 573.9214285172549, - "width": 2.8732383277924782, - "height": 5.890138571974717, - "angle": 0, - "strokeColor": "#343a40", - "backgroundColor": "#ffec99", + "version": 132, + "versionNonce": 575626021, + "isDeleted": false, + "id": "Joxwi2r20qJApxbNkc90w", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, + "angle": 0, + "x": 3172.134224946585, + "y": 573.9214285172549, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "width": 2.8732383277924782, + "height": 5.890138571974717, + "seed": 264640613, "groupIds": [], "frameId": null, - "index": "b2L", "roundness": null, - "seed": 264640613, - "version": 132, - "versionNonce": 575626021, - "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1727116205603, "link": null, "locked": false, @@ -28497,37 +27559,33 @@ 5.028167073636951 ] ], - "pressures": [], + "lastCommittedPoint": null, "simulatePressure": true, - "lastCommittedPoint": [ - -2.2985906622338916, - 5.028167073636951 - ] + "pressures": [] }, { - "id": "P2X2FiQisUdlR-CM3SiDW", "type": "freedraw", - "x": 3213.652518783187, - "y": 575.9326953467096, - "width": 2.873238327792933, - "height": 8.332391150598369, - "angle": 0, - "strokeColor": "#343a40", - "backgroundColor": "#ffec99", + "version": 125, + "versionNonce": 1276228389, + "isDeleted": false, + "id": "P2X2FiQisUdlR-CM3SiDW", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, + "angle": 0, + "x": 3213.652518783187, + "y": 575.9326953467096, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "width": 2.873238327792933, + "height": 8.332391150598369, + "seed": 659105867, "groupIds": [], "frameId": null, - "index": "b2M", "roundness": null, - "seed": 659105867, - "version": 125, - "versionNonce": 1276228389, - "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1727116207564, "link": null, "locked": false, @@ -29029,37 +28087,33 @@ 2.298590662234119 ] ], - "pressures": [], + "lastCommittedPoint": null, "simulatePressure": true, - "lastCommittedPoint": [ - 0.2873238327792933, - 2.298590662234119 - ] + "pressures": [] }, { - "id": "s_4M7ZNcqVVT6upuP7mEb", "type": "freedraw", - "x": 3216.238433278201, - "y": 573.6341046844756, - "width": 8.045067317819303, - "height": 10.918305645611667, - "angle": 0, - "strokeColor": "#343a40", - "backgroundColor": "#ffec99", + "version": 87, + "versionNonce": 2055582469, + "isDeleted": false, + "id": "s_4M7ZNcqVVT6upuP7mEb", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, + "angle": 0, + "x": 3216.238433278201, + "y": 573.6341046844756, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "width": 8.045067317819303, + "height": 10.918305645611667, + "seed": 861098059, "groupIds": [], "frameId": null, - "index": "b2N", "roundness": null, - "seed": 861098059, - "version": 87, - "versionNonce": 2055582469, - "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1727116209674, "link": null, "locked": false, @@ -29409,37 +28463,33 @@ -10.918305645611667 ] ], - "pressures": [], + "lastCommittedPoint": null, "simulatePressure": true, - "lastCommittedPoint": [ - -8.045067317819303, - -10.918305645611667 - ] + "pressures": [] }, { - "id": "EwHCnpxC1ZMSpGU0_AxOV", "type": "freedraw", - "x": 3182.0468971774694, - "y": 560.4172083766299, - "width": 10.630981812832488, - "height": 10.918305645611667, - "angle": 0, - "strokeColor": "#343a40", - "backgroundColor": "#ffec99", + "version": 124, + "versionNonce": 1547579691, + "isDeleted": false, + "id": "EwHCnpxC1ZMSpGU0_AxOV", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, + "angle": 0, + "x": 3182.0468971774694, + "y": 560.4172083766299, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "width": 10.630981812832488, + "height": 10.918305645611667, + "seed": 2106915435, "groupIds": [], "frameId": null, - "index": "b2O", "roundness": null, - "seed": 2106915435, - "version": 124, - "versionNonce": 1547579691, - "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1727116213337, "link": null, "locked": false, @@ -29937,37 +28987,33 @@ 10.918305645611667 ] ], - "pressures": [], + "lastCommittedPoint": null, "simulatePressure": true, - "lastCommittedPoint": [ - -10.630981812832488, - 10.918305645611667 - ] + "pressures": [] }, { - "id": "8pgTqOFi7Msi_nNuwmVV5", "type": "freedraw", - "x": 3169.2609866187927, - "y": 570.6172044402934, - "width": 5.3154909064160165, - "height": 10.056334147273787, - "angle": 0, - "strokeColor": "#343a40", - "backgroundColor": "#ffec99", + "version": 116, + "versionNonce": 650271269, + "isDeleted": false, + "id": "8pgTqOFi7Msi_nNuwmVV5", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, + "angle": 0, + "x": 3169.2609866187927, + "y": 570.6172044402934, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "width": 5.3154909064160165, + "height": 10.056334147273787, + "seed": 1609261669, "groupIds": [], "frameId": null, - "index": "b2P", "roundness": null, - "seed": 1609261669, - "version": 116, - "versionNonce": 650271269, - "isDeleted": false, - "boundElements": null, + "boundElements": [], "updated": 1727116215693, "link": null, "locked": false, @@ -30409,366 +29455,13783 @@ 6.46478623753319 ], [ - 2.7295764114028316, - 6.321124321143543 + 2.7295764114028316, + 6.321124321143543 + ], + [ + 2.7295764114028316, + 6.177462404753896 + ], + [ + 2.7295764114028316, + 6.03380048836425 + ], + [ + 2.7295764114028316, + 5.890138571974717 + ], + [ + 2.7295764114028316, + 5.74647665558507 + ], + [ + 2.7295764114028316, + 5.74647665558507 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 73, + "versionNonce": 75490917, + "isDeleted": false, + "id": "mxaGNWNFDtpqougV0fQYG", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3217.3877286093175, + "y": 576.2200191794889, + "strokeColor": "#343a40", + "backgroundColor": "#ffec99", + "width": 4.740843240857885, + "height": 8.619714983377548, + "seed": 261323083, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727116217187, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.14366191638964665, + 0.14366191638964665 + ], + [ + 0.2873238327792933, + 0.5746476655584729 + ], + [ + 0.2873238327792933, + 0.7183095819481196 + ], + [ + 0.43098574916893995, + 0.7183095819481196 + ], + [ + 0.43098574916893995, + 0.8619714983377662 + ], + [ + 0.43098574916893995, + 1.0056334147274129 + ], + [ + 0.5746476655585866, + 1.2929572475065925 + ], + [ + 0.5746476655585866, + 1.4366191638962391 + ], + [ + 0.5746476655585866, + 1.7239429966755324 + ], + [ + 0.5746476655585866, + 1.867604913065179 + ], + [ + 0.7183095819482332, + 2.2985906622340053 + ], + [ + 0.7183095819482332, + 2.442252578623652 + ], + [ + 0.8619714983378799, + 2.8732383277924782 + ], + [ + 0.8619714983378799, + 3.016900244182125 + ], + [ + 1.0056334147275265, + 3.447885993351065 + ], + [ + 1.0056334147275265, + 3.5915479097407115 + ], + [ + 1.1492953311171732, + 3.878871742519891 + ], + [ + 1.1492953311171732, + 4.022533658909538 + ], + [ + 1.1492953311171732, + 4.309857491688831 + ], + [ + 1.2929572475068198, + 4.309857491688831 + ], + [ + 1.2929572475068198, + 4.597181324468011 + ], + [ + 1.2929572475068198, + 4.740843240857657 + ], + [ + 1.2929572475068198, + 4.884505157247304 + ], + [ + 1.2929572475068198, + 5.028167073636951 + ], + [ + 1.2929572475068198, + 5.171828990026597 + ], + [ + 1.2929572475068198, + 5.31549090641613 + ], + [ + 1.1492953311171732, + 5.31549090641613 + ], + [ + 1.1492953311171732, + 5.459152822805777 + ], + [ + 1.1492953311171732, + 5.6028147391954235 + ], + [ + 1.0056334147275265, + 5.74647665558507 + ], + [ + 1.0056334147275265, + 5.890138571974717 + ], + [ + 0.8619714983378799, + 6.03380048836425 + ], + [ + 0.8619714983378799, + 6.177462404753896 + ], + [ + 0.8619714983378799, + 6.321124321143543 + ], + [ + 0.7183095819482332, + 6.46478623753319 + ], + [ + 0.7183095819482332, + 6.752110070312483 + ], + [ + 0.5746476655585866, + 6.89577198670213 + ], + [ + 0.5746476655585866, + 7.039433903091663 + ], + [ + 0.43098574916893995, + 7.326757735870956 + ], + [ + 0.2873238327792933, + 7.470419652260603 + ], + [ + 0.2873238327792933, + 7.614081568650249 + ], + [ + 0.14366191638964665, + 7.757743485039782 + ], + [ + 0, + 7.901405401429429 + ], + [ + -0.14366191638964665, + 7.901405401429429 + ], + [ + -0.2873238327792933, + 7.901405401429429 + ], + [ + -0.43098574916893995, + 8.045067317819075 + ], + [ + -0.5746476655585866, + 8.045067317819075 + ], + [ + -0.8619714983378799, + 8.188729234208722 + ], + [ + -1.1492953311167184, + 8.332391150598369 + ], + [ + -1.292957247506365, + 8.476053066988015 + ], + [ + -1.5802810802856584, + 8.476053066988015 + ], + [ + -1.8676049130649517, + 8.619714983377548 + ], + [ + -2.0112668294545983, + 8.619714983377548 + ], + [ + -2.154928745844245, + 8.619714983377548 + ], + [ + -2.2985906622338916, + 8.619714983377548 + ], + [ + -2.4422525786235383, + 8.619714983377548 + ], + [ + -2.4422525786235383, + 8.476053066988015 + ], + [ + -2.585914495013185, + 8.476053066988015 + ], + [ + -2.585914495013185, + 8.332391150598369 + ], + [ + -2.7295764114028316, + 8.332391150598369 + ], + [ + -2.8732383277924782, + 8.188729234208722 + ], + [ + -3.016900244182125, + 8.188729234208722 + ], + [ + -3.1605621605717715, + 8.188729234208722 + ], + [ + -3.1605621605717715, + 8.045067317819075 + ], + [ + -3.1605621605717715, + 7.901405401429429 + ], + [ + -3.304224076961418, + 7.757743485039782 + ], + [ + -3.447885993351065, + 7.614081568650249 + ], + [ + -3.447885993351065, + 7.470419652260603 + ], + [ + -3.304224076961418, + 7.470419652260603 + ], + [ + -3.304224076961418, + 7.326757735870956 + ], + [ + -3.304224076961418, + 7.326757735870956 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 109, + "versionNonce": 1382094062, + "isDeleted": false, + "id": "H4_mmra6BQ98ZCBkAVQ-2", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3155.6864082657075, + "y": 474.80370645412006, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 14.455081683966, + "height": 14.455081683966, + "seed": 726521778, + "groupIds": [ + "2OJnQJZYhNFL3bSBzq363" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167818249, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.850298922586262 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -2.550896767758786, + -0.850298922586262 + ], + [ + -3.401195690345048, + -0.850298922586262 + ], + [ + -3.401195690345048, + 0 + ], + [ + -3.401195690345048, + 1.7005978451724673 + ], + [ + -2.550896767758786, + 3.4011956903449345 + ], + [ + -0.850298922586262, + 5.952092458103607 + ], + [ + 0.850298922586262, + 7.652690303276074 + ], + [ + 2.550896767758786, + 9.353288148448598 + ], + [ + 5.101793535517118, + 10.203587071034804 + ], + [ + 6.802391380689642, + 10.203587071034804 + ], + [ + 7.652690303275904, + 10.203587071034804 + ], + [ + 8.502989225862166, + 8.502989225862336 + ], + [ + 9.353288148448428, + 6.802391380689869 + ], + [ + 10.20358707103469, + 5.101793535517402 + ], + [ + 11.053885993620952, + 3.4011956903449345 + ], + [ + 11.053885993620952, + 1.7005978451724673 + ], + [ + 10.20358707103469, + 0 + ], + [ + 9.353288148448428, + -1.7005978451724673 + ], + [ + 7.652690303275904, + -2.5508967677587293 + ], + [ + 5.95209245810338, + -3.4011956903449345 + ], + [ + 2.550896767758786, + -4.251494612931197 + ], + [ + 0.850298922586262, + -4.251494612931197 + ], + [ + 0, + -4.251494612931197 + ], + [ + -0.850298922586262, + -4.251494612931197 + ], + [ + -0.850298922586262, + -2.5508967677587293 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -0.850298922586262, + 1.7005978451724673 + ], + [ + 0, + 4.25149461293114 + ], + [ + 1.700597845172524, + 6.802391380689869 + ], + [ + 3.4011956903445935, + 7.652690303276074 + ], + [ + 5.101793535517118, + 8.502989225862336 + ], + [ + 5.95209245810338, + 8.502989225862336 + ], + [ + 7.652690303275904, + 7.652690303276074 + ], + [ + 7.652690303275904, + 5.101793535517402 + ], + [ + 7.652690303275904, + 2.5508967677586725 + ], + [ + 7.652690303275904, + 0 + ], + [ + 5.95209245810338, + -1.7005978451724673 + ], + [ + 5.101793535517118, + -2.5508967677587293 + ], + [ + 5.101793535517118, + -3.4011956903449345 + ], + [ + 4.2514946129308555, + -3.4011956903449345 + ], + [ + 3.4011956903445935, + -1.7005978451724673 + ], + [ + 1.700597845172524, + 0 + ], + [ + 0.850298922586262, + 1.7005978451724673 + ], + [ + 0.850298922586262, + 3.4011956903449345 + ], + [ + 0.850298922586262, + 5.101793535517402 + ], + [ + 1.700597845172524, + 5.101793535517402 + ], + [ + 3.4011956903445935, + 5.952092458103607 + ], + [ + 5.101793535517118, + 5.101793535517402 + ], + [ + 5.95209245810338, + 4.25149461293114 + ], + [ + 6.802391380689642, + 1.7005978451724673 + ], + [ + 5.101793535517118, + -0.850298922586262 + ], + [ + 3.4011956903445935, + -1.7005978451724673 + ], + [ + 0.850298922586262, + -1.7005978451724673 + ], + [ + 0, + 0 + ], + [ + -0.850298922586262, + 2.5508967677586725 + ], + [ + 1.700597845172524, + 4.25149461293114 + ], + [ + 5.101793535517118, + 5.952092458103607 + ], + [ + 8.502989225862166, + 5.101793535517402 + ], + [ + 8.502989225862166, + 4.25149461293114 + ], + [ + 6.802391380689642, + 3.4011956903449345 + ], + [ + 6.802391380689642, + 3.4011956903449345 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 318, + "versionNonce": 730613550, + "isDeleted": false, + "id": "b_yDq_21EL_BsveOC_It4", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3161.638500723811, + "y": 490.10908706067227, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 31.461060135690786, + "height": 34.86225582603561, + "seed": 1870252850, + "groupIds": [ + "2OJnQJZYhNFL3bSBzq363" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167818249, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + 0 + ], + [ + -1.700597845172524, + 0 + ], + [ + -2.550896767758786, + 0 + ], + [ + -5.101793535517118, + 0 + ], + [ + -6.802391380689642, + 0 + ], + [ + -8.502989225862166, + 0 + ], + [ + -9.353288148448428, + 0 + ], + [ + -9.353288148448428, + 0.850298922586262 + ], + [ + -10.20358707103469, + 3.4011956903449345 + ], + [ + -10.20358707103469, + 4.251494612931197 + ], + [ + -11.053885993620952, + 5.101793535517402 + ], + [ + -11.904184916207214, + 5.952092458103664 + ], + [ + -12.754483838793476, + 7.652690303276131 + ], + [ + -13.604782761379738, + 9.353288148448598 + ], + [ + -13.604782761379738, + 11.904184916207328 + ], + [ + -12.754483838793476, + 13.604782761379795 + ], + [ + -12.754483838793476, + 15.305380606552262 + ], + [ + -11.904184916207214, + 17.00597845172473 + ], + [ + -11.053885993620952, + 18.706576296897197 + ], + [ + -11.053885993620952, + 19.556875219483402 + ], + [ + -10.20358707103469, + 21.25747306465587 + ], + [ + -9.353288148448428, + 22.958070909828336 + ], + [ + -8.502989225862166, + 24.658668755000804 + ], + [ + -6.802391380689642, + 26.35926660017327 + ], + [ + -5.95209245810338, + 28.910163367932 + ], + [ + -4.2514946129308555, + 30.610761213104468 + ], + [ + -2.550896767758786, + 31.46106013569073 + ], + [ + 0, + 32.311359058276935 + ], + [ + 1.700597845172524, + 33.1616579808632 + ], + [ + 4.25149461293131, + 33.1616579808632 + ], + [ + 5.952092458103834, + 34.0119569034494 + ], + [ + 8.50298922586262, + 34.0119569034494 + ], + [ + 10.20358707103469, + 33.1616579808632 + ], + [ + 12.754483838793476, + 31.46106013569073 + ], + [ + 14.455081683966, + 29.760462290518205 + ], + [ + 16.155679529138524, + 28.059864445345738 + ], + [ + 17.005978451724786, + 26.35926660017327 + ], + [ + 17.85627737431105, + 24.658668755000804 + ], + [ + 17.85627737431105, + 22.10777198724213 + ], + [ + 17.85627737431105, + 19.556875219483402 + ], + [ + 17.005978451724786, + 17.00597845172473 + ], + [ + 16.155679529138524, + 15.305380606552262 + ], + [ + 14.455081683966, + 11.904184916207328 + ], + [ + 13.604782761379738, + 8.502989225862336 + ], + [ + 12.754483838793476, + 6.802391380689869 + ], + [ + 11.904184916207214, + 4.251494612931197 + ], + [ + 11.053885993620952, + 2.5508967677587293 + ], + [ + 10.20358707103469, + 1.7005978451724673 + ], + [ + 9.353288148448883, + 0.850298922586262 + ], + [ + 7.6526903032763585, + 0 + ], + [ + 5.952092458103834, + 0 + ], + [ + 5.101793535517572, + -0.8502989225862052 + ], + [ + 3.401195690345048, + -0.8502989225862052 + ], + [ + 1.700597845172524, + -0.8502989225862052 + ], + [ + 0.850298922586262, + 0 + ], + [ + 0, + 0 + ], + [ + -1.700597845172524, + 0.850298922586262 + ], + [ + -2.550896767758786, + 1.7005978451724673 + ], + [ + -3.4011956903445935, + 3.4011956903449345 + ], + [ + -4.2514946129308555, + 6.802391380689869 + ], + [ + -4.2514946129308555, + 11.053885993621066 + ], + [ + -4.2514946129308555, + 15.305380606552262 + ], + [ + -3.4011956903445935, + 19.556875219483402 + ], + [ + -0.850298922586262, + 24.658668755000804 + ], + [ + 1.700597845172524, + 27.209565522759533 + ], + [ + 4.25149461293131, + 28.910163367932 + ], + [ + 6.802391380690096, + 28.910163367932 + ], + [ + 8.50298922586262, + 28.059864445345738 + ], + [ + 9.353288148448883, + 24.658668755000804 + ], + [ + 9.353288148448883, + 19.556875219483402 + ], + [ + 7.6526903032763585, + 15.305380606552262 + ], + [ + 5.101793535517572, + 10.20358707103486 + ], + [ + 2.550896767758786, + 6.802391380689869 + ], + [ + 0, + 4.251494612931197 + ], + [ + -0.850298922586262, + 3.4011956903449345 + ], + [ + -2.550896767758786, + 3.4011956903449345 + ], + [ + -4.2514946129308555, + 5.952092458103664 + ], + [ + -5.101793535517118, + 9.353288148448598 + ], + [ + -5.95209245810338, + 12.754483838793533 + ], + [ + -5.95209245810338, + 16.155679529138467 + ], + [ + -4.2514946129308555, + 19.556875219483402 + ], + [ + -2.550896767758786, + 21.25747306465587 + ], + [ + 0, + 22.10777198724213 + ], + [ + 1.700597845172524, + 21.25747306465587 + ], + [ + 2.550896767758786, + 19.556875219483402 + ], + [ + 2.550896767758786, + 16.155679529138467 + ], + [ + 0.850298922586262, + 11.904184916207328 + ], + [ + -2.550896767758786, + 6.802391380689869 + ], + [ + -5.101793535517118, + 3.4011956903449345 + ], + [ + -6.802391380689642, + 1.7005978451724673 + ], + [ + -8.502989225862166, + 1.7005978451724673 + ], + [ + -10.20358707103469, + 4.251494612931197 + ], + [ + -11.053885993620952, + 10.20358707103486 + ], + [ + -10.20358707103469, + 17.856277374310935 + ], + [ + -8.502989225862166, + 22.10777198724213 + ], + [ + -5.101793535517118, + 24.658668755000804 + ], + [ + -1.700597845172524, + 26.35926660017327 + ], + [ + 0.850298922586262, + 26.35926660017327 + ], + [ + 1.700597845172524, + 26.35926660017327 + ], + [ + 2.550896767758786, + 22.958070909828336 + ], + [ + 2.550896767758786, + 20.407174142069664 + ], + [ + 2.550896767758786, + 17.00597845172473 + ], + [ + 0.850298922586262, + 12.754483838793533 + ], + [ + -0.850298922586262, + 9.353288148448598 + ], + [ + -2.550896767758786, + 5.952092458103664 + ], + [ + -4.2514946129308555, + 5.101793535517402 + ], + [ + -5.101793535517118, + 5.101793535517402 + ], + [ + -5.101793535517118, + 7.652690303276131 + ], + [ + -5.95209245810338, + 11.053885993621066 + ], + [ + -5.95209245810338, + 15.305380606552262 + ], + [ + -5.101793535517118, + 19.556875219483402 + ], + [ + -4.2514946129308555, + 22.958070909828336 + ], + [ + -2.550896767758786, + 24.658668755000804 + ], + [ + -1.700597845172524, + 23.8083698324146 + ], + [ + -1.700597845172524, + 22.10777198724213 + ], + [ + -1.700597845172524, + 19.556875219483402 + ], + [ + -1.700597845172524, + 15.305380606552262 + ], + [ + -3.4011956903445935, + 11.053885993621066 + ], + [ + -6.802391380689642, + 5.952092458103664 + ], + [ + -7.652690303275904, + 5.101793535517402 + ], + [ + -8.502989225862166, + 5.952092458103664 + ], + [ + -9.353288148448428, + 9.353288148448598 + ], + [ + -10.20358707103469, + 14.455081683966 + ], + [ + -10.20358707103469, + 22.10777198724213 + ], + [ + -8.502989225862166, + 26.35926660017327 + ], + [ + -6.802391380689642, + 28.910163367932 + ], + [ + -3.4011956903445935, + 29.760462290518205 + ], + [ + -1.700597845172524, + 29.760462290518205 + ], + [ + 0, + 27.209565522759533 + ], + [ + 0, + 22.10777198724213 + ], + [ + -0.850298922586262, + 17.00597845172473 + ], + [ + -2.550896767758786, + 11.904184916207328 + ], + [ + -5.101793535517118, + 6.802391380689869 + ], + [ + -6.802391380689642, + 4.251494612931197 + ], + [ + -7.652690303275904, + 4.251494612931197 + ], + [ + -7.652690303275904, + 8.502989225862336 + ], + [ + -7.652690303275904, + 14.455081683966 + ], + [ + -5.101793535517118, + 22.10777198724213 + ], + [ + -1.700597845172524, + 27.209565522759533 + ], + [ + 1.700597845172524, + 30.610761213104468 + ], + [ + 5.101793535517572, + 31.46106013569073 + ], + [ + 6.802391380690096, + 30.610761213104468 + ], + [ + 7.6526903032763585, + 28.059864445345738 + ], + [ + 7.6526903032763585, + 24.658668755000804 + ], + [ + 6.802391380690096, + 19.556875219483402 + ], + [ + 5.101793535517572, + 14.455081683966 + ], + [ + 2.550896767758786, + 10.20358707103486 + ], + [ + -0.850298922586262, + 8.502989225862336 + ], + [ + -1.700597845172524, + 8.502989225862336 + ], + [ + -3.4011956903445935, + 11.053885993621066 + ], + [ + -4.2514946129308555, + 16.155679529138467 + ], + [ + -3.4011956903445935, + 21.25747306465587 + ], + [ + -1.700597845172524, + 24.658668755000804 + ], + [ + 0.850298922586262, + 27.209565522759533 + ], + [ + 3.401195690345048, + 27.209565522759533 + ], + [ + 5.101793535517572, + 26.35926660017327 + ], + [ + 5.952092458103834, + 24.658668755000804 + ], + [ + 6.802391380690096, + 21.25747306465587 + ], + [ + 6.802391380690096, + 17.00597845172473 + ], + [ + 5.101793535517572, + 10.20358707103486 + ], + [ + 3.401195690345048, + 6.802391380689869 + ], + [ + 1.700597845172524, + 4.251494612931197 + ], + [ + 0.850298922586262, + 4.251494612931197 + ], + [ + 0, + 5.952092458103664 + ], + [ + -0.850298922586262, + 10.20358707103486 + ], + [ + 0, + 17.00597845172473 + ], + [ + 0.850298922586262, + 22.10777198724213 + ], + [ + 2.550896767758786, + 25.508967677587066 + ], + [ + 5.101793535517572, + 26.35926660017327 + ], + [ + 5.952092458103834, + 26.35926660017327 + ], + [ + 7.6526903032763585, + 24.658668755000804 + ], + [ + 8.50298922586262, + 19.556875219483402 + ], + [ + 8.50298922586262, + 14.455081683966 + ], + [ + 5.952092458103834, + 8.502989225862336 + ], + [ + 4.25149461293131, + 3.4011956903449345 + ], + [ + 1.700597845172524, + 0.850298922586262 + ], + [ + 0.850298922586262, + 0.850298922586262 + ], + [ + 0, + 6.802391380689869 + ], + [ + 1.700597845172524, + 16.155679529138467 + ], + [ + 4.25149461293131, + 22.958070909828336 + ], + [ + 6.802391380690096, + 26.35926660017327 + ], + [ + 10.20358707103469, + 28.059864445345738 + ], + [ + 11.904184916207214, + 27.209565522759533 + ], + [ + 12.754483838793476, + 25.508967677587066 + ], + [ + 12.754483838793476, + 21.25747306465587 + ], + [ + 11.904184916207214, + 16.155679529138467 + ], + [ + 10.20358707103469, + 11.053885993621066 + ], + [ + 8.50298922586262, + 6.802391380689869 + ], + [ + 5.952092458103834, + 3.4011956903449345 + ], + [ + 4.25149461293131, + 0.850298922586262 + ], + [ + 3.401195690345048, + 4.251494612931197 + ], + [ + 3.401195690345048, + 10.20358707103486 + ], + [ + 4.25149461293131, + 17.856277374310935 + ], + [ + 5.952092458103834, + 23.8083698324146 + ], + [ + 7.6526903032763585, + 27.209565522759533 + ], + [ + 11.053885993620952, + 28.910163367932 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 13.604782761379738, + 28.059864445345738 + ], + [ + 13.604782761379738, + 24.658668755000804 + ], + [ + 12.754483838793476, + 21.25747306465587 + ], + [ + 11.904184916207214, + 16.155679529138467 + ], + [ + 9.353288148448883, + 10.20358707103486 + ], + [ + 7.6526903032763585, + 6.802391380689869 + ], + [ + 5.952092458103834, + 5.952092458103664 + ], + [ + 5.101793535517572, + 5.101793535517402 + ], + [ + 5.101793535517572, + 5.952092458103664 + ], + [ + 5.952092458103834, + 9.353288148448598 + ], + [ + 7.6526903032763585, + 14.455081683966 + ], + [ + 8.50298922586262, + 19.556875219483402 + ], + [ + 9.353288148448883, + 23.8083698324146 + ], + [ + 10.20358707103469, + 25.508967677587066 + ], + [ + 10.20358707103469, + 26.35926660017327 + ], + [ + 10.20358707103469, + 25.508967677587066 + ], + [ + 10.20358707103469, + 22.958070909828336 + ], + [ + 8.50298922586262, + 18.706576296897197 + ], + [ + 7.6526903032763585, + 14.455081683966 + ], + [ + 5.952092458103834, + 10.20358707103486 + ], + [ + 5.101793535517572, + 6.802391380689869 + ], + [ + 5.101793535517572, + 5.952092458103664 + ], + [ + 5.101793535517572, + 6.802391380689869 + ], + [ + 5.952092458103834, + 10.20358707103486 + ], + [ + 9.353288148448883, + 17.00597845172473 + ], + [ + 10.20358707103469, + 18.706576296897197 + ], + [ + 11.904184916207214, + 24.658668755000804 + ], + [ + 12.754483838793476, + 27.209565522759533 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 12.754483838793476, + 27.209565522759533 + ], + [ + 11.904184916207214, + 23.8083698324146 + ], + [ + 11.053885993620952, + 19.556875219483402 + ], + [ + 10.20358707103469, + 17.856277374310935 + ], + [ + 9.353288148448883, + 13.604782761379795 + ], + [ + 7.6526903032763585, + 10.20358707103486 + ], + [ + 6.802391380690096, + 7.652690303276131 + ], + [ + 6.802391380690096, + 5.952092458103664 + ], + [ + 5.952092458103834, + 5.101793535517402 + ], + [ + 5.952092458103834, + 4.251494612931197 + ], + [ + 5.952092458103834, + 3.4011956903449345 + ], + [ + 5.952092458103834, + 2.5508967677587293 + ], + [ + 5.952092458103834, + 1.7005978451724673 + ], + [ + 5.952092458103834, + 0 + ], + [ + 6.802391380690096, + 0.850298922586262 + ], + [ + 7.6526903032763585, + 3.4011956903449345 + ], + [ + 8.50298922586262, + 3.4011956903449345 + ], + [ + 11.904184916207214, + 10.20358707103486 + ], + [ + 14.455081683966, + 16.155679529138467 + ], + [ + 17.005978451724786, + 21.25747306465587 + ], + [ + 17.85627737431105, + 23.8083698324146 + ], + [ + 17.85627737431105, + 25.508967677587066 + ], + [ + 17.85627737431105, + 27.209565522759533 + ], + [ + 17.005978451724786, + 28.059864445345738 + ], + [ + 15.305380606552262, + 28.910163367932 + ], + [ + 13.604782761379738, + 28.910163367932 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 11.053885993620952, + 29.760462290518205 + ], + [ + 9.353288148448883, + 29.760462290518205 + ], + [ + 8.50298922586262, + 29.760462290518205 + ], + [ + 9.353288148448883, + 29.760462290518205 + ], + [ + 10.20358707103469, + 28.910163367932 + ], + [ + 11.053885993620952, + 28.059864445345738 + ], + [ + 11.053885993620952, + 27.209565522759533 + ], + [ + 11.053885993620952, + 26.35926660017327 + ], + [ + 11.053885993620952, + 27.209565522759533 + ], + [ + 10.20358707103469, + 28.059864445345738 + ], + [ + 9.353288148448883, + 28.059864445345738 + ], + [ + 8.50298922586262, + 28.910163367932 + ], + [ + 7.6526903032763585, + 29.760462290518205 + ], + [ + 6.802391380690096, + 30.610761213104468 + ], + [ + 5.952092458103834, + 30.610761213104468 + ], + [ + 5.101793535517572, + 29.760462290518205 + ], + [ + 4.25149461293131, + 28.910163367932 + ], + [ + 3.401195690345048, + 28.910163367932 + ], + [ + 3.401195690345048, + 28.910163367932 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 59, + "versionNonce": 353841518, + "isDeleted": false, + "id": "uqoBU26ODWakgNfJX_Nwf", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3147.183419039845, + "y": 501.16297305429333, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 13.604782761379738, + "height": 4.251494612931197, + "seed": 1948428142, + "groupIds": [ + "2OJnQJZYhNFL3bSBzq363" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167818249, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.850298922586262, + 0.850298922586262 + ], + [ + -0.850298922586262, + -0.8502989225862052 + ], + [ + -2.550896767758786, + -1.7005978451724673 + ], + [ + -4.25149461293131, + -2.5508967677587293 + ], + [ + -5.101793535517118, + -2.5508967677587293 + ], + [ + -7.652690303275904, + -3.4011956903449345 + ], + [ + -9.353288148448428, + -3.4011956903449345 + ], + [ + -11.053885993620952, + -3.4011956903449345 + ], + [ + -11.904184916207214, + -3.4011956903449345 + ], + [ + -12.754483838793476, + -3.4011956903449345 + ], + [ + -12.754483838793476, + -2.5508967677587293 + ], + [ + -11.904184916207214, + -1.7005978451724673 + ], + [ + -11.904184916207214, + -1.7005978451724673 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 59, + "versionNonce": 1514853294, + "isDeleted": false, + "id": "URY3RSmeHfQYL1o3_6jcO", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3153.1355114979488, + "y": 510.51626120274193, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 11.053885993620952, + "height": 6.802391380689869, + "seed": 1345354670, + "groupIds": [ + "2OJnQJZYhNFL3bSBzq363" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167818249, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + 0 + ], + [ + -3.401195690345048, + 0 + ], + [ + -5.101793535517572, + 0 + ], + [ + -5.952092458103834, + 0 + ], + [ + -8.50298922586262, + 0.8502989225862052 + ], + [ + -9.353288148448883, + 2.5508967677586725 + ], + [ + -10.203587071035145, + 2.5508967677586725 + ], + [ + -11.053885993620952, + 4.25149461293114 + ], + [ + -11.053885993620952, + 5.101793535517402 + ], + [ + -11.053885993620952, + 5.952092458103607 + ], + [ + -11.053885993620952, + 6.802391380689869 + ], + [ + -9.353288148448883, + 6.802391380689869 + ], + [ + -9.353288148448883, + 6.802391380689869 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 56, + "versionNonce": 1968188910, + "isDeleted": false, + "id": "-dXRDoTEw3R66fvDr4_y0", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3158.2373050334663, + "y": 521.570147196363, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 5.952092458103834, + "height": 9.353288148448542, + "seed": 1891207150, + "groupIds": [ + "2OJnQJZYhNFL3bSBzq363" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167818249, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.8502989225862052 + ], + [ + -0.850298922586262, + 1.7005978451724673 + ], + [ + -1.700597845172524, + 2.5508967677586725 + ], + [ + -4.25149461293131, + 5.952092458103607 + ], + [ + -5.101793535517572, + 7.652690303276074 + ], + [ + -5.952092458103834, + 8.502989225862336 + ], + [ + -5.952092458103834, + 9.353288148448542 + ], + [ + -5.101793535517572, + 8.502989225862336 + ], + [ + -4.25149461293131, + 8.502989225862336 + ], + [ + -4.25149461293131, + 8.502989225862336 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 55, + "versionNonce": 858800174, + "isDeleted": false, + "id": "RnIwEIituQRXc7WhWGy6D", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3167.5905931819148, + "y": 493.5102827510172, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 12.754483838793476, + "height": 9.353288148448542, + "seed": 978139698, + "groupIds": [ + "2OJnQJZYhNFL3bSBzq363" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167818249, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.8502989225862052 + ], + [ + 0.850298922586262, + -1.7005978451724673 + ], + [ + 2.550896767758786, + -3.4011956903449345 + ], + [ + 6.802391380689642, + -5.952092458103607 + ], + [ + 8.502989225862166, + -7.652690303276074 + ], + [ + 11.053885993620952, + -8.502989225862336 + ], + [ + 11.904184916207214, + -9.353288148448542 + ], + [ + 12.754483838793476, + -9.353288148448542 + ], + [ + 12.754483838793476, + -9.353288148448542 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 53, + "versionNonce": 1473514094, + "isDeleted": false, + "id": "LMllwqSUxvcHdWiUoE89p", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3176.943881330363, + "y": 501.16297305429333, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 14.455081683966, + "height": 2.5508967677587293, + "seed": 802162290, + "groupIds": [ + "2OJnQJZYhNFL3bSBzq363" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167818249, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.700597845172524, + -0.8502989225862052 + ], + [ + 4.25149461293131, + -1.7005978451724673 + ], + [ + 6.802391380690096, + -1.7005978451724673 + ], + [ + 7.6526903032763585, + -1.7005978451724673 + ], + [ + 12.754483838793476, + -2.5508967677587293 + ], + [ + 14.455081683966, + -2.5508967677587293 + ], + [ + 14.455081683966, + -2.5508967677587293 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 57, + "versionNonce": 1107594414, + "isDeleted": false, + "id": "tvi0-NRSL_EJTUqDrr5MF", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3179.494778098122, + "y": 509.66596228015567, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 17.005978451724786, + "height": 5.952092458103664, + "seed": 661194994, + "groupIds": [ + "2OJnQJZYhNFL3bSBzq363" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167818249, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.850298922586262, + 0 + ], + [ + 1.700597845172524, + 0.850298922586262 + ], + [ + 4.25149461293131, + 0.850298922586262 + ], + [ + 8.502989225862166, + 1.7005978451724673 + ], + [ + 11.053885993620952, + 2.5508967677587293 + ], + [ + 12.754483838793476, + 4.251494612931197 + ], + [ + 15.305380606552262, + 5.101793535517402 + ], + [ + 17.005978451724786, + 5.952092458103664 + ], + [ + 16.155679529138524, + 5.952092458103664 + ], + [ + 15.305380606552262, + 4.251494612931197 + ], + [ + 15.305380606552262, + 4.251494612931197 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 61, + "versionNonce": 258655982, + "isDeleted": false, + "id": "OFJMTsDxDpVDV0-d8R9DY", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3151.4349136527762, + "y": 468.8516139960164, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 23.808369832414428, + "height": 18.706576296897197, + "seed": 1778087666, + "groupIds": [ + "2OJnQJZYhNFL3bSBzq363" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167818249, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.850298922586262 + ], + [ + 0, + -2.5508967677587293 + ], + [ + 0, + -3.4011956903449345 + ], + [ + -0.850298922586262, + -7.652690303276131 + ], + [ + -3.401195690345048, + -10.203587071034804 + ], + [ + -5.952092458103834, + -13.604782761379738 + ], + [ + -8.50298922586262, + -16.155679529138467 + ], + [ + -11.904184916207214, + -17.856277374310935 + ], + [ + -15.305380606552262, + -18.706576296897197 + ], + [ + -18.70657629689731, + -18.706576296897197 + ], + [ + -20.407174142069834, + -18.706576296897197 + ], + [ + -22.958070909828166, + -18.706576296897197 + ], + [ + -23.808369832414428, + -17.856277374310935 + ], + [ + -22.958070909828166, + -17.856277374310935 + ], + [ + -22.958070909828166, + -17.856277374310935 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 58, + "versionNonce": 1083160878, + "isDeleted": false, + "id": "H547cM76o9gwZW4iBPiCu", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3160.7882018012247, + "y": 469.70191291860266, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 5.952092458103834, + "height": 19.55687521948346, + "seed": 1673318514, + "groupIds": [ + "2OJnQJZYhNFL3bSBzq363" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167818249, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -0.850298922586262, + -2.5508967677587293 + ], + [ + -0.850298922586262, + -5.952092458103664 + ], + [ + -0.850298922586262, + -6.802391380689926 + ], + [ + -0.850298922586262, + -11.904184916207328 + ], + [ + -0.850298922586262, + -15.305380606552262 + ], + [ + 0.850298922586262, + -17.00597845172473 + ], + [ + 2.550896767758786, + -18.706576296897197 + ], + [ + 4.25149461293131, + -18.706576296897197 + ], + [ + 4.25149461293131, + -19.55687521948346 + ], + [ + 5.101793535517572, + -19.55687521948346 + ], + [ + 5.101793535517572, + -19.55687521948346 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 197, + "versionNonce": 342375214, + "isDeleted": false, + "id": "JPow47A24eyUVZbjsn_V9", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.9055331641117128, + "x": 3306.744574515656, + "y": 637.7161494598589, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 14.455081683966, + "height": 14.455081683966, + "seed": 1248455214, + "groupIds": [ + "PX8TGmRrSCDCbMWRLjfGJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167816384, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.850298922586262 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -2.550896767758786, + -0.850298922586262 + ], + [ + -3.401195690345048, + -0.850298922586262 + ], + [ + -3.401195690345048, + 0 + ], + [ + -3.401195690345048, + 1.7005978451724673 + ], + [ + -2.550896767758786, + 3.4011956903449345 + ], + [ + -0.850298922586262, + 5.952092458103607 + ], + [ + 0.850298922586262, + 7.652690303276074 + ], + [ + 2.550896767758786, + 9.353288148448598 + ], + [ + 5.101793535517118, + 10.203587071034804 + ], + [ + 6.802391380689642, + 10.203587071034804 + ], + [ + 7.652690303275904, + 10.203587071034804 + ], + [ + 8.502989225862166, + 8.502989225862336 + ], + [ + 9.353288148448428, + 6.802391380689869 + ], + [ + 10.20358707103469, + 5.101793535517402 + ], + [ + 11.053885993620952, + 3.4011956903449345 + ], + [ + 11.053885993620952, + 1.7005978451724673 + ], + [ + 10.20358707103469, + 0 + ], + [ + 9.353288148448428, + -1.7005978451724673 + ], + [ + 7.652690303275904, + -2.5508967677587293 + ], + [ + 5.95209245810338, + -3.4011956903449345 + ], + [ + 2.550896767758786, + -4.251494612931197 + ], + [ + 0.850298922586262, + -4.251494612931197 + ], + [ + 0, + -4.251494612931197 + ], + [ + -0.850298922586262, + -4.251494612931197 + ], + [ + -0.850298922586262, + -2.5508967677587293 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -0.850298922586262, + 1.7005978451724673 + ], + [ + 0, + 4.25149461293114 + ], + [ + 1.700597845172524, + 6.802391380689869 + ], + [ + 3.4011956903445935, + 7.652690303276074 + ], + [ + 5.101793535517118, + 8.502989225862336 + ], + [ + 5.95209245810338, + 8.502989225862336 + ], + [ + 7.652690303275904, + 7.652690303276074 + ], + [ + 7.652690303275904, + 5.101793535517402 + ], + [ + 7.652690303275904, + 2.5508967677586725 + ], + [ + 7.652690303275904, + 0 + ], + [ + 5.95209245810338, + -1.7005978451724673 + ], + [ + 5.101793535517118, + -2.5508967677587293 + ], + [ + 5.101793535517118, + -3.4011956903449345 + ], + [ + 4.2514946129308555, + -3.4011956903449345 + ], + [ + 3.4011956903445935, + -1.7005978451724673 + ], + [ + 1.700597845172524, + 0 + ], + [ + 0.850298922586262, + 1.7005978451724673 + ], + [ + 0.850298922586262, + 3.4011956903449345 + ], + [ + 0.850298922586262, + 5.101793535517402 + ], + [ + 1.700597845172524, + 5.101793535517402 + ], + [ + 3.4011956903445935, + 5.952092458103607 + ], + [ + 5.101793535517118, + 5.101793535517402 + ], + [ + 5.95209245810338, + 4.25149461293114 + ], + [ + 6.802391380689642, + 1.7005978451724673 + ], + [ + 5.101793535517118, + -0.850298922586262 + ], + [ + 3.4011956903445935, + -1.7005978451724673 + ], + [ + 0.850298922586262, + -1.7005978451724673 + ], + [ + 0, + 0 + ], + [ + -0.850298922586262, + 2.5508967677586725 + ], + [ + 1.700597845172524, + 4.25149461293114 + ], + [ + 5.101793535517118, + 5.952092458103607 + ], + [ + 8.502989225862166, + 5.101793535517402 + ], + [ + 8.502989225862166, + 4.25149461293114 + ], + [ + 6.802391380689642, + 3.4011956903449345 + ], + [ + 6.802391380689642, + 3.4011956903449345 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 406, + "versionNonce": 1716823406, + "isDeleted": false, + "id": "OWll8LCvzDF4NXyGymvFL", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.9055331641117128, + "x": 3279.7429120003917, + "y": 618.6293048048182, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 31.461060135690786, + "height": 34.86225582603561, + "seed": 1850242158, + "groupIds": [ + "PX8TGmRrSCDCbMWRLjfGJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167816384, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + 0 + ], + [ + -1.700597845172524, + 0 + ], + [ + -2.550896767758786, + 0 + ], + [ + -5.101793535517118, + 0 + ], + [ + -6.802391380689642, + 0 + ], + [ + -8.502989225862166, + 0 + ], + [ + -9.353288148448428, + 0 + ], + [ + -9.353288148448428, + 0.850298922586262 + ], + [ + -10.20358707103469, + 3.4011956903449345 + ], + [ + -10.20358707103469, + 4.251494612931197 + ], + [ + -11.053885993620952, + 5.101793535517402 + ], + [ + -11.904184916207214, + 5.952092458103664 + ], + [ + -12.754483838793476, + 7.652690303276131 + ], + [ + -13.604782761379738, + 9.353288148448598 + ], + [ + -13.604782761379738, + 11.904184916207328 + ], + [ + -12.754483838793476, + 13.604782761379795 + ], + [ + -12.754483838793476, + 15.305380606552262 + ], + [ + -11.904184916207214, + 17.00597845172473 + ], + [ + -11.053885993620952, + 18.706576296897197 + ], + [ + -11.053885993620952, + 19.556875219483402 + ], + [ + -10.20358707103469, + 21.25747306465587 + ], + [ + -9.353288148448428, + 22.958070909828336 + ], + [ + -8.502989225862166, + 24.658668755000804 + ], + [ + -6.802391380689642, + 26.35926660017327 + ], + [ + -5.95209245810338, + 28.910163367932 + ], + [ + -4.2514946129308555, + 30.610761213104468 + ], + [ + -2.550896767758786, + 31.46106013569073 + ], + [ + 0, + 32.311359058276935 + ], + [ + 1.700597845172524, + 33.1616579808632 + ], + [ + 4.25149461293131, + 33.1616579808632 + ], + [ + 5.952092458103834, + 34.0119569034494 + ], + [ + 8.50298922586262, + 34.0119569034494 + ], + [ + 10.20358707103469, + 33.1616579808632 + ], + [ + 12.754483838793476, + 31.46106013569073 + ], + [ + 14.455081683966, + 29.760462290518205 + ], + [ + 16.155679529138524, + 28.059864445345738 + ], + [ + 17.005978451724786, + 26.35926660017327 + ], + [ + 17.85627737431105, + 24.658668755000804 + ], + [ + 17.85627737431105, + 22.10777198724213 + ], + [ + 17.85627737431105, + 19.556875219483402 + ], + [ + 17.005978451724786, + 17.00597845172473 + ], + [ + 16.155679529138524, + 15.305380606552262 + ], + [ + 14.455081683966, + 11.904184916207328 + ], + [ + 13.604782761379738, + 8.502989225862336 + ], + [ + 12.754483838793476, + 6.802391380689869 + ], + [ + 11.904184916207214, + 4.251494612931197 + ], + [ + 11.053885993620952, + 2.5508967677587293 + ], + [ + 10.20358707103469, + 1.7005978451724673 + ], + [ + 9.353288148448883, + 0.850298922586262 + ], + [ + 7.6526903032763585, + 0 + ], + [ + 5.952092458103834, + 0 + ], + [ + 5.101793535517572, + -0.8502989225862052 + ], + [ + 3.401195690345048, + -0.8502989225862052 + ], + [ + 1.700597845172524, + -0.8502989225862052 + ], + [ + 0.850298922586262, + 0 + ], + [ + 0, + 0 + ], + [ + -1.700597845172524, + 0.850298922586262 + ], + [ + -2.550896767758786, + 1.7005978451724673 + ], + [ + -3.4011956903445935, + 3.4011956903449345 + ], + [ + -4.2514946129308555, + 6.802391380689869 + ], + [ + -4.2514946129308555, + 11.053885993621066 + ], + [ + -4.2514946129308555, + 15.305380606552262 + ], + [ + -3.4011956903445935, + 19.556875219483402 + ], + [ + -0.850298922586262, + 24.658668755000804 + ], + [ + 1.700597845172524, + 27.209565522759533 + ], + [ + 4.25149461293131, + 28.910163367932 + ], + [ + 6.802391380690096, + 28.910163367932 + ], + [ + 8.50298922586262, + 28.059864445345738 + ], + [ + 9.353288148448883, + 24.658668755000804 + ], + [ + 9.353288148448883, + 19.556875219483402 + ], + [ + 7.6526903032763585, + 15.305380606552262 + ], + [ + 5.101793535517572, + 10.20358707103486 + ], + [ + 2.550896767758786, + 6.802391380689869 + ], + [ + 0, + 4.251494612931197 + ], + [ + -0.850298922586262, + 3.4011956903449345 + ], + [ + -2.550896767758786, + 3.4011956903449345 + ], + [ + -4.2514946129308555, + 5.952092458103664 + ], + [ + -5.101793535517118, + 9.353288148448598 + ], + [ + -5.95209245810338, + 12.754483838793533 + ], + [ + -5.95209245810338, + 16.155679529138467 + ], + [ + -4.2514946129308555, + 19.556875219483402 + ], + [ + -2.550896767758786, + 21.25747306465587 + ], + [ + 0, + 22.10777198724213 + ], + [ + 1.700597845172524, + 21.25747306465587 + ], + [ + 2.550896767758786, + 19.556875219483402 + ], + [ + 2.550896767758786, + 16.155679529138467 + ], + [ + 0.850298922586262, + 11.904184916207328 + ], + [ + -2.550896767758786, + 6.802391380689869 + ], + [ + -5.101793535517118, + 3.4011956903449345 + ], + [ + -6.802391380689642, + 1.7005978451724673 + ], + [ + -8.502989225862166, + 1.7005978451724673 + ], + [ + -10.20358707103469, + 4.251494612931197 + ], + [ + -11.053885993620952, + 10.20358707103486 + ], + [ + -10.20358707103469, + 17.856277374310935 + ], + [ + -8.502989225862166, + 22.10777198724213 + ], + [ + -5.101793535517118, + 24.658668755000804 + ], + [ + -1.700597845172524, + 26.35926660017327 + ], + [ + 0.850298922586262, + 26.35926660017327 + ], + [ + 1.700597845172524, + 26.35926660017327 + ], + [ + 2.550896767758786, + 22.958070909828336 + ], + [ + 2.550896767758786, + 20.407174142069664 + ], + [ + 2.550896767758786, + 17.00597845172473 + ], + [ + 0.850298922586262, + 12.754483838793533 + ], + [ + -0.850298922586262, + 9.353288148448598 + ], + [ + -2.550896767758786, + 5.952092458103664 + ], + [ + -4.2514946129308555, + 5.101793535517402 + ], + [ + -5.101793535517118, + 5.101793535517402 + ], + [ + -5.101793535517118, + 7.652690303276131 + ], + [ + -5.95209245810338, + 11.053885993621066 + ], + [ + -5.95209245810338, + 15.305380606552262 + ], + [ + -5.101793535517118, + 19.556875219483402 + ], + [ + -4.2514946129308555, + 22.958070909828336 + ], + [ + -2.550896767758786, + 24.658668755000804 + ], + [ + -1.700597845172524, + 23.8083698324146 + ], + [ + -1.700597845172524, + 22.10777198724213 + ], + [ + -1.700597845172524, + 19.556875219483402 + ], + [ + -1.700597845172524, + 15.305380606552262 + ], + [ + -3.4011956903445935, + 11.053885993621066 + ], + [ + -6.802391380689642, + 5.952092458103664 + ], + [ + -7.652690303275904, + 5.101793535517402 + ], + [ + -8.502989225862166, + 5.952092458103664 + ], + [ + -9.353288148448428, + 9.353288148448598 + ], + [ + -10.20358707103469, + 14.455081683966 + ], + [ + -10.20358707103469, + 22.10777198724213 + ], + [ + -8.502989225862166, + 26.35926660017327 + ], + [ + -6.802391380689642, + 28.910163367932 + ], + [ + -3.4011956903445935, + 29.760462290518205 + ], + [ + -1.700597845172524, + 29.760462290518205 + ], + [ + 0, + 27.209565522759533 + ], + [ + 0, + 22.10777198724213 + ], + [ + -0.850298922586262, + 17.00597845172473 + ], + [ + -2.550896767758786, + 11.904184916207328 + ], + [ + -5.101793535517118, + 6.802391380689869 + ], + [ + -6.802391380689642, + 4.251494612931197 + ], + [ + -7.652690303275904, + 4.251494612931197 + ], + [ + -7.652690303275904, + 8.502989225862336 + ], + [ + -7.652690303275904, + 14.455081683966 + ], + [ + -5.101793535517118, + 22.10777198724213 + ], + [ + -1.700597845172524, + 27.209565522759533 + ], + [ + 1.700597845172524, + 30.610761213104468 + ], + [ + 5.101793535517572, + 31.46106013569073 + ], + [ + 6.802391380690096, + 30.610761213104468 + ], + [ + 7.6526903032763585, + 28.059864445345738 + ], + [ + 7.6526903032763585, + 24.658668755000804 + ], + [ + 6.802391380690096, + 19.556875219483402 + ], + [ + 5.101793535517572, + 14.455081683966 + ], + [ + 2.550896767758786, + 10.20358707103486 + ], + [ + -0.850298922586262, + 8.502989225862336 + ], + [ + -1.700597845172524, + 8.502989225862336 + ], + [ + -3.4011956903445935, + 11.053885993621066 + ], + [ + -4.2514946129308555, + 16.155679529138467 + ], + [ + -3.4011956903445935, + 21.25747306465587 + ], + [ + -1.700597845172524, + 24.658668755000804 + ], + [ + 0.850298922586262, + 27.209565522759533 + ], + [ + 3.401195690345048, + 27.209565522759533 + ], + [ + 5.101793535517572, + 26.35926660017327 + ], + [ + 5.952092458103834, + 24.658668755000804 + ], + [ + 6.802391380690096, + 21.25747306465587 + ], + [ + 6.802391380690096, + 17.00597845172473 + ], + [ + 5.101793535517572, + 10.20358707103486 + ], + [ + 3.401195690345048, + 6.802391380689869 + ], + [ + 1.700597845172524, + 4.251494612931197 + ], + [ + 0.850298922586262, + 4.251494612931197 + ], + [ + 0, + 5.952092458103664 + ], + [ + -0.850298922586262, + 10.20358707103486 + ], + [ + 0, + 17.00597845172473 + ], + [ + 0.850298922586262, + 22.10777198724213 + ], + [ + 2.550896767758786, + 25.508967677587066 + ], + [ + 5.101793535517572, + 26.35926660017327 + ], + [ + 5.952092458103834, + 26.35926660017327 + ], + [ + 7.6526903032763585, + 24.658668755000804 + ], + [ + 8.50298922586262, + 19.556875219483402 + ], + [ + 8.50298922586262, + 14.455081683966 + ], + [ + 5.952092458103834, + 8.502989225862336 + ], + [ + 4.25149461293131, + 3.4011956903449345 + ], + [ + 1.700597845172524, + 0.850298922586262 + ], + [ + 0.850298922586262, + 0.850298922586262 + ], + [ + 0, + 6.802391380689869 + ], + [ + 1.700597845172524, + 16.155679529138467 + ], + [ + 4.25149461293131, + 22.958070909828336 + ], + [ + 6.802391380690096, + 26.35926660017327 + ], + [ + 10.20358707103469, + 28.059864445345738 + ], + [ + 11.904184916207214, + 27.209565522759533 + ], + [ + 12.754483838793476, + 25.508967677587066 + ], + [ + 12.754483838793476, + 21.25747306465587 + ], + [ + 11.904184916207214, + 16.155679529138467 + ], + [ + 10.20358707103469, + 11.053885993621066 + ], + [ + 8.50298922586262, + 6.802391380689869 + ], + [ + 5.952092458103834, + 3.4011956903449345 + ], + [ + 4.25149461293131, + 0.850298922586262 + ], + [ + 3.401195690345048, + 4.251494612931197 + ], + [ + 3.401195690345048, + 10.20358707103486 + ], + [ + 4.25149461293131, + 17.856277374310935 + ], + [ + 5.952092458103834, + 23.8083698324146 + ], + [ + 7.6526903032763585, + 27.209565522759533 + ], + [ + 11.053885993620952, + 28.910163367932 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 13.604782761379738, + 28.059864445345738 + ], + [ + 13.604782761379738, + 24.658668755000804 + ], + [ + 12.754483838793476, + 21.25747306465587 + ], + [ + 11.904184916207214, + 16.155679529138467 + ], + [ + 9.353288148448883, + 10.20358707103486 + ], + [ + 7.6526903032763585, + 6.802391380689869 + ], + [ + 5.952092458103834, + 5.952092458103664 + ], + [ + 5.101793535517572, + 5.101793535517402 + ], + [ + 5.101793535517572, + 5.952092458103664 + ], + [ + 5.952092458103834, + 9.353288148448598 + ], + [ + 7.6526903032763585, + 14.455081683966 + ], + [ + 8.50298922586262, + 19.556875219483402 + ], + [ + 9.353288148448883, + 23.8083698324146 + ], + [ + 10.20358707103469, + 25.508967677587066 + ], + [ + 10.20358707103469, + 26.35926660017327 + ], + [ + 10.20358707103469, + 25.508967677587066 + ], + [ + 10.20358707103469, + 22.958070909828336 + ], + [ + 8.50298922586262, + 18.706576296897197 + ], + [ + 7.6526903032763585, + 14.455081683966 + ], + [ + 5.952092458103834, + 10.20358707103486 + ], + [ + 5.101793535517572, + 6.802391380689869 + ], + [ + 5.101793535517572, + 5.952092458103664 + ], + [ + 5.101793535517572, + 6.802391380689869 + ], + [ + 5.952092458103834, + 10.20358707103486 + ], + [ + 9.353288148448883, + 17.00597845172473 + ], + [ + 10.20358707103469, + 18.706576296897197 + ], + [ + 11.904184916207214, + 24.658668755000804 + ], + [ + 12.754483838793476, + 27.209565522759533 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 12.754483838793476, + 27.209565522759533 + ], + [ + 11.904184916207214, + 23.8083698324146 + ], + [ + 11.053885993620952, + 19.556875219483402 + ], + [ + 10.20358707103469, + 17.856277374310935 + ], + [ + 9.353288148448883, + 13.604782761379795 + ], + [ + 7.6526903032763585, + 10.20358707103486 + ], + [ + 6.802391380690096, + 7.652690303276131 + ], + [ + 6.802391380690096, + 5.952092458103664 + ], + [ + 5.952092458103834, + 5.101793535517402 + ], + [ + 5.952092458103834, + 4.251494612931197 + ], + [ + 5.952092458103834, + 3.4011956903449345 + ], + [ + 5.952092458103834, + 2.5508967677587293 + ], + [ + 5.952092458103834, + 1.7005978451724673 + ], + [ + 5.952092458103834, + 0 + ], + [ + 6.802391380690096, + 0.850298922586262 + ], + [ + 7.6526903032763585, + 3.4011956903449345 + ], + [ + 8.50298922586262, + 3.4011956903449345 + ], + [ + 11.904184916207214, + 10.20358707103486 + ], + [ + 14.455081683966, + 16.155679529138467 + ], + [ + 17.005978451724786, + 21.25747306465587 + ], + [ + 17.85627737431105, + 23.8083698324146 + ], + [ + 17.85627737431105, + 25.508967677587066 + ], + [ + 17.85627737431105, + 27.209565522759533 + ], + [ + 17.005978451724786, + 28.059864445345738 + ], + [ + 15.305380606552262, + 28.910163367932 + ], + [ + 13.604782761379738, + 28.910163367932 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 11.053885993620952, + 29.760462290518205 + ], + [ + 9.353288148448883, + 29.760462290518205 + ], + [ + 8.50298922586262, + 29.760462290518205 + ], + [ + 9.353288148448883, + 29.760462290518205 + ], + [ + 10.20358707103469, + 28.910163367932 + ], + [ + 11.053885993620952, + 28.059864445345738 + ], + [ + 11.053885993620952, + 27.209565522759533 + ], + [ + 11.053885993620952, + 26.35926660017327 + ], + [ + 11.053885993620952, + 27.209565522759533 + ], + [ + 10.20358707103469, + 28.059864445345738 + ], + [ + 9.353288148448883, + 28.059864445345738 + ], + [ + 8.50298922586262, + 28.910163367932 + ], + [ + 7.6526903032763585, + 29.760462290518205 + ], + [ + 6.802391380690096, + 30.610761213104468 + ], + [ + 5.952092458103834, + 30.610761213104468 + ], + [ + 5.101793535517572, + 29.760462290518205 + ], + [ + 4.25149461293131, + 28.910163367932 + ], + [ + 3.401195690345048, + 28.910163367932 + ], + [ + 3.401195690345048, + 28.910163367932 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 147, + "versionNonce": 196724654, + "isDeleted": false, + "id": "PTAQGJ2tIldrKiBNAlKGT", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.9055331641117128, + "x": 3301.64811806999, + "y": 617.4380359180356, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 13.604782761379738, + "height": 4.251494612931197, + "seed": 576435886, + "groupIds": [ + "PX8TGmRrSCDCbMWRLjfGJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167816384, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.850298922586262, + 0.850298922586262 + ], + [ + -0.850298922586262, + -0.8502989225862052 + ], + [ + -2.550896767758786, + -1.7005978451724673 + ], + [ + -4.25149461293131, + -2.5508967677587293 + ], + [ + -5.101793535517118, + -2.5508967677587293 + ], + [ + -7.652690303275904, + -3.4011956903449345 + ], + [ + -9.353288148448428, + -3.4011956903449345 + ], + [ + -11.053885993620952, + -3.4011956903449345 + ], + [ + -11.904184916207214, + -3.4011956903449345 + ], + [ + -12.754483838793476, + -3.4011956903449345 + ], + [ + -12.754483838793476, + -2.5508967677587293 + ], + [ + -11.904184916207214, + -1.7005978451724673 + ], + [ + -11.904184916207214, + -1.7005978451724673 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 147, + "versionNonce": 79733230, + "isDeleted": false, + "id": "mqmUKH7Yg0J7e8qskWn9p", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.9055331641117128, + "x": 3285.8766870656127, + "y": 614.175554306826, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 11.053885993620952, + "height": 6.802391380689869, + "seed": 1867297006, + "groupIds": [ + "PX8TGmRrSCDCbMWRLjfGJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167816384, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + 0 + ], + [ + -3.401195690345048, + 0 + ], + [ + -5.101793535517572, + 0 + ], + [ + -5.952092458103834, + 0 + ], + [ + -8.50298922586262, + 0.8502989225862052 + ], + [ + -9.353288148448883, + 2.5508967677586725 + ], + [ + -10.203587071035145, + 2.5508967677586725 + ], + [ + -11.053885993620952, + 4.25149461293114 + ], + [ + -11.053885993620952, + 5.101793535517402 + ], + [ + -11.053885993620952, + 5.952092458103607 + ], + [ + -11.053885993620952, + 6.802391380689869 + ], + [ + -9.353288148448883, + 6.802391380689869 + ], + [ + -9.353288148448883, + 6.802391380689869 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 144, + "versionNonce": 1685206062, + "isDeleted": false, + "id": "nqVKm6mrZTGHRyKee8uMq", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.9055331641117128, + "x": 3269.1667068718734, + "y": 616.0776067657694, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 5.952092458103834, + "height": 9.353288148448542, + "seed": 1621386030, + "groupIds": [ + "PX8TGmRrSCDCbMWRLjfGJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167816384, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.8502989225862052 + ], + [ + -0.850298922586262, + 1.7005978451724673 + ], + [ + -1.700597845172524, + 2.5508967677586725 + ], + [ + -4.25149461293131, + 5.952092458103607 + ], + [ + -5.101793535517572, + 7.652690303276074 + ], + [ + -5.952092458103834, + 8.502989225862336 + ], + [ + -5.952092458103834, + 9.353288148448542 + ], + [ + -5.101793535517572, + 8.502989225862336 + ], + [ + -4.25149461293131, + 8.502989225862336 + ], + [ + -4.25149461293131, + 8.502989225862336 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 143, + "versionNonce": 1958182510, + "isDeleted": false, + "id": "BKyGUqo3egzW602I1AwDT", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.9055331641117128, + "x": 3289.0045253673748, + "y": 655.3901885751138, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 12.754483838793476, + "height": 9.353288148448542, + "seed": 684445038, + "groupIds": [ + "PX8TGmRrSCDCbMWRLjfGJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167816384, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.8502989225862052 + ], + [ + 0.850298922586262, + -1.7005978451724673 + ], + [ + 2.550896767758786, + -3.4011956903449345 + ], + [ + 6.802391380689642, + -5.952092458103607 + ], + [ + 8.502989225862166, + -7.652690303276074 + ], + [ + 11.053885993620952, + -8.502989225862336 + ], + [ + 11.904184916207214, + -9.353288148448542 + ], + [ + 12.754483838793476, + -9.353288148448542 + ], + [ + 12.754483838793476, + -9.353288148448542 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 141, + "versionNonce": 1515815086, + "isDeleted": false, + "id": "fY03Nr-__e4wjLANtO7-k", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.9055331641117128, + "x": 3274.3617777314385, + "y": 657.9948186536187, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 14.455081683966, + "height": 2.5508967677587293, + "seed": 1158410158, + "groupIds": [ + "PX8TGmRrSCDCbMWRLjfGJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167816384, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.700597845172524, + -0.8502989225862052 + ], + [ + 4.25149461293131, + -1.7005978451724673 + ], + [ + 6.802391380690096, + -1.7005978451724673 + ], + [ + 7.6526903032763585, + -1.7005978451724673 + ], + [ + 12.754483838793476, + -2.5508967677587293 + ], + [ + 14.455081683966, + -2.5508967677587293 + ], + [ + 14.455081683966, + -2.5508967677587293 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 145, + "versionNonce": 956381934, + "isDeleted": false, + "id": "RoDLn0tRrak3-_J9mVn11", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.9055331641117128, + "x": 3259.782726917722, + "y": 653.1671837779793, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 17.005978451724786, + "height": 5.952092458103664, + "seed": 562940398, + "groupIds": [ + "PX8TGmRrSCDCbMWRLjfGJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167816384, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.850298922586262, + 0 + ], + [ + 1.700597845172524, + 0.850298922586262 + ], + [ + 4.25149461293131, + 0.850298922586262 + ], + [ + 8.502989225862166, + 1.7005978451724673 + ], + [ + 11.053885993620952, + 2.5508967677587293 + ], + [ + 12.754483838793476, + 4.251494612931197 + ], + [ + 15.305380606552262, + 5.101793535517402 + ], + [ + 17.005978451724786, + 5.952092458103664 + ], + [ + 16.155679529138524, + 5.952092458103664 + ], + [ + 15.305380606552262, + 4.251494612931197 + ], + [ + 15.305380606552262, + 4.251494612931197 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 149, + "versionNonce": 1463266606, + "isDeleted": false, + "id": "2vtA1iyJCmoBWX9QVhKCh", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.9055331641117128, + "x": 3346.3063609046685, + "y": 637.1783513035119, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 23.808369832414428, + "height": 18.706576296897197, + "seed": 1029332014, + "groupIds": [ + "PX8TGmRrSCDCbMWRLjfGJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167816384, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.850298922586262 + ], + [ + 0, + -2.5508967677587293 + ], + [ + 0, + -3.4011956903449345 + ], + [ + -0.850298922586262, + -7.652690303276131 + ], + [ + -3.401195690345048, + -10.203587071034804 + ], + [ + -5.952092458103834, + -13.604782761379738 + ], + [ + -8.50298922586262, + -16.155679529138467 + ], + [ + -11.904184916207214, + -17.856277374310935 + ], + [ + -15.305380606552262, + -18.706576296897197 + ], + [ + -18.70657629689731, + -18.706576296897197 + ], + [ + -20.407174142069834, + -18.706576296897197 + ], + [ + -22.958070909828166, + -18.706576296897197 + ], + [ + -23.808369832414428, + -17.856277374310935 + ], + [ + -22.958070909828166, + -17.856277374310935 + ], + [ + -22.958070909828166, + -17.856277374310935 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 146, + "versionNonce": 1113970542, + "isDeleted": false, + "id": "dqummOedFkTcKmoD4xsX7", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 1.9055331641117128, + "x": 3324.193006208222, + "y": 659.5492071352871, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 5.952092458103834, + "height": 19.55687521948346, + "seed": 1314563694, + "groupIds": [ + "PX8TGmRrSCDCbMWRLjfGJ" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167816384, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -0.850298922586262, + -2.5508967677587293 + ], + [ + -0.850298922586262, + -5.952092458103664 + ], + [ + -0.850298922586262, + -6.802391380689926 + ], + [ + -0.850298922586262, + -11.904184916207328 + ], + [ + -0.850298922586262, + -15.305380606552262 + ], + [ + 0.850298922586262, + -17.00597845172473 + ], + [ + 2.550896767758786, + -18.706576296897197 + ], + [ + 4.25149461293131, + -18.706576296897197 + ], + [ + 4.25149461293131, + -19.55687521948346 + ], + [ + 5.101793535517572, + -19.55687521948346 + ], + [ + 5.101793535517572, + -19.55687521948346 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 166, + "versionNonce": 1221050606, + "isDeleted": false, + "id": "rGtK5LeuBqtkemJ60nsJq", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.8894981015070558, + "x": 3337.022797875674, + "y": 494.12479665427503, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 14.455081683966, + "height": 14.455081683966, + "seed": 370969266, + "groupIds": [ + "1plcW9JNzADbXkFtCromn" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167810251, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.850298922586262 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -2.550896767758786, + -0.850298922586262 + ], + [ + -3.401195690345048, + -0.850298922586262 + ], + [ + -3.401195690345048, + 0 + ], + [ + -3.401195690345048, + 1.7005978451724673 + ], + [ + -2.550896767758786, + 3.4011956903449345 + ], + [ + -0.850298922586262, + 5.952092458103607 + ], + [ + 0.850298922586262, + 7.652690303276074 + ], + [ + 2.550896767758786, + 9.353288148448598 + ], + [ + 5.101793535517118, + 10.203587071034804 + ], + [ + 6.802391380689642, + 10.203587071034804 + ], + [ + 7.652690303275904, + 10.203587071034804 + ], + [ + 8.502989225862166, + 8.502989225862336 + ], + [ + 9.353288148448428, + 6.802391380689869 + ], + [ + 10.20358707103469, + 5.101793535517402 + ], + [ + 11.053885993620952, + 3.4011956903449345 + ], + [ + 11.053885993620952, + 1.7005978451724673 + ], + [ + 10.20358707103469, + 0 + ], + [ + 9.353288148448428, + -1.7005978451724673 + ], + [ + 7.652690303275904, + -2.5508967677587293 + ], + [ + 5.95209245810338, + -3.4011956903449345 + ], + [ + 2.550896767758786, + -4.251494612931197 + ], + [ + 0.850298922586262, + -4.251494612931197 + ], + [ + 0, + -4.251494612931197 + ], + [ + -0.850298922586262, + -4.251494612931197 + ], + [ + -0.850298922586262, + -2.5508967677587293 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -0.850298922586262, + 1.7005978451724673 + ], + [ + 0, + 4.25149461293114 + ], + [ + 1.700597845172524, + 6.802391380689869 + ], + [ + 3.4011956903445935, + 7.652690303276074 + ], + [ + 5.101793535517118, + 8.502989225862336 + ], + [ + 5.95209245810338, + 8.502989225862336 + ], + [ + 7.652690303275904, + 7.652690303276074 + ], + [ + 7.652690303275904, + 5.101793535517402 + ], + [ + 7.652690303275904, + 2.5508967677586725 + ], + [ + 7.652690303275904, + 0 + ], + [ + 5.95209245810338, + -1.7005978451724673 + ], + [ + 5.101793535517118, + -2.5508967677587293 + ], + [ + 5.101793535517118, + -3.4011956903449345 + ], + [ + 4.2514946129308555, + -3.4011956903449345 + ], + [ + 3.4011956903445935, + -1.7005978451724673 + ], + [ + 1.700597845172524, + 0 + ], + [ + 0.850298922586262, + 1.7005978451724673 + ], + [ + 0.850298922586262, + 3.4011956903449345 + ], + [ + 0.850298922586262, + 5.101793535517402 + ], + [ + 1.700597845172524, + 5.101793535517402 + ], + [ + 3.4011956903445935, + 5.952092458103607 + ], + [ + 5.101793535517118, + 5.101793535517402 + ], + [ + 5.95209245810338, + 4.25149461293114 + ], + [ + 6.802391380689642, + 1.7005978451724673 + ], + [ + 5.101793535517118, + -0.850298922586262 + ], + [ + 3.4011956903445935, + -1.7005978451724673 + ], + [ + 0.850298922586262, + -1.7005978451724673 + ], + [ + 0, + 0 + ], + [ + -0.850298922586262, + 2.5508967677586725 + ], + [ + 1.700597845172524, + 4.25149461293114 + ], + [ + 5.101793535517118, + 5.952092458103607 + ], + [ + 8.502989225862166, + 5.101793535517402 + ], + [ + 8.502989225862166, + 4.25149461293114 + ], + [ + 6.802391380689642, + 3.4011956903449345 + ], + [ + 6.802391380689642, + 3.4011956903449345 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 375, + "versionNonce": 2017293102, + "isDeleted": false, + "id": "2mpiTS-n3nQ4Ag5RFvY9q", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.8894981015070558, + "x": 3318.9448597618243, + "y": 502.0300642815394, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 31.461060135690786, + "height": 34.86225582603561, + "seed": 123495538, + "groupIds": [ + "1plcW9JNzADbXkFtCromn" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167810251, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + 0 + ], + [ + -1.700597845172524, + 0 + ], + [ + -2.550896767758786, + 0 + ], + [ + -5.101793535517118, + 0 + ], + [ + -6.802391380689642, + 0 + ], + [ + -8.502989225862166, + 0 + ], + [ + -9.353288148448428, + 0 + ], + [ + -9.353288148448428, + 0.850298922586262 + ], + [ + -10.20358707103469, + 3.4011956903449345 + ], + [ + -10.20358707103469, + 4.251494612931197 + ], + [ + -11.053885993620952, + 5.101793535517402 + ], + [ + -11.904184916207214, + 5.952092458103664 + ], + [ + -12.754483838793476, + 7.652690303276131 + ], + [ + -13.604782761379738, + 9.353288148448598 + ], + [ + -13.604782761379738, + 11.904184916207328 + ], + [ + -12.754483838793476, + 13.604782761379795 + ], + [ + -12.754483838793476, + 15.305380606552262 + ], + [ + -11.904184916207214, + 17.00597845172473 + ], + [ + -11.053885993620952, + 18.706576296897197 + ], + [ + -11.053885993620952, + 19.556875219483402 + ], + [ + -10.20358707103469, + 21.25747306465587 + ], + [ + -9.353288148448428, + 22.958070909828336 + ], + [ + -8.502989225862166, + 24.658668755000804 + ], + [ + -6.802391380689642, + 26.35926660017327 + ], + [ + -5.95209245810338, + 28.910163367932 + ], + [ + -4.2514946129308555, + 30.610761213104468 + ], + [ + -2.550896767758786, + 31.46106013569073 + ], + [ + 0, + 32.311359058276935 + ], + [ + 1.700597845172524, + 33.1616579808632 + ], + [ + 4.25149461293131, + 33.1616579808632 + ], + [ + 5.952092458103834, + 34.0119569034494 + ], + [ + 8.50298922586262, + 34.0119569034494 + ], + [ + 10.20358707103469, + 33.1616579808632 + ], + [ + 12.754483838793476, + 31.46106013569073 + ], + [ + 14.455081683966, + 29.760462290518205 + ], + [ + 16.155679529138524, + 28.059864445345738 + ], + [ + 17.005978451724786, + 26.35926660017327 + ], + [ + 17.85627737431105, + 24.658668755000804 + ], + [ + 17.85627737431105, + 22.10777198724213 + ], + [ + 17.85627737431105, + 19.556875219483402 + ], + [ + 17.005978451724786, + 17.00597845172473 + ], + [ + 16.155679529138524, + 15.305380606552262 + ], + [ + 14.455081683966, + 11.904184916207328 + ], + [ + 13.604782761379738, + 8.502989225862336 + ], + [ + 12.754483838793476, + 6.802391380689869 + ], + [ + 11.904184916207214, + 4.251494612931197 + ], + [ + 11.053885993620952, + 2.5508967677587293 + ], + [ + 10.20358707103469, + 1.7005978451724673 + ], + [ + 9.353288148448883, + 0.850298922586262 + ], + [ + 7.6526903032763585, + 0 + ], + [ + 5.952092458103834, + 0 + ], + [ + 5.101793535517572, + -0.8502989225862052 + ], + [ + 3.401195690345048, + -0.8502989225862052 + ], + [ + 1.700597845172524, + -0.8502989225862052 + ], + [ + 0.850298922586262, + 0 + ], + [ + 0, + 0 + ], + [ + -1.700597845172524, + 0.850298922586262 + ], + [ + -2.550896767758786, + 1.7005978451724673 + ], + [ + -3.4011956903445935, + 3.4011956903449345 + ], + [ + -4.2514946129308555, + 6.802391380689869 + ], + [ + -4.2514946129308555, + 11.053885993621066 + ], + [ + -4.2514946129308555, + 15.305380606552262 + ], + [ + -3.4011956903445935, + 19.556875219483402 + ], + [ + -0.850298922586262, + 24.658668755000804 + ], + [ + 1.700597845172524, + 27.209565522759533 + ], + [ + 4.25149461293131, + 28.910163367932 + ], + [ + 6.802391380690096, + 28.910163367932 + ], + [ + 8.50298922586262, + 28.059864445345738 + ], + [ + 9.353288148448883, + 24.658668755000804 + ], + [ + 9.353288148448883, + 19.556875219483402 + ], + [ + 7.6526903032763585, + 15.305380606552262 + ], + [ + 5.101793535517572, + 10.20358707103486 + ], + [ + 2.550896767758786, + 6.802391380689869 + ], + [ + 0, + 4.251494612931197 + ], + [ + -0.850298922586262, + 3.4011956903449345 + ], + [ + -2.550896767758786, + 3.4011956903449345 + ], + [ + -4.2514946129308555, + 5.952092458103664 + ], + [ + -5.101793535517118, + 9.353288148448598 + ], + [ + -5.95209245810338, + 12.754483838793533 + ], + [ + -5.95209245810338, + 16.155679529138467 + ], + [ + -4.2514946129308555, + 19.556875219483402 + ], + [ + -2.550896767758786, + 21.25747306465587 + ], + [ + 0, + 22.10777198724213 + ], + [ + 1.700597845172524, + 21.25747306465587 + ], + [ + 2.550896767758786, + 19.556875219483402 + ], + [ + 2.550896767758786, + 16.155679529138467 + ], + [ + 0.850298922586262, + 11.904184916207328 + ], + [ + -2.550896767758786, + 6.802391380689869 + ], + [ + -5.101793535517118, + 3.4011956903449345 + ], + [ + -6.802391380689642, + 1.7005978451724673 + ], + [ + -8.502989225862166, + 1.7005978451724673 + ], + [ + -10.20358707103469, + 4.251494612931197 + ], + [ + -11.053885993620952, + 10.20358707103486 + ], + [ + -10.20358707103469, + 17.856277374310935 + ], + [ + -8.502989225862166, + 22.10777198724213 + ], + [ + -5.101793535517118, + 24.658668755000804 + ], + [ + -1.700597845172524, + 26.35926660017327 + ], + [ + 0.850298922586262, + 26.35926660017327 + ], + [ + 1.700597845172524, + 26.35926660017327 + ], + [ + 2.550896767758786, + 22.958070909828336 + ], + [ + 2.550896767758786, + 20.407174142069664 + ], + [ + 2.550896767758786, + 17.00597845172473 + ], + [ + 0.850298922586262, + 12.754483838793533 + ], + [ + -0.850298922586262, + 9.353288148448598 + ], + [ + -2.550896767758786, + 5.952092458103664 + ], + [ + -4.2514946129308555, + 5.101793535517402 + ], + [ + -5.101793535517118, + 5.101793535517402 + ], + [ + -5.101793535517118, + 7.652690303276131 + ], + [ + -5.95209245810338, + 11.053885993621066 + ], + [ + -5.95209245810338, + 15.305380606552262 + ], + [ + -5.101793535517118, + 19.556875219483402 + ], + [ + -4.2514946129308555, + 22.958070909828336 + ], + [ + -2.550896767758786, + 24.658668755000804 + ], + [ + -1.700597845172524, + 23.8083698324146 + ], + [ + -1.700597845172524, + 22.10777198724213 + ], + [ + -1.700597845172524, + 19.556875219483402 + ], + [ + -1.700597845172524, + 15.305380606552262 + ], + [ + -3.4011956903445935, + 11.053885993621066 + ], + [ + -6.802391380689642, + 5.952092458103664 + ], + [ + -7.652690303275904, + 5.101793535517402 + ], + [ + -8.502989225862166, + 5.952092458103664 + ], + [ + -9.353288148448428, + 9.353288148448598 + ], + [ + -10.20358707103469, + 14.455081683966 + ], + [ + -10.20358707103469, + 22.10777198724213 + ], + [ + -8.502989225862166, + 26.35926660017327 + ], + [ + -6.802391380689642, + 28.910163367932 + ], + [ + -3.4011956903445935, + 29.760462290518205 + ], + [ + -1.700597845172524, + 29.760462290518205 + ], + [ + 0, + 27.209565522759533 + ], + [ + 0, + 22.10777198724213 + ], + [ + -0.850298922586262, + 17.00597845172473 + ], + [ + -2.550896767758786, + 11.904184916207328 + ], + [ + -5.101793535517118, + 6.802391380689869 + ], + [ + -6.802391380689642, + 4.251494612931197 + ], + [ + -7.652690303275904, + 4.251494612931197 + ], + [ + -7.652690303275904, + 8.502989225862336 + ], + [ + -7.652690303275904, + 14.455081683966 + ], + [ + -5.101793535517118, + 22.10777198724213 + ], + [ + -1.700597845172524, + 27.209565522759533 + ], + [ + 1.700597845172524, + 30.610761213104468 + ], + [ + 5.101793535517572, + 31.46106013569073 + ], + [ + 6.802391380690096, + 30.610761213104468 + ], + [ + 7.6526903032763585, + 28.059864445345738 + ], + [ + 7.6526903032763585, + 24.658668755000804 + ], + [ + 6.802391380690096, + 19.556875219483402 + ], + [ + 5.101793535517572, + 14.455081683966 + ], + [ + 2.550896767758786, + 10.20358707103486 + ], + [ + -0.850298922586262, + 8.502989225862336 + ], + [ + -1.700597845172524, + 8.502989225862336 + ], + [ + -3.4011956903445935, + 11.053885993621066 + ], + [ + -4.2514946129308555, + 16.155679529138467 + ], + [ + -3.4011956903445935, + 21.25747306465587 + ], + [ + -1.700597845172524, + 24.658668755000804 + ], + [ + 0.850298922586262, + 27.209565522759533 + ], + [ + 3.401195690345048, + 27.209565522759533 + ], + [ + 5.101793535517572, + 26.35926660017327 + ], + [ + 5.952092458103834, + 24.658668755000804 + ], + [ + 6.802391380690096, + 21.25747306465587 + ], + [ + 6.802391380690096, + 17.00597845172473 + ], + [ + 5.101793535517572, + 10.20358707103486 + ], + [ + 3.401195690345048, + 6.802391380689869 + ], + [ + 1.700597845172524, + 4.251494612931197 + ], + [ + 0.850298922586262, + 4.251494612931197 + ], + [ + 0, + 5.952092458103664 + ], + [ + -0.850298922586262, + 10.20358707103486 + ], + [ + 0, + 17.00597845172473 + ], + [ + 0.850298922586262, + 22.10777198724213 + ], + [ + 2.550896767758786, + 25.508967677587066 + ], + [ + 5.101793535517572, + 26.35926660017327 + ], + [ + 5.952092458103834, + 26.35926660017327 + ], + [ + 7.6526903032763585, + 24.658668755000804 + ], + [ + 8.50298922586262, + 19.556875219483402 + ], + [ + 8.50298922586262, + 14.455081683966 + ], + [ + 5.952092458103834, + 8.502989225862336 + ], + [ + 4.25149461293131, + 3.4011956903449345 + ], + [ + 1.700597845172524, + 0.850298922586262 + ], + [ + 0.850298922586262, + 0.850298922586262 + ], + [ + 0, + 6.802391380689869 + ], + [ + 1.700597845172524, + 16.155679529138467 + ], + [ + 4.25149461293131, + 22.958070909828336 + ], + [ + 6.802391380690096, + 26.35926660017327 + ], + [ + 10.20358707103469, + 28.059864445345738 + ], + [ + 11.904184916207214, + 27.209565522759533 + ], + [ + 12.754483838793476, + 25.508967677587066 + ], + [ + 12.754483838793476, + 21.25747306465587 + ], + [ + 11.904184916207214, + 16.155679529138467 + ], + [ + 10.20358707103469, + 11.053885993621066 + ], + [ + 8.50298922586262, + 6.802391380689869 + ], + [ + 5.952092458103834, + 3.4011956903449345 + ], + [ + 4.25149461293131, + 0.850298922586262 + ], + [ + 3.401195690345048, + 4.251494612931197 + ], + [ + 3.401195690345048, + 10.20358707103486 + ], + [ + 4.25149461293131, + 17.856277374310935 + ], + [ + 5.952092458103834, + 23.8083698324146 + ], + [ + 7.6526903032763585, + 27.209565522759533 + ], + [ + 11.053885993620952, + 28.910163367932 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 13.604782761379738, + 28.059864445345738 + ], + [ + 13.604782761379738, + 24.658668755000804 + ], + [ + 12.754483838793476, + 21.25747306465587 + ], + [ + 11.904184916207214, + 16.155679529138467 + ], + [ + 9.353288148448883, + 10.20358707103486 + ], + [ + 7.6526903032763585, + 6.802391380689869 + ], + [ + 5.952092458103834, + 5.952092458103664 + ], + [ + 5.101793535517572, + 5.101793535517402 + ], + [ + 5.101793535517572, + 5.952092458103664 + ], + [ + 5.952092458103834, + 9.353288148448598 + ], + [ + 7.6526903032763585, + 14.455081683966 + ], + [ + 8.50298922586262, + 19.556875219483402 + ], + [ + 9.353288148448883, + 23.8083698324146 + ], + [ + 10.20358707103469, + 25.508967677587066 + ], + [ + 10.20358707103469, + 26.35926660017327 + ], + [ + 10.20358707103469, + 25.508967677587066 + ], + [ + 10.20358707103469, + 22.958070909828336 + ], + [ + 8.50298922586262, + 18.706576296897197 + ], + [ + 7.6526903032763585, + 14.455081683966 + ], + [ + 5.952092458103834, + 10.20358707103486 + ], + [ + 5.101793535517572, + 6.802391380689869 + ], + [ + 5.101793535517572, + 5.952092458103664 + ], + [ + 5.101793535517572, + 6.802391380689869 + ], + [ + 5.952092458103834, + 10.20358707103486 + ], + [ + 9.353288148448883, + 17.00597845172473 + ], + [ + 10.20358707103469, + 18.706576296897197 + ], + [ + 11.904184916207214, + 24.658668755000804 + ], + [ + 12.754483838793476, + 27.209565522759533 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 12.754483838793476, + 27.209565522759533 + ], + [ + 11.904184916207214, + 23.8083698324146 + ], + [ + 11.053885993620952, + 19.556875219483402 + ], + [ + 10.20358707103469, + 17.856277374310935 + ], + [ + 9.353288148448883, + 13.604782761379795 + ], + [ + 7.6526903032763585, + 10.20358707103486 + ], + [ + 6.802391380690096, + 7.652690303276131 + ], + [ + 6.802391380690096, + 5.952092458103664 + ], + [ + 5.952092458103834, + 5.101793535517402 + ], + [ + 5.952092458103834, + 4.251494612931197 + ], + [ + 5.952092458103834, + 3.4011956903449345 + ], + [ + 5.952092458103834, + 2.5508967677587293 + ], + [ + 5.952092458103834, + 1.7005978451724673 + ], + [ + 5.952092458103834, + 0 + ], + [ + 6.802391380690096, + 0.850298922586262 + ], + [ + 7.6526903032763585, + 3.4011956903449345 + ], + [ + 8.50298922586262, + 3.4011956903449345 + ], + [ + 11.904184916207214, + 10.20358707103486 + ], + [ + 14.455081683966, + 16.155679529138467 + ], + [ + 17.005978451724786, + 21.25747306465587 + ], + [ + 17.85627737431105, + 23.8083698324146 + ], + [ + 17.85627737431105, + 25.508967677587066 + ], + [ + 17.85627737431105, + 27.209565522759533 + ], + [ + 17.005978451724786, + 28.059864445345738 + ], + [ + 15.305380606552262, + 28.910163367932 + ], + [ + 13.604782761379738, + 28.910163367932 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 11.053885993620952, + 29.760462290518205 + ], + [ + 9.353288148448883, + 29.760462290518205 + ], + [ + 8.50298922586262, + 29.760462290518205 + ], + [ + 9.353288148448883, + 29.760462290518205 + ], + [ + 10.20358707103469, + 28.910163367932 + ], + [ + 11.053885993620952, + 28.059864445345738 + ], + [ + 11.053885993620952, + 27.209565522759533 + ], + [ + 11.053885993620952, + 26.35926660017327 + ], + [ + 11.053885993620952, + 27.209565522759533 + ], + [ + 10.20358707103469, + 28.059864445345738 + ], + [ + 9.353288148448883, + 28.059864445345738 + ], + [ + 8.50298922586262, + 28.910163367932 + ], + [ + 7.6526903032763585, + 29.760462290518205 + ], + [ + 6.802391380690096, + 30.610761213104468 + ], + [ + 5.952092458103834, + 30.610761213104468 + ], + [ + 5.101793535517572, + 29.760462290518205 + ], + [ + 4.25149461293131, + 28.910163367932 + ], + [ + 3.401195690345048, + 28.910163367932 + ], + [ + 3.401195690345048, + 28.910163367932 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 116, + "versionNonce": 180542830, + "isDeleted": false, + "id": "OjRg4dUKWwPQ60tPgvs0U", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.8894981015070558, + "x": 3318.115218079147, + "y": 498.0996059717503, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 13.604782761379738, + "height": 4.251494612931197, + "seed": 2130380338, + "groupIds": [ + "1plcW9JNzADbXkFtCromn" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167810251, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.850298922586262, + 0.850298922586262 + ], + [ + -0.850298922586262, + -0.8502989225862052 + ], + [ + -2.550896767758786, + -1.7005978451724673 + ], + [ + -4.25149461293131, + -2.5508967677587293 + ], + [ + -5.101793535517118, + -2.5508967677587293 + ], + [ + -7.652690303275904, + -3.4011956903449345 + ], + [ + -9.353288148448428, + -3.4011956903449345 + ], + [ + -11.053885993620952, + -3.4011956903449345 + ], + [ + -11.904184916207214, + -3.4011956903449345 + ], + [ + -12.754483838793476, + -3.4011956903449345 + ], + [ + -12.754483838793476, + -2.5508967677587293 + ], + [ + -11.904184916207214, + -1.7005978451724673 + ], + [ + -11.904184916207214, + -1.7005978451724673 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 116, + "versionNonce": 1973485486, + "isDeleted": false, + "id": "V7oJPE8Y4Td4EgYHISrDT", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.8894981015070558, + "x": 3310.808637559909, + "y": 507.2126000097483, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 11.053885993620952, + "height": 6.802391380689869, + "seed": 1440706546, + "groupIds": [ + "1plcW9JNzADbXkFtCromn" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167810251, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + 0 + ], + [ + -3.401195690345048, + 0 + ], + [ + -5.101793535517572, + 0 + ], + [ + -5.952092458103834, + 0 + ], + [ + -8.50298922586262, + 0.8502989225862052 + ], + [ + -9.353288148448883, + 2.5508967677586725 + ], + [ + -10.203587071035145, + 2.5508967677586725 + ], + [ + -11.053885993620952, + 4.25149461293114 + ], + [ + -11.053885993620952, + 5.101793535517402 + ], + [ + -11.053885993620952, + 5.952092458103607 + ], + [ + -11.053885993620952, + 6.802391380689869 + ], + [ + -9.353288148448883, + 6.802391380689869 + ], + [ + -9.353288148448883, + 6.802391380689869 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 113, + "versionNonce": 486595054, + "isDeleted": false, + "id": "f9fJdrWiI7_jsA1RniHLP", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.8894981015070558, + "x": 3303.5005387767364, + "y": 519.6464617490885, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 5.952092458103834, + "height": 9.353288148448542, + "seed": 806396338, + "groupIds": [ + "1plcW9JNzADbXkFtCromn" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167810251, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.8502989225862052 + ], + [ + -0.850298922586262, + 1.7005978451724673 + ], + [ + -1.700597845172524, + 2.5508967677586725 + ], + [ + -4.25149461293131, + 5.952092458103607 + ], + [ + -5.101793535517572, + 7.652690303276074 + ], + [ + -5.952092458103834, + 8.502989225862336 + ], + [ + -5.952092458103834, + 9.353288148448542 + ], + [ + -5.101793535517572, + 8.502989225862336 + ], + [ + -4.25149461293131, + 8.502989225862336 + ], + [ + -4.25149461293131, + 8.502989225862336 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 112, + "versionNonce": 2011917358, + "isDeleted": false, + "id": "ZK7av7tOrQmyBI4HwknNj", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.8894981015070558, + "x": 3334.98957037038, + "y": 519.9673138007294, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 12.754483838793476, + "height": 9.353288148448542, + "seed": 414554994, + "groupIds": [ + "1plcW9JNzADbXkFtCromn" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167810251, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.8502989225862052 + ], + [ + 0.850298922586262, + -1.7005978451724673 + ], + [ + 2.550896767758786, + -3.4011956903449345 + ], + [ + 6.802391380689642, + -5.952092458103607 + ], + [ + 8.502989225862166, + -7.652690303276074 + ], + [ + 11.053885993620952, + -8.502989225862336 + ], + [ + 11.904184916207214, + -9.353288148448542 + ], + [ + 12.754483838793476, + -9.353288148448542 + ], + [ + 12.754483838793476, + -9.353288148448542 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 110, + "versionNonce": 877246062, + "isDeleted": false, + "id": "JdCj2oBoLtBkQZmjno66h", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.8894981015070558, + "x": 3331.9793410768175, + "y": 531.4535720723177, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 14.455081683966, + "height": 2.5508967677587293, + "seed": 504716594, + "groupIds": [ + "1plcW9JNzADbXkFtCromn" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167810251, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.700597845172524, + -0.8502989225862052 + ], + [ + 4.25149461293131, + -1.7005978451724673 + ], + [ + 6.802391380690096, + -1.7005978451724673 + ], + [ + 7.6526903032763585, + -1.7005978451724673 + ], + [ + 12.754483838793476, + -2.5508967677587293 + ], + [ + 14.455081683966, + -2.5508967677587293 + ], + [ + 14.455081683966, + -2.5508967677587293 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 114, + "versionNonce": 1524938926, + "isDeleted": false, + "id": "KMtipc3ad5RjomHORijQo", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.8894981015070558, + "x": 3323.2066137193024, + "y": 538.2070119528304, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 17.005978451724786, + "height": 5.952092458103664, + "seed": 1838606066, + "groupIds": [ + "1plcW9JNzADbXkFtCromn" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167810251, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.850298922586262, + 0 + ], + [ + 1.700597845172524, + 0.850298922586262 + ], + [ + 4.25149461293131, + 0.850298922586262 + ], + [ + 8.502989225862166, + 1.7005978451724673 + ], + [ + 11.053885993620952, + 2.5508967677587293 + ], + [ + 12.754483838793476, + 4.251494612931197 + ], + [ + 15.305380606552262, + 5.101793535517402 + ], + [ + 17.005978451724786, + 5.952092458103664 + ], + [ + 16.155679529138524, + 5.952092458103664 + ], + [ + 15.305380606552262, + 4.251494612931197 + ], + [ + 15.305380606552262, + 4.251494612931197 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 118, + "versionNonce": 1936028398, + "isDeleted": false, + "id": "570THfJOFjCSrGbGICzhr", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.8894981015070558, + "x": 3354.3688130556952, + "y": 479.4193000661293, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 23.808369832414428, + "height": 18.706576296897197, + "seed": 1874933938, + "groupIds": [ + "1plcW9JNzADbXkFtCromn" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167810251, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.850298922586262 + ], + [ + 0, + -2.5508967677587293 + ], + [ + 0, + -3.4011956903449345 + ], + [ + -0.850298922586262, + -7.652690303276131 + ], + [ + -3.401195690345048, + -10.203587071034804 + ], + [ + -5.952092458103834, + -13.604782761379738 + ], + [ + -8.50298922586262, + -16.155679529138467 + ], + [ + -11.904184916207214, + -17.856277374310935 + ], + [ + -15.305380606552262, + -18.706576296897197 + ], + [ + -18.70657629689731, + -18.706576296897197 + ], + [ + -20.407174142069834, + -18.706576296897197 + ], + [ + -22.958070909828166, + -18.706576296897197 + ], + [ + -23.808369832414428, + -17.856277374310935 + ], + [ + -22.958070909828166, + -17.856277374310935 + ], + [ + -22.958070909828166, + -17.856277374310935 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 115, + "versionNonce": 676652334, + "isDeleted": false, + "id": "ndB4TKOxIlpeuBEz1PRdE", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0.8894981015070558, + "x": 3354.7354415288255, + "y": 498.27526033761075, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 5.952092458103834, + "height": 19.55687521948346, + "seed": 1681609330, + "groupIds": [ + "1plcW9JNzADbXkFtCromn" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167810251, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -0.850298922586262, + -2.5508967677587293 + ], + [ + -0.850298922586262, + -5.952092458103664 + ], + [ + -0.850298922586262, + -6.802391380689926 + ], + [ + -0.850298922586262, + -11.904184916207328 + ], + [ + -0.850298922586262, + -15.305380606552262 + ], + [ + 0.850298922586262, + -17.00597845172473 + ], + [ + 2.550896767758786, + -18.706576296897197 + ], + [ + 4.25149461293131, + -18.706576296897197 + ], + [ + 4.25149461293131, + -19.55687521948346 + ], + [ + 5.101793535517572, + -19.55687521948346 + ], + [ + 5.101793535517572, + -19.55687521948346 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 144, + "versionNonce": 1154284910, + "isDeleted": false, + "id": "OA23uZVQHyPzhDC5XZsXQ", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 5.585282484584528, + "x": 3104.2962472794416, + "y": 661.814169075441, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 14.455081683966, + "height": 14.455081683966, + "seed": 1987014254, + "groupIds": [ + "ffzOuLtTdfZZzobRroJ5w" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167797886, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.850298922586262 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -2.550896767758786, + -0.850298922586262 + ], + [ + -3.401195690345048, + -0.850298922586262 + ], + [ + -3.401195690345048, + 0 + ], + [ + -3.401195690345048, + 1.7005978451724673 + ], + [ + -2.550896767758786, + 3.4011956903449345 + ], + [ + -0.850298922586262, + 5.952092458103607 + ], + [ + 0.850298922586262, + 7.652690303276074 + ], + [ + 2.550896767758786, + 9.353288148448598 + ], + [ + 5.101793535517118, + 10.203587071034804 + ], + [ + 6.802391380689642, + 10.203587071034804 + ], + [ + 7.652690303275904, + 10.203587071034804 + ], + [ + 8.502989225862166, + 8.502989225862336 + ], + [ + 9.353288148448428, + 6.802391380689869 + ], + [ + 10.20358707103469, + 5.101793535517402 + ], + [ + 11.053885993620952, + 3.4011956903449345 + ], + [ + 11.053885993620952, + 1.7005978451724673 + ], + [ + 10.20358707103469, + 0 + ], + [ + 9.353288148448428, + -1.7005978451724673 + ], + [ + 7.652690303275904, + -2.5508967677587293 + ], + [ + 5.95209245810338, + -3.4011956903449345 + ], + [ + 2.550896767758786, + -4.251494612931197 + ], + [ + 0.850298922586262, + -4.251494612931197 + ], + [ + 0, + -4.251494612931197 + ], + [ + -0.850298922586262, + -4.251494612931197 + ], + [ + -0.850298922586262, + -2.5508967677587293 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -0.850298922586262, + 1.7005978451724673 + ], + [ + 0, + 4.25149461293114 + ], + [ + 1.700597845172524, + 6.802391380689869 + ], + [ + 3.4011956903445935, + 7.652690303276074 + ], + [ + 5.101793535517118, + 8.502989225862336 + ], + [ + 5.95209245810338, + 8.502989225862336 + ], + [ + 7.652690303275904, + 7.652690303276074 + ], + [ + 7.652690303275904, + 5.101793535517402 + ], + [ + 7.652690303275904, + 2.5508967677586725 + ], + [ + 7.652690303275904, + 0 + ], + [ + 5.95209245810338, + -1.7005978451724673 + ], + [ + 5.101793535517118, + -2.5508967677587293 + ], + [ + 5.101793535517118, + -3.4011956903449345 + ], + [ + 4.2514946129308555, + -3.4011956903449345 + ], + [ + 3.4011956903445935, + -1.7005978451724673 + ], + [ + 1.700597845172524, + 0 + ], + [ + 0.850298922586262, + 1.7005978451724673 + ], + [ + 0.850298922586262, + 3.4011956903449345 + ], + [ + 0.850298922586262, + 5.101793535517402 + ], + [ + 1.700597845172524, + 5.101793535517402 + ], + [ + 3.4011956903445935, + 5.952092458103607 + ], + [ + 5.101793535517118, + 5.101793535517402 + ], + [ + 5.95209245810338, + 4.25149461293114 + ], + [ + 6.802391380689642, + 1.7005978451724673 + ], + [ + 5.101793535517118, + -0.850298922586262 + ], + [ + 3.4011956903445935, + -1.7005978451724673 + ], + [ + 0.850298922586262, + -1.7005978451724673 + ], + [ + 0, + 0 + ], + [ + -0.850298922586262, + 2.5508967677586725 + ], + [ + 1.700597845172524, + 4.25149461293114 + ], + [ + 5.101793535517118, + 5.952092458103607 + ], + [ + 8.502989225862166, + 5.101793535517402 + ], + [ + 8.502989225862166, + 4.25149461293114 + ], + [ + 6.802391380689642, + 3.4011956903449345 + ], + [ + 6.802391380689642, + 3.4011956903449345 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 353, + "versionNonce": 2023643250, + "isDeleted": false, + "id": "dUownSOjHMeFjugQ-Uq03", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 5.585282484584528, + "x": 3127.832329818663, + "y": 667.6280464269595, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 31.461060135690786, + "height": 34.86225582603561, + "seed": 1066627246, + "groupIds": [ + "ffzOuLtTdfZZzobRroJ5w" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167797886, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + 0 + ], + [ + -1.700597845172524, + 0 + ], + [ + -2.550896767758786, + 0 + ], + [ + -5.101793535517118, + 0 + ], + [ + -6.802391380689642, + 0 + ], + [ + -8.502989225862166, + 0 + ], + [ + -9.353288148448428, + 0 + ], + [ + -9.353288148448428, + 0.850298922586262 + ], + [ + -10.20358707103469, + 3.4011956903449345 + ], + [ + -10.20358707103469, + 4.251494612931197 + ], + [ + -11.053885993620952, + 5.101793535517402 + ], + [ + -11.904184916207214, + 5.952092458103664 + ], + [ + -12.754483838793476, + 7.652690303276131 + ], + [ + -13.604782761379738, + 9.353288148448598 + ], + [ + -13.604782761379738, + 11.904184916207328 + ], + [ + -12.754483838793476, + 13.604782761379795 + ], + [ + -12.754483838793476, + 15.305380606552262 + ], + [ + -11.904184916207214, + 17.00597845172473 + ], + [ + -11.053885993620952, + 18.706576296897197 + ], + [ + -11.053885993620952, + 19.556875219483402 + ], + [ + -10.20358707103469, + 21.25747306465587 + ], + [ + -9.353288148448428, + 22.958070909828336 + ], + [ + -8.502989225862166, + 24.658668755000804 + ], + [ + -6.802391380689642, + 26.35926660017327 + ], + [ + -5.95209245810338, + 28.910163367932 + ], + [ + -4.2514946129308555, + 30.610761213104468 + ], + [ + -2.550896767758786, + 31.46106013569073 + ], + [ + 0, + 32.311359058276935 + ], + [ + 1.700597845172524, + 33.1616579808632 + ], + [ + 4.25149461293131, + 33.1616579808632 + ], + [ + 5.952092458103834, + 34.0119569034494 + ], + [ + 8.50298922586262, + 34.0119569034494 + ], + [ + 10.20358707103469, + 33.1616579808632 + ], + [ + 12.754483838793476, + 31.46106013569073 + ], + [ + 14.455081683966, + 29.760462290518205 + ], + [ + 16.155679529138524, + 28.059864445345738 + ], + [ + 17.005978451724786, + 26.35926660017327 + ], + [ + 17.85627737431105, + 24.658668755000804 + ], + [ + 17.85627737431105, + 22.10777198724213 + ], + [ + 17.85627737431105, + 19.556875219483402 + ], + [ + 17.005978451724786, + 17.00597845172473 + ], + [ + 16.155679529138524, + 15.305380606552262 + ], + [ + 14.455081683966, + 11.904184916207328 + ], + [ + 13.604782761379738, + 8.502989225862336 + ], + [ + 12.754483838793476, + 6.802391380689869 + ], + [ + 11.904184916207214, + 4.251494612931197 + ], + [ + 11.053885993620952, + 2.5508967677587293 + ], + [ + 10.20358707103469, + 1.7005978451724673 + ], + [ + 9.353288148448883, + 0.850298922586262 + ], + [ + 7.6526903032763585, + 0 + ], + [ + 5.952092458103834, + 0 + ], + [ + 5.101793535517572, + -0.8502989225862052 + ], + [ + 3.401195690345048, + -0.8502989225862052 + ], + [ + 1.700597845172524, + -0.8502989225862052 + ], + [ + 0.850298922586262, + 0 + ], + [ + 0, + 0 + ], + [ + -1.700597845172524, + 0.850298922586262 + ], + [ + -2.550896767758786, + 1.7005978451724673 + ], + [ + -3.4011956903445935, + 3.4011956903449345 + ], + [ + -4.2514946129308555, + 6.802391380689869 + ], + [ + -4.2514946129308555, + 11.053885993621066 + ], + [ + -4.2514946129308555, + 15.305380606552262 + ], + [ + -3.4011956903445935, + 19.556875219483402 + ], + [ + -0.850298922586262, + 24.658668755000804 + ], + [ + 1.700597845172524, + 27.209565522759533 + ], + [ + 4.25149461293131, + 28.910163367932 + ], + [ + 6.802391380690096, + 28.910163367932 + ], + [ + 8.50298922586262, + 28.059864445345738 + ], + [ + 9.353288148448883, + 24.658668755000804 + ], + [ + 9.353288148448883, + 19.556875219483402 + ], + [ + 7.6526903032763585, + 15.305380606552262 + ], + [ + 5.101793535517572, + 10.20358707103486 + ], + [ + 2.550896767758786, + 6.802391380689869 + ], + [ + 0, + 4.251494612931197 + ], + [ + -0.850298922586262, + 3.4011956903449345 + ], + [ + -2.550896767758786, + 3.4011956903449345 + ], + [ + -4.2514946129308555, + 5.952092458103664 + ], + [ + -5.101793535517118, + 9.353288148448598 + ], + [ + -5.95209245810338, + 12.754483838793533 + ], + [ + -5.95209245810338, + 16.155679529138467 + ], + [ + -4.2514946129308555, + 19.556875219483402 + ], + [ + -2.550896767758786, + 21.25747306465587 + ], + [ + 0, + 22.10777198724213 + ], + [ + 1.700597845172524, + 21.25747306465587 + ], + [ + 2.550896767758786, + 19.556875219483402 + ], + [ + 2.550896767758786, + 16.155679529138467 + ], + [ + 0.850298922586262, + 11.904184916207328 + ], + [ + -2.550896767758786, + 6.802391380689869 + ], + [ + -5.101793535517118, + 3.4011956903449345 + ], + [ + -6.802391380689642, + 1.7005978451724673 + ], + [ + -8.502989225862166, + 1.7005978451724673 + ], + [ + -10.20358707103469, + 4.251494612931197 + ], + [ + -11.053885993620952, + 10.20358707103486 + ], + [ + -10.20358707103469, + 17.856277374310935 + ], + [ + -8.502989225862166, + 22.10777198724213 + ], + [ + -5.101793535517118, + 24.658668755000804 + ], + [ + -1.700597845172524, + 26.35926660017327 + ], + [ + 0.850298922586262, + 26.35926660017327 + ], + [ + 1.700597845172524, + 26.35926660017327 + ], + [ + 2.550896767758786, + 22.958070909828336 + ], + [ + 2.550896767758786, + 20.407174142069664 + ], + [ + 2.550896767758786, + 17.00597845172473 + ], + [ + 0.850298922586262, + 12.754483838793533 + ], + [ + -0.850298922586262, + 9.353288148448598 + ], + [ + -2.550896767758786, + 5.952092458103664 + ], + [ + -4.2514946129308555, + 5.101793535517402 + ], + [ + -5.101793535517118, + 5.101793535517402 + ], + [ + -5.101793535517118, + 7.652690303276131 + ], + [ + -5.95209245810338, + 11.053885993621066 + ], + [ + -5.95209245810338, + 15.305380606552262 + ], + [ + -5.101793535517118, + 19.556875219483402 + ], + [ + -4.2514946129308555, + 22.958070909828336 + ], + [ + -2.550896767758786, + 24.658668755000804 + ], + [ + -1.700597845172524, + 23.8083698324146 + ], + [ + -1.700597845172524, + 22.10777198724213 + ], + [ + -1.700597845172524, + 19.556875219483402 + ], + [ + -1.700597845172524, + 15.305380606552262 + ], + [ + -3.4011956903445935, + 11.053885993621066 + ], + [ + -6.802391380689642, + 5.952092458103664 + ], + [ + -7.652690303275904, + 5.101793535517402 + ], + [ + -8.502989225862166, + 5.952092458103664 + ], + [ + -9.353288148448428, + 9.353288148448598 + ], + [ + -10.20358707103469, + 14.455081683966 + ], + [ + -10.20358707103469, + 22.10777198724213 + ], + [ + -8.502989225862166, + 26.35926660017327 + ], + [ + -6.802391380689642, + 28.910163367932 + ], + [ + -3.4011956903445935, + 29.760462290518205 + ], + [ + -1.700597845172524, + 29.760462290518205 + ], + [ + 0, + 27.209565522759533 + ], + [ + 0, + 22.10777198724213 + ], + [ + -0.850298922586262, + 17.00597845172473 + ], + [ + -2.550896767758786, + 11.904184916207328 + ], + [ + -5.101793535517118, + 6.802391380689869 + ], + [ + -6.802391380689642, + 4.251494612931197 + ], + [ + -7.652690303275904, + 4.251494612931197 + ], + [ + -7.652690303275904, + 8.502989225862336 + ], + [ + -7.652690303275904, + 14.455081683966 + ], + [ + -5.101793535517118, + 22.10777198724213 + ], + [ + -1.700597845172524, + 27.209565522759533 + ], + [ + 1.700597845172524, + 30.610761213104468 + ], + [ + 5.101793535517572, + 31.46106013569073 + ], + [ + 6.802391380690096, + 30.610761213104468 + ], + [ + 7.6526903032763585, + 28.059864445345738 + ], + [ + 7.6526903032763585, + 24.658668755000804 + ], + [ + 6.802391380690096, + 19.556875219483402 + ], + [ + 5.101793535517572, + 14.455081683966 + ], + [ + 2.550896767758786, + 10.20358707103486 + ], + [ + -0.850298922586262, + 8.502989225862336 + ], + [ + -1.700597845172524, + 8.502989225862336 + ], + [ + -3.4011956903445935, + 11.053885993621066 + ], + [ + -4.2514946129308555, + 16.155679529138467 + ], + [ + -3.4011956903445935, + 21.25747306465587 + ], + [ + -1.700597845172524, + 24.658668755000804 + ], + [ + 0.850298922586262, + 27.209565522759533 + ], + [ + 3.401195690345048, + 27.209565522759533 + ], + [ + 5.101793535517572, + 26.35926660017327 + ], + [ + 5.952092458103834, + 24.658668755000804 + ], + [ + 6.802391380690096, + 21.25747306465587 + ], + [ + 6.802391380690096, + 17.00597845172473 + ], + [ + 5.101793535517572, + 10.20358707103486 + ], + [ + 3.401195690345048, + 6.802391380689869 + ], + [ + 1.700597845172524, + 4.251494612931197 + ], + [ + 0.850298922586262, + 4.251494612931197 + ], + [ + 0, + 5.952092458103664 + ], + [ + -0.850298922586262, + 10.20358707103486 + ], + [ + 0, + 17.00597845172473 + ], + [ + 0.850298922586262, + 22.10777198724213 + ], + [ + 2.550896767758786, + 25.508967677587066 + ], + [ + 5.101793535517572, + 26.35926660017327 + ], + [ + 5.952092458103834, + 26.35926660017327 + ], + [ + 7.6526903032763585, + 24.658668755000804 + ], + [ + 8.50298922586262, + 19.556875219483402 + ], + [ + 8.50298922586262, + 14.455081683966 + ], + [ + 5.952092458103834, + 8.502989225862336 + ], + [ + 4.25149461293131, + 3.4011956903449345 + ], + [ + 1.700597845172524, + 0.850298922586262 + ], + [ + 0.850298922586262, + 0.850298922586262 + ], + [ + 0, + 6.802391380689869 + ], + [ + 1.700597845172524, + 16.155679529138467 + ], + [ + 4.25149461293131, + 22.958070909828336 + ], + [ + 6.802391380690096, + 26.35926660017327 + ], + [ + 10.20358707103469, + 28.059864445345738 + ], + [ + 11.904184916207214, + 27.209565522759533 + ], + [ + 12.754483838793476, + 25.508967677587066 + ], + [ + 12.754483838793476, + 21.25747306465587 + ], + [ + 11.904184916207214, + 16.155679529138467 + ], + [ + 10.20358707103469, + 11.053885993621066 + ], + [ + 8.50298922586262, + 6.802391380689869 + ], + [ + 5.952092458103834, + 3.4011956903449345 + ], + [ + 4.25149461293131, + 0.850298922586262 + ], + [ + 3.401195690345048, + 4.251494612931197 + ], + [ + 3.401195690345048, + 10.20358707103486 + ], + [ + 4.25149461293131, + 17.856277374310935 + ], + [ + 5.952092458103834, + 23.8083698324146 + ], + [ + 7.6526903032763585, + 27.209565522759533 + ], + [ + 11.053885993620952, + 28.910163367932 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 13.604782761379738, + 28.059864445345738 + ], + [ + 13.604782761379738, + 24.658668755000804 + ], + [ + 12.754483838793476, + 21.25747306465587 + ], + [ + 11.904184916207214, + 16.155679529138467 + ], + [ + 9.353288148448883, + 10.20358707103486 + ], + [ + 7.6526903032763585, + 6.802391380689869 + ], + [ + 5.952092458103834, + 5.952092458103664 + ], + [ + 5.101793535517572, + 5.101793535517402 + ], + [ + 5.101793535517572, + 5.952092458103664 + ], + [ + 5.952092458103834, + 9.353288148448598 + ], + [ + 7.6526903032763585, + 14.455081683966 + ], + [ + 8.50298922586262, + 19.556875219483402 + ], + [ + 9.353288148448883, + 23.8083698324146 + ], + [ + 10.20358707103469, + 25.508967677587066 + ], + [ + 10.20358707103469, + 26.35926660017327 + ], + [ + 10.20358707103469, + 25.508967677587066 + ], + [ + 10.20358707103469, + 22.958070909828336 + ], + [ + 8.50298922586262, + 18.706576296897197 + ], + [ + 7.6526903032763585, + 14.455081683966 + ], + [ + 5.952092458103834, + 10.20358707103486 + ], + [ + 5.101793535517572, + 6.802391380689869 + ], + [ + 5.101793535517572, + 5.952092458103664 + ], + [ + 5.101793535517572, + 6.802391380689869 + ], + [ + 5.952092458103834, + 10.20358707103486 + ], + [ + 9.353288148448883, + 17.00597845172473 + ], + [ + 10.20358707103469, + 18.706576296897197 + ], + [ + 11.904184916207214, + 24.658668755000804 + ], + [ + 12.754483838793476, + 27.209565522759533 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 12.754483838793476, + 27.209565522759533 + ], + [ + 11.904184916207214, + 23.8083698324146 + ], + [ + 11.053885993620952, + 19.556875219483402 + ], + [ + 10.20358707103469, + 17.856277374310935 + ], + [ + 9.353288148448883, + 13.604782761379795 + ], + [ + 7.6526903032763585, + 10.20358707103486 + ], + [ + 6.802391380690096, + 7.652690303276131 + ], + [ + 6.802391380690096, + 5.952092458103664 + ], + [ + 5.952092458103834, + 5.101793535517402 + ], + [ + 5.952092458103834, + 4.251494612931197 + ], + [ + 5.952092458103834, + 3.4011956903449345 + ], + [ + 5.952092458103834, + 2.5508967677587293 + ], + [ + 5.952092458103834, + 1.7005978451724673 + ], + [ + 5.952092458103834, + 0 + ], + [ + 6.802391380690096, + 0.850298922586262 + ], + [ + 7.6526903032763585, + 3.4011956903449345 + ], + [ + 8.50298922586262, + 3.4011956903449345 + ], + [ + 11.904184916207214, + 10.20358707103486 + ], + [ + 14.455081683966, + 16.155679529138467 + ], + [ + 17.005978451724786, + 21.25747306465587 + ], + [ + 17.85627737431105, + 23.8083698324146 + ], + [ + 17.85627737431105, + 25.508967677587066 + ], + [ + 17.85627737431105, + 27.209565522759533 + ], + [ + 17.005978451724786, + 28.059864445345738 + ], + [ + 15.305380606552262, + 28.910163367932 + ], + [ + 13.604782761379738, + 28.910163367932 + ], + [ + 12.754483838793476, + 28.910163367932 + ], + [ + 11.053885993620952, + 29.760462290518205 + ], + [ + 9.353288148448883, + 29.760462290518205 + ], + [ + 8.50298922586262, + 29.760462290518205 + ], + [ + 9.353288148448883, + 29.760462290518205 + ], + [ + 10.20358707103469, + 28.910163367932 + ], + [ + 11.053885993620952, + 28.059864445345738 + ], + [ + 11.053885993620952, + 27.209565522759533 + ], + [ + 11.053885993620952, + 26.35926660017327 + ], + [ + 11.053885993620952, + 27.209565522759533 + ], + [ + 10.20358707103469, + 28.059864445345738 + ], + [ + 9.353288148448883, + 28.059864445345738 + ], + [ + 8.50298922586262, + 28.910163367932 + ], + [ + 7.6526903032763585, + 29.760462290518205 + ], + [ + 6.802391380690096, + 30.610761213104468 + ], + [ + 5.952092458103834, + 30.610761213104468 + ], + [ + 5.101793535517572, + 29.760462290518205 + ], + [ + 4.25149461293131, + 28.910163367932 + ], + [ + 3.401195690345048, + 28.910163367932 + ], + [ + 3.401195690345048, + 28.910163367932 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 94, + "versionNonce": 694624174, + "isDeleted": false, + "id": "XFWIMW3ZFwY6HDOUTkpE6", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 5.585282484584528, + "x": 3114.274335615452, + "y": 694.7523206733354, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 13.604782761379738, + "height": 4.251494612931197, + "seed": 1098722030, + "groupIds": [ + "ffzOuLtTdfZZzobRroJ5w" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167797886, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.850298922586262, + 0.850298922586262 + ], + [ + -0.850298922586262, + -0.8502989225862052 + ], + [ + -2.550896767758786, + -1.7005978451724673 + ], + [ + -4.25149461293131, + -2.5508967677587293 + ], + [ + -5.101793535517118, + -2.5508967677587293 + ], + [ + -7.652690303275904, + -3.4011956903449345 + ], + [ + -9.353288148448428, + -3.4011956903449345 + ], + [ + -11.053885993620952, + -3.4011956903449345 + ], + [ + -11.904184916207214, + -3.4011956903449345 + ], + [ + -12.754483838793476, + -3.4011956903449345 + ], + [ + -12.754483838793476, + -2.5508967677587293 + ], + [ + -11.904184916207214, + -1.7005978451724673 + ], + [ + -11.904184916207214, + -1.7005978451724673 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 94, + "versionNonce": 1541306930, + "isDeleted": false, + "id": "dMkfDKZHjNQQAezBa2oYp", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 5.585282484584528, + "x": 3127.7511814622567, + "y": 696.727198163756, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 11.053885993620952, + "height": 6.802391380689869, + "seed": 1253416238, + "groupIds": [ + "ffzOuLtTdfZZzobRroJ5w" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167797886, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + 0 + ], + [ + -3.401195690345048, + 0 + ], + [ + -5.101793535517572, + 0 + ], + [ + -5.952092458103834, + 0 + ], + [ + -8.50298922586262, + 0.8502989225862052 + ], + [ + -9.353288148448883, + 2.5508967677586725 + ], + [ + -10.203587071035145, + 2.5508967677586725 + ], + [ + -11.053885993620952, + 4.25149461293114 + ], + [ + -11.053885993620952, + 5.101793535517402 + ], + [ + -11.053885993620952, + 5.952092458103607 + ], + [ + -11.053885993620952, + 6.802391380689869 + ], + [ + -9.353288148448883, + 6.802391380689869 + ], + [ + -9.353288148448883, + 6.802391380689869 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 91, + "versionNonce": 855833070, + "isDeleted": false, + "id": "v5Ndpp8kGZiHn7pOmTtje", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 5.585282484584528, + "x": 3138.9866927400194, + "y": 699.9806688862939, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 5.952092458103834, + "height": 9.353288148448542, + "seed": 1889912686, + "groupIds": [ + "ffzOuLtTdfZZzobRroJ5w" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167797886, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.8502989225862052 + ], + [ + -0.850298922586262, + 1.7005978451724673 + ], + [ + -1.700597845172524, + 2.5508967677586725 + ], + [ + -4.25149461293131, + 5.952092458103607 + ], + [ + -5.101793535517572, + 7.652690303276074 + ], + [ + -5.952092458103834, + 8.502989225862336 + ], + [ + -5.952092458103834, + 9.353288148448542 + ], + [ + -5.101793535517572, + 8.502989225862336 + ], + [ + -4.25149461293131, + 8.502989225862336 + ], + [ + -4.25149461293131, + 8.502989225862336 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 90, + "versionNonce": 775185394, + "isDeleted": false, + "id": "-8XPh0vAqFdF6l7-_h0kI", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 5.585282484584528, + "x": 3119.9240745351217, + "y": 668.6472406077175, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 12.754483838793476, + "height": 9.353288148448542, + "seed": 865511854, + "groupIds": [ + "ffzOuLtTdfZZzobRroJ5w" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167797886, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.8502989225862052 + ], + [ + 0.850298922586262, + -1.7005978451724673 + ], + [ + 2.550896767758786, + -3.4011956903449345 + ], + [ + 6.802391380689642, + -5.952092458103607 + ], + [ + 8.502989225862166, + -7.652690303276074 + ], + [ + 11.053885993620952, + -8.502989225862336 + ], + [ + 11.904184916207214, + -9.353288148448542 + ], + [ + 12.754483838793476, + -9.353288148448542 + ], + [ + 12.754483838793476, + -9.353288148448542 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 88, + "versionNonce": 1219071022, + "isDeleted": false, + "id": "cTSRpVCAbaAMKy_ZIQhUf", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 5.585282484584528, + "x": 3133.9950404178994, + "y": 667.1584887163712, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 14.455081683966, + "height": 2.5508967677587293, + "seed": 582042606, + "groupIds": [ + "ffzOuLtTdfZZzobRroJ5w" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167797886, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 1.700597845172524, + -0.8502989225862052 + ], + [ + 4.25149461293131, + -1.7005978451724673 + ], + [ + 6.802391380690096, + -1.7005978451724673 + ], + [ + 7.6526903032763585, + -1.7005978451724673 + ], + [ + 12.754483838793476, + -2.5508967677587293 + ], + [ + 14.455081683966, + -2.5508967677587293 + ], + [ + 14.455081683966, + -2.5508967677587293 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 92, + "versionNonce": 430380466, + "isDeleted": false, + "id": "z9GlLpVSDwe9fr-GCMhs1", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 5.585282484584528, + "x": 3143.847493040276, + "y": 670.2205154446608, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 17.005978451724786, + "height": 5.952092458103664, + "seed": 2147391022, + "groupIds": [ + "ffzOuLtTdfZZzobRroJ5w" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167797886, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.850298922586262, + 0 + ], + [ + 1.700597845172524, + 0.850298922586262 + ], + [ + 4.25149461293131, + 0.850298922586262 + ], + [ + 8.502989225862166, + 1.7005978451724673 + ], + [ + 11.053885993620952, + 2.5508967677587293 + ], + [ + 12.754483838793476, + 4.251494612931197 + ], + [ + 15.305380606552262, + 5.101793535517402 + ], + [ + 17.005978451724786, + 5.952092458103664 + ], + [ + 16.155679529138524, + 5.952092458103664 + ], + [ + 15.305380606552262, + 4.251494612931197 + ], + [ + 15.305380606552262, + 4.251494612931197 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 96, + "versionNonce": 138663534, + "isDeleted": false, + "id": "goABPUzeeU_68cwDhFec-", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 5.585282484584528, + "x": 3092.96884997136, + "y": 672.977122894212, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 23.808369832414428, + "height": 18.706576296897197, + "seed": 1684512878, + "groupIds": [ + "ffzOuLtTdfZZzobRroJ5w" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167797886, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.850298922586262 + ], + [ + 0, + -2.5508967677587293 + ], + [ + 0, + -3.4011956903449345 + ], + [ + -0.850298922586262, + -7.652690303276131 + ], + [ + -3.401195690345048, + -10.203587071034804 + ], + [ + -5.952092458103834, + -13.604782761379738 + ], + [ + -8.50298922586262, + -16.155679529138467 + ], + [ + -11.904184916207214, + -17.856277374310935 + ], + [ + -15.305380606552262, + -18.706576296897197 + ], + [ + -18.70657629689731, + -18.706576296897197 + ], + [ + -20.407174142069834, + -18.706576296897197 + ], + [ + -22.958070909828166, + -18.706576296897197 + ], + [ + -23.808369832414428, + -17.856277374310935 + ], + [ + -22.958070909828166, + -17.856277374310935 + ], + [ + -22.958070909828166, + -17.856277374310935 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 93, + "versionNonce": 1868402546, + "isDeleted": false, + "id": "4P9LdPhNzVn6MrwbnjadD", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 5.585282484584528, + "x": 3097.1281497050873, + "y": 658.7016741423316, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 5.952092458103834, + "height": 19.55687521948346, + "seed": 1672831662, + "groupIds": [ + "ffzOuLtTdfZZzobRroJ5w" + ], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727167797886, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.850298922586262, + -0.850298922586262 + ], + [ + -0.850298922586262, + -2.5508967677587293 + ], + [ + -0.850298922586262, + -5.952092458103664 + ], + [ + -0.850298922586262, + -6.802391380689926 + ], + [ + -0.850298922586262, + -11.904184916207328 + ], + [ + -0.850298922586262, + -15.305380606552262 + ], + [ + 0.850298922586262, + -17.00597845172473 + ], + [ + 2.550896767758786, + -18.706576296897197 + ], + [ + 4.25149461293131, + -18.706576296897197 + ], + [ + 4.25149461293131, + -19.55687521948346 + ], + [ + 5.101793535517572, + -19.55687521948346 + ], + [ + 5.101793535517572, + -19.55687521948346 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 106, + "versionNonce": 325456114, + "isDeleted": false, + "id": "NcrYMUm4bGk7L57sECYHH", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3094.840172481938, + "y": 661.6075700371342, + "strokeColor": "#eebefa", + "backgroundColor": "transparent", + "width": 6.049140529152282, + "height": 7.0573306173439505, + "seed": 664493106, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727168617005, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.11202112091029903 + ], + [ + 0, + 0.22404224182048438 + ], + [ + 0.11202112091041272, + 0.3360633627306697 + ], + [ + 0.22404224182082544, + 0.44808448364085507 + ], + [ + 0.22404224182082544, + 0.5601056045511541 + ], + [ + 0.3360633627307834, + 0.6721267254613394 + ], + [ + 0.44808448364119613, + 0.7841478463715248 + ], + [ + 0.6721267254615668, + 0.8961689672818238 + ], + [ + 0.7841478463715248, + 1.0081900881920092 + ], + [ + 0.8961689672819375, + 1.1202112091021945 + ], + [ + 1.0081900881923502, + 1.1202112091021945 + ], + [ + 1.1202112091023082, + 1.2322323300124935 + ], + [ + 1.232232330012721, + 1.344253450922679 + ], + [ + 1.4562745718330916, + 1.344253450922679 + ], + [ + 1.792337934563875, + 1.5682956927431633 + ], + [ + 2.0163801763842457, + 1.6803168136533486 + ], + [ + 2.352443539115029, + 1.6803168136533486 + ], + [ + 2.688506901845358, + 1.904359055473833 + ], + [ + 2.9125491436657285, + 1.904359055473833 + ], + [ + 3.136591385486554, + 2.0163801763840183 + ], + [ + 3.4726547482168826, + 2.1284012972942037 + ], + [ + 3.6966969900372533, + 2.2404224182045027 + ], + [ + 4.032760352768037, + 2.352443539114688 + ], + [ + 4.36882371549882, + 2.4644646600248734 + ], + [ + 4.592865957319191, + 2.4644646600248734 + ], + [ + 4.8169081991395615, + 2.5764857809350588 + ], + [ + 4.928929320049974, + 2.5764857809350588 + ], + [ + 5.040950440959932, + 2.5764857809350588 + ], + [ + 5.152971561870345, + 2.688506901845358 + ], + [ + 5.264992682780758, + 2.800528022755543 + ], + [ + 5.377013803690716, + 2.800528022755543 + ], + [ + 5.489034924601128, + 2.800528022755543 + ], + [ + 5.489034924601128, + 2.9125491436657285 + ], + [ + 5.601056045511086, + 2.9125491436657285 + ], + [ + 5.713077166421499, + 3.0245702645760275 + ], + [ + 5.825098287331457, + 3.0245702645760275 + ], + [ + 5.825098287331457, + 3.136591385486213 + ], + [ + 5.93711940824187, + 3.136591385486213 + ], + [ + 5.93711940824187, + 3.0245702645760275 + ], + [ + 5.93711940824187, + 2.9125491436657285 + ], + [ + 5.93711940824187, + 2.800528022755543 + ], + [ + 5.93711940824187, + 2.688506901845358 + ], + [ + 5.93711940824187, + 2.5764857809350588 + ], + [ + 5.93711940824187, + 2.4644646600248734 + ], + [ + 5.93711940824187, + 2.2404224182045027 + ], + [ + 5.93711940824187, + 2.0163801763840183 + ], + [ + 5.93711940824187, + 1.792337934563534 + ], + [ + 5.93711940824187, + 1.5682956927431633 + ], + [ + 5.93711940824187, + 1.344253450922679 + ], + [ + 5.93711940824187, + 1.1202112091021945 + ], + [ + 5.93711940824187, + 0.8961689672818238 + ], + [ + 5.93711940824187, + 0.6721267254613394 + ], + [ + 5.93711940824187, + 0.5601056045511541 + ], + [ + 5.93711940824187, + 0.3360633627306697 + ], + [ + 6.049140529152282, + 0.22404224182048438 + ], + [ + 6.049140529152282, + 0 + ], + [ + 6.049140529152282, + -0.11202112091018535 + ], + [ + 6.049140529152282, + -0.3360633627306697 + ], + [ + 6.049140529152282, + -0.5601056045510404 + ], + [ + 6.049140529152282, + -0.7841478463715248 + ], + [ + 6.049140529152282, + -1.0081900881920092 + ], + [ + 6.049140529152282, + -1.3442534509225652 + ], + [ + 6.049140529152282, + -1.5682956927430496 + ], + [ + 6.049140529152282, + -1.792337934563534 + ], + [ + 6.049140529152282, + -2.0163801763839047 + ], + [ + 6.049140529152282, + -2.240422418204389 + ], + [ + 6.049140529152282, + -2.4644646600248734 + ], + [ + 6.049140529152282, + -2.5764857809350588 + ], + [ + 6.049140529152282, + -2.688506901845244 + ], + [ + 6.049140529152282, + -2.9125491436657285 + ], + [ + 6.049140529152282, + -3.024570264575914 + ], + [ + 6.049140529152282, + -3.136591385486213 + ], + [ + 6.049140529152282, + -3.248612506396398 + ], + [ + 6.049140529152282, + -3.3606336273065835 + ], + [ + 6.049140529152282, + -3.472654748216769 + ], + [ + 6.049140529152282, + -3.584675869127068 + ], + [ + 6.049140529152282, + -3.6966969900372533 + ], + [ + 6.049140529152282, + -3.8087181109474386 + ], + [ + 6.049140529152282, + -3.9207392318577376 + ], + [ + 6.049140529152282, + -3.8087181109474386 + ], + [ + 5.93711940824187, + -3.8087181109474386 + ], + [ + 5.93711940824187, + -3.6966969900372533 + ], + [ + 5.713077166421499, + -3.584675869127068 + ], + [ + 5.713077166421499, + -3.3606336273065835 + ], + [ + 5.489034924601128, + -3.248612506396398 + ], + [ + 5.377013803690716, + -3.136591385486213 + ], + [ + 5.152971561870345, + -2.9125491436657285 + ], + [ + 4.928929320049974, + -2.800528022755543 + ], + [ + 4.592865957319191, + -2.5764857809350588 + ], + [ + 4.256802594588407, + -2.4644646600248734 + ], + [ + 4.032760352768037, + -2.3524435391145744 + ], + [ + 3.6966969900372533, + -2.240422418204389 + ], + [ + 3.4726547482168826, + -2.1284012972942037 + ], + [ + 3.136591385486554, + -2.1284012972942037 + ], + [ + 2.9125491436657285, + -2.0163801763839047 + ], + [ + 2.688506901845358, + -1.9043590554737193 + ], + [ + 2.464464660024987, + -1.792337934563534 + ], + [ + 2.2404224182046164, + -1.792337934563534 + ], + [ + 2.0163801763842457, + -1.680316813653235 + ], + [ + 1.6803168136534623, + -1.680316813653235 + ], + [ + 1.4562745718330916, + -1.680316813653235 + ], + [ + 1.344253450922679, + -1.680316813653235 + ], + [ + 1.232232330012721, + -1.680316813653235 + ], + [ + 1.232232330012721, + -1.680316813653235 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 58, + "versionNonce": 212762418, + "isDeleted": false, + "id": "fo5VUURIlZ9smNuiHz-10", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3102.233566462013, + "y": 654.9983239034312, + "strokeColor": "#eebefa", + "backgroundColor": "transparent", + "width": 1.904359055473833, + "height": 2.800528022755543, + "seed": 916658542, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727168618641, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.11202112091041272, + 0 + ], + [ + -0.3360633627307834, + 0 + ], + [ + -0.44808448364119613, + 0.11202112091018535 + ], + [ + -0.44808448364119613, + 0.3360633627306697 + ], + [ + -0.5601056045511541, + 0.5601056045510404 + ], + [ + -0.5601056045511541, + 0.6721267254613394 + ], + [ + -0.5601056045511541, + 0.8961689672817101 + ], + [ + -0.44808448364119613, + 1.0081900881920092 + ], + [ + -0.3360633627307834, + 1.0081900881920092 + ], + [ + -0.22404224182082544, + 1.0081900881920092 + ], + [ + -0.11202112091041272, + 0.8961689672817101 + ], + [ + -0.11202112091041272, + 0.7841478463715248 + ], + [ + 0, + 0.44808448364085507 + ], + [ + 0, + 0.2240422418203707 + ], + [ + 0, + 0 + ], + [ + 0, + -0.11202112091029903 + ], + [ + -0.22404224182082544, + -0.22404224182048438 + ], + [ + -0.3360633627307834, + -0.3360633627306697 + ], + [ + -0.44808448364119613, + -0.3360633627306697 + ], + [ + -0.5601056045511541, + -0.11202112091029903 + ], + [ + -0.5601056045511541, + 0.11202112091018535 + ], + [ + -0.44808448364119613, + 0.44808448364085507 + ], + [ + -0.22404224182082544, + 0.7841478463715248 + ], + [ + 0.11202112090995797, + 1.1202112091021945 + ], + [ + 0.6721267254611121, + 1.4562745718328642 + ], + [ + 0.8961689672814828, + 1.4562745718328642 + ], + [ + 1.0081900881918955, + 1.344253450922679 + ], + [ + 1.0081900881918955, + 1.0081900881920092 + ], + [ + 1.0081900881918955, + 0.6721267254613394 + ], + [ + 0.8961689672814828, + 0.2240422418203707 + ], + [ + 0.6721267254611121, + -0.44808448364085507 + ], + [ + 0.2240422418203707, + -1.0081900881920092 + ], + [ + 0, + -1.344253450922679 + ], + [ + -0.11202112091041272, + -1.344253450922679 + ], + [ + -0.22404224182082544, + -1.344253450922679 + ], + [ + -0.44808448364119613, + -1.2322323300124935 + ], + [ + -0.6721267254615668, + -1.0081900881920092 + ], + [ + -0.8961689672819375, + -0.6721267254613394 + ], + [ + -0.8961689672819375, + -0.3360633627306697 + ], + [ + -0.8961689672819375, + 0.11202112091018535 + ], + [ + -0.8961689672819375, + 0.44808448364085507 + ], + [ + -0.7841478463715248, + 0.6721267254613394 + ], + [ + -0.6721267254615668, + 0.6721267254613394 + ], + [ + -0.5601056045511541, + 0.5601056045510404 + ], + [ + -0.5601056045511541, + 0.44808448364085507 + ], + [ + -0.5601056045511541, + 0.3360633627306697 + ], + [ + -0.5601056045511541, + 0.2240422418203707 + ], + [ + -0.5601056045511541, + 0.11202112091018535 + ], + [ + -0.5601056045511541, + 0 + ], + [ + -0.5601056045511541, + -0.11202112091029903 + ], + [ + -0.5601056045511541, + -0.3360633627306697 + ], + [ + -0.5601056045511541, + -0.44808448364085507 + ], + [ + -0.44808448364119613, + -0.44808448364085507 + ], + [ + -0.3360633627307834, + -0.44808448364085507 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 93, + "versionNonce": 1334678322, + "isDeleted": false, + "id": "JD37gLYErAz1XjSsHPM3W", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3102.0095242201924, + "y": 652.869922606137, + "strokeColor": "#eebefa", + "backgroundColor": "transparent", + "width": 10.081900881919864, + "height": 9.073710793727855, + "seed": 544667438, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727168620953, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.11202112091018535 + ], + [ + 0, + -0.22404224182048438 + ], + [ + 0, + -0.3360633627306697 + ], + [ + 0, + -0.6721267254613394 + ], + [ + 0, + -0.8961689672818238 + ], + [ + -0.11202112090995797, + -1.2322323300124935 + ], + [ + -0.11202112090995797, + -1.6803168136533486 + ], + [ + -0.11202112090995797, + -1.9043590554737193 + ], + [ + -0.11202112090995797, + -2.240422418204389 + ], + [ + -0.11202112090995797, + -2.5764857809350588 + ], + [ + -0.11202112090995797, + -2.9125491436657285 + ], + [ + -0.11202112090995797, + -3.248612506396398 + ], + [ + -0.11202112090995797, + -3.584675869127068 + ], + [ + -0.11202112090995797, + -3.9207392318577376 + ], + [ + 0, + -4.144781473678222 + ], + [ + 0, + -4.480844836408892 + ], + [ + 0.11202112091041272, + -4.704887078229262 + ], + [ + 0.11202112091041272, + -4.928929320049747 + ], + [ + 0.11202112091041272, + -5.2649926827804165 + ], + [ + 0.22404224182082544, + -5.489034924600787 + ], + [ + 0.22404224182082544, + -5.713077166421272 + ], + [ + 0.22404224182082544, + -6.049140529151941 + ], + [ + 0.22404224182082544, + -6.273182770972426 + ], + [ + 0.22404224182082544, + -6.497225012792796 + ], + [ + 0.22404224182082544, + -6.721267254613281 + ], + [ + 0.3360633627307834, + -6.833288375523466 + ], + [ + 0.3360633627307834, + -7.0573306173439505 + ], + [ + 0.3360633627307834, + -7.281372859164435 + ], + [ + 0.3360633627307834, + -7.39339398007462 + ], + [ + 0.3360633627307834, + -7.617436221894991 + ], + [ + 0.3360633627307834, + -7.72945734280529 + ], + [ + 0.3360633627307834, + -7.953499584625661 + ], + [ + 0.3360633627307834, + -8.06552070553596 + ], + [ + 0.3360633627307834, + -8.177541826446145 + ], + [ + 0.44808448364119613, + -8.06552070553596 + ], + [ + 0.5601056045511541, + -7.953499584625661 + ], + [ + 0.6721267254615668, + -7.841478463715475 + ], + [ + 1.0081900881923502, + -7.39339398007462 + ], + [ + 1.232232330012721, + -7.0573306173439505 + ], + [ + 1.5682956927435043, + -6.609246133703095 + ], + [ + 2.1284012972942037, + -6.049140529151941 + ], + [ + 2.5764857809354, + -5.601056045511086 + ], + [ + 3.024570264576141, + -5.2649926827804165 + ], + [ + 3.248612506396512, + -4.928929320049747 + ], + [ + 3.5846758691272953, + -4.592865957319077 + ], + [ + 3.808718110947666, + -4.368823715498593 + ], + [ + 4.256802594588407, + -4.144781473678222 + ], + [ + 4.592865957319191, + -3.9207392318577376 + ], + [ + 4.928929320049974, + -3.584675869127068 + ], + [ + 5.264992682780758, + -3.3606336273066972 + ], + [ + 5.601056045511086, + -3.0245702645760275 + ], + [ + 5.93711940824187, + -2.800528022755543 + ], + [ + 6.273182770972653, + -2.4644646600248734 + ], + [ + 6.7212672546133945, + -2.240422418204389 + ], + [ + 6.945309496433765, + -2.1284012972942037 + ], + [ + 7.169351738254136, + -1.9043590554737193 + ], + [ + 7.281372859164549, + -1.792337934563534 + ], + [ + 7.505415100984919, + -1.5682956927431633 + ], + [ + 7.617436221895332, + -1.344253450922679 + ], + [ + 7.841478463715703, + -1.1202112091021945 + ], + [ + 8.065520705536073, + -0.8961689672818238 + ], + [ + 8.289562947356444, + -0.6721267254613394 + ], + [ + 8.625626310087227, + -0.44808448364085507 + ], + [ + 9.073710793727969, + 0 + ], + [ + 9.29775303554834, + 0.22404224182048438 + ], + [ + 9.521795277369165, + 0.5601056045510404 + ], + [ + 9.745837519189536, + 0.7841478463715248 + ], + [ + 9.969879761009906, + 0.8961689672817101 + ], + [ + 9.745837519189536, + 0.8961689672817101 + ], + [ + 9.29775303554834, + 0.7841478463715248 + ], + [ + 8.401584068266857, + 0.7841478463715248 + ], + [ + 7.281372859164549, + 0.7841478463715248 + ], + [ + 6.6092461337034365, + 0.7841478463715248 + ], + [ + 6.049140529152282, + 0.7841478463715248 + ], + [ + 5.377013803690716, + 0.6721267254613394 + ], + [ + 4.7048870782296035, + 0.5601056045510404 + ], + [ + 4.144781473678449, + 0.44808448364085507 + ], + [ + 3.808718110947666, + 0.44808448364085507 + ], + [ + 3.696696990037708, + 0.44808448364085507 + ], + [ + 3.5846758691272953, + 0.3360633627306697 + ], + [ + 3.4726547482168826, + 0.3360633627306697 + ], + [ + 3.3606336273069246, + 0.3360633627306697 + ], + [ + 3.024570264576141, + 0.3360633627306697 + ], + [ + 2.8005280227557705, + 0.3360633627306697 + ], + [ + 2.688506901845358, + 0.3360633627306697 + ], + [ + 2.5764857809354, + 0.22404224182048438 + ], + [ + 2.352443539115029, + 0.22404224182048438 + ], + [ + 2.1284012972942037, + 0.11202112091018535 + ], + [ + 1.792337934563875, + 0.11202112091018535 + ], + [ + 1.6803168136534623, + 0 + ], + [ + 1.6803168136534623, + 0 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 94, + "versionNonce": 166862062, + "isDeleted": false, + "id": "gFIlFpIyRoOTHFPOV8CoB", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3316.051450426788, + "y": 629.9107717732028, + "strokeColor": "#eebefa", + "backgroundColor": "transparent", + "width": 9.443719454376605, + "height": 8.394417292779508, + "seed": 727556910, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727168627694, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0.11658912906614205, + 0 + ], + [ + 0.23317825813273885, + 0 + ], + [ + 0.23317825813273885, + 0.11658912906636942 + ], + [ + 0.3497673871988809, + 0.11658912906636942 + ], + [ + 0.5829456453316197, + 0.11658912906636942 + ], + [ + 0.9327130325309554, + 0.23317825813285253 + ], + [ + 1.399069548796433, + 0.34976738719922196 + ], + [ + 1.9820151941285076, + 0.34976738719922196 + ], + [ + 2.5649608394601273, + 0.4663565162655914 + ], + [ + 3.3810847429249407, + 0.5829456453319608 + ], + [ + 3.9640303882565604, + 0.5829456453319608 + ], + [ + 4.313797775455896, + 0.6995347743983302 + ], + [ + 4.663565162655232, + 0.6995347743983302 + ], + [ + 4.8967434207879705, + 0.6995347743983302 + ], + [ + 5.246510807986851, + 0.8161239034646997 + ], + [ + 5.712867324252329, + 0.9327130325310691 + ], + [ + 6.179223840517807, + 1.0493021615974385 + ], + [ + 6.645580356783739, + 1.0493021615974385 + ], + [ + 7.111936873049217, + 1.165891290663808 + ], + [ + 7.461704260248098, + 1.2824804197301773 + ], + [ + 7.9280607765135755, + 1.2824804197301773 + ], + [ + 8.161239034646314, + 1.3990695487966605 + ], + [ + 8.394417292779053, + 1.3990695487966605 + ], + [ + 8.627595550912247, + 1.5156586778630299 + ], + [ + 8.860773809044986, + 1.5156586778630299 + ], + [ + 8.977362938111128, + 1.6322478069293993 + ], + [ + 9.093952067177725, + 1.6322478069293993 + ], + [ + 9.210541196243867, + 1.6322478069293993 + ], + [ + 9.327130325310463, + 1.6322478069293993 + ], + [ + 9.443719454376605, + 1.6322478069293993 + ], + [ + 9.443719454376605, + 1.7488369359957687 + ], + [ + 9.443719454376605, + 1.8654260650621382 + ], + [ + 9.443719454376605, + 1.9820151941285076 + ], + [ + 9.327130325310463, + 2.098604323194877 + ], + [ + 9.093952067177725, + 2.448371710394099 + ], + [ + 8.744184679978389, + 2.681549968526838 + ], + [ + 8.394417292779053, + 3.031317355725946 + ], + [ + 8.044649905580172, + 3.3810847429250543 + ], + [ + 7.578293389314695, + 3.7308521301242763 + ], + [ + 7.345115131181956, + 3.964030388257015 + ], + [ + 7.111936873049217, + 4.197208646389754 + ], + [ + 6.99534774398262, + 4.313797775456123 + ], + [ + 6.878758614916478, + 4.546976033588862 + ], + [ + 6.762169485849881, + 4.663565162655232 + ], + [ + 6.5289912277171425, + 4.780154291721715 + ], + [ + 6.412402098651, + 5.013332549854454 + ], + [ + 6.179223840517807, + 5.129921678920823 + ], + [ + 6.062634711451665, + 5.363099937053562 + ], + [ + 5.829456453318926, + 5.596278195186301 + ], + [ + 5.596278195186187, + 5.82945645331904 + ], + [ + 5.47968906611959, + 5.946045582385523 + ], + [ + 5.246510807986851, + 6.179223840518262 + ], + [ + 5.013332549854113, + 6.295812969584631 + ], + [ + 4.8967434207879705, + 6.52899122771737 + ], + [ + 4.780154291721374, + 6.645580356783739 + ], + [ + 4.663565162655232, + 6.878758614916478 + ], + [ + 4.546976033588635, + 6.995347743982961 + ], + [ + 4.313797775455896, + 7.111936873049331 + ], + [ + 4.197208646389754, + 7.3451151311820695 + ], + [ + 3.9640303882565604, + 7.578293389314808 + ], + [ + 3.8474412591904184, + 7.811471647447547 + ], + [ + 3.7308521301238216, + 8.044649905580286 + ], + [ + 3.4976738719910827, + 8.277828163713139 + ], + [ + 3.4976738719910827, + 8.394417292779508 + ], + [ + 3.3810847429249407, + 8.394417292779508 + ], + [ + 3.3810847429249407, + 8.277828163713139 + ], + [ + 3.3810847429249407, + 8.044649905580286 + ], + [ + 3.264495613858344, + 7.811471647447547 + ], + [ + 3.264495613858344, + 7.694882518381178 + ], + [ + 3.264495613858344, + 7.3451151311820695 + ], + [ + 3.147906484792202, + 6.995347743982961 + ], + [ + 3.031317355725605, + 6.412402098651 + ], + [ + 2.798139097592866, + 5.946045582385523 + ], + [ + 2.681549968526724, + 5.479689066119931 + ], + [ + 2.5649608394601273, + 5.129921678920823 + ], + [ + 2.3317825813273885, + 4.780154291721715 + ], + [ + 2.3317825813273885, + 4.430386904522493 + ], + [ + 2.0986043231946496, + 4.080619517323385 + ], + [ + 2.0986043231946496, + 3.8474412591906457 + ], + [ + 1.9820151941285076, + 3.614263001057793 + ], + [ + 1.8654260650619108, + 3.3810847429250543 + ], + [ + 1.7488369359957687, + 3.264495613858685 + ], + [ + 1.7488369359957687, + 3.031317355725946 + ], + [ + 1.5156586778625751, + 2.7981390975932072 + ], + [ + 1.5156586778625751, + 2.681549968526838 + ], + [ + 1.399069548796433, + 2.5649608394604684 + ], + [ + 1.2824804197298363, + 2.448371710394099 + ], + [ + 1.2824804197298363, + 2.331782581327616 + ], + [ + 1.2824804197298363, + 2.2151934522612464 + ], + [ + 1.2824804197298363, + 2.098604323194877 + ], + [ + 1.1658912906636942, + 2.098604323194877 + ], + [ + 1.1658912906636942, + 2.098604323194877 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 90, + "versionNonce": 1643624818, + "isDeleted": false, + "id": "33-eo_IXplKM2CEVkja6K", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3313.3699004582613, + "y": 648.9147998110229, + "strokeColor": "#eebefa", + "backgroundColor": "transparent", + "width": 9.093952067177725, + "height": 9.327130325310463, + "seed": 139854514, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727168630514, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.11658912906636942 + ], + [ + 0.11658912906614205, + -0.23317825813273885 + ], + [ + 0.11658912906614205, + -0.34976738719910827 + ], + [ + 0.23317825813273885, + -0.4663565162654777 + ], + [ + 0.5829456453316197, + -0.9327130325310691 + ], + [ + 0.6995347743982165, + -1.165891290663808 + ], + [ + 0.9327130325309554, + -1.3990695487965468 + ], + [ + 1.1658912906636942, + -1.6322478069292856 + ], + [ + 1.399069548796433, + -1.8654260650621382 + ], + [ + 1.632247806929172, + -2.2151934522612464 + ], + [ + 1.8654260650619108, + -2.5649608394603547 + ], + [ + 2.0986043231946496, + -2.7981390975930935 + ], + [ + 2.3317825813273885, + -3.031317355725946 + ], + [ + 2.5649608394601273, + -3.264495613858685 + ], + [ + 2.681549968526724, + -3.4976738719914238 + ], + [ + 2.914728226659463, + -3.847441259190532 + ], + [ + 3.031317355725605, + -4.080619517323385 + ], + [ + 3.264495613858344, + -4.313797775456123 + ], + [ + 3.3810847429249407, + -4.546976033588862 + ], + [ + 3.4976738719910827, + -4.780154291721601 + ], + [ + 3.7308521301238216, + -5.01333254985434 + ], + [ + 3.8474412591904184, + -5.363099937053562 + ], + [ + 4.080619517323157, + -5.596278195186301 + ], + [ + 4.197208646389299, + -5.82945645331904 + ], + [ + 4.313797775455896, + -6.0626347114517785 + ], + [ + 4.430386904522493, + -6.295812969584631 + ], + [ + 4.663565162655232, + -6.52899122771737 + ], + [ + 4.780154291721374, + -6.645580356783739 + ], + [ + 4.8967434207879705, + -6.878758614916478 + ], + [ + 5.013332549854113, + -6.9953477439828475 + ], + [ + 5.013332549854113, + -7.111936873049217 + ], + [ + 5.129921678920709, + -7.228526002115586 + ], + [ + 5.246510807986851, + -7.345115131181956 + ], + [ + 5.246510807986851, + -7.461704260248325 + ], + [ + 5.363099937053448, + -7.461704260248325 + ], + [ + 5.363099937053448, + -7.345115131181956 + ], + [ + 5.596278195186187, + -7.228526002115586 + ], + [ + 5.596278195186187, + -7.111936873049217 + ], + [ + 5.712867324252329, + -7.111936873049217 + ], + [ + 6.062634711451665, + -6.412402098651 + ], + [ + 6.295812969584404, + -5.946045582385409 + ], + [ + 6.6455803567832845, + -5.363099937053562 + ], + [ + 6.99534774398262, + -4.780154291721601 + ], + [ + 7.228526002115359, + -4.430386904522493 + ], + [ + 7.461704260248098, + -4.080619517323385 + ], + [ + 7.694882518380837, + -3.614263001057793 + ], + [ + 7.8114716474474335, + -3.264495613858685 + ], + [ + 7.9280607765135755, + -3.031317355725946 + ], + [ + 8.044649905580172, + -2.5649608394603547 + ], + [ + 8.044649905580172, + -2.331782581327616 + ], + [ + 8.161239034646314, + -1.9820151941285076 + ], + [ + 8.277828163712911, + -1.6322478069292856 + ], + [ + 8.277828163712911, + -1.2824804197301773 + ], + [ + 8.394417292779053, + -0.9327130325310691 + ], + [ + 8.51100642184565, + -0.4663565162654777 + ], + [ + 8.51100642184565, + -0.11658912906636942 + ], + [ + 8.627595550911792, + 0.11658912906636942 + ], + [ + 8.627595550911792, + 0.34976738719910827 + ], + [ + 8.744184679978389, + 0.5829456453318471 + ], + [ + 8.744184679978389, + 0.8161239034646997 + ], + [ + 8.860773809044531, + 1.0493021615974385 + ], + [ + 8.860773809044531, + 1.165891290663808 + ], + [ + 8.977362938111128, + 1.2824804197301773 + ], + [ + 8.977362938111128, + 1.3990695487965468 + ], + [ + 8.977362938111128, + 1.5156586778629162 + ], + [ + 9.093952067177725, + 1.5156586778629162 + ], + [ + 9.093952067177725, + 1.7488369359957687 + ], + [ + 9.093952067177725, + 1.8654260650621382 + ], + [ + 9.093952067177725, + 1.7488369359957687 + ], + [ + 8.860773809044531, + 1.6322478069293993 + ], + [ + 8.161239034646314, + 1.3990695487965468 + ], + [ + 8.044649905580172, + 1.2824804197301773 + ], + [ + 6.062634711451665, + 1.0493021615974385 + ], + [ + 5.013332549854113, + 1.0493021615974385 + ], + [ + 4.197208646389299, + 1.0493021615974385 + ], + [ + 3.6142630010576795, + 1.0493021615974385 + ], + [ + 3.147906484792202, + 1.0493021615974385 + ], + [ + 2.798139097592866, + 1.0493021615974385 + ], + [ + 2.4483717103939853, + 1.0493021615974385 + ], + [ + 1.9820151941280528, + 1.0493021615974385 + ], + [ + 1.632247806929172, + 1.0493021615974385 + ], + [ + 1.2824804197298363, + 1.165891290663808 + ], + [ + 0.9327130325309554, + 1.165891290663808 + ], + [ + 0.8161239034643586, + 1.165891290663808 + ], + [ + 0.8161239034643586, + 1.0493021615974385 + ], + [ + 0.8161239034643586, + 0.9327130325310691 + ], + [ + 0.9327130325309554, + 0.6995347743983302 + ], + [ + 0.9327130325309554, + 0.6995347743983302 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 90, + "versionNonce": 577318446, + "isDeleted": false, + "id": "77BrC1d14fo5_LtiDzU-5", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3318.9661786534475, + "y": 639.121312969447, + "strokeColor": "#eebefa", + "backgroundColor": "transparent", + "width": 7.345115131181956, + "height": 3.614263001057793, + "seed": 1345210606, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727168632593, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.11658912906648311 + ], + [ + -0.34976738719933564, + -0.23317825813285253 + ], + [ + -0.5829456453320745, + -0.34976738719922196 + ], + [ + -0.8161239034648133, + -0.34976738719922196 + ], + [ + -0.9327130325309554, + -0.23317825813285253 + ], + [ + -1.1658912906636942, + -0.11658912906648311 + ], + [ + -1.3990695487968878, + 0.34976738719910827 + ], + [ + -1.5156586778630299, + 0.5829456453318471 + ], + [ + -1.6322478069296267, + 0.9327130325309554 + ], + [ + -1.6322478069296267, + 1.0493021615973248 + ], + [ + -1.6322478069296267, + 1.1658912906636942 + ], + [ + -1.6322478069296267, + 1.2824804197301773 + ], + [ + -1.6322478069296267, + 1.5156586778629162 + ], + [ + -1.6322478069296267, + 1.6322478069292856 + ], + [ + -1.6322478069296267, + 1.8654260650620245 + ], + [ + -1.6322478069296267, + 1.9820151941283939 + ], + [ + -1.6322478069296267, + 2.0986043231947633 + ], + [ + -1.6322478069296267, + 2.2151934522611327 + ], + [ + -1.6322478069296267, + 2.331782581327616 + ], + [ + -1.5156586778630299, + 2.5649608394603547 + ], + [ + -1.282480419730291, + 2.7981390975930935 + ], + [ + -1.1658912906636942, + 2.914728226659463 + ], + [ + -0.9327130325309554, + 3.0313173557258324 + ], + [ + -0.5829456453320745, + 3.0313173557258324 + ], + [ + -0.23317825813273885, + 3.147906484792202 + ], + [ + 0.23317825813273885, + 3.147906484792202 + ], + [ + 0.4663565162654777, + 3.2644956138585712 + ], + [ + 0.6995347743982165, + 3.2644956138585712 + ], + [ + 0.8161239034643586, + 3.2644956138585712 + ], + [ + 0.9327130325309554, + 3.147906484792202 + ], + [ + 1.0493021615970974, + 3.147906484792202 + ], + [ + 1.282480419730291, + 3.0313173557258324 + ], + [ + 1.5156586778630299, + 3.0313173557258324 + ], + [ + 1.9820151941285076, + 3.0313173557258324 + ], + [ + 2.0986043231946496, + 2.914728226659463 + ], + [ + 2.2151934522612464, + 2.914728226659463 + ], + [ + 2.3317825813273885, + 2.7981390975930935 + ], + [ + 2.5649608394601273, + 2.681549968526724 + ], + [ + 2.681549968526724, + 2.5649608394603547 + ], + [ + 2.914728226659463, + 2.4483717103939853 + ], + [ + 3.031317355725605, + 2.331782581327616 + ], + [ + 3.147906484792202, + 2.2151934522611327 + ], + [ + 3.264495613858344, + 2.0986043231947633 + ], + [ + 3.3810847429249407, + 1.9820151941283939 + ], + [ + 3.4976738719915375, + 1.8654260650620245 + ], + [ + 3.6142630010576795, + 1.6322478069292856 + ], + [ + 3.6142630010576795, + 1.5156586778629162 + ], + [ + 3.6142630010576795, + 1.2824804197301773 + ], + [ + 3.6142630010576795, + 1.1658912906636942 + ], + [ + 3.6142630010576795, + 0.9327130325309554 + ], + [ + 3.4976738719915375, + 0.816123903464586 + ], + [ + 3.3810847429249407, + 0.5829456453318471 + ], + [ + 3.264495613858344, + 0.4663565162654777 + ], + [ + 3.031317355725605, + 0.34976738719910827 + ], + [ + 2.798139097592866, + 0.23317825813273885 + ], + [ + 2.681549968526724, + 0.11658912906636942 + ], + [ + 2.4483717103939853, + 0.11658912906636942 + ], + [ + 2.0986043231946496, + 0 + ], + [ + 1.8654260650619108, + -0.11658912906648311 + ], + [ + 1.632247806929172, + -0.11658912906648311 + ], + [ + 1.399069548796433, + -0.23317825813285253 + ], + [ + 0.9327130325309554, + -0.23317825813285253 + ], + [ + 0.5829456453316197, + -0.34976738719922196 + ], + [ + 0.23317825813273885, + -0.34976738719922196 + ], + [ + -0.1165891290665968, + -0.34976738719922196 + ], + [ + -0.34976738719933564, + -0.34976738719922196 + ], + [ + -0.5829456453320745, + -0.34976738719922196 + ], + [ + -0.9327130325309554, + -0.34976738719922196 + ], + [ + -1.282480419730291, + -0.34976738719922196 + ], + [ + -1.6322478069296267, + -0.23317825813285253 + ], + [ + -2.0986043231951044, + -0.23317825813285253 + ], + [ + -2.4483717103939853, + -0.23317825813285253 + ], + [ + -2.681549968526724, + -0.11658912906648311 + ], + [ + -2.798139097593321, + 0 + ], + [ + -2.914728226659463, + 0 + ], + [ + -3.0313173557260598, + 0.11658912906636942 + ], + [ + -3.147906484792202, + 0.23317825813273885 + ], + [ + -3.3810847429253954, + 0.34976738719910827 + ], + [ + -3.4976738719915375, + 0.5829456453318471 + ], + [ + -3.6142630010581343, + 0.6995347743982165 + ], + [ + -3.7308521301242763, + 0.9327130325309554 + ], + [ + -3.7308521301242763, + 1.1658912906636942 + ], + [ + -3.6142630010581343, + 1.3990695487965468 + ], + [ + -3.6142630010581343, + 1.6322478069292856 + ], + [ + -3.6142630010581343, + 1.748836935995655 + ], + [ + -3.4976738719915375, + 1.748836935995655 + ], + [ + -3.3810847429253954, + 1.6322478069292856 + ], + [ + -3.3810847429253954, + 1.6322478069292856 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 75, + "versionNonce": 1530418670, + "isDeleted": false, + "id": "tcCFcVo2Opwe6_RqLdTsI", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3345.131896503167, + "y": 483.15359349676123, + "strokeColor": "#eebefa", + "backgroundColor": "transparent", + "width": 7.919653577804183, + "height": 6.264800591397204, + "seed": 457264498, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727168637439, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + -0.11820378474340032, + 0 + ], + [ + -0.23640756948680064, + 0.11820378474334348 + ], + [ + -0.3546113542297462, + 0.23640756948668695 + ], + [ + -0.47281513897314653, + 0.35461135423003043 + ], + [ + -0.5910189237165469, + 0.4728151389733739 + ], + [ + -1.1820378474335484, + 0.9456302779467478 + ], + [ + -1.5366492016632947, + 1.1820378474334348 + ], + [ + -1.8912605558934956, + 1.5366492016634652 + ], + [ + -2.1276681253802963, + 1.7730567711501521 + ], + [ + -2.245871910123242, + 2.009464340636839 + ], + [ + -2.4822794796100425, + 2.245871910123526 + ], + [ + -2.718687049096843, + 2.6004832643535565 + ], + [ + -3.073298403327044, + 2.8368908338402434 + ], + [ + -3.4279097575567903, + 3.0732984033269304 + ], + [ + -3.664317327043591, + 3.3097059728136173 + ], + [ + -3.9007248965303916, + 3.6643173270436478 + ], + [ + -4.1371324660167375, + 3.9007248965303347 + ], + [ + -4.373540035503538, + 4.137132466017022 + ], + [ + -4.609947604990339, + 4.373540035503709 + ], + [ + -4.846355174477139, + 4.609947604990396 + ], + [ + -5.082762743963485, + 4.8463551744770825 + ], + [ + -5.200966528706886, + 5.200966528707113 + ], + [ + -5.437374098193686, + 5.4373740981938 + ], + [ + -5.5555778829370865, + 5.673781667680487 + ], + [ + -5.5555778829370865, + 5.910189237167174 + ], + [ + -5.673781667680487, + 6.028393021910517 + ], + [ + -5.791985452423887, + 6.146596806653861 + ], + [ + -5.9101892371672875, + 6.146596806653861 + ], + [ + -5.9101892371672875, + 6.264800591397204 + ], + [ + -5.791985452423887, + 6.264800591397204 + ], + [ + -5.5555778829370865, + 6.146596806653861 + ], + [ + -5.437374098193686, + 6.146596806653861 + ], + [ + -4.609947604990339, + 6.146596806653861 + ], + [ + -4.018928681273792, + 6.146596806653861 + ], + [ + -3.4279097575567903, + 6.028393021910517 + ], + [ + -2.8368908338402434, + 6.028393021910517 + ], + [ + -2.364075694866642, + 5.910189237167174 + ], + [ + -2.009464340636896, + 5.910189237167174 + ], + [ + -1.654852986406695, + 5.910189237167174 + ], + [ + -1.4184454169198943, + 5.910189237167174 + ], + [ + -1.1820378474335484, + 5.910189237167174 + ], + [ + -0.9456302779467478, + 5.910189237167174 + ], + [ + -0.5910189237165469, + 5.910189237167174 + ], + [ + -0.11820378474340032, + 5.910189237167174 + ], + [ + 0.23640756948680064, + 5.910189237167174 + ], + [ + 0.5910189237165469, + 5.910189237167174 + ], + [ + 0.8274264932033475, + 6.028393021910517 + ], + [ + 1.0638340626901481, + 6.028393021910517 + ], + [ + 1.3002416321769488, + 6.028393021910517 + ], + [ + 1.418445416920349, + 6.028393021910517 + ], + [ + 1.654852986406695, + 6.028393021910517 + ], + [ + 1.7730567711500953, + 5.910189237167174 + ], + [ + 1.7730567711500953, + 5.673781667680487 + ], + [ + 1.8912605558934956, + 5.4373740981938 + ], + [ + 2.009464340636896, + 4.728151389733739 + ], + [ + 2.009464340636896, + 4.255336250760365 + ], + [ + 2.009464340636896, + 3.7825211117869912 + ], + [ + 2.009464340636896, + 3.191502188070274 + ], + [ + 1.8912605558934956, + 2.6004832643535565 + ], + [ + 1.654852986406695, + 2.3640756948668695 + ], + [ + 1.654852986406695, + 2.009464340636839 + ], + [ + 1.5366492016632947, + 1.7730567711501521 + ], + [ + 1.5366492016632947, + 1.5366492016634652 + ], + [ + 1.418445416920349, + 1.4184454169201217 + ], + [ + 1.418445416920349, + 1.3002416321767782 + ], + [ + 1.418445416920349, + 1.1820378474334348 + ], + [ + 1.3002416321769488, + 1.0638340626900913 + ], + [ + 1.3002416321769488, + 0.9456302779467478 + ], + [ + 1.0638340626901481, + 0.8274264932034043 + ], + [ + 1.0638340626901481, + 0.7092227084600609 + ], + [ + 1.0638340626901481, + 0.8274264932034043 + ], + [ + 1.1820378474335484, + 0.9456302779467478 + ], + [ + 1.1820378474335484, + 0.9456302779467478 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 87, + "versionNonce": 1511660334, + "isDeleted": false, + "id": "0g_VgO5XP4oqgvpUi0usb", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3349.7418441081572, + "y": 488.59096759495503, + "strokeColor": "#eebefa", + "backgroundColor": "transparent", + "width": 9.929117918440625, + "height": 9.810914133697452, + "seed": 1301871026, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727168639476, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.11820378474334348 + ], + [ + 0, + 0.23640756948668695 + ], + [ + 0.11820378474340032, + 0.35461135423003043 + ], + [ + 0.23640756948680064, + 0.4728151389733739 + ], + [ + 0.7092227084599472, + 0.7092227084600609 + ], + [ + 1.3002416321769488, + 0.8274264932034043 + ], + [ + 1.8912605558934956, + 1.0638340626900344 + ], + [ + 2.718687049096843, + 1.1820378474334348 + ], + [ + 3.427909757557245, + 1.3002416321767214 + ], + [ + 4.018928681273792, + 1.4184454169201217 + ], + [ + 4.491743820246938, + 1.5366492016634083 + ], + [ + 4.846355174477139, + 1.6548529864068087 + ], + [ + 5.20096652870734, + 1.6548529864068087 + ], + [ + 5.5555778829370865, + 1.7730567711500953 + ], + [ + 6.028393021910688, + 1.8912605558934956 + ], + [ + 6.383004376140434, + 2.0094643406367823 + ], + [ + 6.737615730370635, + 2.1276681253801826 + ], + [ + 7.092227084600836, + 2.1276681253801826 + ], + [ + 7.446838438830582, + 2.245871910123469 + ], + [ + 7.801449793060783, + 2.245871910123469 + ], + [ + 8.27426493203393, + 2.3640756948668695 + ], + [ + 8.62887628626413, + 2.482279479610156 + ], + [ + 8.865283855750931, + 2.482279479610156 + ], + [ + 9.101691425237277, + 2.6004832643535565 + ], + [ + 9.219895209980677, + 2.6004832643535565 + ], + [ + 9.338098994724078, + 2.718687049096843 + ], + [ + 9.219895209980677, + 2.8368908338402434 + ], + [ + 9.101691425237277, + 2.95509461858353 + ], + [ + 8.747080071007531, + 3.191502188070217 + ], + [ + 8.27426493203393, + 3.664317327043591 + ], + [ + 8.037857362547584, + 3.900724896530278 + ], + [ + 7.801449793060783, + 4.137132466016965 + ], + [ + 7.5650422235739825, + 4.609947604990339 + ], + [ + 7.328634654087182, + 4.964558959220369 + ], + [ + 7.092227084600836, + 5.3191703134504 + ], + [ + 6.855819515114035, + 5.67378166768043 + ], + [ + 6.501208160883834, + 6.0283930219104604 + ], + [ + 6.264800591397034, + 6.383004376140491 + ], + [ + 6.028393021910688, + 6.737615730370521 + ], + [ + 5.9101892371672875, + 6.974023299857208 + ], + [ + 5.791985452423887, + 7.210430869343895 + ], + [ + 5.673781667680487, + 7.328634654087239 + ], + [ + 5.437374098193686, + 7.565042223573926 + ], + [ + 5.20096652870734, + 7.683246008317269 + ], + [ + 4.846355174477139, + 8.0378573625473 + ], + [ + 4.609947604990339, + 8.274264932033986 + ], + [ + 4.373540035503993, + 8.510672501520673 + ], + [ + 4.137132466017192, + 8.628876286264017 + ], + [ + 3.7825211117869912, + 8.983487640494047 + ], + [ + 3.5461135423001906, + 9.219895209980734 + ], + [ + 3.1915021880704444, + 9.456302779467421 + ], + [ + 2.9550946185836438, + 9.692710348954108 + ], + [ + 2.8368908338402434, + 9.692710348954108 + ], + [ + 2.8368908338402434, + 9.810914133697452 + ], + [ + 2.8368908338402434, + 9.692710348954108 + ], + [ + 2.718687049096843, + 9.456302779467421 + ], + [ + 2.600483264353443, + 9.219895209980734 + ], + [ + 2.600483264353443, + 8.983487640494047 + ], + [ + 2.482279479610497, + 8.628876286264017 + ], + [ + 2.364075694867097, + 8.274264932033986 + ], + [ + 2.1276681253802963, + 7.919653577803956 + ], + [ + 1.7730567711500953, + 7.328634654087239 + ], + [ + 1.418445416920349, + 6.737615730370521 + ], + [ + 1.1820378474335484, + 6.501208160883834 + ], + [ + 1.1820378474335484, + 6.264800591397147 + ], + [ + 0.9456302779467478, + 6.0283930219104604 + ], + [ + 0.8274264932033475, + 5.7919854524237735 + ], + [ + 0.5910189237170016, + 5.3191703134504 + ], + [ + 0.35461135423020096, + 4.846355174477026 + ], + [ + 0.11820378474340032, + 4.609947604990339 + ], + [ + 0, + 4.491743820246995 + ], + [ + -0.11820378474340032, + 4.373540035503652 + ], + [ + -0.11820378474340032, + 4.137132466016965 + ], + [ + -0.23640756948680064, + 3.900724896530278 + ], + [ + -0.23640756948680064, + 3.5461135423003043 + ], + [ + -0.3546113542297462, + 3.191502188070217 + ], + [ + -0.3546113542297462, + 2.95509461858353 + ], + [ + -0.47281513897314653, + 2.718687049096843 + ], + [ + -0.47281513897314653, + 2.6004832643535565 + ], + [ + -0.47281513897314653, + 2.3640756948668695 + ], + [ + -0.47281513897314653, + 2.245871910123469 + ], + [ + -0.47281513897314653, + 2.1276681253801826 + ], + [ + -0.5910189237165469, + 1.8912605558934956 + ], + [ + -0.5910189237165469, + 1.7730567711500953 + ], + [ + -0.5910189237165469, + 1.7730567711500953 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 69, + "versionNonce": 576838702, + "isDeleted": false, + "id": "WwYcI5jUF5n1hhFubGjWq", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3147.621313377997, + "y": 466.0044783536315, + "strokeColor": "#eebefa", + "backgroundColor": "transparent", + "width": 7.3724254437147465, + "height": 9.478832713347515, + "seed": 1897746546, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727168644149, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + 0.601830648466489 + ], + [ + 0, + 0.9027459726997904 + ], + [ + 0, + 1.0532036348163842 + ], + [ + 0.15045766211687805, + 1.203661296933035 + ], + [ + 0.4513729863501794, + 2.40732259386607 + ], + [ + 0.6018306484666027, + 3.009153242332559 + ], + [ + 0.9027459726999041, + 3.610983890799048 + ], + [ + 1.0532036348167821, + 4.212814539265537 + ], + [ + 1.3541189590496288, + 4.814645187732083 + ], + [ + 1.5045766211665068, + 5.266018174081921 + ], + [ + 1.6550342832829301, + 5.867848822548467 + ], + [ + 1.8054919453998082, + 6.168764146781712 + ], + [ + 1.9559496075162315, + 6.469679471014956 + ], + [ + 2.1064072696331095, + 6.770594795248201 + ], + [ + 2.1064072696331095, + 6.921052457364851 + ], + [ + 2.256864931749533, + 7.221967781598096 + ], + [ + 2.407322593866411, + 7.52288310583134 + ], + [ + 2.557780255982834, + 7.823798430064585 + ], + [ + 2.7082379180997123, + 8.12471375429783 + ], + [ + 2.8586955802161356, + 8.425629078531074 + ], + [ + 2.8586955802161356, + 8.726544402764375 + ], + [ + 3.009153242332559, + 8.726544402764375 + ], + [ + 3.009153242332559, + 8.576086740647725 + ], + [ + 3.159610904449437, + 8.27517141641448 + ], + [ + 3.159610904449437, + 8.12471375429783 + ], + [ + 3.6109838907991616, + 7.52288310583134 + ], + [ + 3.911899215032463, + 7.221967781598096 + ], + [ + 4.212814539265764, + 6.770594795248201 + ], + [ + 4.363272201382188, + 6.469679471014956 + ], + [ + 4.814645187732367, + 6.018306484665061 + ], + [ + 5.115560511965668, + 5.566933498315166 + ], + [ + 5.41647583619897, + 5.115560511965327 + ], + [ + 5.566933498315393, + 4.814645187732083 + ], + [ + 5.867848822548694, + 4.664187525615432 + ], + [ + 6.018306484665118, + 4.513729863498838 + ], + [ + 6.018306484665118, + 4.212814539265537 + ], + [ + 6.319221808898419, + 4.062356877148943 + ], + [ + 6.469679471015297, + 3.9118992150322924 + ], + [ + 6.6201371331317205, + 3.7614415529156986 + ], + [ + 6.7705947952485985, + 3.610983890799048 + ], + [ + 6.7705947952485985, + 3.4605262286823972 + ], + [ + 6.921052457365022, + 3.3100685665658034 + ], + [ + 7.071510119481445, + 3.3100685665658034 + ], + [ + 7.071510119481445, + 3.1596109044491527 + ], + [ + 7.221967781598323, + 3.1596109044491527 + ], + [ + 7.221967781598323, + 3.009153242332559 + ], + [ + 7.3724254437147465, + 3.009153242332559 + ], + [ + 7.3724254437147465, + 2.858695580215908 + ], + [ + 7.221967781598323, + 2.7082379180993144 + ], + [ + 6.921052457365022, + 2.5577802559826637 + ], + [ + 6.921052457365022, + 2.40732259386607 + ], + [ + 6.168764146781996, + 1.805491945399524 + ], + [ + 5.717391160431816, + 1.5045766211662794 + ], + [ + 5.115560511965668, + 0.9027459726997904 + ], + [ + 4.814645187732367, + 0.7522883105831397 + ], + [ + 4.363272201382188, + 0.4513729863498952 + ], + [ + 4.212814539265764, + 0.30091532423330136 + ], + [ + 3.911899215032463, + 0.15045766211665068 + ], + [ + 3.7614415529160397, + 0.15045766211665068 + ], + [ + 3.4605262286827383, + -0.15045766211659384 + ], + [ + 3.159610904449437, + -0.3009153242332445 + ], + [ + 2.7082379180997123, + -0.45137298634983836 + ], + [ + 2.256864931749533, + -0.601830648466489 + ], + [ + 1.9559496075162315, + -0.7522883105831397 + ], + [ + 1.8054919453998082, + -0.7522883105831397 + ], + [ + 1.5045766211665068, + -0.7522883105831397 + ], + [ + 1.5045766211665068, + -0.7522883105831397 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 90, + "versionNonce": 591150642, + "isDeleted": false, + "id": "HobrExEoVo_UA0X2Yejnx", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3158.3038073882776, + "y": 467.3585973126811, + "strokeColor": "#eebefa", + "backgroundColor": "transparent", + "width": 10.983409334513453, + "height": 9.027459726997563, + "seed": 1308376946, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727168647159, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.15045766211659384 + ], + [ + 0.30091532423330136, + -0.45137298634983836 + ], + [ + 0.6018306484666027, + -0.9027459726997336 + ], + [ + 0.752288310583026, + -0.9027459726997336 + ], + [ + 1.3541189590496288, + -1.8054919453994671 + ], + [ + 1.9559496075162315, + -2.2568649317493623 + ], + [ + 2.407322593865956, + -2.7082379180992575 + ], + [ + 2.8586955802161356, + -3.159610904449096 + ], + [ + 3.159610904449437, + -3.4605262286823972 + ], + [ + 3.4605262286822835, + -3.7614415529156418 + ], + [ + 3.761441552915585, + -4.212814539265537 + ], + [ + 4.062356877148886, + -4.5137298634987815 + ], + [ + 4.212814539265764, + -4.814645187732026 + ], + [ + 4.513729863499066, + -4.965102849848677 + ], + [ + 4.664187525615489, + -5.1155605119652705 + ], + [ + 4.814645187731912, + -5.266018174081864 + ], + [ + 4.814645187731912, + -5.416475836198515 + ], + [ + 4.96510284984879, + -5.416475836198515 + ], + [ + 5.115560511965214, + -5.266018174081864 + ], + [ + 5.266018174082092, + -5.1155605119652705 + ], + [ + 5.416475836198515, + -4.814645187732026 + ], + [ + 6.018306484665118, + -4.062356877148886 + ], + [ + 6.469679471014842, + -3.7614415529156418 + ], + [ + 6.921052457365022, + -3.3100685665657466 + ], + [ + 7.221967781598323, + -2.858695580215908 + ], + [ + 7.52288310583117, + -2.2568649317493623 + ], + [ + 7.673340767948048, + -1.9559496075161178 + ], + [ + 7.974256092181349, + -1.5045766211662226 + ], + [ + 8.124713754297773, + -1.203661296932978 + ], + [ + 8.27517141641465, + -0.9027459726997336 + ], + [ + 8.425629078531074, + -0.601830648466489 + ], + [ + 8.726544402764375, + -0.15045766211659384 + ], + [ + 8.726544402764375, + 0.15045766211665068 + ], + [ + 8.877002064881253, + 0.4513729863498952 + ], + [ + 9.027459726997677, + 0.7522883105831397 + ], + [ + 9.1779173891141, + 0.9027459726997904 + ], + [ + 9.1779173891141, + 1.203661296933035 + ], + [ + 9.328375051230978, + 1.5045766211662794 + ], + [ + 9.478832713347401, + 1.6550342832829301 + ], + [ + 9.478832713347401, + 1.805491945399524 + ], + [ + 9.478832713347401, + 1.9559496075161746 + ], + [ + 9.62929037546428, + 2.1064072696327685 + ], + [ + 9.62929037546428, + 2.256864931749419 + ], + [ + 9.62929037546428, + 2.5577802559826637 + ], + [ + 9.62929037546428, + 2.7082379180993144 + ], + [ + 9.779748037580703, + 2.858695580215908 + ], + [ + 9.779748037580703, + 3.1596109044492096 + ], + [ + 9.779748037580703, + 3.3100685665658034 + ], + [ + 9.779748037580703, + 3.460526228682454 + ], + [ + 9.779748037580703, + 3.610983890799048 + ], + [ + 9.478832713347401, + 3.610983890799048 + ], + [ + 8.877002064881253, + 3.610983890799048 + ], + [ + 7.974256092181349, + 3.460526228682454 + ], + [ + 7.823798430064471, + 3.460526228682454 + ], + [ + 6.6201371331317205, + 3.460526228682454 + ], + [ + 6.018306484665118, + 3.460526228682454 + ], + [ + 5.266018174082092, + 3.460526228682454 + ], + [ + 4.664187525615489, + 3.3100685665658034 + ], + [ + 4.062356877148886, + 3.009153242332559 + ], + [ + 3.4605262286822835, + 2.858695580215908 + ], + [ + 3.009153242332559, + 2.858695580215908 + ], + [ + 2.557780255982834, + 2.7082379180993144 + ], + [ + 1.9559496075162315, + 2.7082379180993144 + ], + [ + 1.5045766211665068, + 2.7082379180993144 + ], + [ + 1.0532036348163274, + 2.7082379180993144 + ], + [ + 0.6018306484666027, + 2.5577802559826637 + ], + [ + 0, + 2.40732259386607 + ], + [ + -0.6018306484666027, + 2.256864931749419 + ], + [ + -0.9027459726999041, + 2.256864931749419 + ], + [ + -1.0532036348163274, + 2.256864931749419 + ], + [ + -1.2036612969327507, + 2.256864931749419 + ], + [ + -1.2036612969327507, + 2.1064072696327685 + ], + [ + -1.2036612969327507, + 1.9559496075161746 + ], + [ + -1.0532036348163274, + 1.805491945399524 + ], + [ + -0.9027459726999041, + 1.805491945399524 + ], + [ + 0.15045766211687805, + 1.5045766211662794 + ], + [ + 1.0532036348163274, + 1.203661296933035 + ], + [ + 2.106407269632655, + 1.053203634816441 + ], + [ + 3.159610904449437, + 0.9027459726997904 + ], + [ + 3.911899215032463, + 0.7522883105831397 + ], + [ + 4.664187525615489, + 0.4513729863498952 + ], + [ + 5.115560511965214, + 0.4513729863498952 + ], + [ + 5.566933498315393, + 0.4513729863498952 + ], + [ + 6.018306484665118, + 0.4513729863498952 + ], + [ + 6.168764146781541, + 0.30091532423330136 + ], + [ + 6.168764146781541, + 0.15045766211665068 + ], + [ + 6.168764146781541, + 0 + ], + [ + 6.168764146781541, + 0 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "freedraw", + "version": 66, + "versionNonce": 2114816882, + "isDeleted": false, + "id": "5obrCtdwf6U5S7x5OFR6x", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 3158.0028920640443, + "y": 466.75676666421464, + "strokeColor": "#eebefa", + "backgroundColor": "transparent", + "width": 5.115560511965214, + "height": 5.86784882254841, + "seed": 2055899182, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727168648685, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 0, + -0.15045766211665068 + ], + [ + -0.1504576621164233, + -0.15045766211665068 + ], + [ + -0.30091532423330136, + -0.3009153242332445 + ], + [ + -0.45137298634972467, + -0.3009153242332445 + ], + [ + -0.6018306484666027, + -0.45137298634983836 + ], + [ + -0.752288310583026, + -0.45137298634983836 + ], + [ + -0.9027459726994493, + -0.45137298634983836 + ], + [ + -1.0532036348163274, + -0.601830648466489 + ], + [ + -1.2036612969327507, + -0.601830648466489 + ], + [ + -1.3541189590496288, + -0.601830648466489 + ], + [ + -1.504576621166052, + -0.601830648466489 + ], + [ + -1.8054919453993534, + -0.45137298634983836 + ], + [ + -2.256864931749078, + -0.3009153242332445 + ], + [ + -2.7082379180992575, + -0.15045766211665068 + ], + [ + -2.7082379180992575, + 0 + ], + [ + -2.858695580215681, + 0 + ], + [ + -3.009153242332559, + 0.15045766211665068 + ], + [ + -3.009153242332559, + 0.4513729863498952 + ], + [ + -3.159610904448982, + 0.601830648466489 + ], + [ + -3.3100685665658602, + 0.9027459726997904 + ], + [ + -3.4605262286822835, + 1.203661296933035 + ], + [ + -3.610983890798707, + 1.5045766211662794 + ], + [ + -3.761441552915585, + 1.805491945399524 + ], + [ + -3.761441552915585, + 2.256864931749419 + ], + [ + -3.610983890798707, + 2.7082379180992575 + ], + [ + -3.610983890798707, + 3.009153242332559 + ], + [ + -3.4605262286822835, + 3.3100685665658034 + ], + [ + -3.3100685665658602, + 3.610983890799048 + ], + [ + -3.159610904448982, + 3.9118992150322924 + ], + [ + -2.858695580215681, + 4.212814539265537 + ], + [ + -2.5577802559823795, + 4.363272201382188 + ], + [ + -2.256864931749078, + 4.664187525615432 + ], + [ + -1.9559496075162315, + 4.814645187732026 + ], + [ + -1.6550342832829301, + 4.965102849848677 + ], + [ + -1.3541189590496288, + 5.115560511965327 + ], + [ + -1.0532036348163274, + 5.115560511965327 + ], + [ + -0.752288310583026, + 5.266018174081921 + ], + [ + -0.45137298634972467, + 5.266018174081921 + ], + [ + -0.1504576621164233, + 5.115560511965327 + ], + [ + 0.15045766211687805, + 4.965102849848677 + ], + [ + 0.30091532423330136, + 4.814645187732026 + ], + [ + 0.4513729863501794, + 4.363272201382188 + ], + [ + 0.752288310583026, + 4.062356877148943 + ], + [ + 0.9027459726999041, + 3.610983890799048 + ], + [ + 1.0532036348163274, + 3.1596109044491527 + ], + [ + 1.2036612969332054, + 2.858695580215908 + ], + [ + 1.3541189590496288, + 2.407322593866013 + ], + [ + 1.3541189590496288, + 1.9559496075161746 + ], + [ + 1.3541189590496288, + 1.5045766211662794 + ], + [ + 1.3541189590496288, + 1.203661296933035 + ], + [ + 1.3541189590496288, + 0.9027459726997904 + ], + [ + 1.2036612969332054, + 0.7522883105831397 + ], + [ + 1.0532036348163274, + 0.601830648466489 + ], + [ + 1.0532036348163274, + 0.4513729863498952 + ], + [ + 0.9027459726999041, + 0.3009153242332445 + ], + [ + 0.752288310583026, + 0.15045766211665068 + ], + [ + 0.4513729863501794, + 0 + ], + [ + 0.15045766211687805, + -0.15045766211665068 + ], + [ + -0.30091532423330136, + -0.3009153242332445 + ], + [ + -0.6018306484666027, + -0.45137298634983836 + ], + [ + -0.752288310583026, + -0.45137298634983836 + ], + [ + -0.9027459726994493, + -0.45137298634983836 + ], + [ + 0, + 0 + ] + ], + "lastCommittedPoint": null, + "simulatePressure": true, + "pressures": [] + }, + { + "type": "rectangle", + "version": 83, + "versionNonce": 1773584124, + "isDeleted": false, + "id": "pVxFYg6-5Tv4jl8TsKj0I", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1079.7042454139407, + "y": 2592.683689941259, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 157.44344699837507, + "height": 94.4660681990249, + "seed": 1735498308, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "9JeNvdBklScRNibp3wKz7" + }, + { + "id": "NFrs7hBfcfS6Sh6nfbpdz", + "type": "arrow" + }, + { + "id": "ldtBH_Mc3-DYjw69pcUaV", + "type": "arrow" + }, + { + "id": "VQweVVOoEtZ8a74fJHKXs", + "type": "arrow" + }, + { + "id": "gmFBoiLPBB3mYCMjdlIJg", + "type": "arrow" + }, + { + "id": "bgmRPtR4-gNH9lKHCrEsm", + "type": "arrow" + } + ], + "updated": 1727281263503, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 52, + "versionNonce": 752882300, + "isDeleted": false, + "id": "9JeNvdBklScRNibp3wKz7", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1117.792633799603, + "y": 2618.3167240407715, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 81.26667022705078, + "height": 43.2, + "seed": 1553209596, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727281197281, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Connection\n(shared)", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "pVxFYg6-5Tv4jl8TsKj0I", + "originalText": "Connection\n(shared)", + "lineHeight": 1.35, + "baseline": 37 + }, + { + "type": "arrow", + "version": 43, + "versionNonce": 464221180, + "isDeleted": false, + "id": "gmFBoiLPBB3mYCMjdlIJg", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1090.656833031219, + "y": 2691.2569784967636, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1.3690734521599097, + "height": 142.38363902461697, + "seed": 2107606396, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1727281273173, + "link": null, + "locked": false, + "startBinding": { + "elementId": "pVxFYg6-5Tv4jl8TsKj0I", + "focus": 0.8621664311247793, + "gap": 4.107220356479729, + "fixedPoint": null + }, + "endBinding": { + "elementId": "2-xZdr1N7h94XCAl94juo", + "focus": -0.6301610521371144, + "gap": 7.669843435648545, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 1.3690734521599097, + 142.38363902461697 + ] + ] + }, + { + "type": "arrow", + "version": 50, + "versionNonce": 171172220, + "isDeleted": false, + "id": "bgmRPtR4-gNH9lKHCrEsm", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1122.145522430894, + "y": 2835.0096909735403, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0, + "height": 141.0145655724573, + "seed": 507855356, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1727281269698, + "link": null, + "locked": false, + "startBinding": { + "elementId": "2-xZdr1N7h94XCAl94juo", + "focus": -0.3322257774355254, + "gap": 6.300769983488863, + "fixedPoint": null + }, + "endBinding": { + "elementId": "pVxFYg6-5Tv4jl8TsKj0I", + "focus": 0.46086956521738914, + "gap": 6.845367260799094, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 0, + -141.0145655724573 + ] + ] + }, + { + "type": "rectangle", + "version": 979, + "versionNonce": 1773016700, + "isDeleted": false, + "id": "cWZP1HvIYSurecnj1amRD", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1994.9915785610522, + "y": 2576.7572908831867, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 778.8793335223208, + "height": 431.6222461467981, + "seed": 1707833596, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "_1zjd_YKPLNWSmBM9zSyU" + } + ], + "updated": 1727281348191, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 775, + "versionNonce": 1025369852, + "isDeleted": false, + "id": "_1zjd_YKPLNWSmBM9zSyU", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2314.372910971627, + "y": 2581.7572908831867, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 140.11666870117188, + "height": 21.6, + "seed": 1147861372, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727281348191, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Client-specific Task", + "textAlign": "center", + "verticalAlign": "top", + "containerId": "cWZP1HvIYSurecnj1amRD", + "originalText": "Client-specific Task", + "lineHeight": 1.35, + "baseline": 16 + }, + { + "type": "rectangle", + "version": 925, + "versionNonce": 1599498876, + "isDeleted": false, + "id": "7a7GmGAWuf8jSu-sYYfTp", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1997.3280202018902, + "y": 2857.0197378854195, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 196.66666666666652, + "height": 95.55555555555566, + "seed": 340158972, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "VpZgUOnXz9b6iNz_Chz1B" + }, + { + "id": "ZH_GmO6NGruR-R8bZ4HZm", + "type": "arrow" + }, + { + "id": "BZfZ8iljhfHWuYcXbbQdx", + "type": "arrow" + }, + { + "id": "WBHsrUKG3MrJV-R22yFiA", + "type": "arrow" + }, + { + "id": "6JqryeENmL-H7rgIaJuh9", + "type": "arrow" + }, + { + "id": "3XGGmdzHBAGFj4zxD6Iw2", + "type": "arrow" + }, + { + "id": "VMLA6b_iBs2FQu4UrFSAx", + "type": "arrow" + }, + { + "id": "sneG026N1n_G0mJImR8vA", + "type": "arrow" + } + ], + "updated": 1727281378970, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 889, + "versionNonce": 1347770364, + "isDeleted": false, + "id": "VpZgUOnXz9b6iNz_Chz1B", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2044.953021473456, + "y": 2893.997515663197, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 101.41666412353516, + "height": 21.6, + "seed": 677366396, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727281348191, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Gateway Task", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "7a7GmGAWuf8jSu-sYYfTp", + "originalText": "Gateway Task", + "lineHeight": 1.35, + "baseline": 16 + }, + { + "type": "rectangle", + "version": 1053, + "versionNonce": 533342972, + "isDeleted": false, + "id": "PTjH3jg7mLB18v6W498da", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2527.883575757446, + "y": 2614.6937368531376, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 196.66666666666652, + "height": 95.55555555555566, + "seed": 595139324, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 3 + }, + "boundElements": [ + { + "type": "text", + "id": "kP3G-MzVviiRb6gKg5Os-" + }, + { + "id": "ZH_GmO6NGruR-R8bZ4HZm", + "type": "arrow" + }, + { + "id": "BZfZ8iljhfHWuYcXbbQdx", + "type": "arrow" + }, + { + "id": "cdGiEkWU5h1FiPzxF__gG", + "type": "arrow" + }, + { + "id": "c8tkIRV-7Xn59xbOVktKK", + "type": "arrow" + } + ], + "updated": 1727281348191, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1042, + "versionNonce": 1875078012, + "isDeleted": false, + "id": "kP3G-MzVviiRb6gKg5Os-", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2570.433575757446, + "y": 2651.671514630915, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 111.56666666666666, + "height": 21.6, + "seed": 1334140, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727281348191, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Heartbeat Task", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "PTjH3jg7mLB18v6W498da", + "originalText": "Heartbeat Task", + "lineHeight": 1.35, + "baseline": 16 + }, + { + "type": "arrow", + "version": 2852, + "versionNonce": 426188228, + "isDeleted": false, + "id": "ZH_GmO6NGruR-R8bZ4HZm", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2127.7174129913365, + "y": 2849.241960107642, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 390.46375598061695, + "height": 129.70537456127022, + "seed": 410225660, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "type": "text", + "id": "W_GkViIuf0KTQVsLngrFG" + } + ], + "updated": 1727281348841, + "link": null, + "locked": false, + "startBinding": { + "elementId": "7a7GmGAWuf8jSu-sYYfTp", + "focus": -0.5582511967384937, + "gap": 7.777777777777374, + "fixedPoint": null + }, + "endBinding": { + "elementId": "PTjH3jg7mLB18v6W498da", + "focus": -0.26326216636567984, + "gap": 9.702406785492485, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 390.46375598061695, + -129.70537456127022 + ] + ] + }, + { + "type": "text", + "version": 49, + "versionNonce": 1711903940, + "isDeleted": false, + "id": "W_GkViIuf0KTQVsLngrFG", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1772.161701643898, + "y": 2534.0014186990447, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 271.9166666666667, + "height": 21.6, + "seed": 781757564, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727281341536, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "forwards heartbeat related messages", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "ZH_GmO6NGruR-R8bZ4HZm", + "originalText": "forwards heartbeat related messages", + "lineHeight": 1.35, + "baseline": 16 + }, + { + "type": "arrow", + "version": 2925, + "versionNonce": 918912196, + "isDeleted": false, + "id": "BZfZ8iljhfHWuYcXbbQdx", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2514.397213308828, + "y": 2701.042633204998, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 336.49418377822326, + "height": 6.073617383433884, + "seed": 1950727420, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [ + { + "type": "text", + "id": "vwGEhagoimZQw4uY-nTFA" + } + ], + "updated": 1727281348841, + "link": null, + "locked": false, + "startBinding": { + "elementId": "PTjH3jg7mLB18v6W498da", + "focus": -0.819117002605131, + "gap": 13.486362448617683, + "fixedPoint": null + }, + "endBinding": { + "elementId": "-E9W5yBlGNCtWEpcR2_2M", + "focus": 0.7794593358170915, + "gap": 1, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + -336.49418377822326, + -6.073617383433884 + ] + ] + }, + { + "type": "text", + "version": 135, + "versionNonce": 1828871236, + "isDeleted": false, + "id": "vwGEhagoimZQw4uY-nTFA", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 1841.854198748636, + "y": 2447.6179703853186, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 178.93333333333334, + "height": 21.6, + "seed": 318553468, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727281341536, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "response to send to user", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "BZfZ8iljhfHWuYcXbbQdx", + "originalText": "response to send to user", + "lineHeight": 1.35, + "baseline": 16 + }, + { + "type": "rectangle", + "version": 822, + "versionNonce": 519204604, + "isDeleted": false, + "id": "Rd3Tj5d0KSDYLdR3co_nS", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2572.0673396415623, + "y": 2843.848369592339, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 104, + "height": 106, + "seed": 872394236, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "o8K3y7gmyX-RYepcD0xE1" + }, + { + "id": "WBHsrUKG3MrJV-R22yFiA", + "type": "arrow" + }, + { + "id": "6JqryeENmL-H7rgIaJuh9", + "type": "arrow" + }, + { + "id": "cdGiEkWU5h1FiPzxF__gG", + "type": "arrow" + }, + { + "id": "c8tkIRV-7Xn59xbOVktKK", + "type": "arrow" + } + ], + "updated": 1727281348191, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 747, + "versionNonce": 1766010748, + "isDeleted": false, + "id": "o8K3y7gmyX-RYepcD0xE1", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2605.575672847739, + "y": 2875.2483695923393, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 36.983333587646484, + "height": 43.2, + "seed": 139497084, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [], + "updated": 1727281348191, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Kill\nSend", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "Rd3Tj5d0KSDYLdR3co_nS", + "originalText": "Kill\nSend", + "lineHeight": 1.35, + "baseline": 37 + }, + { + "type": "arrow", + "version": 2714, + "versionNonce": 693640132, + "isDeleted": false, + "id": "WBHsrUKG3MrJV-R22yFiA", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2200.439131313002, + "y": 2884.0338362550115, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 360.62820832856096, + "height": 32.54624939685527, + "seed": 1353801468, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1727281348841, + "link": null, + "locked": false, + "startBinding": { + "elementId": "7a7GmGAWuf8jSu-sYYfTp", + "focus": -0.5334254122892165, + "gap": 6.444444444445139, + "fixedPoint": null + }, + "endBinding": { + "elementId": "Rd3Tj5d0KSDYLdR3co_nS", + "focus": -0.44056326474809493, + "gap": 10.999999999999545, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 + ], + [ + 360.62820832856096, + 32.54624939685527 + ] + ] + }, + { + "type": "arrow", + "version": 2694, + "versionNonce": 1628371652, + "isDeleted": false, + "id": "6JqryeENmL-H7rgIaJuh9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2567.0673396415623, + "y": 2880.90125711863, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 363.18376388411616, + "height": 21.527906649725992, + "seed": 1705254780, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1727281348841, + "link": null, + "locked": false, + "startBinding": { + "elementId": "Rd3Tj5d0KSDYLdR3co_nS", + "focus": 0.22410638896707286, + "gap": 5, + "fixedPoint": null + }, + "endBinding": { + "elementId": "7a7GmGAWuf8jSu-sYYfTp", + "focus": -0.9670293282058773, + "gap": 9.888888888889483, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ + [ + 0, + 0 ], [ - 2.7295764114028316, - 6.177462404753896 - ], + -363.18376388411616, + -21.527906649725992 + ] + ] + }, + { + "type": "arrow", + "version": 2613, + "versionNonce": 299506116, + "isDeleted": false, + "id": "cdGiEkWU5h1FiPzxF__gG", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2579.5097007625372, + "y": 2837.848369592339, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1.93999474353609, + "height": 117.59907718364548, + "seed": 1063964668, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1727281348841, + "link": null, + "locked": false, + "startBinding": { + "elementId": "Rd3Tj5d0KSDYLdR3co_nS", + "focus": -0.8611163144119921, + "gap": 6, + "fixedPoint": null + }, + "endBinding": { + "elementId": "PTjH3jg7mLB18v6W498da", + "focus": 0.442023862129913, + "gap": 10.000000000000455, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ [ - 2.7295764114028316, - 6.03380048836425 + 0, + 0 ], [ - 2.7295764114028316, - 5.890138571974717 - ], + 1.93999474353609, + -117.59907718364548 + ] + ] + }, + { + "type": "arrow", + "version": 2644, + "versionNonce": 866744516, + "isDeleted": false, + "id": "c8tkIRV-7Xn59xbOVktKK", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2621.1893351664958, + "y": 2717.6937368531385, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1.363581222544326, + "height": 117.15463273920068, + "seed": 2114416764, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1727281348842, + "link": null, + "locked": false, + "startBinding": { + "elementId": "PTjH3jg7mLB18v6W498da", + "focus": 0.04434077475452471, + "gap": 7.444444444445253, + "fixedPoint": null + }, + "endBinding": { + "elementId": "Rd3Tj5d0KSDYLdR3co_nS", + "focus": -0.0943274078885894, + "gap": 9, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ [ - 2.7295764114028316, - 5.74647665558507 + 0, + 0 ], [ - 2.7295764114028316, - 5.74647665558507 + -1.363581222544326, + 117.15463273920068 ] - ], - "pressures": [], - "simulatePressure": true, - "lastCommittedPoint": [ - 2.7295764114028316, - 5.74647665558507 ] }, { - "id": "mxaGNWNFDtpqougV0fQYG", - "type": "freedraw", - "x": 3217.3877286093175, - "y": 576.2200191794889, - "width": 4.740843240857885, - "height": 8.619714983377548, + "type": "rectangle", + "version": 196, + "versionNonce": 46452348, + "isDeleted": false, + "id": "-E9W5yBlGNCtWEpcR2_2M", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, "angle": 0, - "strokeColor": "#343a40", - "backgroundColor": "#ffec99", + "x": 2020.551208403777, + "y": 2608.3929668696496, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 157.44344699837507, + "height": 94.4660681990249, + "seed": 1899757820, + "groupIds": [], + "frameId": null, + "roundness": null, + "boundElements": [ + { + "type": "text", + "id": "xMziosa7KpcbYpi6eQglJ" + }, + { + "id": "BZfZ8iljhfHWuYcXbbQdx", + "type": "arrow" + }, + { + "id": "3XGGmdzHBAGFj4zxD6Iw2", + "type": "arrow" + }, + { + "id": "VMLA6b_iBs2FQu4UrFSAx", + "type": "arrow" + }, + { + "id": "qUGaP5OV93rxoqqy7V6Au", + "type": "arrow" + }, + { + "id": "sneG026N1n_G0mJImR8vA", + "type": "arrow" + } + ], + "updated": 1727281358219, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 164, + "versionNonce": 1518158972, + "isDeleted": false, + "id": "xMziosa7KpcbYpi6eQglJ", "fillStyle": "solid", "strokeWidth": 1, "strokeStyle": "solid", "roughness": 0, "opacity": 100, + "angle": 0, + "x": 2058.639596789439, + "y": 2634.026000969162, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 81.26667022705078, + "height": 43.2, + "seed": 545948028, "groupIds": [], "frameId": null, - "index": "b2Q", "roundness": null, - "seed": 261323083, - "version": 73, - "versionNonce": 75490917, + "boundElements": [], + "updated": 1727281348191, + "link": null, + "locked": false, + "fontSize": 16, + "fontFamily": 6, + "text": "Connection\n(shared)", + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "-E9W5yBlGNCtWEpcR2_2M", + "originalText": "Connection\n(shared)", + "lineHeight": 1.35, + "baseline": 37 + }, + { + "type": "arrow", + "version": 430, + "versionNonce": 40703940, "isDeleted": false, - "boundElements": null, - "updated": 1727116217187, + "id": "3XGGmdzHBAGFj4zxD6Iw2", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2031.5037960210555, + "y": 2706.966255425154, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 1.3690734521599097, + "height": 142.38363902461697, + "seed": 1395978748, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1727281348842, "link": null, "locked": false, + "startBinding": { + "elementId": "-E9W5yBlGNCtWEpcR2_2M", + "focus": 0.8621664311247793, + "gap": 4.107220356479274, + "fixedPoint": null + }, + "endBinding": { + "elementId": "7a7GmGAWuf8jSu-sYYfTp", + "focus": -0.6301610521371122, + "gap": 7.669843435648545, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", "points": [ [ 0, 0 ], [ - 0.14366191638964665, - 0.14366191638964665 - ], - [ - 0.2873238327792933, - 0.5746476655584729 - ], - [ - 0.2873238327792933, - 0.7183095819481196 - ], - [ - 0.43098574916893995, - 0.7183095819481196 - ], - [ - 0.43098574916893995, - 0.8619714983377662 - ], - [ - 0.43098574916893995, - 1.0056334147274129 - ], - [ - 0.5746476655585866, - 1.2929572475065925 - ], - [ - 0.5746476655585866, - 1.4366191638962391 - ], - [ - 0.5746476655585866, - 1.7239429966755324 - ], - [ - 0.5746476655585866, - 1.867604913065179 - ], - [ - 0.7183095819482332, - 2.2985906622340053 - ], - [ - 0.7183095819482332, - 2.442252578623652 - ], - [ - 0.8619714983378799, - 2.8732383277924782 - ], - [ - 0.8619714983378799, - 3.016900244182125 - ], - [ - 1.0056334147275265, - 3.447885993351065 - ], - [ - 1.0056334147275265, - 3.5915479097407115 - ], - [ - 1.1492953311171732, - 3.878871742519891 - ], - [ - 1.1492953311171732, - 4.022533658909538 - ], - [ - 1.1492953311171732, - 4.309857491688831 - ], - [ - 1.2929572475068198, - 4.309857491688831 - ], - [ - 1.2929572475068198, - 4.597181324468011 - ], - [ - 1.2929572475068198, - 4.740843240857657 - ], - [ - 1.2929572475068198, - 4.884505157247304 - ], - [ - 1.2929572475068198, - 5.028167073636951 - ], - [ - 1.2929572475068198, - 5.171828990026597 - ], - [ - 1.2929572475068198, - 5.31549090641613 - ], - [ - 1.1492953311171732, - 5.31549090641613 - ], - [ - 1.1492953311171732, - 5.459152822805777 - ], - [ - 1.1492953311171732, - 5.6028147391954235 - ], - [ - 1.0056334147275265, - 5.74647665558507 - ], - [ - 1.0056334147275265, - 5.890138571974717 - ], - [ - 0.8619714983378799, - 6.03380048836425 - ], - [ - 0.8619714983378799, - 6.177462404753896 - ], - [ - 0.8619714983378799, - 6.321124321143543 - ], - [ - 0.7183095819482332, - 6.46478623753319 - ], - [ - 0.7183095819482332, - 6.752110070312483 - ], - [ - 0.5746476655585866, - 6.89577198670213 - ], - [ - 0.5746476655585866, - 7.039433903091663 - ], - [ - 0.43098574916893995, - 7.326757735870956 - ], - [ - 0.2873238327792933, - 7.470419652260603 - ], - [ - 0.2873238327792933, - 7.614081568650249 - ], - [ - 0.14366191638964665, - 7.757743485039782 - ], + 1.3690734521599097, + 142.38363902461697 + ] + ] + }, + { + "type": "arrow", + "version": 437, + "versionNonce": 2115303108, + "isDeleted": false, + "id": "VMLA6b_iBs2FQu4UrFSAx", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "angle": 0, + "x": 2062.99248542073, + "y": 2850.7189679019307, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "width": 0, + "height": 141.0145655724573, + "seed": 406671996, + "groupIds": [], + "frameId": null, + "roundness": { + "type": 2 + }, + "boundElements": [], + "updated": 1727281348842, + "link": null, + "locked": false, + "startBinding": { + "elementId": "7a7GmGAWuf8jSu-sYYfTp", + "focus": -0.3322257774355254, + "gap": 6.300769983488863, + "fixedPoint": null + }, + "endBinding": { + "elementId": "-E9W5yBlGNCtWEpcR2_2M", + "focus": 0.46086956521739203, + "gap": 6.845367260798639, + "fixedPoint": null + }, + "lastCommittedPoint": null, + "startArrowhead": null, + "endArrowhead": "arrow", + "points": [ [ 0, - 7.901405401429429 - ], - [ - -0.14366191638964665, - 7.901405401429429 - ], - [ - -0.2873238327792933, - 7.901405401429429 - ], - [ - -0.43098574916893995, - 8.045067317819075 - ], - [ - -0.5746476655585866, - 8.045067317819075 - ], - [ - -0.8619714983378799, - 8.188729234208722 - ], - [ - -1.1492953311167184, - 8.332391150598369 - ], - [ - -1.292957247506365, - 8.476053066988015 - ], - [ - -1.5802810802856584, - 8.476053066988015 - ], - [ - -1.8676049130649517, - 8.619714983377548 - ], - [ - -2.0112668294545983, - 8.619714983377548 - ], - [ - -2.154928745844245, - 8.619714983377548 - ], - [ - -2.2985906622338916, - 8.619714983377548 - ], - [ - -2.4422525786235383, - 8.619714983377548 - ], - [ - -2.4422525786235383, - 8.476053066988015 - ], - [ - -2.585914495013185, - 8.476053066988015 - ], - [ - -2.585914495013185, - 8.332391150598369 - ], - [ - -2.7295764114028316, - 8.332391150598369 - ], - [ - -2.8732383277924782, - 8.188729234208722 - ], - [ - -3.016900244182125, - 8.188729234208722 - ], - [ - -3.1605621605717715, - 8.188729234208722 - ], - [ - -3.1605621605717715, - 8.045067317819075 - ], - [ - -3.1605621605717715, - 7.901405401429429 - ], - [ - -3.304224076961418, - 7.757743485039782 - ], - [ - -3.447885993351065, - 7.614081568650249 - ], - [ - -3.447885993351065, - 7.470419652260603 - ], - [ - -3.304224076961418, - 7.470419652260603 - ], - [ - -3.304224076961418, - 7.326757735870956 + 0 ], [ - -3.304224076961418, - 7.326757735870956 + 0, + -141.0145655724573 ] - ], - "pressures": [], - "simulatePressure": true, - "lastCommittedPoint": [ - -3.304224076961418, - 7.326757735870956 ] } ], "appState": { "gridSize": 20, - "gridStep": 5, - "gridModeEnabled": false, "viewBackgroundColor": "#ffffff" }, "files": {} From ffd56c6a98fb1a03d9d2fc712c25740b1a775a8b Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 00:23:18 +0200 Subject: [PATCH 151/162] some work on gateway_task and TODO notes about the connection object --- src/gateway/gateway_task.rs | 44 ++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs index acd5474..9dd7300 100644 --- a/src/gateway/gateway_task.rs +++ b/src/gateway/gateway_task.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use chorus::types::GatewayHeartbeat; +use futures::StreamExt; use tokio::sync::Mutex; use crate::errors::Error; @@ -8,17 +10,47 @@ use super::{Event, GatewayClient}; /// Handles all messages a client sends to the gateway post-handshake. pub(super) async fn gateway_task( - connection: Arc>, - inbox: tokio::sync::broadcast::Receiver, - kill_receive: tokio::sync::broadcast::Receiver<()>, - kill_send: tokio::sync::broadcast::Sender<()>, + mut connection: Arc>, + mut inbox: tokio::sync::broadcast::Receiver, + mut kill_receive: tokio::sync::broadcast::Receiver<()>, + mut kill_send: tokio::sync::broadcast::Sender<()>, + mut heartbeat_send: tokio::sync::broadcast::Sender, last_sequence_number: Arc>, ) { - // TODO + let inbox_processor = tokio::spawn(process_inbox( + connection.clone(), + inbox.resubscribe(), + kill_receive.resubscribe(), + )); + + loop { + tokio::select! { + _ = kill_receive.recv() => { + return; + }, + // TODO: This locks the connection mutex which is not ideal/deadlock risk. Perhaps we + // should turn our websocket connection into a tokio broadcast channel instead. so that + // we can receive messages from it without having to lock one connection object. + message = connection.lock().await.receiver.next() => { + match message { + Ok(message) => { + handle_message(message, connection.clone()).await; + } + Err(_) => { + kill_send.send(()).expect("Failed to send kill signal"); + } + } + } + } + } + todo!() } -async fn handle_event(event: Event, connection: Arc>) { +async fn handle_event( + event: Event, + connection: Arc>, +) -> Result<(), Error> { // TODO todo!() } From 51ad4b5120f27e76ac73d4058029ac5a03bf2e59 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 00:23:35 +0200 Subject: [PATCH 152/162] pass heartbeat_send to gateway task --- src/gateway/establish_connection.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 9dc1778..115d7c6 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -220,6 +220,7 @@ async fn finish_connecting( gateway_user.lock().await.inbox.resubscribe(), state.kill_receive.resubscribe(), state.kill_send.clone(), + state.heartbeat_send.clone(), state.sequence_number.clone(), )), match heartbeat_handler_handle { From 53545032977f042d1cd446db9c115f3d41a6faaf Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 16:29:41 +0200 Subject: [PATCH 153/162] impl From> for GatewayError --- src/errors.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index 2a39597..655a571 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -9,6 +9,7 @@ use std::fmt::Display; use chorus::types::{APIError, AuthError, Rights}; use poem::{error::ResponseError, http::StatusCode, Response}; +use tokio::sync::broadcast::error::SendError; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -79,6 +80,12 @@ pub enum GatewayError { Internal, } +impl From> for GatewayError { + fn from(value: SendError) -> Self { + Self::Internal + } +} + #[derive(Debug, thiserror::Error)] pub enum UserError { #[error("EMAIL_INVALID")] From 9a83e37a58cf7d1fb4952e5cdd3c530db55d2fb2 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 16:33:47 +0200 Subject: [PATCH 154/162] impl From> for Error --- src/errors.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/errors.rs b/src/errors.rs index 655a571..3e5fbe6 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -86,6 +86,12 @@ impl From> for GatewayError { } } +impl From> for Error { + fn from(value: SendError) -> Self { + Self::Gateway(GatewayError::from(value)) + } +} + #[derive(Debug, thiserror::Error)] pub enum UserError { #[error("EMAIL_INVALID")] From 25c8e066d1cbff34d3574ffe008dd176813f5f06 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 16:35:05 +0200 Subject: [PATCH 155/162] Remove lock acquisition need for WebSocketConnection by adapterfying Sink/Stream to tokio::sync::broadcast channel --- src/gateway/establish_connection.rs | 36 +++++----- src/gateway/gateway_task.rs | 20 ++---- src/gateway/heartbeat.rs | 53 ++++++++------ src/gateway/types.rs | 108 +++++++++++++++++++++++++--- 4 files changed, 154 insertions(+), 63 deletions(-) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index 115d7c6..b2b1ffc 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -36,7 +36,7 @@ use super::{ /// Internal use only state struct to pass around data to the `finish_connecting` function. struct State { - connection: Arc>, + connection: WebSocketConnection, db: PgPool, config: Config, connected_users: ConnectedUsers, @@ -68,17 +68,18 @@ pub(super) async fn establish_connection( let mut connection: WebSocketConnection = ws_stream.split().into(); trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Sending hello message"); // Hello message - connection + match connection .sender .send(Message::Text(json!(GatewayHello::default()).to_string())) - .await?; + { + Ok(_) => (), + Err(e) => { + log::debug!(target: "symfonia::gateway::establish_connection", "Error when sending hello message. Aborting connection: {e}"); + return Err(GatewayError::Internal.into()); + } + }; trace!(target: "symfonia::gateway::establish_connection::establish_connection", "Sent hello message"); - // Wrap the connection in an Arc> to allow for shared ownership and mutation. - // For example, the `HeartbeatHandler` and `GatewayClient` tasks will need to access the connection - // to send messages. - let connection = Arc::new(Mutex::new(connection)); - let mut received_identify_or_resume = false; let (kill_send, mut kill_receive) = tokio::sync::broadcast::channel::<()>(1); @@ -144,14 +145,18 @@ pub(super) async fn establish_connection( #[allow(clippy::too_many_arguments)] async fn finish_connecting( mut heartbeat_handler_handle: Option>, - state: State, + mut state: State, ) -> Result { loop { trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Waiting for next message..."); - let raw_message = match state.connection.lock().await.receiver.next().await { - Some(next) => next, - None => return Err(GatewayError::Timeout.into()), - }?; + let raw_message = match state.connection.receiver.recv().await { + Ok(next) => next, + Err(_) => { + log::debug!(target: "symfonia::gateway::finish_connecting", "Encountered error when trying to receive message. Sending kill signal..."); + state.kill_send.send(()).expect("Failed to send kill_send"); + return Err(GatewayError::Timeout.into()); + } + }; debug!(target: "symfonia::gateway::establish_connection::finish_connecting", "Received message"); if let Ok(heartbeat) = from_str::(&raw_message.to_string()) { @@ -264,15 +269,12 @@ async fn finish_connecting( log::warn!(target: "symfonia::gateway::establish_connection::finish_connecting", "Resuming connections is not yet implemented. Telling client to identify instead."); state .connection - .lock() - .await .sender .send(Message::Close(Some(CloseFrame { code: CloseCode::from(4007u16), reason: "Resuming connections is not yet implemented. Please identify instead." .into(), - }))) - .await?; + })))?; state .kill_send .send(()) diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs index 9dd7300..6d00864 100644 --- a/src/gateway/gateway_task.rs +++ b/src/gateway/gateway_task.rs @@ -10,7 +10,7 @@ use super::{Event, GatewayClient}; /// Handles all messages a client sends to the gateway post-handshake. pub(super) async fn gateway_task( - mut connection: Arc>, + mut connection: super::WebSocketConnection, mut inbox: tokio::sync::broadcast::Receiver, mut kill_receive: tokio::sync::broadcast::Receiver<()>, mut kill_send: tokio::sync::broadcast::Sender<()>, @@ -24,6 +24,7 @@ pub(super) async fn gateway_task( )); loop { + todo!(); tokio::select! { _ = kill_receive.recv() => { return; @@ -31,32 +32,19 @@ pub(super) async fn gateway_task( // TODO: This locks the connection mutex which is not ideal/deadlock risk. Perhaps we // should turn our websocket connection into a tokio broadcast channel instead. so that // we can receive messages from it without having to lock one connection object. - message = connection.lock().await.receiver.next() => { - match message { - Ok(message) => { - handle_message(message, connection.clone()).await; - } - Err(_) => { - kill_send.send(()).expect("Failed to send kill signal"); - } - } - } } } todo!() } -async fn handle_event( - event: Event, - connection: Arc>, -) -> Result<(), Error> { +async fn handle_event(event: Event, connection: super::WebSocketConnection) -> Result<(), Error> { // TODO todo!() } async fn process_inbox( - connection: Arc>, + connection: super::WebSocketConnection, mut inbox: tokio::sync::broadcast::Receiver, mut kill_receive: tokio::sync::broadcast::Receiver<()>, ) { diff --git a/src/gateway/heartbeat.rs b/src/gateway/heartbeat.rs index b828559..6bb9f32 100644 --- a/src/gateway/heartbeat.rs +++ b/src/gateway/heartbeat.rs @@ -19,7 +19,7 @@ static HEARTBEAT_INTERVAL: std::time::Duration = std::time::Duration::from_secs( static LATENCY_BUFFER: std::time::Duration = std::time::Duration::from_secs(5); pub(super) struct HeartbeatHandler { - connection: Arc>, + connection: WebSocketConnection, kill_receive: tokio::sync::broadcast::Receiver<()>, kill_send: tokio::sync::broadcast::Sender<()>, message_receive: tokio::sync::broadcast::Receiver, @@ -63,7 +63,7 @@ impl HeartbeatHandler { /// let heartbeat_handler = HeartbeatHandler::new(connection, kill_receive, kill_send, message_receive).await; /// ``` pub(super) fn new( - connection: Arc>, + connection: WebSocketConnection, kill_receive: tokio::sync::broadcast::Receiver<()>, kill_send: tokio::sync::broadcast::Sender<()>, message_receive: tokio::sync::broadcast::Receiver, @@ -153,28 +153,30 @@ impl HeartbeatHandler { // TODO: We could potentially send a heartbeat to the client, prompting it to send a new heartbeat. // This would require more logic though. trace!(target: "symfonia::gateway::heartbeat_handler", "Received heartbeat sequence number is way off by {}. This may be due to latency.", diff); - self.connection.lock().await.sender.send(Message::Text(json!(GatewayReconnect::default()).to_string())).await.unwrap_or_else(|_| { - trace!("Failed to send reconnect message in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); - self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); - }); + match self.connection.sender.send(Message::Text(json!(GatewayReconnect::default()).to_string())) { + Ok(_) => (), + Err(e) => { + trace!("Failed to send reconnect message in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); + self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); + } + }; self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); return; } } } self.last_heartbeat = std::time::Instant::now(); - self.connection - .lock() - .await - .sender - .send(Message::Text( - json!(GatewayHeartbeatAck::default()).to_string(), - )) - .await.unwrap_or_else(|_| { - trace!("Failed to send heartbeat ack in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); - self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); - } - ); + match self.connection.sender.send(Message::Text( + json!(GatewayHeartbeatAck::default()).to_string(), + )) { + Ok(_) => (), + Err(_) => { + trace!("Failed to send heartbeat ack in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); + self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); + }, + } + + ; } else => { // TODO: We could potentially send a heartbeat if we haven't received one in ~40 seconds, @@ -203,10 +205,17 @@ impl HeartbeatHandler { /// Shorthand for sending a heartbeat ack message. async fn send_ack(&self) { - self.connection.lock().await.sender.send(Message::Text(json!(GatewayHeartbeatAck::default()).to_string())).await.unwrap_or_else(|_| { - trace!("Failed to send heartbeat ack in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); - self.kill_send.send(()).expect("Failed to send kill signal in heartbeat_handler"); - }); + match self.connection.sender.send(Message::Text( + json!(GatewayHeartbeatAck::default()).to_string(), + )) { + Ok(_) => (), + Err(_) => { + trace!("Failed to send heartbeat ack in heartbeat_handler. Stopping gateway_task and heartbeat_handler"); + self.kill_send + .send(()) + .expect("Failed to send kill signal in heartbeat_handler"); + } + }; } } diff --git a/src/gateway/types.rs b/src/gateway/types.rs index 0028042..bb78a1b 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -20,11 +20,16 @@ use chorus::types::{ UserUpdate, VoiceServerUpdate, VoiceStateUpdate, WebhooksUpdate, }; use futures::stream::{SplitSink, SplitStream}; +use futures::{SinkExt, StreamExt}; +use log::log; use pubserve::Subscriber; use sqlx::PgPool; use sqlx_pg_uint::PgU64; use tokio::net::TcpStream; use tokio::sync::Mutex; +use tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode; +use tokio_tungstenite::tungstenite::protocol::CloseFrame; +use tokio_tungstenite::tungstenite::Message; use tokio_tungstenite::WebSocketStream; use crate::{WebSocketReceive, WebSocketSend}; @@ -240,7 +245,7 @@ pub struct GatewayUser { /// A concrete session, that a [GatewayUser] is connected to the Gateway with. pub struct GatewayClient { - connection: Arc>, + connection: WebSocketConnection, /// A [Weak] reference to the [GatewayUser] this client belongs to. pub parent: Weak>, // Handle to the main Gateway task for this client @@ -344,7 +349,7 @@ impl ConnectedUsers { pub async fn new_client( &self, user: Arc>, - connection: Arc>, + connection: WebSocketConnection, main_task_handle: tokio::task::JoinHandle<()>, heartbeat_task_handle: tokio::task::JoinHandle<()>, kill_send: tokio::sync::broadcast::Sender<()>, @@ -523,8 +528,98 @@ impl RoleUserMap { } pub struct WebSocketConnection { - pub sender: WebSocketSend, - pub receiver: WebSocketReceive, + pub sender: tokio::sync::broadcast::Sender, + pub receiver: tokio::sync::broadcast::Receiver, + sender_task: Arc>, + receiver_task: Arc>, +} + +impl WebSocketConnection { + pub fn new(mut sink: WebSocketSend, mut stream: WebSocketReceive) -> Self { + let (mut sender, mut receiver) = tokio::sync::broadcast::channel(100); + let mut sender_sender_task = sender.clone(); + let mut receiver_sender_task = receiver.resubscribe(); + // The sender task concerns itself with sending messages to the WebSocket client. + let sender_task = tokio::spawn(async move { + loop { + let message: Result = + receiver_sender_task.recv().await; + match message { + Ok(msg) => { + let send_result = sink.send(msg).await; + match send_result { + Ok(_) => (), + Err(_) => { + sender_sender_task.send(Message::Close(Some(CloseFrame { + code: CloseCode::Error, + reason: "Channel closed or error encountered".into(), + }))); + return; + } + } + } + Err(_) => return, + } + } + }); + let sender_receiver_task = sender.clone(); + // The receiver task receives messages from the WebSocket client and sends them to the + // broadcast channel. + let receiver_task = tokio::spawn(async move { + loop { + let web_socket_recieve_result = match stream.next().await { + Some(res) => res, + None => { + log::debug!(target: "symfonia::gateway::WebSocketConnection", "WebSocketReceive yielded None. Sending close message..."); + sender_receiver_task.send(Message::Close(Some(CloseFrame { + code: CloseCode::Error, + reason: "Channel closed or error encountered".into(), + }))); + return; + } + }; + let web_socket_receive_message = match web_socket_recieve_result { + Ok(message) => message, + Err(e) => { + log::error!(target: "symfonia::gateway::WebSocketConnection", "Received malformed message, closing channel: {e}"); + sender_receiver_task.send(Message::Close(Some(CloseFrame { + code: CloseCode::Error, + reason: "Channel closed or error encountered".into(), + }))); + return; + } + }; + match sender_receiver_task.send(web_socket_receive_message) { + Ok(_) => (), + Err(e) => { + log::error!(target: "symfonia::gateway::WebSocketConnection", "Unable to send received WebSocket message to channel recipients. Closing channel: {e}"); + sender_receiver_task.send(Message::Close(Some(CloseFrame { + code: CloseCode::Error, + reason: "Channel closed or error encountered".into(), + }))); + return; + } + } + } + }); + Self { + sender, + receiver, + sender_task: Arc::new(sender_task), + receiver_task: Arc::new(receiver_task), + } + } +} + +impl Clone for WebSocketConnection { + fn clone(&self) -> Self { + Self { + sender: self.sender.clone(), + receiver: self.receiver.resubscribe(), + sender_task: self.sender_task.clone(), + receiver_task: self.receiver_task.clone(), + } + } } #[derive(Clone)] @@ -547,10 +642,7 @@ impl SplitStream>, ), ) -> Self { - Self { - sender: value.0, - receiver: value.1, - } + Self::new(value.0, value.1) } } From ec1e77f23118d64f9c73b18e6780d697457ef336 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 16:40:18 +0200 Subject: [PATCH 156/162] Document WebSocketConnection --- src/gateway/types.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/gateway/types.rs b/src/gateway/types.rs index bb78a1b..d1d8a24 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -527,6 +527,15 @@ impl RoleUserMap { } } +/// Connection to a WebSocket client with sending and receiving capabilities. +/// +/// A [WebSocketConnection] is essentially an adapter from tungstenites sink/stream to a +/// [tokio::sync::broadcast] channel. Broadcast channels are used in favor of sink/stream, because +/// to clone a sink/stream to pass it around to different tasks which need sending/receiving +/// capabilities, an `Arc>` has to be used. This means, that no more than one task can +/// listen for incoming messages at a time, as a lock on the [Mutex] has to be acquired. +/// +/// Read up on [tokio::sync::broadcast] channels if you'd like to understand how they work. pub struct WebSocketConnection { pub sender: tokio::sync::broadcast::Sender, pub receiver: tokio::sync::broadcast::Receiver, @@ -535,6 +544,7 @@ pub struct WebSocketConnection { } impl WebSocketConnection { + /// Create a new [WebSocketConnection] from a tungstenite Sink/Stream pair. pub fn new(mut sink: WebSocketSend, mut stream: WebSocketReceive) -> Self { let (mut sender, mut receiver) = tokio::sync::broadcast::channel(100); let mut sender_sender_task = sender.clone(); From 4ce28ac3e07c7a87f9cfcefb750042c0e11b242d Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 16:40:18 +0200 Subject: [PATCH 157/162] Document WebSocketConnection --- src/gateway/types.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/gateway/types.rs b/src/gateway/types.rs index bb78a1b..2c4a00a 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -527,6 +527,15 @@ impl RoleUserMap { } } +/// Connection to a WebSocket client with sending and receiving capabilities. +/// +/// A [WebSocketConnection] is essentially an adapter from tungstenites sink/stream to a +/// [tokio::sync::broadcast] channel. Broadcast channels are used in favor of sink/stream, because +/// to clone a sink/stream to pass it around to different tasks which need sending/receiving +/// capabilities, an `Arc>` has to be used. This means, that no more than one task can +/// listen for incoming messages at a time, as a lock on the [Mutex] has to be acquired. +/// +/// Read up on [tokio::sync::broadcast] channels if you'd like to understand how they work. pub struct WebSocketConnection { pub sender: tokio::sync::broadcast::Sender, pub receiver: tokio::sync::broadcast::Receiver, @@ -535,7 +544,9 @@ pub struct WebSocketConnection { } impl WebSocketConnection { + /// Create a new [WebSocketConnection] from a tungstenite Sink/Stream pair. pub fn new(mut sink: WebSocketSend, mut stream: WebSocketReceive) -> Self { + // "100" is an arbitrary limit. Feel free to adjust this, if you have a good reason for it. -bitfl0wer let (mut sender, mut receiver) = tokio::sync::broadcast::channel(100); let mut sender_sender_task = sender.clone(); let mut receiver_sender_task = receiver.resubscribe(); From ccf17339706b3bb2aca79b6c92ea3167e3a319be Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 17:54:26 +0200 Subject: [PATCH 158/162] fix typo --- src/gateway/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gateway/types.rs b/src/gateway/types.rs index 2c4a00a..50196bf 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -578,7 +578,7 @@ impl WebSocketConnection { // broadcast channel. let receiver_task = tokio::spawn(async move { loop { - let web_socket_recieve_result = match stream.next().await { + let web_socket_receive_result = match stream.next().await { Some(res) => res, None => { log::debug!(target: "symfonia::gateway::WebSocketConnection", "WebSocketReceive yielded None. Sending close message..."); @@ -589,7 +589,7 @@ impl WebSocketConnection { return; } }; - let web_socket_receive_message = match web_socket_recieve_result { + let web_socket_receive_message = match web_socket_receive_result { Ok(message) => message, Err(e) => { log::error!(target: "symfonia::gateway::WebSocketConnection", "Received malformed message, closing channel: {e}"); From 1f423257709edfddd5947b1d42655ffa007da05f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 21:32:38 +0200 Subject: [PATCH 159/162] Majorly update EventType and Event Enums - Normalized Event enum to be a full GatewayPayload --- src/gateway/types.rs | 97 +++++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/src/gateway/types.rs b/src/gateway/types.rs index 50196bf..e4e3120 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -9,7 +9,8 @@ use std::sync::{Arc, Weak}; use ::serde::de::DeserializeOwned; use ::serde::{Deserialize, Serialize}; use chorus::types::{ - ChannelCreate, ChannelDelete, ChannelUpdate, GatewayHello, GatewayInvalidSession, GatewayReady, + ChannelCreate, ChannelDelete, ChannelUpdate, GatewayHeartbeat, GatewayHello, + GatewayIdentifyPayload, GatewayInvalidSession, GatewayReady, GatewayRequestGuildMembers, GatewayResume, GuildBanAdd, GuildBanRemove, GuildCreate, GuildDelete, GuildEmojisUpdate, GuildIntegrationsUpdate, GuildMemberAdd, GuildMemberRemove, GuildMemberUpdate, GuildMembersChunk, GuildUpdate, InteractionCreate, InviteCreate, InviteDelete, MessageCreate, @@ -54,7 +55,8 @@ use super::ResumableClientsStore; pub enum EventType { Hello, Ready, - Resumed, + Heartbeat, + Resume, InvalidSession, ChannelCreate, ChannelUpdate, @@ -103,7 +105,7 @@ pub enum EventType { StageInstanceCreate, StageInstanceUpdate, StageInstanceDelete, - RequestMembers, + GuildMembersRequest, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -113,49 +115,52 @@ pub enum EventType { #[serde(rename_all = "PascalCase")] pub enum Event { Hello(GatewayHello), - Ready(GatewayReady), - Resumed(GatewayResume), - InvalidSession(GatewayInvalidSession), - ChannelCreate(ChannelCreate), - ChannelUpdate(ChannelUpdate), - ChannelDelete(ChannelDelete), - ThreadCreate(ThreadCreate), - ThreadUpdate(ThreadUpdate), - ThreadDelete(ThreadDelete), - ThreadListSync(ThreadListSync), - ThreadMemberUpdate(ThreadMemberUpdate), - ThreadMembersUpdate(ThreadMembersUpdate), - GuildCreate(GuildCreate), - GuildUpdate(GuildUpdate), - GuildDelete(GuildDelete), - GuildBanAdd(GuildBanAdd), - GuildBanRemove(GuildBanRemove), - GuildEmojisUpdate(GuildEmojisUpdate), - GuildIntegrationsUpdate(GuildIntegrationsUpdate), - GuildMemberAdd(GuildMemberAdd), - GuildMemberRemove(GuildMemberRemove), - GuildMemberUpdate(GuildMemberUpdate), - GuildMembersChunk(GuildMembersChunk), - InteractionCreate(InteractionCreate), - InviteCreate(InviteCreate), - InviteDelete(InviteDelete), - MessageCreate(MessageCreate), - MessageUpdate(MessageUpdate), - MessageDelete(MessageDelete), - MessageDeleteBulk(MessageDeleteBulk), - MessageReactionAdd(MessageReactionAdd), - MessageReactionRemove(MessageReactionRemove), - MessageReactionRemoveAll(MessageReactionRemoveAll), - MessageReactionRemoveEmoji(MessageReactionRemoveEmoji), - PresenceUpdate(PresenceUpdate), - TypingStart(TypingStartEvent), - UserUpdate(UserUpdate), - VoiceStateUpdate(VoiceStateUpdate), - VoiceServerUpdate(VoiceServerUpdate), - WebhooksUpdate(WebhooksUpdate), - StageInstanceCreate(StageInstanceCreate), - StageInstanceUpdate(StageInstanceUpdate), - StageInstanceDelete(StageInstanceDelete), + Heartbeat(GatewayHeartbeat), + Ready(GatewayPayload), + Identify(GatewayPayload), + Resume(GatewayPayload), + InvalidSession(GatewayPayload), + ChannelCreate(GatewayPayload), + ChannelUpdate(GatewayPayload), + ChannelDelete(GatewayPayload), + ThreadCreate(GatewayPayload), + ThreadUpdate(GatewayPayload), + ThreadDelete(GatewayPayload), + ThreadListSync(GatewayPayload), + ThreadMemberUpdate(GatewayPayload), + ThreadMembersUpdate(GatewayPayload), + GuildCreate(GatewayPayload), + GuildUpdate(GatewayPayload), + GuildDelete(GatewayPayload), + GuildBanAdd(GatewayPayload), + GuildBanRemove(GatewayPayload), + GuildEmojisUpdate(GatewayPayload), + GuildIntegrationsUpdate(GatewayPayload), + GuildMemberAdd(GatewayPayload), + GuildMemberRemove(GatewayPayload), + GuildMemberUpdate(GatewayPayload), + GuildMembersChunk(GatewayPayload), + GuildMembersRequest(GatewayPayload), + InteractionCreate(GatewayPayload), + InviteCreate(GatewayPayload), + InviteDelete(GatewayPayload), + MessageCreate(GatewayPayload), + MessageUpdate(GatewayPayload), + MessageDelete(GatewayPayload), + MessageDeleteBulk(GatewayPayload), + MessageReactionAdd(GatewayPayload), + MessageReactionRemove(GatewayPayload), + MessageReactionRemoveAll(GatewayPayload), + MessageReactionRemoveEmoji(GatewayPayload), + PresenceUpdate(GatewayPayload), + TypingStart(GatewayPayload), + UserUpdate(GatewayPayload), + VoiceStateUpdate(GatewayPayload), + VoiceServerUpdate(GatewayPayload), + WebhooksUpdate(GatewayPayload), + StageInstanceCreate(GatewayPayload), + StageInstanceUpdate(GatewayPayload), + StageInstanceDelete(GatewayPayload), } #[derive(Serialize, Clone, PartialEq, Debug)] From 8e95e49d0fb1054aeba059ea3490b62a375f9281 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 21:32:47 +0200 Subject: [PATCH 160/162] fn received_message_to_event(message: Message) --- src/gateway/gateway_task.rs | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs index 6d00864..8ba43c6 100644 --- a/src/gateway/gateway_task.rs +++ b/src/gateway/gateway_task.rs @@ -1,12 +1,14 @@ use std::sync::Arc; -use chorus::types::GatewayHeartbeat; +use chorus::types::{GatewayHeartbeat, GatewaySendPayload}; use futures::StreamExt; +use serde_json::from_str; use tokio::sync::Mutex; +use tokio_tungstenite::tungstenite::Message; use crate::errors::Error; -use super::{Event, GatewayClient}; +use super::{Event, GatewayClient, GatewayPayload}; /// Handles all messages a client sends to the gateway post-handshake. pub(super) async fn gateway_task( @@ -38,6 +40,29 @@ pub(super) async fn gateway_task( todo!() } +/// Convert a [Message] into an [Event], if the event message is a valid event that a server can +/// expect to receive from a client. +fn received_message_to_event(message: Message) -> Result { + if !message.is_text() { + return Err(Error::Custom( + "Tungstenite message must be of type text".to_string(), + )); + } + let message_text = message.to_string(); + let gateway_payload = from_str::>(&message_text)?; + match gateway_payload.op_code { + 1 => Ok(Event::Heartbeat(from_str(&message_text)?)), + 2 => Ok(Event::Identify(from_str(&message_text)?)), + 3 => Ok(Event::PresenceUpdate(from_str(&message_text)?)), + 4 => Ok(Event::VoiceStateUpdate(from_str(&message_text)?)), + 6 => Ok(Event::Resume(from_str(&message_text)?)), + 8 => Ok(Event::GuildMembersRequest(from_str(&message_text)?)), + o => Err(Error::Custom(format!( + "opcode {o} is not a valid event to receive from a client" + ))), + } +} + async fn handle_event(event: Event, connection: super::WebSocketConnection) -> Result<(), Error> { // TODO todo!() From 4ba0de4a59450a67b95552a47978a2cbdeaa5d1f Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Thu, 26 Sep 2024 21:48:21 +0200 Subject: [PATCH 161/162] Start implementing gateway_task --- src/gateway/gateway_task.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs index 8ba43c6..b17a394 100644 --- a/src/gateway/gateway_task.rs +++ b/src/gateway/gateway_task.rs @@ -4,6 +4,7 @@ use chorus::types::{GatewayHeartbeat, GatewaySendPayload}; use futures::StreamExt; use serde_json::from_str; use tokio::sync::Mutex; +use tokio_tungstenite::tungstenite::protocol::CloseFrame; use tokio_tungstenite::tungstenite::Message; use crate::errors::Error; @@ -31,9 +32,27 @@ pub(super) async fn gateway_task( _ = kill_receive.recv() => { return; }, - // TODO: This locks the connection mutex which is not ideal/deadlock risk. Perhaps we - // should turn our websocket connection into a tokio broadcast channel instead. so that - // we can receive messages from it without having to lock one connection object. + message_result = connection.receiver.recv() => { + match message_result { + Ok(message) => { + let maybe_event = received_message_to_event(message); + let event = match maybe_event { + Ok(event) => event, + Err(e) => { + connection.sender.send(Message::Close(Some(CloseFrame { code: tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode::Library(4001), reason: e.to_string().into() }))); + kill_send.send(()).expect("Failed to send kill_send"); + return; + }, + }; + // TODO: Do something with the event + }, + Err(error) => { + connection.sender.send(Message::Close(Some(CloseFrame { code: tokio_tungstenite::tungstenite::protocol::frame::coding::CloseCode::Library(4000), reason: "INTERNAL_SERVER_ERROR".into() }))); + kill_send.send(()).expect("Failed to send kill_send"); + return; + }, + } + } } } From ca9f8d1d5a84a56f880a4923beb7df2b58fa2a23 Mon Sep 17 00:00:00 2001 From: bitfl0wer Date: Fri, 27 Sep 2024 00:50:53 +0200 Subject: [PATCH 162/162] additional logging, fix some deadlocks --- src/gateway/establish_connection.rs | 3 +++ src/gateway/gateway_task.rs | 5 +++++ src/gateway/types.rs | 15 +++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/src/gateway/establish_connection.rs b/src/gateway/establish_connection.rs index b2b1ffc..735a726 100644 --- a/src/gateway/establish_connection.rs +++ b/src/gateway/establish_connection.rs @@ -214,7 +214,9 @@ async fn finish_connecting( return Err(crate::errors::UserError::InvalidToken.into()); } }; + log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Getting gateway_user"); let mut gateway_user = state.connected_users.get_user_or_new(claims.id).await; + log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Creating gateway_client"); let gateway_client = state .connected_users .new_client( @@ -260,6 +262,7 @@ async fn finish_connecting( return Err(GatewayError::Internal.into()); } } + log::trace!(target: "symfonia::gateway::establish_connection::finish_connecting", "Done!"); return Ok(NewWebSocketConnection { user: gateway_user, client: gateway_client.clone(), diff --git a/src/gateway/gateway_task.rs b/src/gateway/gateway_task.rs index b17a394..abaae3a 100644 --- a/src/gateway/gateway_task.rs +++ b/src/gateway/gateway_task.rs @@ -1,9 +1,11 @@ use std::sync::Arc; +use std::time::Duration; use chorus::types::{GatewayHeartbeat, GatewaySendPayload}; use futures::StreamExt; use serde_json::from_str; use tokio::sync::Mutex; +use tokio::time::sleep; use tokio_tungstenite::tungstenite::protocol::CloseFrame; use tokio_tungstenite::tungstenite::Message; @@ -20,6 +22,7 @@ pub(super) async fn gateway_task( mut heartbeat_send: tokio::sync::broadcast::Sender, last_sequence_number: Arc>, ) { + log::trace!(target: "symfonia::gateway::gateway_task", "Started a new gateway task!"); let inbox_processor = tokio::spawn(process_inbox( connection.clone(), inbox.resubscribe(), @@ -27,6 +30,8 @@ pub(super) async fn gateway_task( )); loop { + // TODO remove sleep and implement stuff + sleep(Duration::from_secs(3600)).await; todo!(); tokio::select! { _ = kill_receive.recv() => { diff --git a/src/gateway/types.rs b/src/gateway/types.rs index e4e3120..a0c7b96 100644 --- a/src/gateway/types.rs +++ b/src/gateway/types.rs @@ -292,10 +292,15 @@ impl ConnectedUsers { /// [GatewayUser] if it does not exist using [ConnectedUsers::new_user]. pub async fn get_user_or_new(&self, id: Snowflake) -> Arc> { let inner = self.store.clone(); + log::trace!(target: "symfonia::gateway::types::ConnectedUsers::get_user_or_new", "Acquiring lock on ConnectedUsersInner..."); let mut lock = inner.lock().await; + log::trace!(target: "symfonia::gateway::types::ConnectedUsers::get_user_or_new", "Lock acquired!"); if let Some(user) = lock.users.get(&id) { + log::trace!(target: "symfonia::gateway::types::ConnectedUsers::get_user_or_new", "Found user {id} in store"); user.clone() } else { + drop(lock); + log::trace!(target: "symfonia::gateway::types::ConnectedUsers::get_user_or_new", "Creating new user {id} in store"); self.new_user(HashMap::new(), id, Vec::new()).await } } @@ -306,14 +311,17 @@ impl ConnectedUsers { /// Register a new [GatewayUser] with the [ConnectedUsers] instance. async fn register(&self, user: GatewayUser) -> Arc> { + log::trace!(target: "symfonia::gateway::types::ConnectedUsers::register", "Acquiring lock on ConnectedUsersInner..."); self.store .lock() .await .inboxes .insert(user.id, user.outbox.clone()); + log::trace!(target: "symfonia::gateway::types::ConnectedUsers::register", "Lock acquired!"); let id = user.id; let arc = Arc::new(Mutex::new(user)); self.store.lock().await.users.insert(id, arc.clone()); + log::trace!(target: "symfonia::gateway::types::ConnectedUsers::register", "Inserted user {id} into users store"); arc } @@ -371,10 +379,14 @@ impl ConnectedUsers { last_sequence, }; let arc = Arc::new(Mutex::new(client)); + log::trace!(target: "symfonia::gateway::ConnectedUsers::new_client", "Acquiring lock..."); user.lock() .await .clients .insert(session_token.to_string(), arc.clone()); + // TODO: Deadlock here + log::trace!(target: "symfonia::gateway::ConnectedUsers::new_client", "Lock acquired!"); + log::trace!(target: "symfonia::gateway::ConnectedUsers::new_client", "Inserted into map. Done."); arc } } @@ -557,6 +569,7 @@ impl WebSocketConnection { let mut receiver_sender_task = receiver.resubscribe(); // The sender task concerns itself with sending messages to the WebSocket client. let sender_task = tokio::spawn(async move { + log::trace!(target: "symfonia::gateway::types::WebSocketConnection", "spawned sender_task"); loop { let message: Result = receiver_sender_task.recv().await; @@ -582,6 +595,7 @@ impl WebSocketConnection { // The receiver task receives messages from the WebSocket client and sends them to the // broadcast channel. let receiver_task = tokio::spawn(async move { + log::trace!(target: "symfonia::gateway::types::WebSocketConnection", "spawned receiver_task"); loop { let web_socket_receive_result = match stream.next().await { Some(res) => res, @@ -629,6 +643,7 @@ impl WebSocketConnection { impl Clone for WebSocketConnection { fn clone(&self) -> Self { + log::trace!(target: "symfonia::gateway::WebSocketConnection", "WebSocketConnection cloned!"); Self { sender: self.sender.clone(), receiver: self.receiver.resubscribe(),