diff --git a/Cargo.lock b/Cargo.lock index c119415..4f2454a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3615,7 +3615,7 @@ dependencies = [ [[package]] name = "rainbeam" -version = "1.11.5" +version = "1.12.0" dependencies = [ "ammonia", "askama", diff --git a/crates/authbeam/src/database.rs b/crates/authbeam/src/database.rs index 7701b3d..90ef754 100644 --- a/crates/authbeam/src/database.rs +++ b/crates/authbeam/src/database.rs @@ -737,6 +737,10 @@ impl Database { } } + if !metadata.check() { + return Err(AuthError::TooLong); + } + // update user let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { "UPDATE \"xprofiles\" SET \"metadata\" = ? WHERE \"username\" = ?" diff --git a/crates/authbeam/src/model.rs b/crates/authbeam/src/model.rs index f67d688..3cc4c13 100644 --- a/crates/authbeam/src/model.rs +++ b/crates/authbeam/src/model.rs @@ -158,6 +158,30 @@ impl ProfileMetadata { self.kv.get(key).unwrap() == "true" } + + /// Check `kv` lengths + /// + /// # Returns + /// * `true`: ok + /// * `false`: invalid + pub fn check(&self) -> bool { + for field in &self.kv { + if field.0 == "sparkler:custom_css" { + // custom_css gets an extra long value + if field.1.len() > 64 * 128 { + return false; + } + + continue; + } + + if field.1.len() > 64 * 64 { + return false; + } + } + + true + } } impl Default for ProfileMetadata { @@ -387,6 +411,7 @@ pub enum AuthError { NotAllowed, ValueError, NotFound, + TooLong, Other, } @@ -398,6 +423,7 @@ impl AuthError { NotAllowed => String::from("You are not allowed to access this resource."), ValueError => String::from("One of the field values given is invalid."), NotFound => String::from("No asset with this ID could be found."), + TooLong => String::from("Given data is too long."), _ => String::from("An unspecified error has occured"), } } diff --git a/crates/rainbeam/Cargo.toml b/crates/rainbeam/Cargo.toml index 1e942af..ccddc2a 100644 --- a/crates/rainbeam/Cargo.toml +++ b/crates/rainbeam/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rainbeam" -version = "1.11.5" +version = "1.12.0" edition = "2021" authors = ["trisuaso", "swmff"] description = "Ask, share, socialize!" diff --git a/crates/rainbeam/src/database.rs b/crates/rainbeam/src/database.rs index 2b39454..c1ec878 100644 --- a/crates/rainbeam/src/database.rs +++ b/crates/rainbeam/src/database.rs @@ -319,7 +319,7 @@ impl Database { None }, followers: if options.followers | options.all { - match self.auth.get_following(user.clone()).await { + match self.auth.get_followers(user.clone()).await { Ok(r) => Some(r), Err(_) => return Err(DatabaseError::Other), } @@ -2322,7 +2322,11 @@ impl Database { /// # Arguments /// * `props` - [`ResponseCreate`] /// * `author` - the ID of the user creating the response - pub async fn create_response(&self, props: ResponseCreate, author: String) -> Result<()> { + pub async fn create_response( + &self, + props: ResponseCreate, + author: String, + ) -> Result { // make sure the question exists let mut question = if props.question != "0" { // get question from database @@ -2557,7 +2561,7 @@ impl Database { .incr(format!("rbeam.app.response_count:{}", response.author.id)) .await; - return Ok(()); + return Ok(response); } else { // change recipient so it doesn't show up in inbox let query: String = @@ -2602,7 +2606,7 @@ impl Database { } // return - Ok(()) + Ok(response) } Err(_) => Err(DatabaseError::Other), } @@ -4718,6 +4722,10 @@ impl Database { } } + if !metadata.check() { + return Err(DatabaseError::ContentTooLong); + } + // update circle let query: String = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { diff --git a/crates/rainbeam/src/model.rs b/crates/rainbeam/src/model.rs index a4d5f4c..af57793 100644 --- a/crates/rainbeam/src/model.rs +++ b/crates/rainbeam/src/model.rs @@ -304,6 +304,30 @@ impl CircleMetadata { self.kv.get(key).unwrap() == "true" } + + /// Check `kv` lengths + /// + /// # Returns + /// * `true`: ok + /// * `false`: invalid + pub fn check(&self) -> bool { + for field in &self.kv { + if field.0 == "sparkler:custom_css" { + // custom_css gets an extra long value + if field.1.len() > 64 * 128 { + return false; + } + + continue; + } + + if field.1.len() > 64 * 64 { + return false; + } + } + + true + } } /// An export of a user's entire history diff --git a/crates/rainbeam/src/routing/pages/mod.rs b/crates/rainbeam/src/routing/pages/mod.rs index 393cfce..d493bfb 100644 --- a/crates/rainbeam/src/routing/pages/mod.rs +++ b/crates/rainbeam/src/routing/pages/mod.rs @@ -10,9 +10,7 @@ use axum_extra::extract::CookieJar; use ammonia::Builder; use serde::{Deserialize, Serialize}; -use authbeam::model::{ - Notification, Permission, Profile, ProfileMetadata, RelationshipStatus, UserFollow, -}; +use authbeam::model::{Notification, Permission, Profile, ProfileMetadata, RelationshipStatus}; use crate::config::Config; use crate::database::Database; @@ -201,7 +199,7 @@ struct PartialTimelineTemplate { is_helper: bool, } -/// GET /_app/timeline.html +/// GET /_app/timelines/timeline.html pub async fn partial_timeline_request( jar: CookieJar, State(database): State, @@ -761,7 +759,7 @@ struct PartialPostsTemplate { is_helper: bool, } -/// GET /_app/posts.html +/// GET /_app/timelines/posts.html pub async fn partial_posts_request( jar: CookieJar, State(database): State, @@ -1471,16 +1469,13 @@ pub async fn public_global_timeline_request( } #[derive(Template)] -#[template(path = "compose.html")] +#[template(path = "partials/components/compose.html")] struct ComposeTemplate { config: Config, profile: Option, - unread: usize, - notifs: usize, - following: Vec<(UserFollow, Profile, Profile)>, } -/// GET /inbox/compose +/// GET /_app/components/compose.html pub async fn compose_request( jar: CookieJar, State(database): State, @@ -1497,30 +1492,10 @@ pub async fn compose_request( None => return Html(DatabaseError::NotAllowed.to_html(database)), }; - let unread = match database - .get_questions_by_recipient(auth_user.id.to_owned()) - .await - { - Ok(unread) => unread.len(), - Err(_) => 0, - }; - - let notifs = database - .auth - .get_notification_count_by_recipient(auth_user.id.to_owned()) - .await; - Html( ComposeTemplate { config: database.server_options, - following: database - .auth - .get_following(auth_user.id.clone()) - .await - .unwrap_or(Vec::new()), profile: Some(auth_user), - unread, - notifs, } .render() .unwrap(), @@ -1810,7 +1785,6 @@ pub async fn routes(database: Database) -> Router { ) .route("/inbox/global", get(public_global_timeline_request)) .route("/inbox/global/following", get(global_timeline_request)) - .route("/inbox/compose", get(compose_request)) .route("/inbox/notifications", get(notifications_request)) .route("/inbox/reports", get(reports_request)) // staff .route("/inbox/audit", get(audit_log_request)) // staff @@ -1893,8 +1867,12 @@ pub async fn routes(database: Database) -> Router { .route("/+i/:ip", get(api::profiles::expand_ip_request)) .route("/+p/:id", get(api::pages::expand_request)) // partials - .route("/_app/timeline.html", get(partial_timeline_request)) - .route("/_app/posts.html", get(partial_posts_request)) + .route("/_app/components/compose.html", get(compose_request)) + .route( + "/_app/timelines/timeline.html", + get(partial_timeline_request), + ) + .route("/_app/timelines/posts.html", get(partial_posts_request)) // ... .with_state(database) } diff --git a/crates/rainbeam/static/js/app.js b/crates/rainbeam/static/js/app.js index 8bc2804..f845a47 100644 --- a/crates/rainbeam/static/js/app.js +++ b/crates/rainbeam/static/js/app.js @@ -505,6 +505,10 @@ $.clean_date_codes(); $.link_filter(); $["hook.alt"](); + + if (globalThis._app_base.ns_store.$questions) { + trigger("questions:carp"); + } }) .catch(() => { // done scrolling, no more pages (http error) @@ -534,6 +538,56 @@ }, ); + app.define("hook.autosize", function (_) { + for (const textarea of Array.from( + document.querySelectorAll("textarea[autosize]"), + )) { + if (textarea.getAttribute("data-link")) { + // already linked + continue; + } + + const id = window.crypto.randomUUID(); + + textarea.style.overflow = "hidden"; + textarea.setAttribute("data-link", id); + + const pseudo = document.createElement("div"); + pseudo.setAttribute("aria-hidden", "true"); + pseudo.classList.add("hook_autosize_pseudo"); + pseudo.id = id; + + pseudo.style.visibility = "hidden"; + pseudo.style.position = "absolute"; + pseudo.style.pointerEvents = "none"; + pseudo.style.top = "0"; + + const computed = window.getComputedStyle(textarea); // we need to match styles on pseudo + const copy_styles = [ + "width", + "padding-top", + "padding-left", + "padding-right", + "padding-bottom", + ]; + + for (const style of copy_styles) { + pseudo.style[style] = computed.getPropertyValue(style); + } + + function resize() { + const box = pseudo.getBoundingClientRect(); + + pseudo.innerText = textarea.value; // pseudo will auto size naturally + textarea.style.height = `${box.height + 10}px`; + } + + textarea.addEventListener("input", resize); + textarea.addEventListener("keydown", resize); + document.body.appendChild(pseudo); + } + }); + // adomonition app.define("shout", function (_, type, content) { if (document.getElementById("admonition")) { diff --git a/crates/rainbeam/static/js/classes/PartialComponent.js b/crates/rainbeam/static/js/classes/PartialComponent.js new file mode 100644 index 0000000..a0a47ab --- /dev/null +++ b/crates/rainbeam/static/js/classes/PartialComponent.js @@ -0,0 +1,39 @@ +class PartialComponent extends HTMLElement { + static observedAttributes = ["src", "uses"]; + + constructor() { + const self = super(); + self.style.display = "contents"; + } + + attributeChangedCallback(name, _, value) { + switch (name) { + case "src": + fetch(value) + .then((res) => res.text()) + .then((res) => { + this.innerHTML = res; + }) + .catch((err) => { + this.innerHTML = err; + }); + + break; + + case "uses": + setTimeout(() => { + for (const hook of value.split(",")) { + trigger(hook); + } + }, 500); + + break; + + default: + return; + } + } +} + +customElements.define("include-partial", PartialComponent); +define("PartialComponent", PartialComponent); diff --git a/crates/rainbeam/static/js/loader.js b/crates/rainbeam/static/js/loader.js index 803a6bd..8b41c99 100644 --- a/crates/rainbeam/static/js/loader.js +++ b/crates/rainbeam/static/js/loader.js @@ -7,7 +7,7 @@ globalThis.ns_config = globalThis.ns_config || { verbose: true, }; -globalThis._app_base = globalThis._app_base || { ns_store: {} }; +globalThis._app_base = globalThis._app_base || { ns_store: {}, classes: {} }; function regns_log(level, ...args) { if (globalThis.ns_config.verbose) { @@ -148,3 +148,41 @@ globalThis.use = (id, callback) => { callback(res); }); }; + +// classes + +/// Import a class from path (relative to ns_config.root/classes) +globalThis.require = (id, callback) => { + // check if class already exists + const res = globalThis._app_base.classes[id]; + + if (res) { + return callback(res); + } + + // create script to load + const script = document.createElement("script"); + script.src = `${globalThis.ns_config.root}classes/${id}.js?v=${globalThis.ns_config.version}`; + script.id = `${globalThis.ns_config.version}-${id}.class.js`; + document.head.appendChild(script); + + script.setAttribute("data-turbo-permanent", "true"); + script.setAttribute("data-registered", new Date().toISOString()); + script.setAttribute("data-version", globalThis.ns_config.version); + + // run callback once the script loads + script.addEventListener("load", () => { + const res = globalThis._app_base.classes[id]; + + if (!res) { + return console.error("imported class failed to register:", id); + } + + callback(res); + }); +}; + +globalThis.define = (class_name, class_) => { + globalThis._app_base.classes[class_name] = class_; + regns_log("log", "registered class:", class_name); +}; diff --git a/crates/rainbeam/templates/base.html b/crates/rainbeam/templates/base.html index c32e40e..a2a531d 100644 --- a/crates/rainbeam/templates/base.html +++ b/crates/rainbeam/templates/base.html @@ -46,13 +46,14 @@ globalThis.ns_verbose = false; globalThis.ns_config = { root: "/static/js/", - version: "1.11.5", + version: "1.12.0", verbose: globalThis.ns_verbose, }; globalThis._app_base = { name: "rainbeam", ns_store: {}, + classes: {}, }; @@ -119,11 +120,9 @@