20 Commits

Author SHA1 Message Date
7c024bfd6b feat: make mastodon login a optional feature 2022-03-30 01:51:16 +08:00
dd3c208fe1 feat: tmp user only write, no read 2022-03-27 23:52:59 +08:00
4ed92568d8 v1.0.0 2022-03-27 22:54:11 +08:00
b57e1a9233 update README 2022-03-27 22:26:37 +08:00
d6eaec9111 feat: cs login & add vote api file 2022-03-27 21:45:15 +08:00
ffd30f3c03 feat: add to dangers users when banned 2022-03-27 14:42:02 +08:00
8ffbd6fa31 fix: filte reported in for search 2022-03-27 14:38:22 +08:00
caec3e1930 fix: attention list order 2022-03-27 14:34:24 +08:00
79e9a492a7 feat: poll 2022-03-27 04:52:46 +08:00
f5b0dbdbac feat: set and user custom title 2022-03-27 02:52:13 +08:00
5bef37cd62 feat: block and dangerous users 2022-03-27 01:35:46 +08:00
b2cf8475c5 opt: use macro for sql updates, and merge updates 2022-03-26 21:54:50 +08:00
cbf933e74f feat: report 2022-03-26 19:08:51 +08:00
fb9648456a feat: ban user 2022-03-26 16:44:28 +08:00
ce3379c5ae feat: record and show systemlog, admin delete log 2022-03-26 02:49:18 +08:00
59714bfbef feat: annealing for hot score & clean cache 2022-03-26 01:04:22 +08:00
38bacc1ee0 feat: cache for posts list 2022-03-25 03:00:38 +08:00
9dbb02a838 style 2022-03-25 00:01:12 +08:00
c8b8ad0787 feat: post comments cache & use cache everywhere 2022-03-24 23:33:32 +08:00
cc32c318a7 fix migdb.py 2022-03-24 21:39:40 +08:00
19 changed files with 1392 additions and 360 deletions

10
.env.sample Normal file
View File

@@ -0,0 +1,10 @@
MAST_BASE_URL="https://thu.closed.social/"
MAST_CLIENT="<your client id>"
MAST_SECRET="<your client key>"
MAST_SCOPE="read:accounts"
DATABASE_URL="postgres://hole:hole_pass@localhost/hole_v2"
MIGRATION_DIRECTORY=migrations/postgres
REDIS_URL="redis://127.0.0.1:6379"
ROCKET_DATABASES='{pg_v2={url="postgres://hole:hole_pass@localhost/hole_v2"}}'
RUST_LOG=debug

View File

@@ -6,18 +6,22 @@ license = "AGPL-3.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
mastlogin = ["url", "reqwest"]
[dependencies] [dependencies]
rocket = { version = "0.5.0-rc.1", features = ["json"] } rocket = { version = "0.5.0-rc.1", features = ["json"] }
rocket_sync_db_pools = { version = "0.1.0-cr.1", features = ["diesel_postgres_pool"] }
diesel = { version = "1.4.8", features = ["postgres", "chrono"] } diesel = { version = "1.4.8", features = ["postgres", "chrono"] }
diesel_migrations = "1.4.0" diesel_migrations = "1.4.0"
tokio = "1.17.0"
redis = { version="0.21.5", features = ["aio", "tokio-comp"] } redis = { version="0.21.5", features = ["aio", "tokio-comp"] }
chrono = { version="0.*", features =["serde"] } chrono = { version="0.4.19", features = ["serde"] }
rand = "0.*" rand = "0.8.5"
dotenv = "0.*" dotenv = "0.15.0"
sha2 = "0.*" sha2 = "0.10.2"
log = "0.4.16" log = "0.4.16"
env_logger = "0.9.0" env_logger = "0.9.0"
[dependencies.rocket_sync_db_pools] url = { version="2.2.2",optional = true }
version = "0.1.0-rc.1" reqwest = { version = "0.11.10", features = ["json"], optional = true }
features = ["diesel_postgres_pool"]

View File

