Browse Source

feat: add comment & operation & UGC trait

master
hole-thu 3 years ago
parent
commit
e87d8acb7c
  1. 39
      src/api/comment.rs
  2. 74
      src/api/mod.rs
  3. 32
      src/api/operation.rs
  4. 15
      src/api/post.rs
  5. 38
      src/db_conn.rs
  6. 2
      src/main.rs
  7. 76
      src/models.rs

39
src/api/comment.rs

@ -1,13 +1,22 @@
use crate::api::{APIError, CurrentUser, PolicyError::*, API}; use crate::api::{APIError, CurrentUser, PolicyError::*, API};
use crate::db_conn::DbConn;
use crate::models::*; use crate::models::*;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use rocket::form::Form;
use rocket::serde::{ use rocket::serde::{
json::{json, Value}, json::{json, Value},
Serialize, Serialize,
}; };
use crate::db_conn::DbConn;
use std::collections::HashMap; use std::collections::HashMap;
#[derive(FromForm)]
pub struct CommentInput<'r> {
pid: i32,
#[field(validate = len(1..4097))]
text: &'r str,
use_title: Option<i8>,
}
#[derive(Serialize)] #[derive(Serialize)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct CommentOutput { pub struct CommentOutput {
@ -32,12 +41,17 @@ pub fn c2output(p: &Post, cs: &Vec<Comment>, user: &CurrentUser) -> Vec<CommentO
x x
} }
}; };
if c.is_deleted { if false {
// TODO: block
None None
} else { } else {
Some(CommentOutput { Some(CommentOutput {
cid: c.id, cid: c.id,
text: c.content.to_string(), text: if c.is_deleted {
"[已删除]".to_string()
} else {
c.content.to_string()
},
can_del: user.is_admin || c.author_hash == user.namehash, can_del: user.is_admin || c.author_hash == user.namehash,
name_id: name_id, name_id: name_id,
create_time: c.create_time, create_time: c.create_time,
@ -63,3 +77,22 @@ pub fn get_comment(pid: i32, user: CurrentUser, conn: DbConn) -> API<Value> {
"likenum": p.n_likes, "likenum": p.n_likes,
})) }))
} }
#[post("/docomment", data = "<ci>")]
pub fn add_comment(ci: Form<CommentInput>, user: CurrentUser, conn: DbConn) -> API<Value> {
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
}))
}

74
src/api/mod.rs

@ -1,10 +1,10 @@
use crate::db_conn::{Conn, DbPool};
use crate::models::*; use crate::models::*;
use crate::random_hasher::RandomHasher; use crate::random_hasher::RandomHasher;
use rocket::http::Status; use rocket::http::Status;
use rocket::request::{FromRequest, Request, Outcome}; use rocket::request::{FromRequest, Outcome, Request};
use rocket::response::{self, Responder}; use rocket::response::{self, Responder};
use rocket::serde::json::json; use rocket::serde::json::json;
use crate::db_conn::DbPool;
#[catch(401)] #[catch(401)]
pub fn catch_401_error() -> &'static str { 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 { macro_rules! look {
($s:expr) => { ($s:expr) => {
format!("{}...{}", &$s[..2], &$s[$s.len() - 2..]) format!("{}...{}", &$s[..2], &$s[$s.len() - 2..])
@ -102,5 +171,6 @@ macro_rules! look {
pub type API<T> = Result<T, APIError>; pub type API<T> = Result<T, APIError>;
pub mod comment; pub mod comment;
pub mod operation;
pub mod post; pub mod post;
pub mod systemlog; pub mod systemlog;

32
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 = "<di>")]
pub fn delete(di: Form<DeleteInput>, user: CurrentUser, conn: DbConn) -> API<Value> {
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
}))
}

15
src/api/post.rs

