diff --git a/src/api/comment.rs b/src/api/comment.rs index 771999d..2efc735 100644 --- a/src/api/comment.rs +++ b/src/api/comment.rs @@ -1,13 +1,22 @@ use crate::api::{APIError, CurrentUser, PolicyError::*, API}; +use crate::db_conn::DbConn; use crate::models::*; use chrono::NaiveDateTime; +use rocket::form::Form; use rocket::serde::{ json::{json, Value}, Serialize, }; -use crate::db_conn::DbConn; use std::collections::HashMap; +#[derive(FromForm)] +pub struct CommentInput<'r> { + pid: i32, + #[field(validate = len(1..4097))] + text: &'r str, + use_title: Option, +} + #[derive(Serialize)] #[serde(crate = "rocket::serde")] pub struct CommentOutput { @@ -32,12 +41,17 @@ pub fn c2output(p: &Post, cs: &Vec, user: &CurrentUser) -> Vec API { "likenum": p.n_likes, })) } + +#[post("/docomment", data = "")] +pub fn add_comment(ci: Form, user: CurrentUser, conn: DbConn) -> API { + let p = Post::get(&conn, ci.pid).map_err(APIError::from_db)?; + Comment::create( + &conn, + NewComment { + content: &ci.text, + author_hash: &user.namehash, + author_title: "", + post_id: ci.pid, + }, + ) + .map_err(APIError::from_db)?; + p.after_add_comment(&conn).map_err(APIError::from_db)?; + Ok(json!({ + "code": 0 + })) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index a7f5a2c..60eadf3 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,10 +1,10 @@ +use crate::db_conn::{Conn, DbPool}; use crate::models::*; use crate::random_hasher::RandomHasher; use rocket::http::Status; -use rocket::request::{FromRequest, Request, Outcome}; +use rocket::request::{FromRequest, Outcome, Request}; use rocket::response::{self, Responder}; use rocket::serde::json::json; -use crate::db_conn::DbPool; #[catch(401)] pub fn catch_401_error() -> &'static str { @@ -93,6 +93,75 @@ impl<'r> Responder<'r, 'static> for APIError { } } +pub trait UGC { + fn get_author_hash(&self) -> &str; + fn get_is_deleted(&self) -> bool; + fn get_is_reported(&self) -> bool; + fn extra_delete_condition(&self) -> bool; + fn do_set_deleted(&self, conn: &Conn) -> API<()>; + fn check_permission(&self, user: &CurrentUser, mode: &str) -> API<()> { + if user.is_admin { + return Ok(()); + } + if mode.contains('r') && self.get_is_deleted() { + return Err(APIError::PcError(PolicyError::IsDeleted)); + } + if mode.contains('o') && self.get_is_reported() { + return Err(APIError::PcError(PolicyError::IsReported)); + } + if mode.contains('w') && self.get_author_hash() != user.namehash { + return Err(APIError::PcError(PolicyError::NotAllowed)); + } + if mode.contains('d') && !self.extra_delete_condition() { + return Err(APIError::PcError(PolicyError::NotAllowed)); + } + Ok(()) + } + + fn soft_delete(&self, user: &CurrentUser, conn: &Conn) -> API<()> { + self.check_permission(user, "rwd")?; + + self.do_set_deleted(conn)?; + Ok(()) + } +} + +impl UGC for Post { + fn get_author_hash(&self) -> &str { + &self.author_hash + } + fn get_is_reported(&self) -> bool { + self.is_reported + } + fn get_is_deleted(&self) -> bool { + self.is_deleted + } + fn extra_delete_condition(&self) -> bool { + self.n_comments == 0 + } + fn do_set_deleted(&self, conn: &Conn) -> API<()> { + self.set_deleted(conn).map_err(APIError::from_db) + } +} + +impl UGC for Comment { + fn get_author_hash(&self) -> &str { + &self.author_hash + } + fn get_is_reported(&self) -> bool { + false + } + fn get_is_deleted(&self) -> bool { + self.is_deleted + } + fn extra_delete_condition(&self) -> bool { + true + } + fn do_set_deleted(&self, conn: &Conn) -> API<()> { + self.set_deleted(conn).map_err(APIError::from_db) + } +} + macro_rules! look { ($s:expr) => { format!("{}...{}", &$s[..2], &$s[$s.len() - 2..]) @@ -102,5 +171,6 @@ macro_rules! look { pub type API = Result; pub mod comment; +pub mod operation; pub mod post; pub mod systemlog; diff --git a/src/api/operation.rs b/src/api/operation.rs new file mode 100644 index 0000000..b9d51a1 --- /dev/null +++ b/src/api/operation.rs @@ -0,0 +1,32 @@ +use crate::api::{APIError, CurrentUser, PolicyError::*, API, UGC}; +use crate::db_conn::DbConn; +use crate::models::*; +use rocket::form::Form; +use rocket::serde::json::{json, Value}; + +#[derive(FromForm)] +pub struct DeleteInput<'r> { + #[field(name = "type")] + id_type: &'r str, + id: i32, + note: &'r str, +} + +#[post("/delete", data = "")] +pub fn delete(di: Form, user: CurrentUser, conn: DbConn) -> API { + match di.id_type { + "cid" => { + let c = Comment::get(&conn, di.id).map_err(APIError::from_db)?; + c.soft_delete(&user, &conn)?; + } + "pid" => { + let p = Post::get(&conn, di.id).map_err(APIError::from_db)?; + p.soft_delete(&user, &conn)?; + } + _ => return Err(APIError::PcError(NotAllowed)), + } + + Ok(json!({ + "code": 0 + })) +} diff --git a/src/api/post.rs b/src/api/post.rs index 6c19029..c5d17c8 100644 --- a/src/api/post.rs +++ b/src/api/post.rs @@ -1,5 +1,5 @@ use crate::api::comment::{c2output, CommentOutput}; -use crate::api::{APIError, CurrentUser, PolicyError::*, API}; +use crate::api::{APIError, CurrentUser, PolicyError::*, API, UGC}; use crate::db_conn::DbConn; use crate::models::*; use chrono::NaiveDateTime; @@ -89,14 +89,7 @@ fn p2output(p: &Post, user: &CurrentUser, conn: &DbConn) -> PostOutput { #[get("/getone?")] pub fn get_one(pid: i32, user: CurrentUser, conn: DbConn) -> API { let p = Post::get(&conn, pid).map_err(APIError::from_db)?; - if !user.is_admin { - if p.is_reported { - return Err(APIError::PcError(IsReported)); - } - if p.is_deleted { - return Err(APIError::PcError(IsDeleted)); - } - } + p.check_permission(&user, "ro")?; Ok(json!({ "data": p2output(&p, &user, &conn), "code": 0, @@ -106,8 +99,7 @@ pub fn get_one(pid: i32, user: CurrentUser, conn: DbConn) -> API { #[get("/getlist?

