feat: add comment & operation & UGC trait
This commit is contained in:
@@ -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<i8>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct CommentOutput {
|
||||
@@ -32,12 +41,17 @@ pub fn c2output(p: &Post, cs: &Vec<Comment>, user: &CurrentUser) -> Vec<CommentO
|
||||
x
|
||||
}
|
||||
};
|
||||
if c.is_deleted {
|
||||
if false {
|
||||
// TODO: block
|
||||
None
|
||||
} else {
|
||||
Some(CommentOutput {
|
||||
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,
|
||||
name_id: name_id,
|
||||
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,
|
||||
}))
|
||||
}
|
||||
|
||||
#[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
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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<T> = Result<T, APIError>;
|
||||
|
||||
pub mod comment;
|
||||
pub mod operation;
|
||||
pub mod post;
|
||||
pub mod systemlog;
|
||||
|
||||
32
src/api/operation.rs
Normal file
32
src/api/operation.rs
Normal file
@@ -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
|
||||
}))
|
||||
}
|
||||
@@ -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?<pid>")]
|
||||
pub fn get_one(pid: i32, user: CurrentUser, conn: DbConn) -> API<Value> {
|
||||
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<Value> {
|
||||
#[get("/getlist?<p>&<order_mode>")]
|
||||
pub fn get_list(p: Option<u32>, order_mode: u8, user: CurrentUser, conn: DbConn) -> API<Value> {
|
||||
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<CwInput>, user: CurrentUser, conn: DbConn) -> API<Value
|
||||
if !(user.is_admin || p.author_hash == user.namehash) {
|
||||
return Err(APIError::PcError(NotAllowed));
|
||||
}
|
||||
p.check_permission(&user, "w")?;
|
||||
_ = p.update_cw(&conn, cwi.cw);
|
||||
Ok(json!({"code": 0}))
|
||||
}
|
||||
|
||||
38
src/db_conn.rs
Normal file
38
src/db_conn.rs
Normal file
@@ -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")
|
||||
}
|
||||
@@ -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])
|
||||
|
||||
@@ -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<T> = Result<T, diesel::result::Error>;
|
||||
|
||||
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)]
|
||||
pub struct Post {
|
||||
@@ -41,24 +58,16 @@ pub struct NewPost<'a> {
|
||||
}
|
||||
|
||||
impl Post {
|
||||
pub fn get(conn: &Conn, id: i32) -> MR<Self> {
|
||||
posts::table.find(id).first(conn)
|
||||
}
|
||||
get!(posts);
|
||||
|
||||
pub fn gets_by_page(
|
||||
conn: &Conn,
|
||||
order_mode: u8,
|
||||
page: u32,
|
||||
page_size: u32,
|
||||
is_admin: bool,
|
||||
) -> MR<Vec<Self>> {
|
||||
set_deleted!(posts);
|
||||
|
||||
pub fn gets_by_page(conn: &Conn, order_mode: u8, page: u32, page_size: u32) -> MR<Vec<Self>> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
match order_mode {
|
||||
1 => query = query.order(posts::last_comment_time.desc()),
|
||||
@@ -86,9 +95,17 @@ impl Post {
|
||||
pub fn update_cw(&self, conn: &Conn, new_cw: &str) -> MR<usize> {
|
||||
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<usize> {
|
||||
insert_into(comments::table)
|
||||
.values(&new_comment)
|
||||
.execute(conn)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user