@ -1,5 +1,5 @@
use crate::api::comment::{c2output, CommentOutput}; 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::db_conn::DbConn;
use crate::models::*; use crate::models::*;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
@ -89,14 +89,7 @@ fn p2output(p: &Post, user: &CurrentUser, conn: &DbConn) -> PostOutput {
#[get("/getone?<pid>")] #[get("/getone?<pid>")]
pub fn get_one(pid: i32, user: CurrentUser, conn: DbConn) -> API<Value> { pub fn get_one(pid: i32, user: CurrentUser, conn: DbConn) -> API<Value> {
let p = Post::get(&conn, pid).map_err(APIError::from_db)?; let p = Post::get(&conn, pid).map_err(APIError::from_db)?;
if !user.is_admin { p.check_permission(&user, "ro")?;
if p.is_reported {
return Err(APIError::PcError(IsReported));
}
if p.is_deleted {
return Err(APIError::PcError(IsDeleted));
}
}
Ok(json!({ Ok(json!({
"data": p2output(&p, &user, &conn), "data": p2output(&p, &user, &conn),
"code": 0, "code": 0,
@ -106,8 +99,7 @@ pub fn get_one(pid: i32, user: CurrentUser, conn: DbConn) -> API<Value> {
#[get("/getlist?<p>&<order_mode>")] #[get("/getlist?<p>&<order_mode>")]
pub fn get_list(p: Option<u32>, order_mode: u8, user: CurrentUser, conn: DbConn) -> API<Value> { pub fn get_list(p: Option<u32>, order_mode: u8, user: CurrentUser, conn: DbConn) -> API<Value> {
let page = p.unwrap_or(1); let page = p.unwrap_or(1);
let ps = Post::gets_by_page(&conn, order_mode, page, 25, user.is_admin) let ps = Post::gets_by_page(&conn, order_mode, page, 25).map_err(APIError::from_db)?;
.map_err(APIError::from_db)?;
let ps_data = ps let ps_data = ps
.iter() .iter()
.map(|p| p2output(p, &user, &conn)) .map(|p| p2output(p, &user, &conn))
@ -145,6 +137,7 @@ pub fn edit_cw(cwi: Form<CwInput>, user: CurrentUser, conn: DbConn) -> API<Value
if !(user.is_admin || p.author_hash == user.namehash) { if !(user.is_admin || p.author_hash == user.namehash) {
return Err(APIError::PcError(NotAllowed)); return Err(APIError::PcError(NotAllowed));
} }
p.check_permission(&user, "w")?;
_ = p.update_cw(&conn, cwi.cw); _ = p.update_cw(&conn, cwi.cw);
Ok(json!({"code": 0})) Ok(json!({"code": 0}))
} }

38
src/db_conn.rs

@ -0,0 +1,38 @@
use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
use std::env;
use std::ops::Deref;
use rocket::http::Status;
use rocket::request::{FromRequest, Request, Outcome};
pub type Conn = diesel::SqliteConnection;
pub type DbPool = Pool<ConnectionManager<Conn>>;
pub struct DbConn(pub PooledConnection<ConnectionManager<Conn>>);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for DbConn {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let pool = request.rocket().state::<DbPool>().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::<Conn>::new(database_url);
Pool::builder()
.build(manager)
.expect("database poll init fail")
}

2
src/main.rs

@ -21,11 +21,13 @@ fn rocket() -> _ {
"/_api/v1", "/_api/v1",
routes![ routes![
api::comment::get_comment, api::comment::get_comment,
api::comment::add_comment,
api::post::get_list, api::post::get_list,
api::post::get_one, api::post::get_one,
api::post::publish_post, api::post::publish_post,
api::post::edit_cw, api::post::edit_cw,
api::systemlog::get_systemlog, api::systemlog::get_systemlog,
api::operation::delete,
], ],
) )
.register("/_api", catchers![api::catch_401_error]) .register("/_api", catchers![api::catch_401_error])

76
src/models.rs

@ -3,14 +3,31 @@
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{insert_into, ExpressionMethods, QueryDsl, RunQueryDsl}; use diesel::{insert_into, ExpressionMethods, QueryDsl, RunQueryDsl};
use crate::schema::*;
use crate::db_conn::Conn; use crate::db_conn::Conn;
use crate::schema::*;
type MR<T> = Result<T, diesel::result::Error>; type MR<T> = Result<T, diesel::result::Error>;
no_arg_sql_function!(RANDOM, (), "Represents the sql RANDOM() function"); no_arg_sql_function!(RANDOM, (), "Represents the sql RANDOM() function");
macro_rules! get {
($table:ident) => {
pub fn get(conn: &Conn, id: i32) -> MR<Self> {
$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)] #[derive(Queryable, Identifiable)]
pub struct Post { pub struct Post {
@ -41,23 +58,15 @@ pub struct NewPost<'a> {
} }
impl Post { impl Post {
pub fn get(conn: &Conn, id: i32) -> MR<Self> { get!(posts);
posts::table.find(id).first(conn)
} set_deleted!(posts);
pub fn gets_by_page( pub fn gets_by_page(conn: &Conn, order_mode: u8, page: u32, page_size: u32) -> MR<Vec<Self>> {
conn: &Conn,
order_mode: u8,
page: u32,
page_size: u32,
is_admin: bool,
) -> MR<Vec<Self>> {
let mut query = posts::table.into_boxed(); let mut query = posts::table.into_boxed();
if !is_admin { query = query.filter(posts::is_deleted.eq(false));
query = query.filter(posts::is_deleted.eq(false)); if order_mode > 0 {
if order_mode > 0 { query = query.filter(posts::is_reported.eq(false))
query = query.filter(posts::is_reported.eq(false))
}
} }
match order_mode { match order_mode {
@ -86,9 +95,17 @@ impl Post {
pub fn update_cw(&self, conn: &Conn, new_cw: &str) -> MR<usize> { pub fn update_cw(&self, conn: &Conn, new_cw: &str) -> MR<usize> {
diesel::update(self).set(posts::cw.eq(new_cw)).execute(conn) 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 struct User {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
@ -102,7 +119,7 @@ impl User {
} }
} }
#[derive(Queryable, Debug)] #[derive(Queryable, Identifiable)]
pub struct Comment { pub struct Comment {
pub id: i32, pub id: i32,
pub author_hash: String, pub author_hash: String,
@ -113,4 +130,23 @@ pub struct Comment {
pub post_id: i32, 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<usize> {
insert_into(comments::table)
.values(&new_comment)
.execute(conn)
}
}

Loading…
Cancel
Save