&")] pub fn get_list(p: Option, order_mode: u8, user: CurrentUser, conn: DbConn) -> API { let page = p.unwrap_or(1); - let ps = Post::gets_by_page(&conn, order_mode, page, 25, user.is_admin) - .map_err(APIError::from_db)?; + let ps = Post::gets_by_page(&conn, order_mode, page, 25).map_err(APIError::from_db)?; let ps_data = ps .iter() .map(|p| p2output(p, &user, &conn)) @@ -145,6 +137,7 @@ pub fn edit_cw(cwi: Form, user: CurrentUser, conn: DbConn) -> API>; +pub struct DbConn(pub PooledConnection>); + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for DbConn { + type Error = (); + async fn from_request(request: &'r Request<'_>) -> Outcome { + let pool = request.rocket().state::().unwrap(); + match pool.get() { + Ok(conn) => Outcome::Success(DbConn(conn)), + Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())), + } + } +} + +// For the convenience of using an &DbConn as an &Connection. +impl Deref for DbConn { + type Target = Conn; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub fn init_pool() -> DbPool { + let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let manager = ConnectionManager::::new(database_url); + Pool::builder() + .build(manager) + .expect("database poll init fail") +} diff --git a/src/main.rs b/src/main.rs index c9e79af..d249da2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,11 +21,13 @@ fn rocket() -> _ { "/_api/v1", routes![ api::comment::get_comment, + api::comment::add_comment, api::post::get_list, api::post::get_one, api::post::publish_post, api::post::edit_cw, api::systemlog::get_systemlog, + api::operation::delete, ], ) .register("/_api", catchers![api::catch_401_error]) diff --git a/src/models.rs b/src/models.rs index 5a11dd7..0d88f10 100644 --- a/src/models.rs +++ b/src/models.rs @@ -3,14 +3,31 @@ use chrono::NaiveDateTime; use diesel::{insert_into, ExpressionMethods, QueryDsl, RunQueryDsl}; -use crate::schema::*; use crate::db_conn::Conn; - +use crate::schema::*; type MR = Result; no_arg_sql_function!(RANDOM, (), "Represents the sql RANDOM() function"); +macro_rules! get { + ($table:ident) => { + pub fn get(conn: &Conn, id: i32) -> MR { + $table::table.find(id).first(conn) + } + }; +} + +macro_rules! set_deleted { + ($table:ident) => { + pub fn set_deleted(&self, conn: &Conn) -> MR<()> { + diesel::update(self) + .set($table::is_deleted.eq(true)) + .execute(conn)?; + Ok(()) + } + }; +} #[derive(Queryable, Identifiable)] pub struct Post { @@ -41,23 +58,15 @@ pub struct NewPost<'a> { } impl Post { - pub fn get(conn: &Conn, id: i32) -> MR { - posts::table.find(id).first(conn) - } + get!(posts); + + set_deleted!(posts); - pub fn gets_by_page( - conn: &Conn, - order_mode: u8, - page: u32, - page_size: u32, - is_admin: bool, - ) -> MR> { + pub fn gets_by_page(conn: &Conn, order_mode: u8, page: u32, page_size: u32) -> MR> { let mut query = posts::table.into_boxed(); - if !is_admin { - query = query.filter(posts::is_deleted.eq(false)); - if order_mode > 0 { - query = query.filter(posts::is_reported.eq(false)) - } + query = query.filter(posts::is_deleted.eq(false)); + if order_mode > 0 { + query = query.filter(posts::is_reported.eq(false)) } match order_mode { @@ -86,9 +95,17 @@ impl Post { pub fn update_cw(&self, conn: &Conn, new_cw: &str) -> MR { diesel::update(self).set(posts::cw.eq(new_cw)).execute(conn) } + + pub fn after_add_comment(&self, conn: &Conn) -> MR<()> { + diesel::update(self) + .set(posts::n_comments.eq(posts::n_comments + 1)) + .execute(conn)?; + // TODO: attention, hot_score + Ok(()) + } } -#[derive(Queryable, Debug)] +#[derive(Queryable, Identifiable)] pub struct User { pub id: i32, pub name: String, @@ -102,7 +119,7 @@ impl User { } } -#[derive(Queryable, Debug)] +#[derive(Queryable, Identifiable)] pub struct Comment { pub id: i32, pub author_hash: String, @@ -113,4 +130,23 @@ pub struct Comment { pub post_id: i32, } -impl Comment {} +#[derive(Insertable)] +#[table_name = "comments"] +pub struct NewComment<'a> { + pub content: &'a str, + pub author_hash: &'a str, + pub author_title: &'a str, + pub post_id: i32, +} + +impl Comment { + get!(comments); + + set_deleted!(comments); + + pub fn create(conn: &Conn, new_comment: NewComment) -> MR { + insert_into(comments::table) + .values(&new_comment) + .execute(conn) + } +}