@@ -1,14 +1,26 @@
# hole-backend-rust # hole-backend-rust v1.1.0
## 部署 ## 部署
### prepare database *以下内容假设你使用 Ubuntu 20.04*
目前只支持postgresql对支持sqlite的追踪见 issue #1
安装postgresql (略)
安装redis (略)
### 准备数据库
进入:
``` ```
sudo -u postgres psql sudo -u postgres psql
``` ```
执行 (替换`'hole_pass'`为实际希望使用的密码):
```postgresql ```postgresql
postgres=# CREATE USER hole WITH PASSWORD 'hole_pass'; postgres=# CREATE USER hole WITH PASSWORD 'hole_pass';
CREATE ROLE CREATE ROLE
@@ -20,7 +32,39 @@ hole_v2=# CREATE EXTENSION pg_trgm;
CREATE EXTENSION CREATE EXTENSION
hole_v2=# \q hole_v2=# \q
``` ```
### 运行
创建 .env 文件,写入必要的环境变量。可参考 .env.sample。
#### 基于二进制文件
从[release](https://git.thu.monster/newthuhole/hole-backend-rust/releases)直接下载二进制文件
``` ```
./hole-thu --init-database ./hole-thu --init-database
./hole-thu
``` ```
#### 基于源码
安装rust与cargo环境 (略)
clone 代码 (略)
```
cargo run --release -- --init-database
cargo run --release
```
或安装`diesel_cli`
```
diesel migration run
cargo run --release
```
### 关于账号系统
+ 如果你希望使用自己的登录系统,将 `/_login/` 路径交由另外的后端处理只需最终将用户名和token写入users表并跳转到 `/?token=<token>`
+ 如果你希望也使用闭社提供的授权来维护账号系统,使用 `https://thu.closed.social/api/v1/apps` 接口创建应用,并在.env或环境变量中填入client与secret。此操作不需要闭社账号。详情见[文档](https://docs.joinmastodon.org/client/token/#app)。编译运行时,增加`--features mastlogin`: `cargo run --release --features mastlogin`

View File

@@ -1,11 +1,14 @@
use crate::api::post::ps2outputs; use crate::api::post::ps2outputs;
use crate::api::{APIError, CurrentUser, PolicyError::*, API, UGC}; use crate::api::{CurrentUser, JsonAPI, PolicyError::*, UGC};
use crate::db_conn::Db; use crate::db_conn::Db;
use crate::libs::diesel_logger::LoggingConnection;
use crate::models::*; use crate::models::*;
use crate::rds_conn::RdsConn; use crate::rds_conn::RdsConn;
use crate::rds_models::*; use crate::rds_models::*;
use crate::schema;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::form::Form; use rocket::form::Form;
use rocket::serde::json::{json, Value}; use rocket::serde::json::json;
#[derive(FromForm)] #[derive(FromForm)]
pub struct AttentionInput { pub struct AttentionInput {
@@ -20,13 +23,15 @@ pub async fn attention_post(
user: CurrentUser, user: CurrentUser,
db: Db, db: Db,
rconn: RdsConn, rconn: RdsConn,
) -> API<Value> { ) -> JsonAPI {
user.id.ok_or_else(|| APIError::PcError(NotAllowed))?; // 临时用户不允许手动关注
let mut p = Post::get(&db, ai.pid).await?; user.id.ok_or_else(|| YouAreTmp)?;
let mut p = Post::get(&db, &rconn, ai.pid).await?;
p.check_permission(&user, "r")?; p.check_permission(&user, "r")?;
let mut att = Attention::init(&user.namehash, &rconn); let mut att = Attention::init(&user.namehash, &rconn);
let switch_to = ai.switch == 1; let switch_to = ai.switch == 1;
let mut delta: i32 = 0; let delta: i32;
if att.has(ai.pid).await? != switch_to { if att.has(ai.pid).await? != switch_to {
if switch_to { if switch_to {
att.add(ai.pid).await?; att.add(ai.pid).await?;
@@ -35,28 +40,34 @@ pub async fn attention_post(
att.remove(ai.pid).await?; att.remove(ai.pid).await?;
delta = -1; delta = -1;
} }
p = p.change_n_attentions(&db, delta).await?; update!(
p = p.change_hot_score(&db, delta * 2).await?; p,
posts,
&db,
{ n_attentions, add delta },
{ hot_score, add delta * 2 }
);
if switch_to && user.is_admin {
update!(p, posts, &db, { is_reported, to false });
}
p.refresh_cache(&rconn, false).await; p.refresh_cache(&rconn, false).await;
} }
Ok(json!({ Ok(json!({
"code": 0, "code": 0,
"attention": ai.switch == 1, "attention": ai.switch == 1,
"n_attentions": p.n_attentions + delta, "n_attentions": p.n_attentions,
// for old version frontend // for old version frontend
"likenum": p.n_attentions + delta, "likenum": p.n_attentions,
})) }))
} }
#[get("/getattention")] #[get("/getattention")]
pub async fn get_attention(user: CurrentUser, db: Db, rconn: RdsConn) -> API<Value> { pub async fn get_attention(user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
let ids = Attention::init(&user.namehash, &rconn).all().await?; let mut ids = Attention::init(&user.namehash, &rconn).all().await?;
let ps = Post::get_multi(&db, ids).await?; ids.sort_by_key(|x| -x);
let ps = Post::get_multi(&db, &rconn, &ids).await?;
let ps_data = ps2outputs(&ps, &user, &db, &rconn).await; let ps_data = ps2outputs(&ps, &user, &db, &rconn).await;
Ok(json!({ code0!(ps_data)
"code": 0,
"data": ps_data,
}))
} }

View File

@@ -1,21 +1,22 @@
use crate::api::{APIError, CurrentUser, PolicyError::*, API, UGC}; use crate::api::{APIError, CurrentUser, JsonAPI, PolicyError::*, UGC};
use crate::db_conn::Db; use crate::db_conn::Db;
use crate::libs::diesel_logger::LoggingConnection;
use crate::models::*; use crate::models::*;
use crate::rds_conn::RdsConn; use crate::rds_conn::RdsConn;
use crate::rds_models::*; use crate::rds_models::*;
use crate::schema;
use chrono::{offset::Utc, DateTime}; use chrono::{offset::Utc, DateTime};
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::form::Form; use rocket::form::Form;
use rocket::futures::{future::TryFutureExt, try_join}; use rocket::futures::future;
use rocket::serde::{ use rocket::futures::join;
json::{json, Value}, use rocket::serde::{json::json, Serialize};
Serialize,
};
use std::collections::HashMap; use std::collections::HashMap;
#[derive(FromForm)] #[derive(FromForm)]
pub struct CommentInput { pub struct CommentInput {
pid: i32, pid: i32,
#[field(validate = len(1..4097))] #[field(validate = len(1..12289))]
text: String, text: String,
use_title: Option<i8>, use_title: Option<i8>,
} }
@@ -30,54 +31,74 @@ pub struct CommentOutput {
name_id: i32, name_id: i32,
is_tmp: bool, is_tmp: bool,
create_time: DateTime<Utc>, create_time: DateTime<Utc>,
is_blocked: bool,
blocked_count: Option<i32>,
// for old version frontend // for old version frontend
timestamp: i64, timestamp: i64,
blocked: bool,
} }
pub fn c2output<'r>( pub async fn c2output<'r>(
p: &'r Post, p: &'r Post,
cs: &Vec<Comment>, cs: &Vec<Comment>,
user: &CurrentUser, user: &CurrentUser,
rconn: &RdsConn,
) -> Vec<CommentOutput> { ) -> Vec<CommentOutput> {
let mut hash2id = HashMap::<&String, i32>::from([(&p.author_hash, 0)]); let mut hash2id = HashMap::<&String, i32>::from([(&p.author_hash, 0)]);
cs.iter() let name_ids_iter = cs.iter().map(|c| match hash2id.get(&c.author_hash) {
.filter_map(|c| {
let name_id: i32 = match hash2id.get(&c.author_hash) {
Some(id) => *id, Some(id) => *id,
None => { None => {
let x = hash2id.len().try_into().unwrap(); let x = hash2id.len().try_into().unwrap();
hash2id.insert(&c.author_hash, x); hash2id.insert(&c.author_hash, x);
x x
} }
}; });
future::join_all(cs.iter().zip(name_ids_iter).map(|(c, name_id)| async move {
if c.is_deleted { if c.is_deleted {
// TODO: block
None None
} else { } else {
let is_blocked =
BlockedUsers::check_blocked(rconn, user.id, &user.namehash, &c.author_hash)
.await
.unwrap_or_default();
let can_view = !is_blocked && user.id.is_some() || user.namehash.eq(&c.author_hash);
Some(CommentOutput { Some(CommentOutput {
cid: c.id, cid: c.id,
text: format!("{}{}", if c.is_tmp { "[tmp]\n" } else { "" }, c.content), text: format!(
"{}{}",
if c.is_tmp { "[tmp]\n" } else { "" },
if can_view { &c.content } else { "" }
),
author_title: c.author_title.to_string(), author_title: c.author_title.to_string(),
can_del: c.check_permission(user, "wd").is_ok(), can_del: c.check_permission(user, "wd").is_ok(),
name_id: name_id, name_id: name_id,
is_tmp: c.is_tmp, is_tmp: c.is_tmp,
create_time: c.create_time, create_time: c.create_time,
is_blocked: is_blocked,
blocked_count: if user.is_admin {
BlockCounter::get_count(rconn, &c.author_hash).await.ok()
} else {
None
},
timestamp: c.create_time.timestamp(), timestamp: c.create_time.timestamp(),
blocked: is_blocked,
}) })
} }
}) }))
.await
.into_iter()
.filter_map(|x| x)
.collect() .collect()
} }
#[get("/getcomment?<pid>")] #[get("/getcomment?<pid>")]
pub async fn get_comment(pid: i32, user: CurrentUser, db: Db, rconn: RdsConn) -> API<Value> { pub async fn get_comment(pid: i32, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
let p = Post::get(&db, pid).await?; let p = Post::get(&db, &rconn, pid).await?;
if p.is_deleted { if p.is_deleted {
return Err(APIError::PcError(IsDeleted)); return Err(APIError::PcError(IsDeleted));
} }
let pid = p.id; let cs = p.get_comments(&db, &rconn).await?;
let cs = Comment::gets_by_post_id(&db, pid).await?; let data = c2output(&p, &cs, &user, &rconn).await;
let data = c2output(&p, &cs, &user);
Ok(json!({ Ok(json!({
"code": 0, "code": 0,
@@ -95,39 +116,52 @@ pub async fn add_comment(
user: CurrentUser, user: CurrentUser,
db: Db, db: Db,
rconn: RdsConn, rconn: RdsConn,
) -> API<Value> { ) -> JsonAPI {
let mut p = Post::get(&db, ci.pid).await?; let mut p = Post::get(&db, &rconn, ci.pid).await?;
Comment::create( let c = Comment::create(
&db, &db,
NewComment { NewComment {
content: ci.text.to_string(), content: ci.text.to_string(),
author_hash: user.namehash.to_string(), author_hash: user.namehash.to_string(),
author_title: "".to_string(), author_title: (if ci.use_title.is_some() {
CustomTitle::get(&rconn, &user.namehash).await?
} else {
None
})
.unwrap_or_default(),
is_tmp: user.id.is_none(), is_tmp: user.id.is_none(),
post_id: ci.pid, post_id: ci.pid,
}, },
) )
.await?; .await?;
p = p.change_n_comments(&db, 1).await?;
// auto attention after comment
let mut att = Attention::init(&user.namehash, &rconn);
let mut hs_delta = 1; let mut att = Attention::init(&user.namehash, &rconn);
let hs_delta;
let at_delta;
if !att.has(p.id).await? { if !att.has(p.id).await? {
hs_delta += 2; hs_delta = 3;
try_join!( at_delta = 1;
att.add(p.id).err_into::<APIError>(), att.add(p.id).await?;
async { } else {
p = p.change_n_attentions(&db, 1).await?; hs_delta = 1;
Ok::<(), APIError>(()) at_delta = 0;
}
.err_into::<APIError>(),
)?;
} }
p = p.change_hot_score(&db, hs_delta).await?; update!(
p.refresh_cache(&rconn, false).await; p,
posts,
&db,
{ n_comments, add 1 },
{ last_comment_time, to c.create_time },
{ n_attentions, add at_delta },
{ hot_score, add hs_delta }
);
join!(
p.refresh_cache(&rconn, false),
p.clear_comments_cache(&rconn),
);
Ok(json!({ Ok(json!({
"code": 0 "code": 0

View File

@@ -1,18 +1,40 @@
use crate::db_conn::Db; use crate::db_conn::Db;
use crate::libs::diesel_logger::LoggingConnection;
use crate::models::*; use crate::models::*;
use crate::random_hasher::RandomHasher; use crate::random_hasher::RandomHasher;
use crate::rds_conn::RdsConn; use crate::rds_conn::RdsConn;
use crate::rds_models::BannedUsers;
use crate::schema;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::http::Status; use rocket::http::Status;
use rocket::outcome::try_outcome; use rocket::outcome::try_outcome;
use rocket::request::{FromRequest, Outcome, Request}; use rocket::request::{FromRequest, Outcome, Request};
use rocket::response::{self, Responder}; use rocket::response::{self, Responder};
use rocket::serde::json::{json, Value}; use rocket::serde::json::{json, Value};
macro_rules! code0 {
() => (
Ok(json!({"code": 0}))
);
($data:expr) => (
Ok(json!({
"code": 0,
"data": $data,
}))
);
}
#[catch(401)] #[catch(401)]
pub fn catch_401_error() -> &'static str { pub fn catch_401_error() -> &'static str {
"未登录或token过期" "未登录或token过期"
} }
#[catch(403)]
pub fn catch_403_error() -> &'static str {
"可能被封禁了,等下次重置吧"
}
pub struct CurrentUser { pub struct CurrentUser {
id: Option<i32>, // tmp user has no id, only for block id: Option<i32>, // tmp user has no id, only for block
namehash: String, namehash: String,
@@ -25,34 +47,40 @@ impl<'r> FromRequest<'r> for CurrentUser {
type Error = (); type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let rh = request.rocket().state::<RandomHasher>().unwrap(); let rh = request.rocket().state::<RandomHasher>().unwrap();
let mut cu: Option<CurrentUser> = None; let rconn = try_outcome!(request.guard::<RdsConn>().await);
let mut id = None;
let mut namehash = None;
let mut is_admin = false;
if let Some(token) = request.headers().get_one("User-Token") { if let Some(token) = request.headers().get_one("User-Token") {
let sp = token.split('_').collect::<Vec<&str>>(); let sp = token.split('_').collect::<Vec<&str>>();
if sp.len() == 2 && sp[0] == rh.get_tmp_token() { if sp.len() == 2 && sp[0] == rh.get_tmp_token() {
let namehash = rh.hash_with_salt(sp[1]); namehash = Some(rh.hash_with_salt(sp[1]));
cu = Some(CurrentUser { id = None;
id: None, is_admin = false;
custom_title: format!("TODO: {}", &namehash),
namehash: namehash,
is_admin: false,
});
} else { } else {
let db = try_outcome!(request.guard::<Db>().await); let db = try_outcome!(request.guard::<Db>().await);
let rconn = try_outcome!(request.guard::<RdsConn>().await); if let Some(u) = User::get_by_token(&db, &rconn, token).await {
if let Some(user) = User::get_by_token_with_cache(&db, &rconn, token).await { id = Some(u.id);
let namehash = rh.hash_with_salt(&user.name); namehash = Some(rh.hash_with_salt(&u.name));
cu = Some(CurrentUser { is_admin = u.is_admin;
id: Some(user.id),
custom_title: format!("TODO: {}", &namehash),
namehash: namehash,
is_admin: user.is_admin,
});
} }
} }
} }
match cu { match namehash {
Some(u) => Outcome::Success(u), Some(nh) => {
if BannedUsers::has(&rconn, &nh).await.unwrap() {
Outcome::Failure((Status::Forbidden, ()))
} else {
Outcome::Success(CurrentUser {
id: id,
custom_title: format!("title todo: {}", &nh),
namehash: nh,
is_admin: is_admin,
})
}
}
None => Outcome::Failure((Status::Unauthorized, ())), None => Outcome::Failure((Status::Unauthorized, ())),
} }
} }
@@ -63,6 +91,8 @@ pub enum PolicyError {
IsReported, IsReported,
IsDeleted, IsDeleted,
NotAllowed, NotAllowed,
TitleUsed,
YouAreTmp,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -74,7 +104,6 @@ pub enum APIError {
impl<'r> Responder<'r, 'static> for APIError { impl<'r> Responder<'r, 'static> for APIError {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
dbg!(&self);
match self { match self {
APIError::DbError(e) => json!({ APIError::DbError(e) => json!({
"code": -1, "code": -1,
@@ -92,6 +121,8 @@ impl<'r> Responder<'r, 'static> for APIError {
PolicyError::IsReported => "内容被举报,处理中", PolicyError::IsReported => "内容被举报,处理中",
PolicyError::IsDeleted => "内容被删除", PolicyError::IsDeleted => "内容被删除",
PolicyError::NotAllowed => "不允许的操作", PolicyError::NotAllowed => "不允许的操作",
PolicyError::TitleUsed => "头衔已被使用",
PolicyError::YouAreTmp => "临时用户只可发布内容和进入单个洞"
} }
}) })
.respond_to(req), .respond_to(req),
@@ -111,6 +142,12 @@ impl From<redis::RedisError> for APIError {
} }
} }
impl From<PolicyError> for APIError {
fn from(err: PolicyError) -> APIError {
APIError::PcError(err)
}
}
pub type API<T> = Result<T, APIError>; pub type API<T> = Result<T, APIError>;
pub type JsonAPI = API<Value>; pub type JsonAPI = API<Value>;
@@ -120,7 +157,7 @@ pub trait UGC {
fn get_is_deleted(&self) -> bool; fn get_is_deleted(&self) -> bool;
fn get_is_reported(&self) -> bool; fn get_is_reported(&self) -> bool;
fn extra_delete_condition(&self) -> bool; fn extra_delete_condition(&self) -> bool;
async fn do_set_deleted(&self, db: &Db) -> API<usize>; async fn do_set_deleted(&mut self, db: &Db) -> API<()>;
fn check_permission(&self, user: &CurrentUser, mode: &str) -> API<()> { fn check_permission(&self, user: &CurrentUser, mode: &str) -> API<()> {
if user.is_admin { if user.is_admin {
return Ok(()); return Ok(());
@@ -140,10 +177,10 @@ pub trait UGC {
Ok(()) Ok(())
} }
async fn soft_delete(&self, user: &CurrentUser, db: &Db) -> API<()> { async fn soft_delete(&mut self, user: &CurrentUser, db: &Db) -> API<()> {
self.check_permission(user, "rwd")?; self.check_permission(user, "rwd")?;
let _ = self.do_set_deleted(db).await?; self.do_set_deleted(db).await?;
Ok(()) Ok(())
} }
} }
@@ -162,8 +199,9 @@ impl UGC for Post {
fn extra_delete_condition(&self) -> bool { fn extra_delete_condition(&self) -> bool {
self.n_comments == 0 self.n_comments == 0
} }
async fn do_set_deleted(&self, db: &Db) -> API<usize> { async fn do_set_deleted(&mut self, db: &Db) -> API<()> {
self.set_deleted(db).await.map_err(From::from) update!(*self, posts, db, { is_deleted, to true });
Ok(())
} }
} }
@@ -181,8 +219,9 @@ impl UGC for Comment {
fn extra_delete_condition(&self) -> bool { fn extra_delete_condition(&self) -> bool {
true true
} }
async fn do_set_deleted(&self, db: &Db) -> API<usize> { async fn do_set_deleted(&mut self, db: &Db) -> API<()> {
self.set_deleted(db).await.map_err(From::from) update!(*self, comments, db, { is_deleted, to true });
Ok(())
} }
} }
@@ -198,3 +237,4 @@ pub mod operation;
pub mod post; pub mod post;
pub mod search; pub mod search;
pub mod systemlog; pub mod systemlog;
pub mod vote;

View File

@@ -1,9 +1,14 @@
use crate::api::{APIError, CurrentUser, PolicyError::*, API, UGC}; use crate::api::{CurrentUser, JsonAPI, PolicyError::*, UGC};
use crate::db_conn::Db; use crate::db_conn::Db;
use crate::rds_conn::RdsConn; use crate::libs::diesel_logger::LoggingConnection;
use crate::models::*; use crate::models::*;
use crate::rds_conn::RdsConn;
use crate::rds_models::*;
use crate::schema;
use chrono::offset::Local;
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::form::Form; use rocket::form::Form;
use rocket::serde::json::{json, Value}; use rocket::serde::json::json;
#[derive(FromForm)] #[derive(FromForm)]
pub struct DeleteInput { pub struct DeleteInput {
@@ -14,24 +19,152 @@ pub struct DeleteInput {
} }
#[post("/delete", data = "<di>")] #[post("/delete", data = "<di>")]
pub async fn delete(di: Form<DeleteInput>, user: CurrentUser, db: Db, rconn: RdsConn) -> API<Value> { pub async fn delete(di: Form<DeleteInput>, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
match di.id_type.as_str() { let (author_hash, p) = match di.id_type.as_str() {
"cid" => { "cid" => {
let c = Comment::get(&db, di.id).await?; let mut c = Comment::get(&db, di.id).await?;
c.soft_delete(&user, &db).await?; c.soft_delete(&user, &db).await?;
let mut p = Post::get(&db, c.post_id).await?; let mut p = Post::get(&db, &rconn, c.post_id).await?;
p = p.change_n_comments(&db, -1).await?; update!(
p = p.change_hot_score(&db, -2).await?; p,
posts,
&db,
{ n_comments, add -1 },
{ hot_score, add -1 }
);
p.refresh_cache(&rconn, false).await; p.refresh_cache(&rconn, false).await;
p.clear_comments_cache(&rconn).await;
(c.author_hash.clone(), p)
} }
"pid" => { "pid" => {
let p = Post::get(&db, di.id).await?; let mut p = Post::get(&db, &rconn, di.id).await?;
p.soft_delete(&user, &db).await?; p.soft_delete(&user, &db).await?;
// 如果是删除需要也从0号缓存队列中去掉
p.refresh_cache(&rconn, true).await;
(p.author_hash.clone(), p)
} }
_ => return Err(APIError::PcError(NotAllowed)), _ => Err(NotAllowed)?,
};
if user.is_admin && !user.namehash.eq(&author_hash) {
Systemlog {
user_hash: user.namehash.clone(),
action_type: LogType::AdminDelete,
target: format!("#{}, {}={}", p.id, di.id_type, di.id),
detail: di.note.clone(),
time: Local::now(),
}
.create(&rconn)
.await?;
if di.note.starts_with("!ban ") {
Systemlog {
user_hash: user.namehash.clone(),
action_type: LogType::Ban,
target: look!(author_hash),
detail: di.note.clone(),
time: Local::now(),
}
.create(&rconn)
.await?;
BannedUsers::add(&rconn, &author_hash).await?;
DangerousUser::add(&rconn, &author_hash).await?;
}
}
code0!()
}
#[derive(FromForm)]
pub struct ReportInput {
pid: i32,
#[field(validate = len(0..1000))]
reason: String,
}
#[post("/report", data = "<ri>")]
pub async fn report(ri: Form<ReportInput>, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
// 临时用户不允许举报
user.id.ok_or_else(|| NotAllowed)?;
let mut p = Post::get(&db, &rconn, ri.pid).await?;
update!(p, posts, &db, { is_reported, to true });
p.refresh_cache(&rconn, false).await;
Systemlog {
user_hash: user.namehash,
action_type: LogType::Report,
target: format!(
"#{} {}",
ri.pid,
if ri.reason.starts_with("评论区") {
"评论区"
} else {
""
}
),
detail: ri.reason.clone(),
time: Local::now(),
}
.create(&rconn)
.await?;
code0!()
}
#[derive(FromForm)]
pub struct BlockInput {
#[field(name = "type")]
content_type: String,
id: i32,
}
#[post("/block", data = "<bi>")]
pub async fn block(bi: Form<BlockInput>, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
user.id.ok_or_else(|| NotAllowed)?;
let mut blk = BlockedUsers::init(user.id.ok_or_else(|| NotAllowed)?, &rconn);
let nh_to_block = match bi.content_type.as_str() {
"post" => Post::get(&db, &rconn, bi.id).await?.author_hash,
"comment" => Comment::get(&db, bi.id).await?.author_hash,
_ => Err(NotAllowed)?,
};
if nh_to_block.eq(&user.namehash) {
Err(NotAllowed)?;
}
blk.add(&nh_to_block).await?;
let curr = BlockCounter::count_incr(&rconn, &nh_to_block).await?;
if curr >= BLOCK_THRESHOLD || user.is_admin {
DangerousUser::add(&rconn, &nh_to_block).await?;
} }
Ok(json!({ Ok(json!({
"code": 0 "code": 0,
"data": {
"curr": curr,
"threshold": BLOCK_THRESHOLD,
},
})) }))
} }
#[derive(FromForm)]
pub struct TitleInput {
#[field(validate = len(1..31))]
title: String,
}
#[post("/title", data = "<ti>")]
pub async fn set_title(ti: Form<TitleInput>, user: CurrentUser, rconn: RdsConn) -> JsonAPI {
if CustomTitle::set(&rconn, &user.namehash, &ti.title).await? {
code0!()
} else {
Err(TitleUsed)?
}
}

View File

@@ -1,22 +1,31 @@
use crate::api::comment::{c2output, CommentOutput}; use crate::api::comment::{c2output, CommentOutput};
use crate::api::{APIError, CurrentUser, JsonAPI, PolicyError::*, UGC}; use crate::api::vote::get_poll_dict;
use crate::api::{CurrentUser, JsonAPI, UGC, PolicyError::*};
use crate::db_conn::Db; use crate::db_conn::Db;
use crate::libs::diesel_logger::LoggingConnection;
use crate::models::*; use crate::models::*;
use crate::rds_conn::RdsConn; use crate::rds_conn::RdsConn;
use crate::rds_models::*; use crate::rds_models::*;
use crate::schema;
use chrono::{offset::Utc, DateTime}; use chrono::{offset::Utc, DateTime};
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
use rocket::form::Form; use rocket::form::Form;
use rocket::futures::future; use rocket::futures::future;
use rocket::serde::{json::json, Serialize}; use rocket::serde::{
json::{json, Value},
Serialize,
};
#[derive(FromForm)] #[derive(FromForm)]
pub struct PostInput { pub struct PostInput {
#[field(validate = len(1..4097))] #[field(validate = len(1..12289))]
text: String, text: String,
#[field(validate = len(0..33))] #[field(validate = len(0..97))]
cw: String, cw: String,
allow_search: Option<i8>, allow_search: Option<i8>,
use_title: Option<i8>, use_title: Option<i8>,
#[field(validate = len(0..97))]
poll_options: Vec<String>,
} }
#[derive(Serialize)] #[derive(Serialize)]
@@ -37,70 +46,80 @@ pub struct PostOutput {
can_del: bool, can_del: bool,
attention: bool, attention: bool,
hot_score: Option<i32>, hot_score: Option<i32>,
is_blocked: bool,
blocked_count: Option<i32>,
poll: Option<Value>,
// for old version frontend // for old version frontend
timestamp: i64, timestamp: i64,
likenum: i32, likenum: i32,
reply: i32, reply: i32,
blocked: bool,
} }
#[derive(FromForm)] #[derive(FromForm)]
pub struct CwInput { pub struct CwInput {
pid: i32, pid: i32,
#[field(validate = len(0..33))] #[field(validate = len(0..97))]
cw: String, cw: String,
} }
async fn p2output( async fn p2output(p: &Post, user: &CurrentUser, db: &Db, rconn: &RdsConn) -> PostOutput {
p: &Post, let is_blocked = BlockedUsers::check_blocked(rconn, user.id, &user.namehash, &p.author_hash)
user: &CurrentUser, .await
db: &Db, .unwrap_or_default();
rconn: &RdsConn, let can_view = !is_blocked && user.id.is_some() || user.namehash.eq(&p.author_hash);
) -> PostOutput {
PostOutput { PostOutput {
pid: p.id, pid: p.id,
text: format!("{}{}", if p.is_tmp { "[tmp]\n" } else { "" }, p.content), text: format!(
cw: if p.cw.len() > 0 { "{}{}",
Some(p.cw.to_string()) if p.is_tmp { "[tmp]\n" } else { "" },
} else { if can_view { &p.content } else { "" }
None ),
}, cw: (!p.cw.is_empty()).then(|| p.cw.to_string()),
n_attentions: p.n_attentions, n_attentions: p.n_attentions,
n_comments: p.n_comments, n_comments: p.n_comments,
create_time: p.create_time, create_time: p.create_time,
last_comment_time: p.last_comment_time, last_comment_time: p.last_comment_time,
allow_search: p.allow_search, allow_search: p.allow_search,
author_title: if p.author_title.len() > 0 { author_title: (!p.author_title.is_empty()).then(|| p.author_title.to_string()),
Some(p.author_title.to_string())
} else {
None
},
is_tmp: p.is_tmp, is_tmp: p.is_tmp,
is_reported: if user.is_admin { is_reported: user.is_admin.then(|| p.is_reported),
Some(p.is_reported)
} else {
None
},
comments: if p.n_comments > 50 { comments: if p.n_comments > 50 {
None None
} else { } else {
// 单个洞还有查询评论的接口,这里挂了不用报错 // 单个洞还有查询评论的接口,这里挂了不用报错
let pid = p.id; Some(
if let Some(cs) = Comment::gets_by_post_id(db, pid).await.ok() { c2output(
Some(c2output(p, &cs, user)) p,
} else { &p.get_comments(db, rconn).await.unwrap_or(vec![]),
None user,
} rconn,
)
.await,
)
}, },
can_del: p.check_permission(user, "wd").is_ok(), can_del: p.check_permission(user, "wd").is_ok(),
attention: Attention::init(&user.namehash, &rconn) attention: Attention::init(&user.namehash, &rconn)
.has(p.id) .has(p.id)
.await .await
.unwrap_or_default(), .unwrap_or_default(),
hot_score: if user.is_admin { Some(p.hot_score) } else { None }, hot_score: user.is_admin.then(|| p.hot_score),
is_blocked: is_blocked,
blocked_count: if user.is_admin {
BlockCounter::get_count(rconn, &p.author_hash).await.ok()
} else {
None
},
poll: if can_view {
get_poll_dict(p.id, rconn, &user.namehash).await
} else {
None
},
// for old version frontend // for old version frontend
timestamp: p.create_time.timestamp(), timestamp: p.create_time.timestamp(),
likenum: p.n_attentions, likenum: p.n_attentions,
reply: p.n_comments, reply: p.n_comments,
blocked: is_blocked,
} }
} }
@@ -119,8 +138,7 @@ pub async fn ps2outputs(
#[get("/getone?<pid>")] #[get("/getone?<pid>")]
pub async fn get_one(pid: i32, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI { pub async fn get_one(pid: i32, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
// let p = Post::get(&db, pid).await?; let p = Post::get(&db, &rconn, pid).await?;
let p = Post::get_with_cache(&db, &rconn, pid).await?;
p.check_permission(&user, "ro")?; p.check_permission(&user, "ro")?;
Ok(json!({ Ok(json!({
"data": p2output(&p, &user,&db, &rconn).await, "data": p2output(&p, &user,&db, &rconn).await,
@@ -136,14 +154,16 @@ pub async fn get_list(
db: Db, db: Db,
rconn: RdsConn, rconn: RdsConn,
) -> JsonAPI { ) -> JsonAPI {
user.id.ok_or_else(|| YouAreTmp)?;
let page = p.unwrap_or(1); let page = p.unwrap_or(1);
let page_size = 25; let page_size = 25;
let start = (page - 1) * page_size; let start = (page - 1) * page_size;
let ps = Post::gets_by_page(&db, order_mode, start.into(), page_size.into()).await?; let ps = Post::gets_by_page(&db, &rconn, order_mode, start.into(), page_size.into()).await?;
let ps_data = ps2outputs(&ps, &user, &db, &rconn).await; let ps_data = ps2outputs(&ps, &user, &db, &rconn).await;
Ok(json!({ Ok(json!({
"data": ps_data, "data": ps_data,
"count": ps_data.len(), "count": ps_data.len(),
"custom_title": CustomTitle::get(&rconn, &user.namehash).await?,
"code": 0 "code": 0
})) }))
} }
@@ -161,7 +181,12 @@ pub async fn publish_post(
content: poi.text.to_string(), content: poi.text.to_string(),
cw: poi.cw.to_string(), cw: poi.cw.to_string(),
author_hash: user.namehash.to_string(), author_hash: user.namehash.to_string(),
author_title: "".to_string(), author_title: (if poi.use_title.is_some() {
CustomTitle::get(&rconn, &user.namehash).await?
} else {
None
})
.unwrap_or_default(),
is_tmp: user.id.is_none(), is_tmp: user.id.is_none(),
n_attentions: 1, n_attentions: 1,
allow_search: poi.allow_search.is_some(), allow_search: poi.allow_search.is_some(),
@@ -169,25 +194,29 @@ pub async fn publish_post(
) )
.await?; .await?;
Attention::init(&user.namehash, &rconn).add(p.id).await?; Attention::init(&user.namehash, &rconn).add(p.id).await?;
Ok(json!({ p.refresh_cache(&rconn, true).await;
"code": 0
})) if !poi.poll_options.is_empty() {
PollOption::init(p.id, &rconn)
.set_list(&poi.poll_options)
.await?;
}
code0!()
} }
#[post("/editcw", data = "<cwi>")] #[post("/editcw", data = "<cwi>")]
pub async fn edit_cw(cwi: Form<CwInput>, user: CurrentUser, db: Db) -> JsonAPI { pub async fn edit_cw(cwi: Form<CwInput>, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
let p = Post::get(&db, cwi.pid).await?; let mut p = Post::get(&db, &rconn, cwi.pid).await?;
if !(user.is_admin || p.author_hash == user.namehash) {
return Err(APIError::PcError(NotAllowed));
}
p.check_permission(&user, "w")?; p.check_permission(&user, "w")?;
_ = p.update_cw(&db, cwi.cw.to_string()).await?; update!(p, posts, &db, { cw, to cwi.cw.to_string() });
Ok(json!({"code": 0})) p.refresh_cache(&rconn, false).await;
code0!()
} }
#[get("/getmulti?<pids>")] #[get("/getmulti?<pids>")]
pub async fn get_multi(pids: Vec<i32>, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI { pub async fn get_multi(pids: Vec<i32>, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
let ps = Post::get_multi_with_cache(&db, &rconn, &pids).await?; user.id.ok_or_else(|| YouAreTmp)?;
let ps = Post::get_multi(&db, &rconn, &pids).await?;
let ps_data = ps2outputs(&ps, &user, &db, &rconn).await; let ps_data = ps2outputs(&ps, &user, &db, &rconn).await;
Ok(json!({ Ok(json!({

View File

@@ -1,5 +1,5 @@
use crate::api::post::ps2outputs; use crate::api::post::ps2outputs;
use crate::api::{CurrentUser, JsonAPI}; use crate::api::{CurrentUser, JsonAPI, PolicyError::*};
use crate::db_conn::Db; use crate::db_conn::Db;
use crate::models::*; use crate::models::*;
use crate::rds_conn::RdsConn; use crate::rds_conn::RdsConn;
@@ -14,15 +14,21 @@ pub async fn search(
db: Db, db: Db,
rconn: RdsConn, rconn: RdsConn,
) -> JsonAPI { ) -> JsonAPI {
user.id.ok_or_else(|| YouAreTmp)?;
let page_size = 25; let page_size = 25;
let start = (page - 1) * page_size; let start = (page - 1) * page_size;
let kws = keywords.split(" ").filter(|x| !x.is_empty()).collect::<Vec<&str>>(); let kws = keywords
.split(" ")
.filter(|x| !x.is_empty())
.collect::<Vec<&str>>();
let ps = if kws.is_empty() { let ps = if kws.is_empty() {
vec![] vec![]
} else { } else {
Post::search( Post::search(
&db, &db,
&rconn,
search_mode, search_mode,
keywords.to_string(), keywords.to_string(),
start.into(), start.into(),
@@ -30,7 +36,6 @@ pub async fn search(
) )
.await? .await?
}; };
let mark_kws = if search_mode == 1 {kws} else {vec![]};
let ps_data = ps2outputs(&ps, &user, &db, &rconn).await; let ps_data = ps2outputs(&ps, &user, &db, &rconn).await;
Ok(json!({ Ok(json!({
"data": ps_data, "data": ps_data,

View File

@@ -1,16 +1,26 @@
use crate::api::{CurrentUser, API}; use crate::api::{CurrentUser, JsonAPI};
use crate::random_hasher::RandomHasher; use crate::random_hasher::RandomHasher;
use crate::rds_conn::RdsConn;
use crate::rds_models::{Systemlog};
use rocket::serde::json::{json, Value}; use rocket::serde::json::{json, Value};
use rocket::State; use rocket::State;
use crate::db_conn::Db;
#[get("/systemlog")] #[get("/systemlog")]
pub async fn get_systemlog(user: CurrentUser, rh: &State<RandomHasher>, db: Db) -> API<Value> { pub async fn get_systemlog(user: CurrentUser, rh: &State<RandomHasher>, rconn: RdsConn) -> JsonAPI {
let logs = Systemlog::get_list(&rconn, 50).await?;
Ok(json!({ Ok(json!({
"tmp_token": rh.get_tmp_token(), "tmp_token": rh.get_tmp_token(),
"salt": look!(rh.salt), "salt": look!(rh.salt),
"start_time": rh.start_time.timestamp(), "start_time": rh.start_time.timestamp(),
"custom_title": user.custom_title, "custom_title": user.custom_title,
"data": [], "data": logs.into_iter().map(|log|
json!({
"type": log.action_type,
"user": look!(log.user_hash),
"timestamp": log.time.timestamp(),
"detail": format!("{}\n{}", &log.target, if user.is_admin || !log.action_type.contains_ugc() { &log.detail } else { "" })
})
).collect::<Vec<Value>>(),
})) }))
} }

72
src/api/vote.rs Normal file
View File

@@ -0,0 +1,72 @@
use crate::api::{CurrentUser, JsonAPI, PolicyError::*};
use crate::rds_conn::RdsConn;
use crate::rds_models::*;
use rocket::form::Form;
use rocket::futures::future;
use rocket::serde::json::{json, Value};
pub async fn get_poll_dict(pid: i32, rconn: &RdsConn, namehash: &str) -> Option<Value> {
let opts = PollOption::init(pid, rconn)
.get_list()
.await
.unwrap_or_default();
if opts.is_empty() {
None
} else {
let choice = future::join_all(opts.iter().enumerate().map(|(idx, opt)| async move {
PollVote::init(pid, idx, rconn)
.has(namehash)
.await
.unwrap_or_default()
.then(|| opt)
}))
.await
.into_iter()
.filter_map(|x| x)
.collect::<Vec<&String>>()
.pop();
Some(json!({
"answers": future::join_all(
opts.iter().enumerate().map(|(idx, opt)| async move {
json!({
"option": opt,
"votes": PollVote::init(pid, idx, rconn).count().await.unwrap_or_default(),
})
})
).await,
"vote": choice,
}))
}
}
#[derive(FromForm)]
pub struct VoteInput {
pid: i32,
vote: String,
}
#[post("/vote", data = "<vi>")]
pub async fn vote(vi: Form<VoteInput>, user: CurrentUser, rconn: RdsConn) -> JsonAPI {
user.id.ok_or_else(|| NotAllowed)?;
let pid = vi.pid;
let opts = PollOption::init(pid, &rconn).get_list().await?;
if opts.is_empty() {
Err(NotAllowed)?;
}
for idx in 0..opts.len() {
if PollVote::init(pid, idx, &rconn).has(&user.namehash).await? {
Err(NotAllowed)?;
}
}
let idx: usize = opts
.iter()
.position(|x| x.eq(&vi.vote))
.ok_or_else(|| NotAllowed)?;
PollVote::init(pid, idx, &rconn).add(&user.namehash).await?;
code0!(get_poll_dict(vi.pid, &rconn, &user.namehash).await)
}

View File

@@ -1,10 +1,17 @@
use crate::models::{Comment, Post, User}; use crate::models::{Comment, Post, User};
use crate::rds_conn::RdsConn; use crate::rds_conn::RdsConn;
use crate::rds_models::init;
use rand::Rng;
use redis::AsyncCommands; use redis::AsyncCommands;
use rocket::serde::json::serde_json; use rocket::serde::json::serde_json;
// can use rocket::serde::json::to_string in master version // can use rocket::serde::json::to_string in master version
const INSTANCE_EXPIRE_TIME: usize = 60 * 60; const INSTANCE_EXPIRE_TIME: usize = 60 * 60;
const MIN_LENGTH: isize = 200;
const MAX_LENGTH: isize = 900;
const CUT_LENGTH: isize = 100;
macro_rules! post_cache_key { macro_rules! post_cache_key {
($id: expr) => { ($id: expr) => {
format!("hole_v2:cache:post:{}", $id) format!("hole_v2:cache:post:{}", $id)
@@ -16,11 +23,7 @@ pub struct PostCache {
} }
impl PostCache { impl PostCache {
pub fn init(rconn: &RdsConn) -> Self { init!();
PostCache {
rconn: rconn.clone(),
}
}
pub async fn sets(&mut self, ps: &Vec<&Post>) { pub async fn sets(&mut self, ps: &Vec<&Post>) {
if ps.is_empty() { if ps.is_empty() {
@@ -28,19 +31,12 @@ impl PostCache {
} }
let kvs: Vec<(String, String)> = ps let kvs: Vec<(String, String)> = ps
.iter() .iter()
.map(|p| ( .map(|p| (post_cache_key!(p.id), serde_json::to_string(p).unwrap()))
post_cache_key!(p.id), .collect();
serde_json::to_string(p).unwrap(), self.rconn.set_multiple(&kvs).await.unwrap_or_else(|e| {
) ).collect();
dbg!(&kvs);
let ret = self.rconn
.set_multiple(&kvs)
.await
.unwrap_or_else(|e| {
warn!("set post cache failed: {}", e); warn!("set post cache failed: {}", e);
"x".to_string() dbg!(&kvs);
}); });
dbg!(ret);
} }
pub async fn get(&mut self, pid: &i32) -> Option<Post> { pub async fn get(&mut self, pid: &i32) -> Option<Post> {
@@ -50,7 +46,7 @@ impl PostCache {
.get::<String, Option<String>>(key) .get::<String, Option<String>>(key)
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
warn!("try to get post cache, connect rds fail, {}", e); warn!("try to get post cache, connect rds failed, {}", e);
None None
}); });
@@ -76,7 +72,7 @@ impl PostCache {
.get::<Vec<String>, Vec<Option<String>>>(ks) .get::<Vec<String>, Vec<Option<String>>>(ks)
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
warn!("try to get posts cache, connect rds fail, {}", e); warn!("try to get posts cache, connect rds failed, {}", e);
vec![None; pids.len()] vec![None; pids.len()]
}); });
// dbg!(&rds_result); // dbg!(&rds_result);
@@ -98,6 +94,187 @@ impl PostCache {
} }
} }
} }
pub async fn clear_all(&mut self) {
let mut keys = self
.rconn
.scan_match::<String, String>(post_cache_key!("*"))
.await
.unwrap(); //.collect::<Vec<String>>().await;
// colllect() does not work
// also see: https://github.com/mitsuhiko/redis-rs/issues/583
let mut ks_for_del = Vec::new();
while let Some(key) = keys.next_item().await {
ks_for_del.push(key);
}
if ks_for_del.is_empty() {
return;
}
self.rconn
.del(ks_for_del)
.await
.unwrap_or_else(|e| warn!("clear all post cache fail, {}", e));
}
}
pub struct PostCommentCache {
key: String,
rconn: RdsConn,
}
impl PostCommentCache {
init!(i32, "hole_v2:cache:post_comments:{}");
pub async fn set(&mut self, cs: &Vec<Comment>) {
self.rconn
.set_ex(
&self.key,
serde_json::to_string(cs).unwrap(),
INSTANCE_EXPIRE_TIME,
)
.await
.unwrap_or_else(|e| {
warn!("set comments cache failed: {}", e);
dbg!(cs);
})
}
pub async fn get(&mut self) -> Option<Vec<Comment>> {
let rds_result = self.rconn.get::<&String, String>(&self.key).await;
// dbg!(&rds_result);
if let Ok(s) = rds_result {
self.rconn
.expire::<&String, bool>(&self.key, INSTANCE_EXPIRE_TIME)
.await
.unwrap_or_else(|e| {
warn!(
"get comments cache, set new expire failed: {}, {}, {} ",
e, &self.key, &s
);
false
});
serde_json::from_str(&s).unwrap_or_else(|e| {
warn!("get comments cache, decode failed {}, {}", e, s);
None
})
} else {
None
}
}
pub async fn clear(&mut self) {
self.rconn.del(&self.key).await.unwrap_or_else(|e| {
warn!("clear commenrs cache fail, {}", e);
});
}
}
pub struct PostListCommentCache {
key: String,
mode: u8,
rconn: RdsConn,
length: isize,
}
impl PostListCommentCache {
pub fn init(mode: u8, rconn: &RdsConn) -> Self {
Self {
key: format!("hole_v2:cache:post_list:{}", &mode),
mode: mode,
rconn: rconn.clone(),
length: 0,
}
}
async fn set_and_check_length(&mut self) {
let mut l = self.rconn.zcard(&self.key).await.unwrap();
if l > MAX_LENGTH {
self.rconn
.zremrangebyrank::<&String, ()>(&self.key, MAX_LENGTH - CUT_LENGTH, -1)
.await
.unwrap_or_else(|e| {
warn!("cut list cache failed, {}, {}", e, &self.key);
});
l = MIN_LENGTH;
}
self.length = l;
}
pub async fn need_fill(&mut self) -> bool {
self.set_and_check_length().await;
self.length < MIN_LENGTH
}
pub fn i64_len(&self) -> i64 {
self.length.try_into().unwrap()
}
pub fn i64_minlen(&self) -> i64 {
MIN_LENGTH.try_into().unwrap()
}
fn p2pair(&self, p: &Post) -> (i64, i32) {
(
match self.mode {
0 => (-p.id).into(),
1 => -p.last_comment_time.timestamp(),
2 => (-p.hot_score).into(),
3 => rand::thread_rng().gen_range(0..i64::MAX),
_ => panic!("wrong mode"),
},
p.id,
)
}
pub async fn fill(&mut self, ps: &Vec<Post>) {
let items: Vec<(i64, i32)> = ps.iter().map(|p| self.p2pair(p)).collect();
self.rconn
.zadd_multiple(&self.key, &items)
.await
.unwrap_or_else(|e| {
warn!("fill list cache failed, {} {}", e, &self.key);
});
self.set_and_check_length().await;
}
pub async fn put(&mut self, p: &Post) {
// 其他都是加到最前面的但热榜不是。可能导致MIN_LENGTH到MAX_LENGTH之间的数据不可靠
// 影响不大,先不管了
if p.is_deleted || (self.mode > 0 && p.is_reported) {
self.rconn.zrem(&self.key, p.id).await.unwrap_or_else(|e| {
warn!(
"remove from list cache failed, {} {} {}",
e, &self.key, p.id
);
});
} else {
let (s, m) = self.p2pair(p);
self.rconn.zadd(&self.key, m, s).await.unwrap_or_else(|e| {
warn!(
"put into list cache failed, {} {} {} {}",
e, &self.key, m, s
);
});
}
}
pub async fn get_pids(&mut self, start: i64, limit: i64) -> Vec<i32> {
self.rconn
.zrange(
&self.key,
start.try_into().unwrap(),
(start + limit - 1).try_into().unwrap(),
)
.await
.unwrap()
}
pub async fn clear(&mut self) {
self.rconn.del(&self.key).await.unwrap_or_else(|e| {
warn!("clear post list cache failed, {}", e);
});
}
} }
pub struct UserCache { pub struct UserCache {
@@ -106,12 +283,7 @@ pub struct UserCache {
} }
impl UserCache { impl UserCache {
pub fn init(token: &str, rconn: &RdsConn) -> Self { init!(&str, "hole_v2:cache:user:{}");
UserCache {
key: format!("hole_v2:cache:user:{}", token),
rconn: rconn.clone(),
}
}
pub async fn set(&mut self, u: &User) { pub async fn set(&mut self, u: &User) {
self.rconn self.rconn
@@ -122,14 +294,14 @@ impl UserCache {
) )
.await .await
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
warn!("set user cache failed: {}, {}, {}", e, u.id, u.name); warn!("set user cache failed: {}", e);
dbg!(u);
}) })
} }
pub async fn get(&mut self) -> Option<User> { pub async fn get(&mut self) -> Option<User> {
let rds_result = self.rconn.get::<&String, String>(&self.key).await; let rds_result = self.rconn.get::<&String, String>(&self.key).await;
if let Ok(s) = rds_result { if let Ok(s) = rds_result {
debug!("hint user cache");
self.rconn self.rconn
.expire::<&String, bool>(&self.key, INSTANCE_EXPIRE_TIME) .expire::<&String, bool>(&self.key, INSTANCE_EXPIRE_TIME)
.await .await

View File

@@ -1,7 +1,17 @@
use rocket_sync_db_pools::{database, diesel}; use rocket_sync_db_pools::{database, diesel};
use diesel::Connection;
use std::env;
pub type Conn = diesel::pg::PgConnection; pub type Conn = diesel::pg::PgConnection;
#[database("pg_v2")] #[database("pg_v2")]
pub struct Db(Conn); pub struct Db(Conn);
// get sync connection, only for annealing
pub fn establish_connection() -> Conn {
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
Conn::establish(&database_url)
.expect(&format!("Error connecting to {}", database_url))
}

114
src/login.rs Normal file
View File

@@ -0,0 +1,114 @@
use crate::db_conn::Db;
use crate::models::User;
use rocket::request::{FromRequest, Outcome, Request};
use rocket::response::Redirect;
use rocket::serde::Deserialize;
use std::env;
use url::Url;
pub struct RefHeader(pub String);
#[rocket::async_trait]
impl<'r> FromRequest<'r> for RefHeader {
type Error = ();
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
match request.headers().get_one("Referer") {
Some(h) => Outcome::Success(RefHeader(h.to_string())),
None => Outcome::Forward(()),
}
}
}
#[get("/?p=cs")]
pub fn cs_login(r: RefHeader) -> Redirect {
let mast_url = env::var("MAST_BASE_URL").unwrap();
let mast_cli = env::var("MAST_CLIENT").unwrap();
let mast_scope = env::var("MAST_SCOPE").unwrap();
let mut redirect_url = Url::parse(&r.0).unwrap();
redirect_url.set_path("/_login/cs/auth");
redirect_url.set_query(None);
redirect_url = Url::parse_with_params(
redirect_url.as_str(),
&[("redirect_url", redirect_url.as_str())],
)
.unwrap();
let url = Url::parse_with_params(
&format!("{}oauth/authorize", mast_url),
&[
("redirect_uri", redirect_url.as_str()),
("client_id", &mast_cli),
("scope", &mast_scope),
("response_type", "code"),
],
)
.unwrap();
Redirect::to(url.to_string())
}
#[derive(Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
struct Token {
pub access_token: String,
}
#[derive(Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
struct Account {
pub id: String,
}
#[get("/cs/auth?<code>&<redirect_url>")]
pub async fn cs_auth(code: String, redirect_url: String, db: Db) -> Redirect {
let mast_url = env::var("MAST_BASE_URL").unwrap();
let mast_cli = env::var("MAST_CLIENT").unwrap();
let mast_sec = env::var("MAST_SECRET").unwrap();
let mast_scope = env::var("MAST_SCOPE").unwrap();
// to keep same
let redirect_url = Url::parse_with_params(
redirect_url.as_str(),
&[("redirect_url", redirect_url.as_str())],
)
.unwrap();
let client = reqwest::Client::new();
let token: Token = client
.post(format!("{}oauth/token", &mast_url))
.form(&[
("client_id", mast_cli.as_str()),
("client_secret", mast_sec.as_str()),
("scope", mast_scope.as_str()),
("redirect_uri", redirect_url.as_str()),
("grant_type", "authorization_code"),
("code", code.as_str()),
])
.send()
.await
.unwrap()
.json()
.await
.unwrap();
//dbg!(&token);
let client = reqwest::Client::new();
let account = client
.get(format!("{}api/v1/accounts/verify_credentials", &mast_url))
.bearer_auth(token.access_token)
.send()
.await
.unwrap()
.json::<Account>()
.await
.unwrap();
//dbg!(&account);
let tk = User::find_or_create_token(&db, &format!("cs_{}", &account.id), false)
.await
.unwrap();
Redirect::to(format!("/?token={}", tk))
}

View File

@@ -1,3 +1,5 @@
#![feature(concat_idents)]
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
@@ -11,20 +13,23 @@ extern crate diesel_migrations;
extern crate log; extern crate log;
mod api; mod api;
mod cache;
mod db_conn; mod db_conn;
mod libs; mod libs;
#[cfg(feature = "mastlogin")]
mod login;
mod models; mod models;
mod random_hasher; mod random_hasher;
mod rds_conn; mod rds_conn;
mod rds_models; mod rds_models;
mod cache;
mod schema; mod schema;
use db_conn::{Conn, Db}; use db_conn::{establish_connection, Conn, Db};
use diesel::Connection; use diesel::Connection;
use random_hasher::RandomHasher; use random_hasher::RandomHasher;
use rds_conn::init_rds_client; use rds_conn::{init_rds_client, RdsConn};
use std::env; use std::env;
use tokio::time::{sleep, Duration};
embed_migrations!("migrations/postgres"); embed_migrations!("migrations/postgres");
@@ -36,6 +41,16 @@ async fn main() -> Result<(), rocket::Error> {
return Ok(()); return Ok(());
} }
env_logger::init(); env_logger::init();
let rmc = init_rds_client().await;
let rconn = RdsConn(rmc.clone());
clear_outdate_redis_data(&rconn.clone()).await;
tokio::spawn(async move {
loop {
sleep(Duration::from_secs(4 * 60 * 60)).await;
models::Post::annealing(establish_connection(), &rconn).await;
}
});
rocket::build() rocket::build()
.mount( .mount(
"/_api/v1", "/_api/v1",
@@ -52,11 +67,29 @@ async fn main() -> Result<(), rocket::Error> {
api::attention::get_attention, api::attention::get_attention,
api::systemlog::get_systemlog, api::systemlog::get_systemlog,
api::operation::delete, api::operation::delete,
api::operation::report,
api::operation::set_title,
api::operation::block,
api::vote::vote,
], ],
) )
.register("/_api", catchers![api::catch_401_error]) .mount(
"/_login",
#[cfg(feature = "mastlogin")]
{
routes![login::cs_login, login::cs_auth]
},
#[cfg(not(feature = "mastlogin"))]
{
[]
},
)
.register(
"/_api",
catchers![api::catch_401_error, api::catch_403_error,],
)
.manage(RandomHasher::get_random_one()) .manage(RandomHasher::get_random_one())
.manage(init_rds_client().await) .manage(rmc)
.attach(Db::fairing()) .attach(Db::fairing())
.launch() .launch()
.await .await
@@ -75,3 +108,8 @@ fn init_database() {
let conn = Conn::establish(&database_url).unwrap(); let conn = Conn::establish(&database_url).unwrap();
embedded_migrations::run(&conn).unwrap(); embedded_migrations::run(&conn).unwrap();
} }
async fn clear_outdate_redis_data(rconn: &RdsConn) {
rds_models::BannedUsers::clear(&rconn).await.unwrap();
rds_models::CustomTitle::clear(&rconn).await.unwrap();
}

View File

@@ -1,25 +1,29 @@
#![allow(clippy::all)] #![allow(clippy::all)]
use crate::cache::{PostCache, UserCache}; use crate::cache::*;
use crate::db_conn::Db; use crate::db_conn::{Conn, Db};
use crate::libs::diesel_logger::LoggingConnection; use crate::libs::diesel_logger::LoggingConnection;
use crate::random_hasher::random_string;
use crate::rds_conn::RdsConn; use crate::rds_conn::RdsConn;
use crate::schema::*; use crate::schema::*;
use chrono::{offset::Utc, DateTime}; use chrono::{offset::Utc, DateTime};
use diesel::dsl::any; use diesel::dsl::any;
use diesel::sql_types::*;
use diesel::{ use diesel::{
insert_into, BoolExpressionMethods, ExpressionMethods, QueryDsl, QueryResult, RunQueryDsl, insert_into, BoolExpressionMethods, ExpressionMethods, QueryDsl, QueryResult, RunQueryDsl,
TextExpressionMethods, TextExpressionMethods,
}; };
use rocket::futures::{future, join};
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::identity;
no_arg_sql_function!(RANDOM, (), "Represents the sql RANDOM() function"); no_arg_sql_function!(RANDOM, (), "Represents the sql RANDOM() function");
sql_function!(fn floor(x: Float) -> Int4);
sql_function!(fn float4(x: Int4) -> Float);
macro_rules! get { macro_rules! _get {
($table:ident) => { ($table:ident) => {
pub async fn get(db: &Db, id: i32) -> QueryResult<Self> { async fn _get(db: &Db, id: i32) -> QueryResult<Self> {
let pid = id; let pid = id;
db.run(move |c| $table::table.find(pid).first(with_log!((c)))) db.run(move |c| $table::table.find(pid).first(with_log!((c))))
.await .await
@@ -27,9 +31,9 @@ macro_rules! get {
}; };
} }
macro_rules! get_multi { macro_rules! _get_multi {
($table:ident) => { ($table:ident) => {
pub async fn get_multi(db: &Db, ids: Vec<i32>) -> QueryResult<Vec<Self>> { async fn _get_multi(db: &Db, ids: Vec<i32>) -> QueryResult<Vec<Self>> {
if ids.is_empty() { if ids.is_empty() {
return Ok(vec![]); return Ok(vec![]);
} }
@@ -45,18 +49,29 @@ macro_rules! get_multi {
}; };
} }
macro_rules! set_deleted { macro_rules! op_to_col_expr {
($table:ident) => { ($col_obj:expr, to $v:expr) => {
pub async fn set_deleted(&self, db: &Db) -> QueryResult<usize> { $v
let pid = self.id;
db.run(move |c| {
diesel::update($table::table.find(pid))
.set($table::is_deleted.eq(true))
.execute(with_log!(c))
})
.await
}
}; };
($col_obj:expr, add $v:expr) => {
$col_obj + $v
};
}
macro_rules! update {
($obj:expr, $table:ident, $db:expr, $({ $col:ident, $op:ident $v:expr }), + ) => {{
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 { macro_rules! base_query {
@@ -73,7 +88,7 @@ macro_rules! with_log {
}; };
} }
#[derive(Queryable, Insertable, Serialize, Deserialize)] #[derive(Queryable, Insertable, Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct Comment { pub struct Comment {
pub id: i32, pub id: i32,
@@ -106,7 +121,7 @@ pub struct Post {
pub allow_search: bool, pub allow_search: bool,
} }
#[derive(Queryable, Insertable, Serialize, Deserialize)] #[derive(Queryable, Insertable, Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")] #[serde(crate = "rocket::serde")]
pub struct User { pub struct User {
pub id: i32, pub id: i32,
@@ -125,21 +140,14 @@ pub struct NewPost {
pub is_tmp: bool, pub is_tmp: bool,
pub n_attentions: i32, pub n_attentions: i32,
pub allow_search: bool, pub allow_search: bool,
// TODO: tags
} }
impl Post { impl Post {
get!(posts); _get!(posts);
get_multi!(posts); _get_multi!(posts);
set_deleted!(posts); pub async fn get_multi(db: &Db, rconn: &RdsConn, ids: &Vec<i32>) -> QueryResult<Vec<Self>> {
pub async fn get_multi_with_cache(
db: &Db,
rconn: &RdsConn,
ids: &Vec<i32>,
) -> QueryResult<Vec<Self>> {
let mut cacher = PostCache::init(&rconn); let mut cacher = PostCache::init(&rconn);
let mut cached_posts = cacher.gets(ids).await; let mut cached_posts = cacher.gets(ids).await;
let mut id2po = HashMap::<i32, &mut Option<Post>>::new(); let mut id2po = HashMap::<i32, &mut Option<Post>>::new();
@@ -153,17 +161,17 @@ impl Post {
None => { None => {
id2po.insert(pid.clone(), p); id2po.insert(pid.clone(), p);
Some(pid) Some(pid)
}, }
_ => None, _ => None,
}) })
.copied() .copied()
.collect(); .collect();
dbg!(&missing_ids); // dbg!(&missing_ids);
let missing_ps = Self::get_multi(db, missing_ids).await?; let missing_ps = Self::_get_multi(db, missing_ids).await?;
// dbg!(&missing_ps); // dbg!(&missing_ps);
cacher.sets(&missing_ps.iter().map(identity).collect()).await; cacher.sets(&missing_ps.iter().collect()).await;
for p in missing_ps.into_iter() { for p in missing_ps.into_iter() {
if let Some(op) = id2po.get_mut(&p.id) { if let Some(op) = id2po.get_mut(&p.id) {
@@ -171,26 +179,69 @@ impl Post {
} }
} }
// dbg!(&cached_posts); // dbg!(&cached_posts);
Ok( Ok(cached_posts
cached_posts.into_iter().filter_map(identity).collect() .into_iter()
) .filter_map(|p| p.filter(|p| !p.is_deleted))
.collect())
} }
pub async fn get_with_cache(db: &Db, rconn: &RdsConn, id: i32) -> QueryResult<Self> { pub async fn get(db: &Db, rconn: &RdsConn, id: i32) -> QueryResult<Self> {
Self::get_multi_with_cache(db, rconn, &vec![id]) // 注意即使is_deleted也应该缓存和返回
.await? let mut cacher = PostCache::init(&rconn);
.pop() if let Some(p) = cacher.get(&id).await {
.ok_or(diesel::result::Error::NotFound) Ok(p)
} else {
let p = Self::_get(db, id).await?;
cacher.sets(&vec![&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( pub async fn gets_by_page(
db: &Db, db: &Db,
rconn: &RdsConn,
order_mode: u8, order_mode: u8,
start: i64, start: i64,
limit: i64, limit: i64,
) -> QueryResult<Vec<Self>> { ) -> QueryResult<Vec<Self>> {
let mut cacher = PostListCommentCache::init(order_mode, &rconn);
if cacher.need_fill().await {
let pids =
Self::_get_ids_by_page(db, order_mode.clone(), 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, 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,
order_mode: u8,
start: i64,
limit: i64,
) -> QueryResult<Vec<i32>> {
db.run(move |c| { db.run(move |c| {
let mut query = base_query!(posts); let mut query = base_query!(posts).select(posts::id);
if order_mode > 0 { if order_mode > 0 {
query = query.filter(posts::is_reported.eq(false)) query = query.filter(posts::is_reported.eq(false))
} }
@@ -210,15 +261,21 @@ impl Post {
pub async fn search( pub async fn search(
db: &Db, db: &Db,
rconn: &RdsConn,
search_mode: u8, search_mode: u8,
search_text: String, search_text: String,
start: i64, start: i64,
limit: i64, limit: i64,
) -> QueryResult<Vec<Self>> { ) -> QueryResult<Vec<Self>> {
let search_text2 = search_text.replace("%", "\\%"); let search_text2 = search_text.replace("%", "\\%");
db.run(move |c| { let pids = db
.run(move |c| {
let pat; let pat;
let mut query = base_query!(posts).distinct().left_join(comments::table); let mut query = base_query!(posts)
.select(posts::id)
.distinct()
.left_join(comments::table)
.filter(posts::is_reported.eq(false));
// 先用搜索+缓存性能有问题了再真的做tag表 // 先用搜索+缓存性能有问题了再真的做tag表
query = match search_mode { query = match search_mode {
0 => { 0 => {
@@ -251,11 +308,11 @@ impl Post {
.limit(limit) .limit(limit)
.load(with_log!(c)) .load(with_log!(c))
}) })
.await .await?;
Self::get_multi(db, rconn, &pids).await
} }
pub async fn create(db: &Db, new_post: NewPost) -> QueryResult<Self> { pub async fn create(db: &Db, new_post: NewPost) -> QueryResult<Self> {
// TODO: tags
db.run(move |c| { db.run(move |c| {
insert_into(posts::table) insert_into(posts::table)
.values(&new_post) .values(&new_post)
@@ -264,56 +321,34 @@ impl Post {
.await .await
} }
pub async fn update_cw(&self, db: &Db, new_cw: String) -> QueryResult<usize> {
let pid = self.id;
db.run(move |c| {
diesel::update(posts::table.find(pid))
.set(posts::cw.eq(new_cw))
.execute(with_log!(c))
})
.await
}
pub async fn change_n_comments(&self, db: &Db, delta: i32) -> QueryResult<Self> {
let pid = self.id;
db.run(move |c| {
diesel::update(posts::table.find(pid))
.set(posts::n_comments.eq(posts::n_comments + delta))
.get_result(with_log!(c))
})
.await
}
pub async fn change_n_attentions(&self, db: &Db, delta: i32) -> QueryResult<Self> {
let pid = self.id;
db.run(move |c| {
diesel::update(posts::table.find(pid))
.set(posts::n_attentions.eq(posts::n_attentions + delta))
.get_result(with_log!(c))
})
.await
}
pub async fn change_hot_score(&self, db: &Db, delta: i32) -> QueryResult<Self> {
let pid = self.id;
db.run(move |c| {
diesel::update(posts::table.find(pid))
.set(posts::hot_score.eq(posts::hot_score + delta))
.get_result(with_log!(c))
})
.await
}
pub async fn set_instance_cache(&self, rconn: &RdsConn) { pub async fn set_instance_cache(&self, rconn: &RdsConn) {
PostCache::init(rconn).sets(&vec![self]).await; PostCache::init(rconn).sets(&vec![self]).await;
} }
pub async fn refresh_cache(&self, rconn: &RdsConn, is_new: bool) { pub async fn refresh_cache(&self, rconn: &RdsConn, is_new: bool) {
self.set_instance_cache(rconn).await; join!(
self.set_instance_cache(rconn),
future::join_all((if is_new { 0..4 } else { 1..4 }).map(|mode| async move {
PostListCommentCache::init(mode, &rconn.clone())
.put(self)
.await
})),
);
}
pub async fn annealing(mut c: Conn, rconn: &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!(&mut c))
.unwrap();
PostCache::init(&rconn).clear_all().await;
PostListCommentCache::init(2, rconn).clear().await
} }
} }
impl User { impl User {
pub async fn get_by_token(db: &Db, token: &str) -> Option<Self> { async fn _get_by_token(db: &Db, token: &str) -> Option<Self> {
let token = token.to_string(); let token = token.to_string();
db.run(move |c| { db.run(move |c| {
users::table users::table
@@ -324,16 +359,48 @@ impl User {
.ok() .ok()
} }
pub async fn get_by_token_with_cache(db: &Db, rconn: &RdsConn, token: &str) -> Option<Self> { pub async fn get_by_token(db: &Db, rconn: &RdsConn, token: &str) -> Option<Self> {
let mut cacher = UserCache::init(token, &rconn); let mut cacher = UserCache::init(token, &rconn);
if let Some(u) = cacher.get().await { if let Some(u) = cacher.get().await {
Some(u) Some(u)
} else { } else {
let u = Self::get_by_token(db, token).await?; let u = Self::_get_by_token(db, token).await?;
cacher.set(&u).await; cacher.set(&u).await;
Some(u) 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
}
} }
#[derive(Insertable)] #[derive(Insertable)]
@@ -347,9 +414,12 @@ pub struct NewComment {
} }
impl Comment { impl Comment {
get!(comments); _get!(comments);
set_deleted!(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> { pub async fn create(db: &Db, new_comment: NewComment) -> QueryResult<Self> {
db.run(move |c| { db.run(move |c| {
@@ -371,3 +441,5 @@ impl Comment {
.await .await
} }
} }
pub(crate) use {op_to_col_expr, update, with_log};

View File

@@ -2,6 +2,14 @@ use chrono::{offset::Local, DateTime};
use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rand::{distributions::Alphanumeric, thread_rng, Rng};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
pub fn random_string(len: usize) -> String {
thread_rng()
.sample_iter(&Alphanumeric)
.take(len)
.map(char::from)
.collect()
}
pub struct RandomHasher { pub struct RandomHasher {
pub salt: String, pub salt: String,
pub start_time: DateTime<Local>, pub start_time: DateTime<Local>,
@@ -10,11 +18,7 @@ pub struct RandomHasher {
impl RandomHasher { impl RandomHasher {
pub fn get_random_one() -> RandomHasher { pub fn get_random_one() -> RandomHasher {
RandomHasher { RandomHasher {
salt: thread_rng() salt: random_string(16),
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect(),
start_time: Local::now(), start_time: Local::now(),
} }
} }

View File

@@ -1,5 +1,59 @@
use crate::rds_conn::RdsConn; use crate::rds_conn::RdsConn;
use chrono::{offset::Local, DateTime};
use redis::{AsyncCommands, RedisResult}; use redis::{AsyncCommands, RedisResult};
use rocket::serde::json::serde_json;
use rocket::serde::{Deserialize, Serialize};
macro_rules! init {
() => {
pub fn init(rconn: &RdsConn) -> Self {
Self {
rconn: rconn.clone(),
}
}
};
($ktype:ty, $formatter:expr) => {
pub fn init(k: $ktype, rconn: &RdsConn) -> Self {
Self {
key: format!($formatter, k),
rconn: rconn.clone(),
}
}
};
($k1type:ty, $k2type:ty, $formatter:expr) => {
pub fn init(k1: $k1type, k2: $k2type, rconn: &RdsConn) -> Self {
Self {
key: format!($formatter, k1, k2),
rconn: rconn.clone(),
}
}
};
}
macro_rules! has {
($vtype:ty) => {
pub async fn has(&mut self, v: $vtype) -> RedisResult<bool> {
self.rconn.sismember(&self.key, v).await
}
};
}
macro_rules! add {
($vtype:ty) => {
pub async fn add(&mut self, v: $vtype) -> RedisResult<()> {
self.rconn.sadd(&self.key, v).await
}
};
}
const KEY_SYSTEMLOG: &str = "hole_v2:systemlog_list";
const KEY_BANNED_USERS: &str = "hole_v2:banned_user_hash_list";
const KEY_BLOCKED_COUNTER: &str = "hole_v2:blocked_counter";
const KEY_DANGEROUS_USERS: &str = "hole_thu:dangerous_users"; //兼容一下旧版
const KEY_CUSTOM_TITLE: &str = "hole_v2:title";
const SYSTEMLOG_MAX_LEN: isize = 1000;
pub const BLOCK_THRESHOLD: i32 = 10;
pub struct Attention { pub struct Attention {
key: String, key: String,
@@ -7,26 +61,202 @@ pub struct Attention {
} }
impl Attention { impl Attention {
pub fn init(namehash: &str, rconn: &RdsConn) -> Self { init!(&str, "hole_v2:attention:{}");
Attention {
key: format!("hole_v2:attention:{}", namehash),
rconn: rconn.clone(),
}
}
pub async fn add(&mut self, pid: i32) -> RedisResult<()> { add!(i32);
self.rconn.sadd(&self.key, pid).await
} has!(i32);
pub async fn remove(&mut self, pid: i32) -> RedisResult<()> { pub async fn remove(&mut self, pid: i32) -> RedisResult<()> {
self.rconn.srem(&self.key, pid).await self.rconn.srem(&self.key, pid).await
} }
pub async fn has(&mut self, pid: i32) -> RedisResult<bool> {
self.rconn.sismember(&self.key, pid).await
}
pub async fn all(&mut self) -> RedisResult<Vec<i32>> { pub async fn all(&mut self) -> RedisResult<Vec<i32>> {
self.rconn.smembers(&self.key).await self.rconn.smembers(&self.key).await
} }
// TODO: clear all
} }
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub enum LogType {
AdminDelete,
Report,
Ban,
}
impl LogType {
pub fn contains_ugc(&self) -> bool {
match self {
Self::Report => true,
_ => false,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(crate = "rocket::serde")]
pub struct Systemlog {
pub user_hash: String,
pub action_type: LogType,
pub target: String,
pub detail: String,
pub time: DateTime<Local>,
}
impl Systemlog {
pub async fn create(&self, rconn: &RdsConn) -> RedisResult<()> {
let mut rconn = rconn.clone();
if rconn.llen::<&str, isize>(KEY_SYSTEMLOG).await? > SYSTEMLOG_MAX_LEN {
rconn.ltrim(KEY_SYSTEMLOG, 0, SYSTEMLOG_MAX_LEN - 1).await?;
}
rconn
.lpush(KEY_SYSTEMLOG, serde_json::to_string(&self).unwrap())
.await
}
pub async fn get_list(rconn: &RdsConn, limit: isize) -> RedisResult<Vec<Self>> {
let rds_result = rconn
.clone()
.lrange::<&str, Vec<String>>(KEY_SYSTEMLOG, 0, limit)
.await?;
Ok(rds_result
.iter()
.map(|s| serde_json::from_str(s).unwrap())
.collect())
}
}
pub struct BannedUsers;
impl BannedUsers {
pub async fn add(rconn: &RdsConn, namehash: &str) -> RedisResult<()> {
rconn
.clone()
.sadd::<&str, &str, ()>(KEY_BANNED_USERS, namehash)
.await
}
pub async fn has(rconn: &RdsConn, namehash: &str) -> RedisResult<bool> {
rconn.clone().sismember(KEY_BANNED_USERS, namehash).await
}
pub async fn clear(rconn: &RdsConn) -> RedisResult<()> {
rconn.clone().del(KEY_BANNED_USERS).await
}
}
pub struct BlockedUsers {
pub key: String,
rconn: RdsConn,
}
impl BlockedUsers {
init!(i32, "hole_v2:blocked_users:{}");
add!(&str);
has!(&str);
pub async fn check_blocked(
rconn: &RdsConn,
viewer_id: Option<i32>,
viewer_hash: &str,
author_hash: &str,
) -> RedisResult<bool> {
Ok(match viewer_id {
Some(id) => Self::init(id, rconn).has(author_hash).await?,
None => false,
} || (DangerousUser::has(rconn, author_hash).await?
&& !DangerousUser::has(rconn, viewer_hash).await?))
}
}
pub struct BlockCounter;
impl BlockCounter {
pub async fn count_incr(rconn: &RdsConn, namehash: &str) -> RedisResult<i32> {
rconn.clone().hincr(KEY_BLOCKED_COUNTER, namehash, 1).await
}
pub async fn get_count(rconn: &RdsConn, namehash: &str) -> RedisResult<i32> {
rconn.clone().hget(KEY_BLOCKED_COUNTER, namehash).await
}
}
pub struct DangerousUser;
impl DangerousUser {
pub async fn add(rconn: &RdsConn, namehash: &str) -> RedisResult<()> {
rconn
.clone()
.sadd::<&str, &str, ()>(KEY_DANGEROUS_USERS, namehash)
.await
}
pub async fn has(rconn: &RdsConn, namehash: &str) -> RedisResult<bool> {
rconn.clone().sismember(KEY_DANGEROUS_USERS, namehash).await
}
}
pub struct CustomTitle;
impl CustomTitle {
// return false if title exits
pub async fn set(rconn: &RdsConn, namehash: &str, title: &str) -> RedisResult<bool> {
let mut rconn = rconn.clone();
if rconn.hexists(KEY_CUSTOM_TITLE, title).await? {
Ok(false)
} else {
rconn.hset(KEY_CUSTOM_TITLE, namehash, title).await?;
rconn.hset(KEY_CUSTOM_TITLE, title, namehash).await?;
Ok(true)
}
}
pub async fn get(rconn: &RdsConn, namehash: &str) -> RedisResult<Option<String>> {
rconn.clone().hget(KEY_CUSTOM_TITLE, namehash).await
}
pub async fn clear(rconn: &RdsConn) -> RedisResult<()> {
rconn.clone().del(KEY_CUSTOM_TITLE).await
}
}
pub struct PollOption {
key: String,
rconn: RdsConn,
}
impl PollOption {
init!(i32, "hole_thu:poll_opts:{}");
pub async fn set_list(&mut self, v: &Vec<String>) -> RedisResult<()> {
self.rconn.del(&self.key).await?;
self.rconn.rpush(&self.key, v).await
}
pub async fn get_list(&mut self) -> RedisResult<Vec<String>> {
self.rconn.lrange(&self.key, 0, -1).await
}
}
pub struct PollVote {
key: String,
rconn: RdsConn,
}
impl PollVote {
init!(i32, usize, "hole_thu:poll_votes:{}:{}");
add!(&str);
has!(&str);
pub async fn count(&mut self) -> RedisResult<usize> {
self.rconn.scard(&self.key).await
}
}
pub(crate) use init;

View File

@@ -27,8 +27,8 @@ def mig_post():
r[3] = r[3] or '' # cw r[3] = r[3] or '' # cw
r[4] = r[4] or '' # author_title r[4] = r[4] or '' # author_title
r[8] = r[8] or r[7] # comment_timestamp r[8] = r[8] or r[7] # comment_timestamp
r[7] = datetime.fromtimestamp(r[7]) r[7] = datetime.fromtimestamp(r[7]).astimezone()
r[8] = datetime.fromtimestamp(r[8]) r[8] = datetime.fromtimestamp(r[8]).astimezone()
r[9] = bool(r[9]) r[9] = bool(r[9])
r[10] = bool(r[10] or False) # comment r[10] = bool(r[10] or False) # comment
r[12] = bool(r[12]) r[12] = bool(r[12])
@@ -82,7 +82,7 @@ def mig_comment():
for r in rs: for r in rs:
r = list(r) r = list(r)
r[2] = r[2] or '' r[2] = r[2] or ''
r[4] = datetime.fromtimestamp(r[4]) r[4] = datetime.fromtimestamp(r[4]).astimezone()
r[5] = bool(r[5] or False) r[5] = bool(r[5] or False)
r.insert(6, searchable[r[6]]) r.insert(6, searchable[r[6]])
r.insert(3, r[3].startswith('[tmp]\n')) r.insert(3, r[3].startswith('[tmp]\n'))