feat: attention & redis & async

This commit is contained in:
2022-03-20 22:22:08 +08:00
parent e87d8acb7c
commit 58eb7aba6f
16 changed files with 495 additions and 211 deletions

63
src/api/attention.rs Normal file
View File

@@ -0,0 +1,63 @@
use crate::api::post::ps2outputs;
use crate::api::{APIError, CurrentUser, MapToAPIError, PolicyError::*, API, UGC};
use crate::db_conn::Db;
use crate::models::*;
use crate::rds_conn::RdsConn;
use crate::rds_models::*;
use rocket::form::Form;
use rocket::serde::json::{json, Value};
#[derive(FromForm)]
pub struct AttentionInput {
pid: i32,
#[field(validate = range(0..2))]
switch: i32,
}
#[post("/attention", data = "<ai>")]
pub async fn attention_post(
ai: Form<AttentionInput>,
user: CurrentUser,
db: Db,
rconn: RdsConn,
) -> API<Value> {
user.id.ok_or_else(|| APIError::PcError(NotAllowed))?;
let p = Post::get(&db, ai.pid).await.m()?;
p.check_permission(&user, "r")?;
let mut att = Attention::init(&user.namehash, rconn);
let switch_to = ai.switch == 1;
let mut delta: i32 = 0;
if att.has(ai.pid).await.m()? != switch_to {
if switch_to {
att.add(ai.pid).await.m()?;
delta = 1;
} else {
att.remove(ai.pid).await.m()?;
delta = -1;
}
p.change_n_attentions(&db, delta).await.m()?;
}
Ok(json!({
"code": 0,
"attention": ai.switch == 1,
"n_attentions": p.n_attentions + delta,
// for old version frontend
"likenum": p.n_attentions + delta,
}))
}
#[get("/getattention")]
pub async fn get_attention(user: CurrentUser, db: Db, rconn: RdsConn) -> API<Value> {
let ids = Attention::init(&user.namehash, rconn.clone())
.all()
.await
.m()?;
let ps = Post::get_multi(&db, ids).await.m()?;
let ps_data = ps2outputs(&ps, &user, &db, rconn.clone()).await;
Ok(json!({
"code": 0,
"data": ps_data,
}))
}

View File

