add: polls backend
TODO: polls frontend
This commit is contained in:
parent
b5e060e8ae
commit
4dfa09207e
18 changed files with 574 additions and 17 deletions
|
@ -22,7 +22,6 @@ use tetratto_core::{
|
|||
use tetratto_l10n::LangFile;
|
||||
use tetratto_shared::hash::salt;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use crate::{create_dir_if_not_exists, write_if_track, write_template};
|
||||
|
||||
// images
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
("style" "display: contents")
|
||||
(text "{{ self::post(post=post, owner=owner, secondary=secondary, community=community, show_community=show_community, can_manage_post=can_manage_post, repost=repost, expect_repost=true) }}"))
|
||||
|
||||
(text "{%- endmacro %} {% macro post(post, owner, question=false, secondary=false, community=false, show_community=true, can_manage_post=false, repost=false, expect_repost=false) -%} {% if community and show_community and community.id != config.town_square or question %}")
|
||||
(text "{%- endmacro %} {% macro post(post, owner, question=false, secondary=false, community=false, show_community=true, can_manage_post=false, repost=false, expect_repost=false, poll=false) -%} {% if community and show_community and community.id != config.town_square or question %}")
|
||||
(div
|
||||
("class" "card-nest")
|
||||
(text "{% if question -%} {{ self::question(question=question[0], owner=question[1], profile=owner) }} {% else %}")
|
||||
|
|
|
@ -522,9 +522,9 @@
|
|||
("class" "card flex flex-col gap-2")
|
||||
(text "{% if is_supporter -%}")
|
||||
(p
|
||||
(text "You")
|
||||
(text "You ")
|
||||
(b
|
||||
(text "are"))
|
||||
(text "are "))
|
||||
(text "a supporter! Thank you for all
|
||||
that you do. You can manage your billing
|
||||
information below.")
|
||||
|
@ -539,9 +539,9 @@
|
|||
(text "Manage billing"))
|
||||
(text "{% else %}")
|
||||
(p
|
||||
(text "You're")
|
||||
(text "You're ")
|
||||
(b
|
||||
(text "not"))
|
||||
(text "not "))
|
||||
(text "currently a supporter! No
|
||||
pressure, but it helps us do some pretty cool
|
||||
things! As a supporter, you'll get:"))
|
||||
|
|
|
@ -30,6 +30,6 @@
|
|||
(text "{%- endif %}")
|
||||
(div
|
||||
("class" "card w-full flex flex-col gap-2")
|
||||
(text "{% for post in list %} {% if post[2].read_access == \"Everybody\" -%} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[3], post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2]) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}")))
|
||||
(text "{% for post in list %} {% if post[2].read_access == \"Everybody\" -%} {% if post[0].context.repost and post[0].context.repost.reposting -%} {{ components::repost(repost=post[3], post=post[0], owner=post[1], secondary=true, community=post[2], show_community=true) }} {% else %} {{ components::post(post=post[0], owner=post[1], question=post[4], secondary=true, community=post[2], poll=post[3]) }} {%- endif %} {%- endif %} {% endfor %} {{ components::pagination(page=page, items=list|length) }}")))
|
||||
|
||||
(text "{% endblock %}")
|
||||
|
|
|
@ -7,7 +7,7 @@ use axum::{
|
|||
use axum_extra::extract::CookieJar;
|
||||
use tetratto_core::model::{
|
||||
addr::RemoteAddr,
|
||||
communities::Post,
|
||||
communities::{Poll, PollVote, Post},
|
||||
permissions::FinePermission,
|
||||
uploads::{MediaType, MediaUpload},
|
||||
ApiReturn, Error,
|
||||
|
@ -15,7 +15,7 @@ use tetratto_core::model::{
|
|||
use crate::{
|
||||
get_user_from_token,
|
||||
image::{save_webp_buffer, JsonMultipart},
|
||||
routes::api::v1::{CreatePost, CreateRepost, UpdatePostContent, UpdatePostContext},
|
||||
routes::api::v1::{CreatePost, CreateRepost, UpdatePostContent, UpdatePostContext, VoteInPoll},
|
||||
State,
|
||||
};
|
||||
|
||||
|
@ -66,6 +66,21 @@ pub async fn create_request(
|
|||
return Json(Error::NotAllowed.into());
|
||||
}
|
||||
|
||||
// create poll
|
||||
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,
|
||||
))
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(e) => return Json(e.into()),
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// ...
|
||||
let mut props = Post::new(
|
||||
req.content,
|
||||
|
@ -82,6 +97,7 @@ pub async fn create_request(
|
|||
None
|
||||
},
|
||||
user.id,
|
||||
poll_id,
|
||||
);
|
||||
|
||||
if !req.answering.is_empty() {
|
||||
|
@ -308,3 +324,39 @@ pub async fn update_context_request(
|
|||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn vote_request(
|
||||
jar: CookieJar,
|
||||
Extension(data): Extension<State>,
|
||||
Path(id): Path<usize>,
|
||||
Json(req): Json<VoteInPoll>,
|
||||
) -> impl IntoResponse {
|
||||
let data = &(data.read().await).0;
|
||||
let user = match get_user_from_token!(jar, data) {
|
||||
Some(ua) => ua,
|
||||
None => return Json(Error::NotAllowed.into()),
|
||||
};
|
||||
|
||||
let post = match data.get_post_by_id(id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
let poll = match data.get_poll_by_id(post.poll_id).await {
|
||||
Ok(p) => p,
|
||||
Err(e) => return Json(e.into()),
|
||||
};
|
||||
|
||||
// ...
|
||||
match data
|
||||
.create_pollvote(PollVote::new(user.id, poll.id, req.option))
|
||||
.await
|
||||
{
|
||||
Ok(_) => Json(ApiReturn {
|
||||
ok: true,
|
||||
message: "Vote cast".to_string(),
|
||||
payload: (),
|
||||
}),
|
||||
Err(e) => Json(e.into()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ use serde::Deserialize;
|
|||
use tetratto_core::model::{
|
||||
communities::{
|
||||
CommunityContext, CommunityJoinAccess, CommunityReadAccess, CommunityWriteAccess,
|
||||
PostContext,
|
||||
PollOption, PostContext,
|
||||
},
|
||||
communities_permissions::CommunityPermission,
|
||||
permissions::FinePermission,
|
||||
|
@ -113,6 +113,10 @@ pub fn routes() -> Router {
|
|||
"/posts/{id}/context",
|
||||
post(communities::posts::update_context_request),
|
||||
)
|
||||
.route(
|
||||
"/posts/{id}/poll_vote",
|
||||
post(communities::posts::vote_request),
|
||||
)
|
||||
// drafts
|
||||
.route("/drafts", post(communities::drafts::create_request))
|
||||
.route("/drafts/{id}", delete(communities::drafts::delete_request))
|
||||
|
@ -419,6 +423,14 @@ pub struct UpdateCommunityJoinAccess {
|
|||
pub access: CommunityJoinAccess,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreatePostPoll {
|
||||
pub option_a: String,
|
||||
pub option_b: String,
|
||||
pub option_c: String,
|
||||
pub option_d: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreatePost {
|
||||
pub content: String,
|
||||
|
@ -427,6 +439,8 @@ pub struct CreatePost {
|
|||
pub replying_to: Option<String>,
|
||||
#[serde(default)]
|
||||
pub answering: String,
|
||||
#[serde(default)]
|
||||
pub poll: Option<CreatePostPoll>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -597,3 +611,8 @@ pub struct UpdateEmojiName {
|
|||
pub struct CreatePostDraft {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct VoteInPoll {
|
||||
pub option: PollOption,
|
||||
}
|
||||
|
|
|
@ -358,6 +358,24 @@ impl DataManager {
|
|||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// delete polls
|
||||
let res = execute!(&conn, "DELETE FROM polls WHERE owner = $1", &[&(id as i64)]);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// delete poll votes
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"DELETE FROM pollvotes WHERE owner = $1",
|
||||
&[&(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// delete user follows... individually since it requires updating user counts
|
||||
for follow in self.get_userfollows_by_receiver_all(id).await? {
|
||||
self.delete_userfollow(follow.id, &user, true).await?;
|
||||
|
|
|
@ -36,6 +36,8 @@ impl DataManager {
|
|||
execute!(&conn, common::CREATE_TABLE_EMOJIS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_STACKS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_DRAFTS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_POLLS).unwrap();
|
||||
execute!(&conn, common::CREATE_TABLE_POLLVOTES).unwrap();
|
||||
|
||||
self.2
|
||||
.set("atto.active_connections:users".to_string(), "0".to_string())
|
||||
|
|
|
@ -19,3 +19,5 @@ pub const CREATE_TABLE_UPLOADS: &str = include_str!("./sql/create_uploads.sql");
|
|||
pub const CREATE_TABLE_EMOJIS: &str = include_str!("./sql/create_emojis.sql");
|
||||
pub const CREATE_TABLE_STACKS: &str = include_str!("./sql/create_stacks.sql");
|
||||
pub const CREATE_TABLE_DRAFTS: &str = include_str!("./sql/create_drafts.sql");
|
||||
pub const CREATE_TABLE_POLLS: &str = include_str!("./sql/create_polls.sql");
|
||||
pub const CREATE_TABLE_POLLVOTES: &str = include_str!("./sql/create_pollvotes.sql");
|
||||
|
|
14
crates/core/src/database/drivers/sql/create_polls.sql
Normal file
14
crates/core/src/database/drivers/sql/create_polls.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
CREATE TABLE IF NOT EXISTS polls (
|
||||
id BIGINT NOT NULL PRIMARY KEY,
|
||||
owner BIGINT NOT NULL,
|
||||
created BIGINT NOT NULL,
|
||||
expires INT NOT NULL DEFAULT 86400000, -- expires in a day by default
|
||||
option_a TEXT NOT NULL,
|
||||
option_b TEXT NOT NULL,
|
||||
option_c TEXT NOT NULL,
|
||||
option_d TEXT NOT NULL,
|
||||
votes_a INT NOT NULL DEFAULT 0,
|
||||
votes_b INT NOT NULL DEFAULT 0,
|
||||
votes_c INT NOT NULL DEFAULT 0,
|
||||
votes_d INT NOT NULL DEFAULT 0
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE IF NOT EXISTS pollvotes (
|
||||
id BIGINT NOT NULL,
|
||||
owner BIGINT NOT NULL,
|
||||
created BIGINT NOT NULL,
|
||||
poll_id BIGINT NOT NULL,
|
||||
vote INT NOT NULL,
|
||||
PRIMARY KEY (owner, poll_id)
|
||||
)
|
|
@ -14,5 +14,6 @@ CREATE TABLE IF NOT EXISTS posts (
|
|||
-- ...
|
||||
uploads TEXT NOT NULL,
|
||||
is_deleted INT NOT NULL,
|
||||
tsvector_content tsvector GENERATED ALWAYS AS (to_tsvector ('english', coalesce(content, ''))) STORED
|
||||
tsvector_content tsvector GENERATED ALWAYS AS (to_tsvector ('english', coalesce(content, ''))) STORED,
|
||||
poll_id BIGINT NOT NULL
|
||||
)
|
||||
|
|
|
@ -10,6 +10,8 @@ mod ipbans;
|
|||
mod ipblocks;
|
||||
mod memberships;
|
||||
mod notifications;
|
||||
mod polls;
|
||||
mod pollvotes;
|
||||
mod posts;
|
||||
mod questions;
|
||||
mod reactions;
|
||||
|
|
141
crates/core/src/database/polls.rs
Normal file
141
crates/core/src/database/polls.rs
Normal file
|
@ -0,0 +1,141 @@
|
|||
use super::*;
|
||||
use crate::cache::Cache;
|
||||
use crate::model::communities::Poll;
|
||||
use crate::model::moderation::AuditLogEntry;
|
||||
use crate::model::{Error, Result, auth::User, permissions::FinePermission};
|
||||
use crate::{auto_method, execute, get, query_row, params};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use rusqlite::Row;
|
||||
|
||||
#[cfg(feature = "postgres")]
|
||||
use tokio_postgres::Row;
|
||||
|
||||
impl DataManager {
|
||||
/// Get a [`Poll`] from an SQL row.
|
||||
pub(crate) fn get_poll_from_row(
|
||||
#[cfg(feature = "sqlite")] x: &Row<'_>,
|
||||
#[cfg(feature = "postgres")] x: &Row,
|
||||
) -> Poll {
|
||||
Poll {
|
||||
id: get!(x->0(i64)) as usize,
|
||||
owner: get!(x->1(i64)) as usize,
|
||||
created: get!(x->2(i64)) as usize,
|
||||
expires: get!(x->3(i64)) as usize,
|
||||
option_a: get!(x->4(String)),
|
||||
option_b: get!(x->5(String)),
|
||||
option_c: get!(x->6(String)),
|
||||
option_d: get!(x->7(String)),
|
||||
votes_a: get!(x->8(i32)) as usize,
|
||||
votes_b: get!(x->9(i32)) as usize,
|
||||
votes_c: get!(x->10(i32)) as usize,
|
||||
votes_d: get!(x->11(i32)) as usize,
|
||||
}
|
||||
}
|
||||
|
||||
auto_method!(get_poll_by_id()@get_poll_from_row -> "SELECT * FROM polls WHERE id = $1" --name="poll" --returns=Poll --cache-key-tmpl="atto.poll:{}");
|
||||
|
||||
/// Create a new poll in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - a mock [`Poll`] object to insert
|
||||
pub async fn create_poll(&self, data: Poll) -> Result<usize> {
|
||||
// check values
|
||||
if data.option_a.len() < 2 {
|
||||
return Err(Error::DataTooShort("option A".to_string()));
|
||||
} else if data.option_a.len() > 128 {
|
||||
return Err(Error::DataTooLong("option A".to_string()));
|
||||
}
|
||||
|
||||
if data.option_b.len() < 2 {
|
||||
return Err(Error::DataTooShort("option B".to_string()));
|
||||
} else if data.option_b.len() > 128 {
|
||||
return Err(Error::DataTooLong("option B".to_string()));
|
||||
}
|
||||
|
||||
if data.option_c.len() > 128 {
|
||||
return Err(Error::DataTooLong("option C".to_string()));
|
||||
}
|
||||
|
||||
if data.option_d.len() > 128 {
|
||||
return Err(Error::DataTooLong("option D".to_string()));
|
||||
}
|
||||
|
||||
// ...
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO polls VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.owner as i64),
|
||||
&(data.created as i64),
|
||||
&(data.expires as i64),
|
||||
&data.option_a,
|
||||
&data.option_b,
|
||||
&data.option_c,
|
||||
&data.option_d,
|
||||
&(data.votes_a as i64),
|
||||
&(data.votes_b as i64),
|
||||
&(data.votes_c as i64),
|
||||
&(data.votes_d as i64),
|
||||
]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
Ok(data.id)
|
||||
}
|
||||
|
||||
pub async fn delete_poll(&self, id: usize, user: User) -> Result<()> {
|
||||
let y = self.get_poll_by_id(id).await?;
|
||||
|
||||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::MANAGE_POSTS) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `delete_poll` with x value `{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 polls WHERE id = $1", &[&(id as i64)]);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!("atto.poll:{}", id)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn cache_clear_poll(&self, poll: &Poll) {
|
||||
self.2.remove(format!("atto.poll:{}", poll.id)).await;
|
||||
}
|
||||
|
||||
auto_method!(incr_votes_a_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_a + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr);
|
||||
auto_method!(decr_votes_a_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_a - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_a);
|
||||
|
||||
auto_method!(incr_votes_b_count()@get_poll_by_id -> "UPDATE users SET votes_b = votes_b + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr);
|
||||
auto_method!(decr_votes_b_count()@get_poll_by_id -> "UPDATE users SET votes_b = votes_b - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_b);
|
||||
|
||||
auto_method!(incr_votes_c_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_d + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr);
|
||||
auto_method!(decr_votes_c_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_d - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_c);
|
||||
|
||||
auto_method!(incr_votes_d_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_d + 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --incr);
|
||||
auto_method!(decr_votes_d_count()@get_poll_by_id -> "UPDATE users SET votes_a = votes_d - 1 WHERE id = $1" --cache-key-tmpl=cache_clear_poll --decr=votes_d);
|
||||
}
|
149
crates/core/src/database/pollvotes.rs
Normal file
149
crates/core/src/database/pollvotes.rs
Normal file
|
@ -0,0 +1,149 @@
|
|||
use super::*;
|
||||
use crate::cache::Cache;
|
||||
use crate::model::communities::PollVote;
|
||||
use crate::model::moderation::AuditLogEntry;
|
||||
use crate::model::{Error, Result, auth::User, permissions::FinePermission};
|
||||
use crate::{auto_method, execute, get, query_row, params};
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use rusqlite::Row;
|
||||
|
||||
use tetratto_shared::unix_epoch_timestamp;
|
||||
#[cfg(feature = "postgres")]
|
||||
use tokio_postgres::Row;
|
||||
|
||||
impl DataManager {
|
||||
/// Get a [`PollVote`] from an SQL row.
|
||||
pub(crate) fn get_pollvote_from_row(
|
||||
#[cfg(feature = "sqlite")] x: &Row<'_>,
|
||||
#[cfg(feature = "postgres")] x: &Row,
|
||||
) -> PollVote {
|
||||
PollVote {
|
||||
id: get!(x->0(i64)) as usize,
|
||||
owner: get!(x->1(i64)) as usize,
|
||||
created: get!(x->2(i64)) as usize,
|
||||
poll_id: get!(x->3(i64)) as usize,
|
||||
vote: (get!(x->4(i32)) as u8).into(),
|
||||
}
|
||||
}
|
||||
|
||||
auto_method!(get_pollvote_by_id()@get_pollvote_from_row -> "SELECT * FROM pollvotes WHERE id = $1" --name="poll vote" --returns=PollVote --cache-key-tmpl="atto.pollvote:{}");
|
||||
|
||||
pub async fn get_pollvote_by_owner_poll(&self, id: usize, poll_id: usize) -> Result<PollVote> {
|
||||
if let Some(cached) = self
|
||||
.2
|
||||
.get(format!("atto.pollvote:{}:{}", id, poll_id))
|
||||
.await
|
||||
{
|
||||
return Ok(serde_json::from_str(&cached).unwrap());
|
||||
}
|
||||
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let res = query_row!(
|
||||
&conn,
|
||||
"SELECT * FROM pollvotes WHERE id = $1 AND poll_id = $2",
|
||||
&[&(id as i64), &(poll_id as i64)],
|
||||
|x| { Ok(Self::get_pollvote_from_row(x)) }
|
||||
);
|
||||
|
||||
if res.is_err() {
|
||||
return Err(Error::GeneralNotFound("poll vote".to_string()));
|
||||
}
|
||||
|
||||
let x = res.unwrap();
|
||||
self.2
|
||||
.set(
|
||||
format!("atto.pollvote:{}:{}", id, poll_id),
|
||||
serde_json::to_string(&x).unwrap(),
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(x)
|
||||
}
|
||||
|
||||
/// Create a new poll vote in the database.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `data` - a mock [`PollVote`] object to insert
|
||||
pub async fn create_pollvote(&self, data: PollVote) -> Result<usize> {
|
||||
// get poll and check permission
|
||||
let poll = self.get_poll_by_id(data.poll_id).await?;
|
||||
|
||||
let now = unix_epoch_timestamp() as usize;
|
||||
let diff = now - poll.created;
|
||||
|
||||
if diff > poll.expires {
|
||||
return Err(Error::MiscError("Poll is closed".to_string()));
|
||||
};
|
||||
|
||||
// ...
|
||||
let conn = match self.connect().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
|
||||
};
|
||||
|
||||
let vote_u8: u8 = data.vote.into();
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO pollvotes VALUES ($1, $2, $3, $4, $5)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.owner as i64),
|
||||
&(data.created as i64),
|
||||
&(data.poll_id as i64),
|
||||
&(vote_u8 as i64),
|
||||
]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
// update poll
|
||||
self.incr_votes_a_count(poll.id).await?;
|
||||
self.incr_votes_b_count(poll.id).await?;
|
||||
self.incr_votes_c_count(poll.id).await?;
|
||||
self.incr_votes_d_count(poll.id).await?;
|
||||
|
||||
// ...
|
||||
Ok(data.id)
|
||||
}
|
||||
|
||||
pub async fn delete_pollvote(&self, id: usize, user: User) -> Result<()> {
|
||||
let y = self.get_pollvote_by_id(id).await?;
|
||||
|
||||
if user.id != y.owner {
|
||||
if !user.permissions.check(FinePermission::MANAGE_POSTS) {
|
||||
return Err(Error::NotAllowed);
|
||||
} else {
|
||||
self.create_audit_log_entry(AuditLogEntry::new(
|
||||
user.id,
|
||||
format!("invoked `delete_pollvote` with x value `{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 pollvotes WHERE id = $1",
|
||||
&[&(id as i64)]
|
||||
);
|
||||
|
||||
if let Err(e) = res {
|
||||
return Err(Error::DatabaseError(e.to_string()));
|
||||
}
|
||||
|
||||
self.2.remove(format!("atto.pollvote:{}", id)).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ use super::*;
|
|||
use crate::cache::Cache;
|
||||
use crate::config::StringBan;
|
||||
use crate::model::auth::Notification;
|
||||
use crate::model::communities::Question;
|
||||
use crate::model::communities::{Poll, Question};
|
||||
use crate::model::communities_permissions::CommunityPermission;
|
||||
use crate::model::moderation::AuditLogEntry;
|
||||
use crate::model::stacks::StackSort;
|
||||
|
@ -108,6 +108,8 @@ impl DataManager {
|
|||
// ...
|
||||
uploads: serde_json::from_str(&get!(x->10(String))).unwrap(),
|
||||
is_deleted: get!(x->11(i32)) as i8 == 1,
|
||||
// SKIP tsvector (12)
|
||||
poll_id: get!(x->13(i64)) as usize,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,14 +236,49 @@ impl DataManager {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get the poll of the given post (if some).
|
||||
pub async fn get_post_poll(
|
||||
&self,
|
||||
post: &Post,
|
||||
user: &Option<User>,
|
||||
) -> Result<Option<(Poll, bool)>> {
|
||||
let user = if let Some(ua) = user {
|
||||
ua
|
||||
} else {
|
||||
return Err(Error::MiscError("Could not get user for pull".to_string()));
|
||||
};
|
||||
|
||||
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(),
|
||||
),
|
||||
Err(_) => return Err(Error::MiscError("Invalid poll ID attached".to_string())),
|
||||
}))
|
||||
} else {
|
||||
return Err(Error::MiscError("Invalid poll ID attached".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete a vector of just posts with their owner as well.
|
||||
pub async fn fill_posts(
|
||||
&self,
|
||||
posts: Vec<Post>,
|
||||
ignore_users: &[usize],
|
||||
user: &Option<User>,
|
||||
) -> Result<Vec<(Post, User, Option<(User, Post)>, Option<(Question, User)>)>> {
|
||||
let mut out: Vec<(Post, User, Option<(User, Post)>, Option<(Question, User)>)> = Vec::new();
|
||||
) -> Result<
|
||||
Vec<(
|
||||
Post,
|
||||
User,
|
||||
Option<(User, Post)>,
|
||||
Option<(Question, User)>,
|
||||
Option<(Poll, bool)>,
|
||||
)>,
|
||||
> {
|
||||
let mut out = Vec::new();
|
||||
|
||||
let mut users: HashMap<usize, User> = HashMap::new();
|
||||
let mut seen_user_follow_statuses: HashMap<(usize, usize), bool> = HashMap::new();
|
||||
|
@ -260,6 +297,7 @@ impl DataManager {
|
|||
ua.clone(),
|
||||
self.get_post_reposting(&post, ignore_users, user).await,
|
||||
self.get_post_question(&post, ignore_users).await?,
|
||||
self.get_post_poll(&post, user).await?,
|
||||
));
|
||||
} else {
|
||||
let ua = self.get_user_by_id(owner).await?;
|
||||
|
@ -314,6 +352,7 @@ impl DataManager {
|
|||
ua,
|
||||
self.get_post_reposting(&post, ignore_users, user).await,
|
||||
self.get_post_question(&post, ignore_users).await?,
|
||||
self.get_post_poll(&post, user).await?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -1384,7 +1423,7 @@ impl DataManager {
|
|||
|
||||
let res = execute!(
|
||||
&conn,
|
||||
"INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
|
||||
"INSERT INTO posts VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, null, $13)",
|
||||
params![
|
||||
&(data.id as i64),
|
||||
&(data.created as i64),
|
||||
|
@ -1401,7 +1440,8 @@ impl DataManager {
|
|||
&0_i32,
|
||||
&0_i32,
|
||||
&serde_json::to_string(&data.uploads).unwrap(),
|
||||
&{ if data.is_deleted { 1 } else { 0 } }
|
||||
&{ if data.is_deleted { 1 } else { 0 } },
|
||||
&(data.poll_id as i64),
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -240,6 +240,8 @@ pub struct Post {
|
|||
pub uploads: Vec<usize>,
|
||||
/// If the post was deleted.
|
||||
pub is_deleted: bool,
|
||||
/// The ID of the poll associated with this post. 0 means no poll is connected.
|
||||
pub poll_id: usize,
|
||||
}
|
||||
|
||||
impl Post {
|
||||
|
@ -249,6 +251,7 @@ impl Post {
|
|||
community: usize,
|
||||
replying_to: Option<usize>,
|
||||
owner: usize,
|
||||
poll_id: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||
|
@ -263,12 +266,13 @@ impl Post {
|
|||
comment_count: 0,
|
||||
uploads: Vec::new(),
|
||||
is_deleted: false,
|
||||
poll_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new [`Post`] (as a repost of the given `post_id`).
|
||||
pub fn repost(content: String, community: usize, owner: usize, post_id: usize) -> Self {
|
||||
let mut post = Self::new(content, community, None, owner);
|
||||
let mut post = Self::new(content, community, None, owner, 0);
|
||||
|
||||
post.context.repost = Some(RepostContext {
|
||||
is_repost: false,
|
||||
|
@ -369,3 +373,107 @@ impl PostDraft {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Poll {
|
||||
pub id: usize,
|
||||
pub owner: usize,
|
||||
pub created: usize,
|
||||
/// The number of milliseconds until this poll can no longer receive votes.
|
||||
pub expires: usize,
|
||||
// options
|
||||
pub option_a: String,
|
||||
pub option_b: String,
|
||||
pub option_c: String,
|
||||
pub option_d: String,
|
||||
// votes
|
||||
pub votes_a: usize,
|
||||
pub votes_b: usize,
|
||||
pub votes_c: usize,
|
||||
pub votes_d: usize,
|
||||
}
|
||||
|
||||
impl Poll {
|
||||
/// Create a new [`Poll`].
|
||||
pub fn new(
|
||||
owner: usize,
|
||||
expires: usize,
|
||||
option_a: String,
|
||||
option_b: String,
|
||||
option_c: String,
|
||||
option_d: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||
owner,
|
||||
created: unix_epoch_timestamp() as usize,
|
||||
expires,
|
||||
// options
|
||||
option_a,
|
||||
option_b,
|
||||
option_c,
|
||||
option_d,
|
||||
// votes
|
||||
votes_a: 0,
|
||||
votes_b: 0,
|
||||
votes_c: 0,
|
||||
votes_d: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Poll option (selectors) are stored in the database as numbers 0 to 3.
|
||||
///
|
||||
/// This enum allows us to convert from these numbers into letters.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum PollOption {
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
}
|
||||
|
||||
impl From<u8> for PollOption {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
0 => Self::A,
|
||||
1 => Self::B,
|
||||
2 => Self::C,
|
||||
3 => Self::D,
|
||||
_ => Self::A,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<u8> for PollOption {
|
||||
fn into(self) -> u8 {
|
||||
match self {
|
||||
Self::A => 0,
|
||||
Self::B => 1,
|
||||
Self::C => 2,
|
||||
Self::D => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PollVote {
|
||||
pub id: usize,
|
||||
pub owner: usize,
|
||||
pub created: usize,
|
||||
pub poll_id: usize,
|
||||
pub vote: PollOption,
|
||||
}
|
||||
|
||||
impl PollVote {
|
||||
/// Create a new [`PollVote`].
|
||||
pub fn new(owner: usize, poll_id: usize, vote: PollOption) -> Self {
|
||||
Self {
|
||||
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
|
||||
owner,
|
||||
created: unix_epoch_timestamp() as usize,
|
||||
poll_id,
|
||||
vote,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
sql_changes/posts_poll_id.sql
Normal file
2
sql_changes/posts_poll_id.sql
Normal file
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE posts
|
||||
ADD COLUMN poll_id BIGINT NOT NULL DEFAULT 0;
|
Loading…
Add table
Add a link
Reference in a new issue