You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
504 lines
15 KiB
504 lines
15 KiB
// #![allow(clippy::all)] |
|
|
|
use crate::cache::*; |
|
use crate::db_conn::{Conn, Db}; |
|
use crate::random_hasher::random_string; |
|
use crate::rds_conn::RdsConn; |
|
use crate::schema::*; |
|
use chrono::{offset::Utc, DateTime}; |
|
use diesel::sql_types::*; |
|
use diesel::{ |
|
insert_into, BoolExpressionMethods, ExpressionMethods, QueryDsl, QueryResult, RunQueryDsl, |
|
TextExpressionMethods, |
|
}; |
|
use rocket::futures::{future, join}; |
|
use rocket::serde::{Deserialize, Serialize}; |
|
use sha2::{Digest, Sha256}; |
|
use std::collections::HashMap; |
|
|
|
sql_function!(fn random()); |
|
sql_function!(fn floor(x: Float) -> Int4); |
|
sql_function!(fn float4(x: Int4) -> Float); |
|
|
|
macro_rules! _get { |
|
($table:ident) => { |
|
async fn _get(db: &Db, id: i32) -> QueryResult<Self> { |
|
let pid = id; |
|
db.run(move |c| $table::table.find(pid).first(with_log!((c)))) |
|
.await |
|
} |
|
}; |
|
} |
|
|
|
macro_rules! _get_multi { |
|
($table:ident) => { |
|
async fn _get_multi(db: &Db, ids: Vec<i32>) -> QueryResult<Vec<Self>> { |
|
if ids.is_empty() { |
|
return Ok(vec![]); |
|
} |
|
// eq(any()) is only for postgres |
|
db.run(move |c| { |
|
$table::table |
|
.filter($table::id.eq_any(ids)) |
|
.filter($table::is_deleted.eq(false)) |
|
.load(with_log!(c)) |
|
}) |
|
.await |
|
} |
|
}; |
|
} |
|
|
|
macro_rules! op_to_col_expr { |
|
($col_obj:expr, to $v:expr) => { |
|
$v |
|
}; |
|
($col_obj:expr, add $v:expr) => { |
|
$col_obj + $v |
|
}; |
|
} |
|
|
|
macro_rules! update { |
|
($obj:expr, $table:ident, $db:expr, $({ $col:ident, $op:ident $v:expr }), + ) => {{ |
|
use crate::schema; |
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; |
|
let id = $obj.id; |
|
$obj = $db |
|
.run(move |c| { |
|
diesel::update(schema::$table::table.find(id)) |
|
.set(( |
|
$(schema::$table::$col.eq(op_to_col_expr!(schema::$table::$col, $op $v))), + |
|
)) |
|
.get_result(with_log!(c)) |
|
}) |
|
.await?; |
|
|
|
}}; |
|
} |
|
|
|
macro_rules! base_query { |
|
($table:ident) => { |
|
$table::table |
|
.into_boxed() |
|
.filter($table::is_deleted.eq(false)) |
|
}; |
|
} |
|
|
|
// TODO: log sql query |
|
macro_rules! with_log { |
|
($c: expr) => { |
|
$c |
|
}; |
|
} |
|
|
|
#[derive(Queryable, Insertable, Serialize, Deserialize, Debug)] |
|
#[serde(crate = "rocket::serde")] |
|
pub struct Comment { |
|
pub id: i32, |
|
pub author_hash: String, |
|
pub author_title: String, |
|
pub is_tmp: bool, |
|
pub content: String, |
|
pub create_time: DateTime<Utc>, |
|
pub is_deleted: bool, |
|
pub allow_search: bool, |
|
pub post_id: i32, |
|
} |
|
|
|
#[derive(Queryable, Insertable, Serialize, Deserialize, Debug)] |
|
#[serde(crate = "rocket::serde")] |
|
pub struct Post { |
|
pub id: i32, |
|
pub author_hash: String, |
|
pub content: String, |
|
pub cw: String, |
|
pub author_title: String, |
|
pub is_tmp: bool, |
|
pub n_attentions: i32, |
|
pub n_comments: i32, |
|
pub create_time: DateTime<Utc>, |
|
pub last_comment_time: DateTime<Utc>, |
|
pub is_deleted: bool, |
|
pub is_reported: bool, |
|
pub hot_score: i32, |
|
pub allow_search: bool, |
|
pub room_id: i32, |
|
pub up_votes: i32, |
|
pub down_votes: i32, |
|
} |
|
|
|
#[derive(Queryable, Insertable, Serialize, Deserialize, Debug)] |
|
#[serde(crate = "rocket::serde")] |
|
pub struct User { |
|
pub id: i32, |
|
pub name: String, |
|
pub token: String, |
|
pub is_admin: bool, |
|
} |
|
|
|
#[derive(Insertable)] |
|
#[diesel(table_name = posts)] |
|
pub struct NewPost { |
|
pub content: String, |
|
pub cw: String, |
|
pub author_hash: String, |
|
pub author_title: String, |
|
pub is_tmp: bool, |
|
pub n_attentions: i32, |
|
pub allow_search: bool, |
|
pub room_id: i32, |
|
} |
|
|
|
impl Post { |
|
_get!(posts); |
|
|
|
_get_multi!(posts); |
|
|
|
pub async fn get_multi(db: &Db, rconn: &RdsConn, ids: &[i32]) -> QueryResult<Vec<Self>> { |
|
let mut cacher = PostCache::init(rconn); |
|
let mut cached_posts = cacher.gets(ids).await; |
|
let mut id2po = HashMap::<i32, &mut Option<Post>>::new(); |
|
|
|
// dbg!(&cached_posts); |
|
|
|
let missing_ids = ids |
|
.iter() |
|
.zip(cached_posts.iter_mut()) |
|
.filter_map(|(pid, p)| match p { |
|
None => { |
|
id2po.insert(*pid, p); |
|
Some(pid) |
|
} |
|
_ => None, |
|
}) |
|
.copied() |
|
.collect(); |
|
|
|
// dbg!(&missing_ids); |
|
let missing_ps = Self::_get_multi(db, missing_ids).await?; |
|
// dbg!(&missing_ps); |
|
|
|
cacher.sets(&missing_ps.iter().collect::<Vec<_>>()).await; |
|
|
|
for p in missing_ps.into_iter() { |
|
if let Some(op) = id2po.get_mut(&p.id) { |
|
**op = Some(p); |
|
} |
|
} |
|
// dbg!(&cached_posts); |
|
Ok(cached_posts |
|
.into_iter() |
|
.filter_map(|p| p.filter(|p| !p.is_deleted)) |
|
.collect()) |
|
} |
|
|
|
pub async fn get(db: &Db, rconn: &RdsConn, id: i32) -> QueryResult<Self> { |
|
// 注意即使is_deleted也应该缓存和返回 |
|
let mut cacher = PostCache::init(rconn); |
|
if let Some(p) = cacher.get(&id).await { |
|
Ok(p) |
|
} else { |
|
let p = Self::_get(db, id).await?; |
|
cacher.sets(&[&p]).await; |
|
Ok(p) |
|
} |
|
} |
|
|
|
pub async fn get_comments(&self, db: &Db, rconn: &RdsConn) -> QueryResult<Vec<Comment>> { |
|
let mut cacher = PostCommentCache::init(self.id, rconn); |
|
if let Some(cs) = cacher.get().await { |
|
Ok(cs) |
|
} else { |
|
let cs = Comment::gets_by_post_id(db, self.id).await?; |
|
cacher.set(&cs).await; |
|
Ok(cs) |
|
} |
|
} |
|
|
|
pub async fn clear_comments_cache(&self, rconn: &RdsConn) { |
|
PostCommentCache::init(self.id, rconn).clear().await; |
|
} |
|
|
|
pub async fn gets_by_page( |
|
db: &Db, |
|
rconn: &RdsConn, |
|
room_id: Option<i32>, |
|
order_mode: u8, |
|
start: i64, |
|
limit: i64, |
|
) -> QueryResult<Vec<Self>> { |
|
let mut cacher = PostListCache::init(room_id, order_mode, rconn); |
|
if cacher.need_fill().await { |
|
let pids = |
|
Self::_get_ids_by_page(db, room_id, order_mode, 0, cacher.i64_minlen()).await?; |
|
let ps = Self::get_multi(db, rconn, &pids).await?; |
|
cacher.fill(&ps).await; |
|
} |
|
let pids = if start + limit > cacher.i64_len() { |
|
Self::_get_ids_by_page(db, room_id, order_mode, start, limit).await? |
|
} else { |
|
cacher.get_pids(start, limit).await |
|
}; |
|
|
|
Self::get_multi(db, rconn, &pids).await |
|
} |
|
async fn _get_ids_by_page( |
|
db: &Db, |
|
room_id: Option<i32>, |
|
order_mode: u8, |
|
start: i64, |
|
limit: i64, |
|
) -> QueryResult<Vec<i32>> { |
|
db.run(move |c| { |
|
let mut query = base_query!(posts).select(posts::id); |
|
if order_mode > 0 { |
|
query = query.filter(posts::is_reported.eq(false)); |
|
} |
|
|
|
if order_mode == 1 { |
|
query = query.filter(posts::n_comments.gt(0)); |
|
} |
|
|
|
if let Some(ri) = room_id { |
|
query = query.filter(posts::room_id.eq(ri)); |
|
} |
|
|
|
query = match order_mode { |
|
0 => query.order(posts::id.desc()), |
|
1 => query.order(posts::last_comment_time.desc()), |
|
2 => query.order(posts::hot_score.desc()), |
|
3 => query.order(random()), |
|
4 => query.order(posts::n_attentions.desc()), |
|
_ => panic!("Wrong order mode!"), |
|
}; |
|
|
|
query.offset(start).limit(limit).load(with_log!(c)) |
|
}) |
|
.await |
|
} |
|
|
|
pub async fn search( |
|
db: &Db, |
|
rconn: &RdsConn, |
|
room_id: Option<i32>, |
|
search_mode: u8, |
|
search_text: String, |
|
start: i64, |
|
limit: i64, |
|
) -> QueryResult<Vec<Self>> { |
|
let search_text2 = search_text.replace('%', "\\%"); |
|
let pids = db |
|
.run(move |c| { |
|
let pat; |
|
let mut query = base_query!(posts) |
|
.select(posts::id) |
|
.distinct() |
|
.left_join(comments::table) |
|
.filter(posts::is_reported.eq(false)); |
|
if let Some(ri) = room_id { |
|
query = query.filter(posts::room_id.eq(ri)); |
|
} |
|
// 先用搜索+缓存,性能有问题了再真的做tag表 |
|
query = match search_mode { |
|
0 => { |
|
pat = format!("%#{}%", &search_text2); |
|
query.filter( |
|
posts::cw |
|
.eq(&search_text) |
|
.or(posts::cw.eq(format!("#{}", &search_text))) |
|
.or(posts::content.like(&pat)) |
|
.or(comments::content |
|
.like(&pat) |
|
.and(comments::is_deleted.eq(false))), |
|
) |
|
} |
|
1 => { |
|
pat = format!("%{}%", search_text2.replace(' ', "%")); |
|
query |
|
.filter( |
|
posts::content.like(&pat).or(comments::content |
|
.like(&pat) |
|
.and(comments::is_deleted.eq(false))), |
|
) |
|
.filter(posts::allow_search.eq(true)) |
|
} |
|
2 => query.filter( |
|
posts::author_title |
|
.eq(&search_text) |
|
.or(comments::author_title.eq(&search_text)), |
|
), |
|
_ => panic!("Wrong search mode!"), |
|
}; |
|
|
|
query |
|
.order(posts::id.desc()) |
|
.offset(start) |
|
.limit(limit) |
|
.load(with_log!(c)) |
|
}) |
|
.await?; |
|
Self::get_multi(db, rconn, &pids).await |
|
} |
|
|
|
pub async fn create(db: &Db, new_post: NewPost) -> QueryResult<Self> { |
|
db.run(move |c| { |
|
insert_into(posts::table) |
|
.values(&new_post) |
|
.get_result(with_log!(c)) |
|
}) |
|
.await |
|
} |
|
|
|
pub async fn set_instance_cache(&self, rconn: &RdsConn) { |
|
PostCache::init(rconn).sets(&[self]).await; |
|
} |
|
pub async fn refresh_cache(&self, rconn: &RdsConn, is_new: bool) { |
|
join!( |
|
self.set_instance_cache(rconn), |
|
future::join_all((if is_new { [0, 2, 3, 4] } else { [1, 2, 3, 4] }).map( |
|
|mode| async move { |
|
PostListCache::init(None, mode, &rconn.clone()) |
|
.put(self) |
|
.await; |
|
PostListCache::init(Some(self.room_id), mode, &rconn.clone()) |
|
.put(self) |
|
.await; |
|
} |
|
)), |
|
); |
|
} |
|
|
|
pub async fn annealing(c: &mut Conn, rconn: &mut RdsConn) { |
|
info!("Time for annealing!"); |
|
diesel::update(posts::table.filter(posts::hot_score.gt(10))) |
|
.set(posts::hot_score.eq(floor(float4(posts::hot_score) * 0.9))) |
|
.execute(with_log!(c)) |
|
.unwrap(); |
|
|
|
PostCache::clear_all(rconn).await; |
|
for room_id in (0..5).map(Some).chain([None, Some(42)]) { |
|
PostListCache::init(room_id, 2, rconn).clear().await; |
|
} |
|
} |
|
} |
|
|
|
impl User { |
|
async fn _get_by_token(db: &Db, token: &str) -> Option<Self> { |
|
let token = token.to_string(); |
|
db.run(move |c| { |
|
users::table |
|
.filter(users::token.eq(token)) |
|
.first(with_log!(c)) |
|
}) |
|
.await |
|
.ok() |
|
} |
|
|
|
pub async fn get_by_token(db: &Db, rconn: &RdsConn, token: &str) -> Option<Self> { |
|
let real_token; |
|
|
|
let token = match &token.split(':').collect::<Vec<&str>>()[..] { |
|
["sha256", tk] => { |
|
let mut h = Sha256::new(); |
|
h.update(tk); |
|
h.update("hole"); |
|
real_token = format!("{:x}", h.finalize())[0..16].to_string(); |
|
&real_token |
|
} |
|
_ => token, |
|
}; |
|
// dbg!(token); |
|
let mut cacher = UserCache::init(token, rconn); |
|
if let Some(u) = cacher.get().await { |
|
Some(u) |
|
} else { |
|
let u = Self::_get_by_token(db, token).await?; |
|
cacher.set(&u).await; |
|
Some(u) |
|
} |
|
} |
|
|
|
pub async fn find_or_create_token( |
|
db: &Db, |
|
name: &str, |
|
force_refresh: bool, |
|
) -> QueryResult<String> { |
|
let name = name.to_string(); |
|
db.run(move |c| { |
|
if let Some(u) = { |
|
if force_refresh { |
|
None |
|
} else { |
|
users::table |
|
.filter(users::name.eq(&name)) |
|
.first::<Self>(with_log!(c)) |
|
.ok() |
|
} |
|
} { |
|
Ok(u.token) |
|
} else { |
|
let token = random_string(16); |
|
diesel::insert_into(users::table) |
|
.values((users::name.eq(&name), users::token.eq(&token))) |
|
.on_conflict(users::name) |
|
.do_update() |
|
.set(users::token.eq(&token)) |
|
.execute(with_log!(c))?; |
|
Ok(token) |
|
} |
|
}) |
|
.await |
|
} |
|
|
|
pub async fn clear_non_admin_users(c: &mut Conn, rconn: &mut RdsConn) { |
|
diesel::delete(users::table.filter(users::is_admin.eq(false))) |
|
.execute(c) |
|
.unwrap(); |
|
UserCache::clear_all(rconn).await; |
|
} |
|
|
|
pub async fn get_count(db: &Db) -> QueryResult<i64> { |
|
db.run(move |c| users::table.count().get_result(with_log!(c))) |
|
.await |
|
} |
|
} |
|
|
|
#[derive(Insertable)] |
|
#[diesel(table_name = comments)] |
|
pub struct NewComment { |
|
pub content: String, |
|
pub author_hash: String, |
|
pub author_title: String, |
|
pub is_tmp: bool, |
|
pub post_id: i32, |
|
} |
|
|
|
impl Comment { |
|
_get!(comments); |
|
|
|
pub async fn get(db: &Db, id: i32) -> QueryResult<Self> { |
|
// no cache for single comment |
|
Self::_get(db, id).await |
|
} |
|
|
|
pub async fn create(db: &Db, new_comment: NewComment) -> QueryResult<Self> { |
|
db.run(move |c| { |
|
insert_into(comments::table) |
|
.values(&new_comment) |
|
.get_result(with_log!(c)) |
|
}) |
|
.await |
|
} |
|
|
|
pub async fn gets_by_post_id(db: &Db, post_id: i32) -> QueryResult<Vec<Self>> { |
|
let pid = post_id; |
|
db.run(move |c| { |
|
comments::table |
|
.filter(comments::post_id.eq(pid)) |
|
.order(comments::id) |
|
.load(with_log!(c)) |
|
}) |
|
.await |
|
} |
|
} |
|
|
|
pub(crate) use {op_to_col_expr, update, with_log};
|
|
|