@@ -1,6 +1,8 @@
use crate::api::{APIError, CurrentUser, PolicyError::*, API};
use crate::db_conn::DbConn;
use crate::api::{APIError, CurrentUser, MapToAPIError, PolicyError::*, API};
use crate::db_conn::Db;
use crate::models::*;
use crate::rds_conn::RdsConn;
use crate::rds_models::*;
use chrono::NaiveDateTime;
use rocket::form::Form;
use rocket::serde::{
@@ -10,10 +12,10 @@ use rocket::serde::{
use std::collections::HashMap;
#[derive(FromForm)]
pub struct CommentInput<'r> {
pub struct CommentInput {
pid: i32,
#[field(validate = len(1..4097))]
text: &'r str,
text: String,
use_title: Option<i8>,
}
@@ -24,12 +26,13 @@ pub struct CommentOutput {
text: String,
can_del: bool,
name_id: i32,
is_tmp: bool,
create_time: NaiveDateTime,
// for old version frontend
timestamp: i64,
}
pub fn c2output(p: &Post, cs: &Vec<Comment>, user: &CurrentUser) -> Vec<CommentOutput> {
pub fn c2output<'r>(p: &'r Post, cs: &Vec<Comment>, user: &CurrentUser) -> Vec<CommentOutput> {
let mut hash2id = HashMap::<&String, i32>::from([(&p.author_hash, 0)]);
cs.iter()
.filter_map(|c| {
@@ -41,19 +44,16 @@ pub fn c2output(p: &Post, cs: &Vec<Comment>, user: &CurrentUser) -> Vec<CommentO
x
}
};
if false {
if c.is_deleted {
// TODO: block
None
} else {
Some(CommentOutput {
cid: c.id,
text: if c.is_deleted {
"[已删除]".to_string()
} else {
c.content.to_string()
},
text: format!("{}{}", if c.is_tmp { "[tmp]\n" } else { "" }, c.content),
can_del: user.is_admin || c.author_hash == user.namehash,
name_id: name_id,
is_tmp: c.is_tmp,
create_time: c.create_time,
timestamp: c.create_time.timestamp(),
})
@@ -63,35 +63,52 @@ pub fn c2output(p: &Post, cs: &Vec<Comment>, user: &CurrentUser) -> Vec<CommentO
}
#[get("/getcomment?<pid>")]
pub fn get_comment(pid: i32, user: CurrentUser, conn: DbConn) -> API<Value> {
let p = Post::get(&conn, pid).map_err(APIError::from_db)?;
pub async fn get_comment(pid: i32, user: CurrentUser, db: Db, rconn: RdsConn) -> API<Value> {
let p = Post::get(&db, pid).await.m()?;
if p.is_deleted {
return Err(APIError::PcError(IsDeleted));
}
let cs = p.get_comments(&conn).map_err(APIError::from_db)?;
let pid = p.id;
let cs = Comment::gets_by_post_id(&db, pid).await.m()?;
let data = c2output(&p, &cs, &user);
Ok(json!({
"code": 0,
"data": c2output(&p, &cs, &user),
"n_likes": p.n_likes,
"data": data,
"n_attentions": p.n_attentions,
// for old version frontend
"likenum": p.n_likes,
"likenum": p.n_attentions,
"attention": Attention::init(&user.namehash, rconn.clone()).has(p.id).await.m()? ,
}))
}
#[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)?;
pub async fn add_comment(
ci: Form<CommentInput>,
user: CurrentUser,
db: Db,
rconn: RdsConn,
) -> API<Value> {
let p = Post::get(&db, ci.pid).await.m()?;
Comment::create(
&conn,
&db,
NewComment {
content: &ci.text,
author_hash: &user.namehash,
author_title: "",
content: ci.text.to_string(),
author_hash: user.namehash.to_string(),
author_title: "".to_string(),
is_tmp: user.id.is_none(),
post_id: ci.pid,
},
)
.map_err(APIError::from_db)?;
p.after_add_comment(&conn).map_err(APIError::from_db)?;
.await
.m()?;
p.change_n_comments(&db, 1).await.m()?;
// auto attention after comment
let mut att = Attention::init(&user.namehash, rconn);
if !att.has(p.id).await.m()? {
att.add(p.id).await.m()?;
p.change_n_attentions(&db, 1).await.m()?;
}
Ok(json!({
"code": 0
}))

View File

@@ -1,10 +1,11 @@
use crate::db_conn::{Conn, DbPool};
use crate::db_conn::Db;
use crate::models::*;
use crate::random_hasher::RandomHasher;
use rocket::http::Status;
use rocket::outcome::try_outcome;
use rocket::request::{FromRequest, Outcome, Request};
use rocket::response::{self, Responder};
use rocket::serde::json::json;
use rocket::serde::json::{json, Value};
#[catch(401)]
pub fn catch_401_error() -> &'static str {
@@ -36,8 +37,8 @@ impl<'r> FromRequest<'r> for CurrentUser {
is_admin: false,
});
} else {
let conn = request.rocket().state::<DbPool>().unwrap().get().unwrap();
if let Some(user) = User::get_by_token(&conn, token) {
let db = try_outcome!(request.guard::<Db>().await);
if let Some(user) = User::get_by_token(&db, token).await {
let namehash = rh.hash_with_salt(&user.name);
cu = Some(CurrentUser {
id: Some(user.id),
@@ -55,14 +56,17 @@ impl<'r> FromRequest<'r> for CurrentUser {
}
}
#[derive(Debug)]
pub enum PolicyError {
IsReported,
IsDeleted,
NotAllowed,
}
#[derive(Debug)]
pub enum APIError {
DbError(diesel::result::Error),
RdsError(redis::RedisError),
PcError(PolicyError),
}
@@ -70,16 +74,26 @@ impl APIError {
fn from_db(err: diesel::result::Error) -> APIError {
APIError::DbError(err)
}
fn from_rds(err: redis::RedisError) -> APIError {
APIError::RdsError(err)
}
}
impl<'r> Responder<'r, 'static> for APIError {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
dbg!(&self);
match self {
APIError::DbError(e) => json!({
"code": -1,
"msg": e.to_string()
})
.respond_to(req),
APIError::RdsError(e) => json!({
"code": -1,
"msg": e.to_string()
})
.respond_to(req),
APIError::PcError(e) => json!({
"code": -1,
"msg": match e {
@@ -93,12 +107,35 @@ impl<'r> Responder<'r, 'static> for APIError {
}
}
pub type API<T> = Result<T, APIError>;
pub type JsonAPI = API<Value>;
pub trait MapToAPIError {
type Data;
fn m(self) -> API<Self::Data>;
}
impl<T> MapToAPIError for redis::RedisResult<T> {
type Data = T;
fn m(self) -> API<Self::Data> {
Ok(self.map_err(APIError::from_rds)?)
}
}
impl<T> MapToAPIError for diesel::QueryResult<T> {
type Data = T;
fn m(self) -> API<Self::Data> {
Ok(self.map_err(APIError::from_db)?)
}
}
#[rocket::async_trait]
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<()>;
async fn do_set_deleted(&self, db: &Db) -> API<usize>;
fn check_permission(&self, user: &CurrentUser, mode: &str) -> API<()> {
if user.is_admin {
return Ok(());
@@ -118,14 +155,15 @@ pub trait UGC {
Ok(())
}
fn soft_delete(&self, user: &CurrentUser, conn: &Conn) -> API<()> {
async fn soft_delete(&self, user: &CurrentUser, db: &Db) -> API<()> {
self.check_permission(user, "rwd")?;
self.do_set_deleted(conn)?;
let _ = self.do_set_deleted(db).await?;
Ok(())
}
}
#[rocket::async_trait]
impl UGC for Post {
fn get_author_hash(&self) -> &str {
&self.author_hash
@@ -139,11 +177,12 @@ impl UGC for Post {
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)
async fn do_set_deleted(&self, db: &Db) -> API<usize> {
self.set_deleted(db).await.m()
}
}
#[rocket::async_trait]
impl UGC for Comment {
fn get_author_hash(&self) -> &str {
&self.author_hash
@@ -157,8 +196,8 @@ impl UGC for Comment {
fn extra_delete_condition(&self) -> bool {
true
}
fn do_set_deleted(&self, conn: &Conn) -> API<()> {
self.set_deleted(conn).map_err(APIError::from_db)
async fn do_set_deleted(&self, db: &Db) -> API<usize> {
self.set_deleted(db).await.m()
}
}
@@ -168,8 +207,7 @@ macro_rules! look {
};
}
pub type API<T> = Result<T, APIError>;
pub mod attention;
pub mod comment;
pub mod operation;
pub mod post;

View File

@@ -1,27 +1,28 @@
use crate::api::{APIError, CurrentUser, PolicyError::*, API, UGC};
use crate::db_conn::DbConn;
use crate::api::{APIError, CurrentUser, PolicyError::*, API, UGC, MapToAPIError};
use crate::db_conn::Db;
use crate::models::*;
use rocket::form::Form;
use rocket::serde::json::{json, Value};
#[derive(FromForm)]
pub struct DeleteInput<'r> {
pub struct DeleteInput {
#[field(name = "type")]
id_type: &'r str,
id_type: String,
id: i32,
note: &'r str,
note: String,
}
#[post("/delete", data = "<di>")]
pub fn delete(di: Form<DeleteInput>, user: CurrentUser, conn: DbConn) -> API<Value> {
match di.id_type {
pub async fn delete(di: Form<DeleteInput>, user: CurrentUser, db: Db) -> API<Value> {
match di.id_type.as_str() {
"cid" => {
let c = Comment::get(&conn, di.id).map_err(APIError::from_db)?;
c.soft_delete(&user, &conn)?;
let c = Comment::get(&db, di.id).await.m()?;
c.soft_delete(&user, &db).await?;
}
"pid" => {
let p = Post::get(&conn, di.id).map_err(APIError::from_db)?;
p.soft_delete(&user, &conn)?;
let p = Post::get(&db, di.id).await.m()?;
p.soft_delete(&user, &db).await?;
p.change_n_comments(&db, -1).await.m()?;
}
_ => return Err(APIError::PcError(NotAllowed)),
}

View File

@@ -1,20 +1,20 @@
use crate::api::comment::{c2output, CommentOutput};
use crate::api::{APIError, CurrentUser, PolicyError::*, API, UGC};
use crate::db_conn::DbConn;
use crate::api::{APIError, CurrentUser, JsonAPI, MapToAPIError, PolicyError::*, UGC};
use crate::db_conn::Db;
use crate::models::*;
use crate::rds_conn::RdsConn;
use crate::rds_models::*;
use chrono::NaiveDateTime;
use rocket::form::Form;
use rocket::serde::{
json::{json, Value},
Serialize,
};
use rocket::futures::future;
use rocket::serde::{json::json, Serialize};
#[derive(FromForm)]
pub struct PostInput<'r> {
pub struct PostInput {
#[field(validate = len(1..4097))]
text: &'r str,
text: String,
#[field(validate = len(0..33))]
cw: &'r str,
cw: String,
allow_search: Option<i8>,
use_title: Option<i8>,
}
@@ -26,7 +26,8 @@ pub struct PostOutput {
text: String,
cw: Option<String>,
custom_title: Option<String>,
n_likes: i32,
is_tmp: bool,
n_attentions: i32,
n_comments: i32,
create_time: NaiveDateTime,
last_comment_time: NaiveDateTime,
@@ -34,6 +35,7 @@ pub struct PostOutput {
is_reported: Option<bool>,
comments: Option<Vec<CommentOutput>>,
can_del: bool,
attention: bool,
// for old version frontend
timestamp: i64,
likenum: i32,
@@ -41,23 +43,22 @@ pub struct PostOutput {
}
#[derive(FromForm)]
pub struct CwInput<'r> {
pub struct CwInput {
pid: i32,
#[field(validate = len(0..33))]
cw: &'r str,
cw: String,
}
fn p2output(p: &Post, user: &CurrentUser, conn: &DbConn) -> PostOutput {
async fn p2output(p: &Post, user: &CurrentUser, db: &Db, rconn: RdsConn) -> PostOutput {
PostOutput {
pid: p.id,
text: p.content.to_string(),
text: format!("{}{}", if p.is_tmp { "[tmp]\n" } else { "" }, p.content),
cw: if p.cw.len() > 0 {
Some(p.cw.to_string())
} else {
None
},
n_likes: p.n_likes,
n_attentions: p.n_attentions,
n_comments: p.n_comments,
create_time: p.create_time,
last_comment_time: p.last_comment_time,
@@ -67,6 +68,7 @@ fn p2output(p: &Post, user: &CurrentUser, conn: &DbConn) -> PostOutput {
} else {
None
},
is_tmp: p.is_tmp,
is_reported: if user.is_admin {
Some(p.is_reported)
} else {
@@ -76,34 +78,59 @@ fn p2output(p: &Post, user: &CurrentUser, conn: &DbConn) -> PostOutput {
None
} else {
// 单个洞还有查询评论的接口,这里挂了不用报错
Some(c2output(p, &p.get_comments(conn).unwrap_or(vec![]), user))
let pid = p.id;
if let Some(cs) = Comment::gets_by_post_id(db, pid).await.ok() {
Some(c2output(p, &cs, user))
} else {
None
}
},
can_del: user.is_admin || p.author_hash == user.namehash,
attention: Attention::init(&user.namehash, rconn.clone())
.has(p.id)
.await
.unwrap_or_default(),
// for old version frontend
timestamp: p.create_time.timestamp(),
likenum: p.n_likes,
likenum: p.n_attentions,
reply: p.n_comments,
}
}
pub async fn ps2outputs(
ps: &Vec<Post>,
user: &CurrentUser,
db: &Db,
rconn: RdsConn,
) -> Vec<PostOutput> {
future::join_all(
ps.iter()
.map(|p| async { p2output(p, &user, &db, rconn.clone()).await }),
)
.await
}
#[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)?;
pub async fn get_one(pid: i32, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
let p = Post::get(&db, pid).await.m()?;
p.check_permission(&user, "ro")?;
Ok(json!({
"data": p2output(&p, &user, &conn),
"data": p2output(&p, &user,&db, rconn).await,
"code": 0,
}))
}
#[get("/getlist?<p>&<order_mode>")]
pub fn get_list(p: Option<u32>, order_mode: u8, user: CurrentUser, conn: DbConn) -> API<Value> {
pub async fn get_list(
p: Option<u32>,
order_mode: u8,
user: CurrentUser,
db: Db,
rconn: RdsConn,
) -> JsonAPI {
let page = p.unwrap_or(1);
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))
.collect::<Vec<PostOutput>>();
let ps = Post::gets_by_page(&db, order_mode, page, 25).await.m()?;
let ps_data = ps2outputs(&ps, &user, &db, rconn.clone()).await;
Ok(json!({
"data": ps_data,
"count": ps_data.len(),
@@ -112,19 +139,22 @@ pub fn get_list(p: Option<u32>, order_mode: u8, user: CurrentUser, conn: DbConn)
}
#[post("/dopost", data = "<poi>")]
pub fn publish_post(poi: Form<PostInput>, user: CurrentUser, conn: DbConn) -> API<Value> {
dbg!(poi.use_title, poi.allow_search);
pub async fn publish_post(poi: Form<PostInput>, user: CurrentUser, db: Db) -> JsonAPI {
let r = Post::create(
&conn,
&db,
NewPost {
content: &poi.text,
cw: &poi.cw,
author_hash: &user.namehash,
author_title: "",
content: poi.text.to_string(),
cw: poi.cw.to_string(),
author_hash: user.namehash.to_string(),
author_title: "".to_string(),
is_tmp: user.id.is_none(),
n_attentions: 1,
allow_search: poi.allow_search.is_some(),
},
)
.map_err(APIError::from_db)?;
.await
.m()?;
// TODO: attention
Ok(json!({
"data": r,
"code": 0
@@ -132,12 +162,23 @@ pub fn publish_post(poi: Form<PostInput>, user: CurrentUser, conn: DbConn) -> AP
}
#[post("/editcw", data = "<cwi>")]
pub fn edit_cw(cwi: Form<CwInput>, user: CurrentUser, conn: DbConn) -> API<Value> {
let p = Post::get(&conn, cwi.pid).map_err(APIError::from_db)?;
pub async fn edit_cw(cwi: Form<CwInput>, user: CurrentUser, db: Db) -> JsonAPI {
let p = Post::get(&db, cwi.pid).await.m()?;
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);
_ = p.update_cw(&db, cwi.cw.to_string()).await.m()?;
Ok(json!({"code": 0}))
}
#[get("/getmulti?<pids>")]
pub async fn get_multi(pids: Vec<i32>, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
let ps = Post::get_multi(&db, pids).await.m()?;
let ps_data = ps2outputs(&ps, &user, &db, rconn.clone()).await;
Ok(json!({
"code": 0,
"data": ps_data,
}))
}

View File

@@ -2,10 +2,10 @@ use crate::api::{CurrentUser, API};
use crate::random_hasher::RandomHasher;
use rocket::serde::json::{json, Value};
use rocket::State;
use crate::db_conn::DbConn;
use crate::db_conn::Db;
#[get("/systemlog")]
pub fn get_systemlog(user: CurrentUser, rh: &State<RandomHasher>, conn: DbConn) -> API<Value> {
pub async fn get_systemlog(user: CurrentUser, rh: &State<RandomHasher>, db: Db) -> API<Value> {
Ok(json!({
"tmp_token": rh.get_tmp_token(),
"salt": look!(rh.salt),