feat: add comment & operation & UGC trait
This commit is contained in:
@@ -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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
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::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
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",
|
"/_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])
|
||||||
|
|||||||
@@ -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,24 +58,16 @@ 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gets_by_page(
|
set_deleted!(posts);
|
||||||
conn: &Conn,
|
|
||||||
order_mode: u8,
|
pub fn gets_by_page(conn: &Conn, order_mode: u8, page: u32, page_size: u32) -> MR<Vec<Self>> {
|
||||||
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 {
|
||||||
1 => query = query.order(posts::last_comment_time.desc()),
|
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> {
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user