add: don't allow poll creator to vote

add: unfollow user when you block them
add: force other user to unfollow you by blocking them
add: leave receiver communities when you block them
This commit is contained in:
trisua 2025-06-05 16:23:57 -04:00
parent 5a330b7a18
commit 460e87e90e
11 changed files with 165 additions and 17 deletions

View file

@ -56,6 +56,14 @@ fn check_banned(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value
.into()) .into())
} }
fn remove_script_tags(value: &Value, _: &HashMap<String, Value>) -> tera::Result<Value> {
Ok(value
.as_str()
.unwrap()
.replace("</script>", "&lt;/script&gt;")
.into())
}
#[tokio::main(flavor = "multi_thread")] #[tokio::main(flavor = "multi_thread")]
async fn main() { async fn main() {
tracing_subscriber::fmt() tracing_subscriber::fmt()
@ -86,6 +94,7 @@ async fn main() {
tera.register_filter("has_supporter", check_supporter); tera.register_filter("has_supporter", check_supporter);
tera.register_filter("has_staff_badge", check_staff_badge); tera.register_filter("has_staff_badge", check_staff_badge);
tera.register_filter("has_banned", check_banned); tera.register_filter("has_banned", check_banned);
tera.register_filter("remove_script_tags", remove_script_tags);
let client = Client::new(); let client = Client::new();

View file

@ -1343,6 +1343,7 @@
window.POLL_OPTION_B = \"\"; window.POLL_OPTION_B = \"\";
window.POLL_OPTION_C = \"\"; window.POLL_OPTION_C = \"\";
window.POLL_OPTION_D = \"\"; window.POLL_OPTION_D = \"\";
window.POLL_EXPIRES = null;
window.get_poll_data = () => { window.get_poll_data = () => {
if (!POLL_OPTION_A && !POLL_OPTION_B) { if (!POLL_OPTION_A && !POLL_OPTION_B) {
@ -1353,11 +1354,16 @@
return [false, \"At least 2 options are required for a poll\"]; return [false, \"At least 2 options are required for a poll\"];
} }
if (POLL_EXPIRES < 0) {
return [false, \"Polls cannot time travel\"];
}
return [true, { return [true, {
option_a: POLL_OPTION_A, option_a: POLL_OPTION_A,
option_b: POLL_OPTION_B, option_b: POLL_OPTION_B,
option_c: POLL_OPTION_C, option_c: POLL_OPTION_C,
option_d: POLL_OPTION_D option_d: POLL_OPTION_D,
expires: POLL_EXPIRES,
}]; }];
}")) }"))
@ -1395,7 +1401,12 @@
(div (div
("class" "card flex flex-col gap-2") ("class" "card flex flex-col gap-2")
(b (text "Option D")) (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) (hr)
(div (div
("class" "flex justify-between") ("class" "flex justify-between")
@ -1414,9 +1425,13 @@
("class" "card tertiary w-full flex flex-col gap-2") ("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 "{% 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 ; already voted, show results
(text "{% if poll[1] %}")
(span ("class" "fade") (text "You've already voted!")) (span ("class" "fade") (text "You've already voted!"))
(text "{% elif poll[2] %}")
(span ("class" "fade") (text "Poll ended!"))
(text "{% endif %}")
; option a ; option a
(div (div
@ -1484,5 +1499,12 @@
; show expiration date + totals ; show expiration date + totals
(div (div
("class" "flex w-full flex-wrap gap-2") ("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 %}") (text "{%- endmacro %}")

View file

@ -855,7 +855,7 @@
(script (script
("type" "application/json") ("type" "application/json")
("id" "settings_json") ("id" "settings_json")
(text "{{ profile.settings|json_encode()|safe }}")) (text "{{ profile.settings|json_encode()|remove_script_tags|safe }}"))
(script (script
(text "setTimeout(() => { (text "setTimeout(() => {
const ui = ns(\"ui\"); const ui = ns(\"ui\");

View file

@ -91,6 +91,7 @@
atto.disconnect_observers(); atto.disconnect_observers();
atto.remove_false_options(); atto.remove_false_options();
atto.clean_date_codes(); atto.clean_date_codes();
atto.clean_poll_date_codes();
atto.link_filter(); atto.link_filter();
atto[\"hooks::scroll\"](document.body, document.documentElement); atto[\"hooks::scroll\"](document.body, document.documentElement);

View file

@ -91,10 +91,11 @@ media_theme_pref();
self.define("rel_date", (_, date) => { self.define("rel_date", (_, date) => {
// stolen and slightly modified because js dates suck // 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); const day_diff = Math.floor(diff / 86400);
if (Number.isNaN(day_diff) || day_diff < 0 || day_diff >= 31) { if (Number.isNaN(day_diff) || day_diff < 0 || day_diff >= 31) {
console.log(diff);
return; 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) => { self.define("copy_text", ({ $ }, text) => {
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);
$.toast("success", "Copied!"); $.toast("success", "Copied!");

View file

@ -70,7 +70,16 @@ pub async fn create_request(
let poll_id = if let Some(p) = req.poll { let poll_id = if let Some(p) = req.poll {
match data match data
.create_poll(Poll::new( .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 .await
{ {

View file

@ -429,6 +429,8 @@ pub struct CreatePostPoll {
pub option_b: String, pub option_b: String,
pub option_c: String, pub option_c: String,
pub option_d: String, pub option_d: String,
#[serde(default)]
pub expires: Option<usize>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]

View file

@ -292,6 +292,32 @@ impl DataManager {
Ok(()) 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` /// Update a membership's role given its `id`
pub async fn update_membership_role( pub async fn update_membership_role(
&self, &self,

View file

@ -237,11 +237,14 @@ impl DataManager {
} }
/// Get the poll of the given post (if some). /// Get the poll of the given post (if some).
///
/// # Returns
/// `Result<Option<(poll, voted, expired)>>`
pub async fn get_post_poll( pub async fn get_post_poll(
&self, &self,
post: &Post, post: &Post,
user: &Option<User>, user: &Option<User>,
) -> Result<Option<(Poll, bool)>> { ) -> Result<Option<(Poll, bool, bool)>> {
let user = if let Some(ua) = user { let user = if let Some(ua) = user {
ua ua
} else { } else {
@ -250,12 +253,16 @@ impl DataManager {
if post.poll_id != 0 { if post.poll_id != 0 {
Ok(Some(match self.get_poll_by_id(post.poll_id).await { Ok(Some(match self.get_poll_by_id(post.poll_id).await {
Ok(p) => ( Ok(p) => {
let expired = (unix_epoch_timestamp() as usize) - p.created > p.expires;
(
p, p,
self.get_pollvote_by_owner_poll(user.id, post.poll_id) self.get_pollvote_by_owner_poll(user.id, post.poll_id)
.await .await
.is_ok(), .is_ok(),
), expired,
)
}
Err(_) => return Err(Error::MiscError("Invalid poll ID attached".to_string())), Err(_) => return Err(Error::MiscError("Invalid poll ID attached".to_string())),
})) }))
} else { } else {
@ -275,7 +282,7 @@ impl DataManager {
User, User,
Option<(User, Post)>, Option<(User, Post)>,
Option<(Question, User)>, Option<(Question, User)>,
Option<(Poll, bool)>, Option<(Poll, bool, bool)>,
)>, )>,
> { > {
let mut out = Vec::new(); let mut out = Vec::new();
@ -374,7 +381,7 @@ impl DataManager {
Community, Community,
Option<(User, Post)>, Option<(User, Post)>,
Option<(Question, User)>, Option<(Question, User)>,
Option<(Poll, bool)>, Option<(Poll, bool, bool)>,
)>, )>,
> { > {
let mut out = Vec::new(); let mut out = Vec::new();

View file

@ -51,7 +51,7 @@ impl DataManager {
Community, Community,
Option<(User, Post)>, Option<(User, Post)>,
Option<(Question, User)>, Option<(Question, User)>,
Option<(Poll, bool)>, Option<(Poll, bool, bool)>,
)>, )>,
> { > {
let stack = self.get_stack_by_id(id).await?; let stack = self.get_stack_by_id(id).await?;

View file

@ -177,6 +177,10 @@ impl DataManager {
/// # Arguments /// # Arguments
/// * `data` - a mock [`UserBlock`] object to insert /// * `data` - a mock [`UserBlock`] object to insert
pub async fn create_userblock(&self, data: UserBlock) -> Result<()> { 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 { let conn = match self.connect().await {
Ok(c) => c, Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())), Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@ -197,6 +201,31 @@ impl DataManager {
return Err(Error::DatabaseError(e.to_string())); 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 // return
Ok(()) Ok(())
} }