diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index b523347..0377385 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -56,6 +56,14 @@ fn check_banned(value: &Value, _: &HashMap) -> tera::Result) -> tera::Result { + Ok(value + .as_str() + .unwrap() + .replace("", "</script>") + .into()) +} + #[tokio::main(flavor = "multi_thread")] async fn main() { tracing_subscriber::fmt() @@ -86,6 +94,7 @@ async fn main() { tera.register_filter("has_supporter", check_supporter); tera.register_filter("has_staff_badge", check_staff_badge); tera.register_filter("has_banned", check_banned); + tera.register_filter("remove_script_tags", remove_script_tags); let client = Client::new(); diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index b183dcc..25d6126 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -1343,6 +1343,7 @@ window.POLL_OPTION_B = \"\"; window.POLL_OPTION_C = \"\"; window.POLL_OPTION_D = \"\"; + window.POLL_EXPIRES = null; window.get_poll_data = () => { if (!POLL_OPTION_A && !POLL_OPTION_B) { @@ -1353,11 +1354,16 @@ return [false, \"At least 2 options are required for a poll\"]; } + if (POLL_EXPIRES < 0) { + return [false, \"Polls cannot time travel\"]; + } + return [true, { option_a: POLL_OPTION_A, option_b: POLL_OPTION_B, option_c: POLL_OPTION_C, - option_d: POLL_OPTION_D + option_d: POLL_OPTION_D, + expires: POLL_EXPIRES, }]; }")) @@ -1395,7 +1401,12 @@ (div ("class" "card flex flex-col gap-2") (b (text "Option D")) - (input ("type" "text") ("placeholder" "option D") ("onchange" "window.POLL_OPTION_D = event.target.value")))) + (input ("type" "text") ("placeholder" "option D") ("onchange" "window.POLL_OPTION_D = event.target.value"))) + + (div + ("class" "card flex flex-col gap-2") + (b (text "Expires")) + (input ("type" "date") ("onchange" "window.POLL_EXPIRES = event.target.valueAsDate.getTime() - new Date().getTime()")))) (hr) (div ("class" "flex justify-between") @@ -1414,9 +1425,13 @@ ("class" "card tertiary w-full flex flex-col gap-2") (text "{% set total = poll[0].votes_a + poll[0].votes_b + poll[0].votes_c + poll[0].votes_d %}") - (text "{% if poll[1] -%}") + (text "{% if poll[1] or poll[2] or user and user.id == poll[0].owner -%}") ; already voted, show results + (text "{% if poll[1] %}") (span ("class" "fade") (text "You've already voted!")) + (text "{% elif poll[2] %}") + (span ("class" "fade") (text "Poll ended!")) + (text "{% endif %}") ; option a (div @@ -1484,5 +1499,12 @@ ; show expiration date + totals (div ("class" "flex w-full flex-wrap gap-2") - (span ("class" "notification chip") (text "{{ total }} votes")))) + (span ("class" "notification chip") (text "{{ total }} votes")) + (span + ("class" "notification chip") + (text "Expires in ") + (span + ("class" "poll_date") + ("data-created" "{{ poll[0].created }}") + ("data-expires" "{{ poll[0].expires }}"))))) (text "{%- endmacro %}") diff --git a/crates/app/src/public/html/profile/settings.lisp b/crates/app/src/public/html/profile/settings.lisp index 70eda76..6324eb1 100644 --- a/crates/app/src/public/html/profile/settings.lisp +++ b/crates/app/src/public/html/profile/settings.lisp @@ -855,7 +855,7 @@ (script ("type" "application/json") ("id" "settings_json") - (text "{{ profile.settings|json_encode()|safe }}")) + (text "{{ profile.settings|json_encode()|remove_script_tags|safe }}")) (script (text "setTimeout(() => { const ui = ns(\"ui\"); diff --git a/crates/app/src/public/html/root.lisp b/crates/app/src/public/html/root.lisp index fb5944a..4438bcd 100644 --- a/crates/app/src/public/html/root.lisp +++ b/crates/app/src/public/html/root.lisp @@ -91,6 +91,7 @@ atto.disconnect_observers(); atto.remove_false_options(); atto.clean_date_codes(); + atto.clean_poll_date_codes(); atto.link_filter(); atto[\"hooks::scroll\"](document.body, document.documentElement); diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index ea4e680..4bccaf9 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -91,10 +91,11 @@ media_theme_pref(); self.define("rel_date", (_, date) => { // stolen and slightly modified because js dates suck - const diff = (new Date().getTime() - date.getTime()) / 1000; + const diff = Math.abs((new Date().getTime() - date.getTime()) / 1000); const day_diff = Math.floor(diff / 86400); if (Number.isNaN(day_diff) || day_diff < 0 || day_diff >= 31) { + console.log(diff); return; } @@ -162,6 +163,48 @@ media_theme_pref(); } }); + self.define("clean_poll_date_codes", ({ $ }) => { + for (const element of Array.from( + document.querySelectorAll(".poll_date"), + )) { + const created = Number.parseInt( + element.getAttribute("data-created"), + ); + const expires = Number.parseInt( + element.getAttribute("data-expires"), + ); + + const then = new Date(created + expires); + + if (Number.isNaN(element.innerText)) { + continue; + } + + element.setAttribute("title", then.toLocaleString()); + + console.log($.rel_date(then)); + const pretty = + $.rel_date(then) + .replaceAll(" minutes ago", "m") + .replaceAll(" minute ago", "m") + .replaceAll(" hours ago", "h") + .replaceAll(" hour ago", "h") + .replaceAll(" days ago", "d") + .replaceAll(" day ago", "d") + .replaceAll(" weeks ago", "w") + .replaceAll(" week ago", "w") + .replaceAll(" months ago", "m") + .replaceAll(" month ago", "m") + .replaceAll(" years ago", "y") + .replaceAll(" year ago", "y") || ""; + + element.innerText = + pretty === undefined ? then.toLocaleDateString() : pretty; + + element.style.display = "inline-block"; + } + }); + self.define("copy_text", ({ $ }, text) => { navigator.clipboard.writeText(text); $.toast("success", "Copied!"); diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs index 155bd7e..0a76fa0 100644 --- a/crates/app/src/routes/api/v1/communities/posts.rs +++ b/crates/app/src/routes/api/v1/communities/posts.rs @@ -70,7 +70,16 @@ pub async fn create_request( let poll_id = if let Some(p) = req.poll { match data .create_poll(Poll::new( - user.id, 86400000, p.option_a, p.option_b, p.option_c, p.option_d, + user.id, + if let Some(expires) = p.expires { + expires + } else { + 86400000 + }, + p.option_a, + p.option_b, + p.option_c, + p.option_d, )) .await { diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 4395569..efd7ea7 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -429,6 +429,8 @@ pub struct CreatePostPoll { pub option_b: String, pub option_c: String, pub option_d: String, + #[serde(default)] + pub expires: Option, } #[derive(Deserialize)] diff --git a/crates/core/src/database/memberships.rs b/crates/core/src/database/memberships.rs index 76feb0a..064ba6f 100644 --- a/crates/core/src/database/memberships.rs +++ b/crates/core/src/database/memberships.rs @@ -292,6 +292,32 @@ impl DataManager { Ok(()) } + /// Delete a membership given its `id` + pub async fn delete_membership_force(&self, id: usize) -> Result<()> { + let y = self.get_membership_by_id(id).await?; + + let conn = match self.connect().await { + Ok(c) => c, + Err(e) => return Err(Error::DatabaseConnection(e.to_string())), + }; + + let res = execute!( + &conn, + "DELETE FROM memberships WHERE id = $1", + &[&(id as i64)] + ); + + if let Err(e) = res { + return Err(Error::DatabaseError(e.to_string())); + } + + self.2.remove(format!("atto.membership:{}", id)).await; + + self.decr_community_member_count(y.community).await.unwrap(); + + Ok(()) + } + /// Update a membership's role given its `id` pub async fn update_membership_role( &self, diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 6381ca0..3362e63 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -237,11 +237,14 @@ impl DataManager { } /// Get the poll of the given post (if some). + /// + /// # Returns + /// `Result>` pub async fn get_post_poll( &self, post: &Post, user: &Option, - ) -> Result> { + ) -> Result> { let user = if let Some(ua) = user { ua } else { @@ -250,12 +253,16 @@ impl DataManager { if post.poll_id != 0 { Ok(Some(match self.get_poll_by_id(post.poll_id).await { - Ok(p) => ( - p, - self.get_pollvote_by_owner_poll(user.id, post.poll_id) - .await - .is_ok(), - ), + Ok(p) => { + let expired = (unix_epoch_timestamp() as usize) - p.created > p.expires; + ( + p, + self.get_pollvote_by_owner_poll(user.id, post.poll_id) + .await + .is_ok(), + expired, + ) + } Err(_) => return Err(Error::MiscError("Invalid poll ID attached".to_string())), })) } else { @@ -275,7 +282,7 @@ impl DataManager { User, Option<(User, Post)>, Option<(Question, User)>, - Option<(Poll, bool)>, + Option<(Poll, bool, bool)>, )>, > { let mut out = Vec::new(); @@ -374,7 +381,7 @@ impl DataManager { Community, Option<(User, Post)>, Option<(Question, User)>, - Option<(Poll, bool)>, + Option<(Poll, bool, bool)>, )>, > { let mut out = Vec::new(); diff --git a/crates/core/src/database/stacks.rs b/crates/core/src/database/stacks.rs index 39b08fc..5a6d302 100644 --- a/crates/core/src/database/stacks.rs +++ b/crates/core/src/database/stacks.rs @@ -51,7 +51,7 @@ impl DataManager { Community, Option<(User, Post)>, Option<(Question, User)>, - Option<(Poll, bool)>, + Option<(Poll, bool, bool)>, )>, > { let stack = self.get_stack_by_id(id).await?; diff --git a/crates/core/src/database/userblocks.rs b/crates/core/src/database/userblocks.rs index 5a2cc1f..5850898 100644 --- a/crates/core/src/database/userblocks.rs +++ b/crates/core/src/database/userblocks.rs @@ -177,6 +177,10 @@ impl DataManager { /// # Arguments /// * `data` - a mock [`UserBlock`] object to insert pub async fn create_userblock(&self, data: UserBlock) -> Result<()> { + let initiator = self.get_user_by_id(data.initiator).await?; + let receiver = self.get_user_by_id(data.receiver).await?; + + // ... let conn = match self.connect().await { Ok(c) => c, Err(e) => return Err(Error::DatabaseConnection(e.to_string())), @@ -197,6 +201,31 @@ impl DataManager { return Err(Error::DatabaseError(e.to_string())); } + // remove initiator from receiver's communities + for community in self.get_communities_by_owner(data.receiver).await? { + if let Ok(membership) = self + .get_membership_by_owner_community_no_void(data.initiator, community.id) + .await + { + self.delete_membership_force(membership.id).await?; + } + } + + // unfollow/remove follower + if let Ok(f) = self + .get_userfollow_by_initiator_receiver(data.initiator, data.receiver) + .await + { + self.delete_userfollow(f.id, &initiator, false).await?; + } + + if let Ok(f) = self + .get_userfollow_by_receiver_initiator(data.initiator, data.receiver) + .await + { + self.delete_userfollow(f.id, &receiver, false).await?; + } + // return Ok(()) }