Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c024bfd6b | |||
| dd3c208fe1 | |||
| 4ed92568d8 | |||
| b57e1a9233 | |||
| d6eaec9111 | |||
| ffd30f3c03 | |||
| 8ffbd6fa31 | |||
| caec3e1930 | |||
| 79e9a492a7 | |||
| f5b0dbdbac | |||
| 5bef37cd62 | |||
| b2cf8475c5 | |||
| cbf933e74f | |||
| fb9648456a | |||
| ce3379c5ae | |||
| 59714bfbef | |||
| 38bacc1ee0 | |||
| 9dbb02a838 | |||
| c8b8ad0787 | |||
| cc32c318a7 |
10
.env.sample
Normal file
10
.env.sample
Normal 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
|
||||
18
Cargo.toml
18
Cargo.toml
@@ -6,18 +6,22 @@ license = "AGPL-3.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
mastlogin = ["url", "reqwest"]
|
||||
|
||||
[dependencies]
|
||||
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_migrations = "1.4.0"
|
||||
tokio = "1.17.0"
|
||||
redis = { version="0.21.5", features = ["aio", "tokio-comp"] }
|
||||
chrono = { version="0.*", features =["serde"] }
|
||||
rand = "0.*"
|
||||
dotenv = "0.*"
|
||||
sha2 = "0.*"
|
||||
chrono = { version="0.4.19", features = ["serde"] }
|
||||
rand = "0.8.5"
|
||||
dotenv = "0.15.0"
|
||||
sha2 = "0.10.2"
|
||||
log = "0.4.16"
|
||||
env_logger = "0.9.0"
|
||||
|
||||
[dependencies.rocket_sync_db_pools]
|
||||
version = "0.1.0-rc.1"
|
||||
features = ["diesel_postgres_pool"]
|
||||
url = { version="2.2.2",optional = true }
|
||||
reqwest = { version = "0.11.10", features = ["json"], optional = true }
|
||||
|
||||
48
README.md
48
README.md
@@ -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
|
||||
```
|
||||
|
||||
执行 (替换`'hole_pass'`为实际希望使用的密码):
|
||||
|
||||
```postgresql
|
||||
postgres=# CREATE USER hole WITH PASSWORD 'hole_pass';
|
||||
CREATE ROLE
|
||||
@@ -20,7 +32,39 @@ hole_v2=# CREATE EXTENSION pg_trgm;
|
||||
CREATE EXTENSION
|
||||
hole_v2=# \q
|
||||
```
|
||||
### 运行
|
||||
|
||||
创建 .env 文件,写入必要的环境变量。可参考 .env.sample。
|
||||
|
||||
#### 基于二进制文件
|
||||
|
||||
从[release](https://git.thu.monster/newthuhole/hole-backend-rust/releases)直接下载二进制文件
|
||||
|
||||
```
|
||||
./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`
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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::libs::diesel_logger::LoggingConnection;
|
||||
use crate::models::*;
|
||||
use crate::rds_conn::RdsConn;
|
||||
use crate::rds_models::*;
|
||||
use crate::schema;
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use rocket::form::Form;
|
||||
use rocket::serde::json::{json, Value};
|
||||
use rocket::serde::json::json;
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct AttentionInput {
|
||||
@@ -20,13 +23,15 @@ pub async fn attention_post(
|
||||
user: CurrentUser,
|
||||
db: Db,
|
||||
rconn: RdsConn,
|
||||
) -> API<Value> {
|
||||
user.id.ok_or_else(|| APIError::PcError(NotAllowed))?;
|
||||
let mut p = Post::get(&db, ai.pid).await?;
|
||||
) -> JsonAPI {
|
||||
// 临时用户不允许手动关注
|
||||
user.id.ok_or_else(|| YouAreTmp)?;
|
||||
|
||||
let mut p = Post::get(&db, &rconn, ai.pid).await?;
|
||||
p.check_permission(&user, "r")?;
|
||||
let mut att = Attention::init(&user.namehash, &rconn);
|
||||
let switch_to = ai.switch == 1;
|
||||
let mut delta: i32 = 0;
|
||||
let delta: i32;
|
||||
if att.has(ai.pid).await? != switch_to {
|
||||
if switch_to {
|
||||
att.add(ai.pid).await?;
|
||||
@@ -35,28 +40,34 @@ pub async fn attention_post(
|
||||
att.remove(ai.pid).await?;
|
||||
delta = -1;
|
||||
}
|
||||
p = p.change_n_attentions(&db, delta).await?;
|
||||
p = p.change_hot_score(&db, delta * 2).await?;
|
||||
update!(
|
||||
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;
|
||||
}
|
||||
|
||||
Ok(json!({
|
||||
"code": 0,
|
||||
"attention": ai.switch == 1,
|
||||
"n_attentions": p.n_attentions + delta,
|
||||
"n_attentions": p.n_attentions,
|
||||
// for old version frontend
|
||||
"likenum": p.n_attentions + delta,
|
||||
"likenum": p.n_attentions,
|
||||
}))
|
||||
}
|
||||
|
||||
#[get("/getattention")]
|
||||
pub async fn get_attention(user: CurrentUser, db: Db, rconn: RdsConn) -> API<Value> {
|
||||
let ids = Attention::init(&user.namehash, &rconn).all().await?;
|
||||
let ps = Post::get_multi(&db, ids).await?;
|
||||
pub async fn get_attention(user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
|
||||
let mut ids = Attention::init(&user.namehash, &rconn).all().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;
|
||||
|
||||
Ok(json!({
|
||||
"code": 0,
|
||||
"data": ps_data,
|
||||
}))
|
||||
code0!(ps_data)
|
||||
}
|
||||
|
||||
@@ -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::libs::diesel_logger::LoggingConnection;
|
||||
use crate::models::*;
|
||||
use crate::rds_conn::RdsConn;
|
||||
use crate::rds_models::*;
|
||||
use crate::schema;
|
||||
use chrono::{offset::Utc, DateTime};
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use rocket::form::Form;
|
||||
use rocket::futures::{future::TryFutureExt, try_join};
|
||||
use rocket::serde::{
|
||||
json::{json, Value},
|
||||
Serialize,
|
||||
};
|
||||
use rocket::futures::future;
|
||||
use rocket::futures::join;
|
||||
use rocket::serde::{json::json, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct CommentInput {
|
||||
pid: i32,
|
||||
#[field(validate = len(1..4097))]
|
||||
#[field(validate = len(1..12289))]
|
||||
text: String,
|
||||
use_title: Option<i8>,
|
||||
}
|
||||
@@ -30,54 +31,74 @@ pub struct CommentOutput {
|
||||
name_id: i32,
|
||||
is_tmp: bool,
|
||||
create_time: DateTime<Utc>,
|
||||
is_blocked: bool,
|
||||
blocked_count: Option<i32>,
|
||||
// for old version frontend
|
||||
timestamp: i64,
|
||||
blocked: bool,
|
||||
}
|
||||
|
||||
pub fn c2output<'r>(
|
||||
pub async fn c2output<'r>(
|
||||
p: &'r Post,
|
||||
cs: &Vec<Comment>,
|
||||
user: &CurrentUser,
|
||||
rconn: &RdsConn,
|
||||
) -> Vec<CommentOutput> {
|
||||
let mut hash2id = HashMap::<&String, i32>::from([(&p.author_hash, 0)]);
|
||||
cs.iter()
|
||||
.filter_map(|c| {
|
||||
let name_id: i32 = match hash2id.get(&c.author_hash) {
|
||||
Some(id) => *id,
|
||||
None => {
|
||||
let x = hash2id.len().try_into().unwrap();
|
||||
hash2id.insert(&c.author_hash, x);
|
||||
x
|
||||
}
|
||||
};
|
||||
if c.is_deleted {
|
||||
// TODO: block
|
||||
None
|
||||
} else {
|
||||
Some(CommentOutput {
|
||||
cid: c.id,
|
||||
text: format!("{}{}", if c.is_tmp { "[tmp]\n" } else { "" }, c.content),
|
||||
author_title: c.author_title.to_string(),
|
||||
can_del: c.check_permission(user, "wd").is_ok(),
|
||||
name_id: name_id,
|
||||
is_tmp: c.is_tmp,
|
||||
create_time: c.create_time,
|
||||
timestamp: c.create_time.timestamp(),
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
let name_ids_iter = cs.iter().map(|c| match hash2id.get(&c.author_hash) {
|
||||
Some(id) => *id,
|
||||
None => {
|
||||
let x = hash2id.len().try_into().unwrap();
|
||||
hash2id.insert(&c.author_hash, x);
|
||||
x
|
||||
}
|
||||
});
|
||||
future::join_all(cs.iter().zip(name_ids_iter).map(|(c, name_id)| async move {
|
||||
if c.is_deleted {
|
||||
None
|
||||
} 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 {
|
||||
cid: c.id,
|
||||
text: format!(
|
||||
"{}{}",
|
||||
if c.is_tmp { "[tmp]\n" } else { "" },
|
||||
if can_view { &c.content } else { "" }
|
||||
),
|
||||
author_title: c.author_title.to_string(),
|
||||
can_del: c.check_permission(user, "wd").is_ok(),
|
||||
name_id: name_id,
|
||||
is_tmp: c.is_tmp,
|
||||
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(),
|
||||
blocked: is_blocked,
|
||||
})
|
||||
}
|
||||
}))
|
||||
.await
|
||||
.into_iter()
|
||||
.filter_map(|x| x)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[get("/getcomment?<pid>")]
|
||||
pub async fn get_comment(pid: i32, user: CurrentUser, db: Db, rconn: RdsConn) -> API<Value> {
|
||||
let p = Post::get(&db, pid).await?;
|
||||
pub async fn get_comment(pid: i32, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
|
||||
let p = Post::get(&db, &rconn, pid).await?;
|
||||
if p.is_deleted {
|
||||
return Err(APIError::PcError(IsDeleted));
|
||||
}
|
||||
let pid = p.id;
|
||||
let cs = Comment::gets_by_post_id(&db, pid).await?;
|
||||
let data = c2output(&p, &cs, &user);
|
||||
let cs = p.get_comments(&db, &rconn).await?;
|
||||
let data = c2output(&p, &cs, &user, &rconn).await;
|
||||
|
||||
Ok(json!({
|
||||
"code": 0,
|
||||
@@ -95,39 +116,52 @@ pub async fn add_comment(
|
||||
user: CurrentUser,
|
||||
db: Db,
|
||||
rconn: RdsConn,
|
||||
) -> API<Value> {
|
||||
let mut p = Post::get(&db, ci.pid).await?;
|
||||
Comment::create(
|
||||
) -> JsonAPI {
|
||||
let mut p = Post::get(&db, &rconn, ci.pid).await?;
|
||||
let c = Comment::create(
|
||||
&db,
|
||||
NewComment {
|
||||
content: ci.text.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(),
|
||||
post_id: ci.pid,
|
||||
},
|
||||
)
|
||||
.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? {
|
||||
hs_delta += 2;
|
||||
try_join!(
|
||||
att.add(p.id).err_into::<APIError>(),
|
||||
async {
|
||||
p = p.change_n_attentions(&db, 1).await?;
|
||||
Ok::<(), APIError>(())
|
||||
}
|
||||
.err_into::<APIError>(),
|
||||
)?;
|
||||
hs_delta = 3;
|
||||
at_delta = 1;
|
||||
att.add(p.id).await?;
|
||||
} else {
|
||||
hs_delta = 1;
|
||||
at_delta = 0;
|
||||
}
|
||||
|
||||
p = p.change_hot_score(&db, hs_delta).await?;
|
||||
p.refresh_cache(&rconn, false).await;
|
||||
update!(
|
||||
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!({
|
||||
"code": 0
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
use crate::db_conn::Db;
|
||||
use crate::libs::diesel_logger::LoggingConnection;
|
||||
use crate::models::*;
|
||||
use crate::random_hasher::RandomHasher;
|
||||
use crate::rds_conn::RdsConn;
|
||||
use crate::rds_models::BannedUsers;
|
||||
use crate::schema;
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
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, Value};
|
||||
|
||||
macro_rules! code0 {
|
||||
() => (
|
||||
Ok(json!({"code": 0}))
|
||||
);
|
||||
|
||||
($data:expr) => (
|
||||
Ok(json!({
|
||||
"code": 0,
|
||||
"data": $data,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[catch(401)]
|
||||
pub fn catch_401_error() -> &'static str {
|
||||
"未登录或token过期"
|
||||
}
|
||||
|
||||
#[catch(403)]
|
||||
pub fn catch_403_error() -> &'static str {
|
||||
"可能被封禁了,等下次重置吧"
|
||||
}
|
||||
|
||||
pub struct CurrentUser {
|
||||
id: Option<i32>, // tmp user has no id, only for block
|
||||
namehash: String,
|
||||
@@ -25,34 +47,40 @@ impl<'r> FromRequest<'r> for CurrentUser {
|
||||
type Error = ();
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
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") {
|
||||
let sp = token.split('_').collect::<Vec<&str>>();
|
||||
if sp.len() == 2 && sp[0] == rh.get_tmp_token() {
|
||||
let namehash = rh.hash_with_salt(sp[1]);
|
||||
cu = Some(CurrentUser {
|
||||
id: None,
|
||||
custom_title: format!("TODO: {}", &namehash),
|
||||
namehash: namehash,
|
||||
is_admin: false,
|
||||
});
|
||||
namehash = Some(rh.hash_with_salt(sp[1]));
|
||||
id = None;
|
||||
is_admin = false;
|
||||
} else {
|
||||
let db = try_outcome!(request.guard::<Db>().await);
|
||||
let rconn = try_outcome!(request.guard::<RdsConn>().await);
|
||||
if let Some(user) = User::get_by_token_with_cache(&db, &rconn, token).await {
|
||||
let namehash = rh.hash_with_salt(&user.name);
|
||||
cu = Some(CurrentUser {
|
||||
id: Some(user.id),
|
||||
custom_title: format!("TODO: {}", &namehash),
|
||||
namehash: namehash,
|
||||
is_admin: user.is_admin,
|
||||
});
|
||||
if let Some(u) = User::get_by_token(&db, &rconn, token).await {
|
||||
id = Some(u.id);
|
||||
namehash = Some(rh.hash_with_salt(&u.name));
|
||||
is_admin = u.is_admin;
|
||||
}
|
||||
}
|
||||
}
|
||||
match cu {
|
||||
Some(u) => Outcome::Success(u),
|
||||
match namehash {
|
||||
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, ())),
|
||||
}
|
||||
}
|
||||
@@ -63,6 +91,8 @@ pub enum PolicyError {
|
||||
IsReported,
|
||||
IsDeleted,
|
||||
NotAllowed,
|
||||
TitleUsed,
|
||||
YouAreTmp,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -74,7 +104,6 @@ pub enum APIError {
|
||||
|
||||
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,
|
||||
@@ -92,6 +121,8 @@ impl<'r> Responder<'r, 'static> for APIError {
|
||||
PolicyError::IsReported => "内容被举报,处理中",
|
||||
PolicyError::IsDeleted => "内容被删除",
|
||||
PolicyError::NotAllowed => "不允许的操作",
|
||||
PolicyError::TitleUsed => "头衔已被使用",
|
||||
PolicyError::YouAreTmp => "临时用户只可发布内容和进入单个洞"
|
||||
}
|
||||
})
|
||||
.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 JsonAPI = API<Value>;
|
||||
|
||||
@@ -120,7 +157,7 @@ pub trait UGC {
|
||||
fn get_is_deleted(&self) -> bool;
|
||||
fn get_is_reported(&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<()> {
|
||||
if user.is_admin {
|
||||
return Ok(());
|
||||
@@ -140,10 +177,10 @@ pub trait UGC {
|
||||
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")?;
|
||||
|
||||
let _ = self.do_set_deleted(db).await?;
|
||||
self.do_set_deleted(db).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -162,8 +199,9 @@ impl UGC for Post {
|
||||
fn extra_delete_condition(&self) -> bool {
|
||||
self.n_comments == 0
|
||||
}
|
||||
async fn do_set_deleted(&self, db: &Db) -> API<usize> {
|
||||
self.set_deleted(db).await.map_err(From::from)
|
||||
async fn do_set_deleted(&mut self, db: &Db) -> API<()> {
|
||||
update!(*self, posts, db, { is_deleted, to true });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,8 +219,9 @@ impl UGC for Comment {
|
||||
fn extra_delete_condition(&self) -> bool {
|
||||
true
|
||||
}
|
||||
async fn do_set_deleted(&self, db: &Db) -> API<usize> {
|
||||
self.set_deleted(db).await.map_err(From::from)
|
||||
async fn do_set_deleted(&mut self, db: &Db) -> API<()> {
|
||||
update!(*self, comments, db, { is_deleted, to true });
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,3 +237,4 @@ pub mod operation;
|
||||
pub mod post;
|
||||
pub mod search;
|
||||
pub mod systemlog;
|
||||
pub mod vote;
|
||||
|
||||
@@ -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::rds_conn::RdsConn;
|
||||
use crate::libs::diesel_logger::LoggingConnection;
|
||||
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::serde::json::{json, Value};
|
||||
use rocket::serde::json::json;
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct DeleteInput {
|
||||
@@ -14,24 +19,152 @@ pub struct DeleteInput {
|
||||
}
|
||||
|
||||
#[post("/delete", data = "<di>")]
|
||||
pub async fn delete(di: Form<DeleteInput>, user: CurrentUser, db: Db, rconn: RdsConn) -> API<Value> {
|
||||
match di.id_type.as_str() {
|
||||
pub async fn delete(di: Form<DeleteInput>, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
|
||||
let (author_hash, p) = match di.id_type.as_str() {
|
||||
"cid" => {
|
||||
let c = Comment::get(&db, di.id).await?;
|
||||
let mut c = Comment::get(&db, di.id).await?;
|
||||
c.soft_delete(&user, &db).await?;
|
||||
let mut p = Post::get(&db, c.post_id).await?;
|
||||
p = p.change_n_comments(&db, -1).await?;
|
||||
p = p.change_hot_score(&db, -2).await?;
|
||||
let mut p = Post::get(&db, &rconn, c.post_id).await?;
|
||||
update!(
|
||||
p,
|
||||
posts,
|
||||
&db,
|
||||
{ n_comments, add -1 },
|
||||
{ hot_score, add -1 }
|
||||
);
|
||||
|
||||
p.refresh_cache(&rconn, false).await;
|
||||
p.clear_comments_cache(&rconn).await;
|
||||
|
||||
(c.author_hash.clone(), p)
|
||||
}
|
||||
"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?;
|
||||
|
||||
// 如果是删除,需要也从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!({
|
||||
"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)?
|
||||
}
|
||||
}
|
||||
|
||||
127
src/api/post.rs
127
src/api/post.rs
@@ -1,22 +1,31 @@
|
||||
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::libs::diesel_logger::LoggingConnection;
|
||||
use crate::models::*;
|
||||
use crate::rds_conn::RdsConn;
|
||||
use crate::rds_models::*;
|
||||
use crate::schema;
|
||||
use chrono::{offset::Utc, DateTime};
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||
use rocket::form::Form;
|
||||
use rocket::futures::future;
|
||||
use rocket::serde::{json::json, Serialize};
|
||||
use rocket::serde::{
|
||||
json::{json, Value},
|
||||
Serialize,
|
||||
};
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct PostInput {
|
||||
#[field(validate = len(1..4097))]
|
||||
#[field(validate = len(1..12289))]
|
||||
text: String,
|
||||
#[field(validate = len(0..33))]
|
||||
#[field(validate = len(0..97))]
|
||||
cw: String,
|
||||
allow_search: Option<i8>,
|
||||
use_title: Option<i8>,
|
||||
#[field(validate = len(0..97))]
|
||||
poll_options: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -37,70 +46,80 @@ pub struct PostOutput {
|
||||
can_del: bool,
|
||||
attention: bool,
|
||||
hot_score: Option<i32>,
|
||||
is_blocked: bool,
|
||||
blocked_count: Option<i32>,
|
||||
poll: Option<Value>,
|
||||
// for old version frontend
|
||||
timestamp: i64,
|
||||
likenum: i32,
|
||||
reply: i32,
|
||||
blocked: bool,
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct CwInput {
|
||||
pid: i32,
|
||||
#[field(validate = len(0..33))]
|
||||
#[field(validate = len(0..97))]
|
||||
cw: String,
|
||||
}
|
||||
|
||||
async fn p2output(
|
||||
p: &Post,
|
||||
user: &CurrentUser,
|
||||
db: &Db,
|
||||
rconn: &RdsConn,
|
||||
) -> PostOutput {
|
||||
async fn p2output(p: &Post, user: &CurrentUser, db: &Db, rconn: &RdsConn) -> PostOutput {
|
||||
let is_blocked = BlockedUsers::check_blocked(rconn, user.id, &user.namehash, &p.author_hash)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let can_view = !is_blocked && user.id.is_some() || user.namehash.eq(&p.author_hash);
|
||||
PostOutput {
|
||||
pid: p.id,
|
||||
text: format!("{}{}", if p.is_tmp { "[tmp]\n" } else { "" }, p.content),
|
||||
cw: if p.cw.len() > 0 {
|
||||
Some(p.cw.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
text: format!(
|
||||
"{}{}",
|
||||
if p.is_tmp { "[tmp]\n" } else { "" },
|
||||
if can_view { &p.content } else { "" }
|
||||
),
|
||||
cw: (!p.cw.is_empty()).then(|| p.cw.to_string()),
|
||||
n_attentions: p.n_attentions,
|
||||
n_comments: p.n_comments,
|
||||
create_time: p.create_time,
|
||||
last_comment_time: p.last_comment_time,
|
||||
allow_search: p.allow_search,
|
||||
author_title: if p.author_title.len() > 0 {
|
||||
Some(p.author_title.to_string())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
author_title: (!p.author_title.is_empty()).then(|| p.author_title.to_string()),
|
||||
is_tmp: p.is_tmp,
|
||||
is_reported: if user.is_admin {
|
||||
Some(p.is_reported)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
is_reported: user.is_admin.then(|| p.is_reported),
|
||||
comments: if p.n_comments > 50 {
|
||||
None
|
||||
} else {
|
||||
// 单个洞还有查询评论的接口,这里挂了不用报错
|
||||
let pid = p.id;
|
||||
if let Some(cs) = Comment::gets_by_post_id(db, pid).await.ok() {
|
||||
Some(c2output(p, &cs, user))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(
|
||||
c2output(
|
||||
p,
|
||||
&p.get_comments(db, rconn).await.unwrap_or(vec![]),
|
||||
user,
|
||||
rconn,
|
||||
)
|
||||
.await,
|
||||
)
|
||||
},
|
||||
can_del: p.check_permission(user, "wd").is_ok(),
|
||||
attention: Attention::init(&user.namehash, &rconn)
|
||||
.has(p.id)
|
||||
.await
|
||||
.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
|
||||
timestamp: p.create_time.timestamp(),
|
||||
likenum: p.n_attentions,
|
||||
reply: p.n_comments,
|
||||
blocked: is_blocked,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,8 +138,7 @@ pub async fn ps2outputs(
|
||||
|
||||
#[get("/getone?<pid>")]
|
||||
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_with_cache(&db, &rconn, pid).await?;
|
||||
let p = Post::get(&db, &rconn, pid).await?;
|
||||
p.check_permission(&user, "ro")?;
|
||||
Ok(json!({
|
||||
"data": p2output(&p, &user,&db, &rconn).await,
|
||||
@@ -136,14 +154,16 @@ pub async fn get_list(
|
||||
db: Db,
|
||||
rconn: RdsConn,
|
||||
) -> JsonAPI {
|
||||
user.id.ok_or_else(|| YouAreTmp)?;
|
||||
let page = p.unwrap_or(1);
|
||||
let page_size = 25;
|
||||
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;
|
||||
Ok(json!({
|
||||
"data": ps_data,
|
||||
"count": ps_data.len(),
|
||||
"custom_title": CustomTitle::get(&rconn, &user.namehash).await?,
|
||||
"code": 0
|
||||
}))
|
||||
}
|
||||
@@ -161,7 +181,12 @@ pub async fn publish_post(
|
||||
content: poi.text.to_string(),
|
||||
cw: poi.cw.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(),
|
||||
n_attentions: 1,
|
||||
allow_search: poi.allow_search.is_some(),
|
||||
@@ -169,25 +194,29 @@ pub async fn publish_post(
|
||||
)
|
||||
.await?;
|
||||
Attention::init(&user.namehash, &rconn).add(p.id).await?;
|
||||
Ok(json!({
|
||||
"code": 0
|
||||
}))
|
||||
p.refresh_cache(&rconn, true).await;
|
||||
|
||||
if !poi.poll_options.is_empty() {
|
||||
PollOption::init(p.id, &rconn)
|
||||
.set_list(&poi.poll_options)
|
||||
.await?;
|
||||
}
|
||||
code0!()
|
||||
}
|
||||
|
||||
#[post("/editcw", data = "<cwi>")]
|
||||
pub async fn edit_cw(cwi: Form<CwInput>, user: CurrentUser, db: Db) -> JsonAPI {
|
||||
let p = Post::get(&db, cwi.pid).await?;
|
||||
if !(user.is_admin || p.author_hash == user.namehash) {
|
||||
return Err(APIError::PcError(NotAllowed));
|
||||
}
|
||||
pub async fn edit_cw(cwi: Form<CwInput>, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonAPI {
|
||||
let mut p = Post::get(&db, &rconn, cwi.pid).await?;
|
||||
p.check_permission(&user, "w")?;
|
||||
_ = p.update_cw(&db, cwi.cw.to_string()).await?;
|
||||
Ok(json!({"code": 0}))
|
||||
update!(p, posts, &db, { cw, to cwi.cw.to_string() });
|
||||
p.refresh_cache(&rconn, false).await;
|
||||
code0!()
|
||||
}
|
||||
|
||||
#[get("/getmulti?<pids>")]
|
||||
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;
|
||||
|
||||
Ok(json!({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::api::post::ps2outputs;
|
||||
use crate::api::{CurrentUser, JsonAPI};
|
||||
use crate::api::{CurrentUser, JsonAPI, PolicyError::*};
|
||||
use crate::db_conn::Db;
|
||||
use crate::models::*;
|
||||
use crate::rds_conn::RdsConn;
|
||||
@@ -14,15 +14,21 @@ pub async fn search(
|
||||
db: Db,
|
||||
rconn: RdsConn,
|
||||
) -> JsonAPI {
|
||||
user.id.ok_or_else(|| YouAreTmp)?;
|
||||
|
||||
let page_size = 25;
|
||||
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() {
|
||||
vec![]
|
||||
} else {
|
||||
Post::search(
|
||||
&db,
|
||||
&rconn,
|
||||
search_mode,
|
||||
keywords.to_string(),
|
||||
start.into(),
|
||||
@@ -30,7 +36,6 @@ pub async fn search(
|
||||
)
|
||||
.await?
|
||||
};
|
||||
let mark_kws = if search_mode == 1 {kws} else {vec![]};
|
||||
let ps_data = ps2outputs(&ps, &user, &db, &rconn).await;
|
||||
Ok(json!({
|
||||
"data": ps_data,
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
use crate::api::{CurrentUser, API};
|
||||
use crate::api::{CurrentUser, JsonAPI};
|
||||
use crate::random_hasher::RandomHasher;
|
||||
use crate::rds_conn::RdsConn;
|
||||
use crate::rds_models::{Systemlog};
|
||||
use rocket::serde::json::{json, Value};
|
||||
use rocket::State;
|
||||
use crate::db_conn::Db;
|
||||
|
||||
#[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!({
|
||||
"tmp_token": rh.get_tmp_token(),
|
||||
"salt": look!(rh.salt),
|
||||
"start_time": rh.start_time.timestamp(),
|
||||
"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
72
src/api/vote.rs
Normal 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)
|
||||
}
|
||||
228
src/cache.rs
228
src/cache.rs
@@ -1,10 +1,17 @@
|
||||
use crate::models::{Comment, Post, User};
|
||||
use crate::rds_conn::RdsConn;
|
||||
use crate::rds_models::init;
|
||||
use rand::Rng;
|
||||
use redis::AsyncCommands;
|
||||
use rocket::serde::json::serde_json;
|
||||
// can use rocket::serde::json::to_string in master version
|
||||
|
||||
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 {
|
||||
($id: expr) => {
|
||||
format!("hole_v2:cache:post:{}", $id)
|
||||
@@ -16,11 +23,7 @@ pub struct PostCache {
|
||||
}
|
||||
|
||||
impl PostCache {
|
||||
pub fn init(rconn: &RdsConn) -> Self {
|
||||
PostCache {
|
||||
rconn: rconn.clone(),
|
||||
}
|
||||
}
|
||||
init!();
|
||||
|
||||
pub async fn sets(&mut self, ps: &Vec<&Post>) {
|
||||
if ps.is_empty() {
|
||||
@@ -28,19 +31,12 @@ impl PostCache {
|
||||
}
|
||||
let kvs: Vec<(String, String)> = ps
|
||||
.iter()
|
||||
.map(|p| (
|
||||
post_cache_key!(p.id),
|
||||
serde_json::to_string(p).unwrap(),
|
||||
) ).collect();
|
||||
dbg!(&kvs);
|
||||
let ret = self.rconn
|
||||
.set_multiple(&kvs)
|
||||
.await
|
||||
.unwrap_or_else(|e| {
|
||||
warn!("set post cache failed: {}", e);
|
||||
"x".to_string()
|
||||
});
|
||||
dbg!(ret);
|
||||
.map(|p| (post_cache_key!(p.id), serde_json::to_string(p).unwrap()))
|
||||
.collect();
|
||||
self.rconn.set_multiple(&kvs).await.unwrap_or_else(|e| {
|
||||
warn!("set post cache failed: {}", e);
|
||||
dbg!(&kvs);
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn get(&mut self, pid: &i32) -> Option<Post> {
|
||||
@@ -50,7 +46,7 @@ impl PostCache {
|
||||
.get::<String, Option<String>>(key)
|
||||
.await
|
||||
.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
|
||||
});
|
||||
|
||||
@@ -76,7 +72,7 @@ impl PostCache {
|
||||
.get::<Vec<String>, Vec<Option<String>>>(ks)
|
||||
.await
|
||||
.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()]
|
||||
});
|
||||
// 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 {
|
||||
@@ -106,12 +283,7 @@ pub struct UserCache {
|
||||
}
|
||||
|
||||
impl UserCache {
|
||||
pub fn init(token: &str, rconn: &RdsConn) -> Self {
|
||||
UserCache {
|
||||
key: format!("hole_v2:cache:user:{}", token),
|
||||
rconn: rconn.clone(),
|
||||
}
|
||||
}
|
||||
init!(&str, "hole_v2:cache:user:{}");
|
||||
|
||||
pub async fn set(&mut self, u: &User) {
|
||||
self.rconn
|
||||
@@ -122,14 +294,14 @@ impl UserCache {
|
||||
)
|
||||
.await
|
||||
.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> {
|
||||
let rds_result = self.rconn.get::<&String, String>(&self.key).await;
|
||||
if let Ok(s) = rds_result {
|
||||
debug!("hint user cache");
|
||||
self.rconn
|
||||
.expire::<&String, bool>(&self.key, INSTANCE_EXPIRE_TIME)
|
||||
.await
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
use rocket_sync_db_pools::{database, diesel};
|
||||
use diesel::Connection;
|
||||
use std::env;
|
||||
|
||||
pub type Conn = diesel::pg::PgConnection;
|
||||
|
||||
#[database("pg_v2")]
|
||||
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
114
src/login.rs
Normal 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))
|
||||
}
|
||||
48
src/main.rs
48
src/main.rs
@@ -1,3 +1,5 @@
|
||||
#![feature(concat_idents)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
||||
@@ -11,20 +13,23 @@ extern crate diesel_migrations;
|
||||
extern crate log;
|
||||
|
||||
mod api;
|
||||
mod cache;
|
||||
mod db_conn;
|
||||
mod libs;
|
||||
#[cfg(feature = "mastlogin")]
|
||||
mod login;
|
||||
mod models;
|
||||
mod random_hasher;
|
||||
mod rds_conn;
|
||||
mod rds_models;
|
||||
mod cache;
|
||||
mod schema;
|
||||
|
||||
use db_conn::{Conn, Db};
|
||||
use db_conn::{establish_connection, Conn, Db};
|
||||
use diesel::Connection;
|
||||
use random_hasher::RandomHasher;
|
||||
use rds_conn::init_rds_client;
|
||||
use rds_conn::{init_rds_client, RdsConn};
|
||||
use std::env;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
embed_migrations!("migrations/postgres");
|
||||
|
||||
@@ -36,6 +41,16 @@ async fn main() -> Result<(), rocket::Error> {
|
||||
return Ok(());
|
||||
}
|
||||
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()
|
||||
.mount(
|
||||
"/_api/v1",
|
||||
@@ -52,11 +67,29 @@ async fn main() -> Result<(), rocket::Error> {
|
||||
api::attention::get_attention,
|
||||
api::systemlog::get_systemlog,
|
||||
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(init_rds_client().await)
|
||||
.manage(rmc)
|
||||
.attach(Db::fairing())
|
||||
.launch()
|
||||
.await
|
||||
@@ -75,3 +108,8 @@ fn init_database() {
|
||||
let conn = Conn::establish(&database_url).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();
|
||||
}
|
||||
|
||||
324
src/models.rs
324
src/models.rs
@@ -1,25 +1,29 @@
|
||||
#![allow(clippy::all)]
|
||||
|
||||
use crate::cache::{PostCache, UserCache};
|
||||
use crate::db_conn::Db;
|
||||
use crate::cache::*;
|
||||
use crate::db_conn::{Conn, Db};
|
||||
use crate::libs::diesel_logger::LoggingConnection;
|
||||
use crate::random_hasher::random_string;
|
||||
use crate::rds_conn::RdsConn;
|
||||
use crate::schema::*;
|
||||
use chrono::{offset::Utc, DateTime};
|
||||
use diesel::dsl::any;
|
||||
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 std::collections::HashMap;
|
||||
use std::convert::identity;
|
||||
|
||||
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) => {
|
||||
pub async fn get(db: &Db, id: i32) -> QueryResult<Self> {
|
||||
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
|
||||
@@ -27,9 +31,9 @@ macro_rules! get {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! get_multi {
|
||||
macro_rules! _get_multi {
|
||||
($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() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
@@ -45,18 +49,29 @@ macro_rules! get_multi {
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! set_deleted {
|
||||
($table:ident) => {
|
||||
pub async fn set_deleted(&self, db: &Db) -> QueryResult<usize> {
|
||||
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
|
||||
}
|
||||
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 }), + ) => {{
|
||||
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 {
|
||||
@@ -73,7 +88,7 @@ macro_rules! with_log {
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Queryable, Insertable, Serialize, Deserialize)]
|
||||
#[derive(Queryable, Insertable, Serialize, Deserialize, Debug)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct Comment {
|
||||
pub id: i32,
|
||||
@@ -106,7 +121,7 @@ pub struct Post {
|
||||
pub allow_search: bool,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Insertable, Serialize, Deserialize)]
|
||||
#[derive(Queryable, Insertable, Serialize, Deserialize, Debug)]
|
||||
#[serde(crate = "rocket::serde")]
|
||||
pub struct User {
|
||||
pub id: i32,
|
||||
@@ -125,21 +140,14 @@ pub struct NewPost {
|
||||
pub is_tmp: bool,
|
||||
pub n_attentions: i32,
|
||||
pub allow_search: bool,
|
||||
// TODO: tags
|
||||
}
|
||||
|
||||
impl Post {
|
||||
get!(posts);
|
||||
_get!(posts);
|
||||
|
||||
get_multi!(posts);
|
||||
_get_multi!(posts);
|
||||
|
||||
set_deleted!(posts);
|
||||
|
||||
pub async fn get_multi_with_cache(
|
||||
db: &Db,
|
||||
rconn: &RdsConn,
|
||||
ids: &Vec<i32>,
|
||||
) -> QueryResult<Vec<Self>> {
|
||||
pub async fn get_multi(db: &Db, rconn: &RdsConn, ids: &Vec<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();
|
||||
@@ -153,17 +161,17 @@ impl Post {
|
||||
None => {
|
||||
id2po.insert(pid.clone(), p);
|
||||
Some(pid)
|
||||
},
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
dbg!(&missing_ids);
|
||||
let missing_ps = Self::get_multi(db, missing_ids).await?;
|
||||
// dbg!(&missing_ids);
|
||||
let missing_ps = Self::_get_multi(db, missing_ids).await?;
|
||||
// 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() {
|
||||
if let Some(op) = id2po.get_mut(&p.id) {
|
||||
@@ -171,26 +179,69 @@ impl Post {
|
||||
}
|
||||
}
|
||||
// dbg!(&cached_posts);
|
||||
Ok(
|
||||
cached_posts.into_iter().filter_map(identity).collect()
|
||||
)
|
||||
Ok(cached_posts
|
||||
.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> {
|
||||
Self::get_multi_with_cache(db, rconn, &vec![id])
|
||||
.await?
|
||||
.pop()
|
||||
.ok_or(diesel::result::Error::NotFound)
|
||||
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(&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(
|
||||
db: &Db,
|
||||
rconn: &RdsConn,
|
||||
order_mode: u8,
|
||||
start: i64,
|
||||
limit: i64,
|
||||
) -> 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| {
|
||||
let mut query = base_query!(posts);
|
||||
let mut query = base_query!(posts).select(posts::id);
|
||||
if order_mode > 0 {
|
||||
query = query.filter(posts::is_reported.eq(false))
|
||||
}
|
||||
@@ -210,52 +261,58 @@ impl Post {
|
||||
|
||||
pub async fn search(
|
||||
db: &Db,
|
||||
rconn: &RdsConn,
|
||||
search_mode: u8,
|
||||
search_text: String,
|
||||
start: i64,
|
||||
limit: i64,
|
||||
) -> QueryResult<Vec<Self>> {
|
||||
let search_text2 = search_text.replace("%", "\\%");
|
||||
db.run(move |c| {
|
||||
let pat;
|
||||
let mut query = base_query!(posts).distinct().left_join(comments::table);
|
||||
// 先用搜索+缓存,性能有问题了再真的做tag表
|
||||
query = match search_mode {
|
||||
0 => {
|
||||
pat = format!("%#{}%", &search_text2);
|
||||
query
|
||||
.filter(posts::cw.eq(&search_text))
|
||||
.or_filter(posts::cw.eq(format!("#{}", &search_text)))
|
||||
.or_filter(posts::content.like(&pat))
|
||||
.or_filter(
|
||||
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)))
|
||||
.filter(posts::allow_search.eq(true))
|
||||
}
|
||||
2 => query
|
||||
.filter(posts::author_title.eq(&search_text))
|
||||
.or_filter(comments::author_title.eq(&search_text)),
|
||||
_ => panic!("Wrong search mode!"),
|
||||
};
|
||||
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));
|
||||
// 先用搜索+缓存,性能有问题了再真的做tag表
|
||||
query = match search_mode {
|
||||
0 => {
|
||||
pat = format!("%#{}%", &search_text2);
|
||||
query
|
||||
.filter(posts::cw.eq(&search_text))
|
||||
.or_filter(posts::cw.eq(format!("#{}", &search_text)))
|
||||
.or_filter(posts::content.like(&pat))
|
||||
.or_filter(
|
||||
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)))
|
||||
.filter(posts::allow_search.eq(true))
|
||||
}
|
||||
2 => query
|
||||
.filter(posts::author_title.eq(&search_text))
|
||||
.or_filter(comments::author_title.eq(&search_text)),
|
||||
_ => panic!("Wrong search mode!"),
|
||||
};
|
||||
|
||||
query
|
||||
.order(posts::id.desc())
|
||||
.offset(start)
|
||||
.limit(limit)
|
||||
.load(with_log!(c))
|
||||
})
|
||||
.await
|
||||
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> {
|
||||
// TODO: tags
|
||||
db.run(move |c| {
|
||||
insert_into(posts::table)
|
||||
.values(&new_post)
|
||||
@@ -264,56 +321,34 @@ impl Post {
|
||||
.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) {
|
||||
PostCache::init(rconn).sets(&vec![self]).await;
|
||||
}
|
||||
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 {
|
||||
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();
|
||||
db.run(move |c| {
|
||||
users::table
|
||||
@@ -324,16 +359,48 @@ impl User {
|
||||
.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);
|
||||
if let Some(u) = cacher.get().await {
|
||||
Some(u)
|
||||
} else {
|
||||
let u = Self::get_by_token(db, token).await?;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable)]
|
||||
@@ -347,9 +414,12 @@ pub struct NewComment {
|
||||
}
|
||||
|
||||
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> {
|
||||
db.run(move |c| {
|
||||
@@ -371,3 +441,5 @@ impl Comment {
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) use {op_to_col_expr, update, with_log};
|
||||
|
||||
@@ -2,6 +2,14 @@ use chrono::{offset::Local, DateTime};
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
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 salt: String,
|
||||
pub start_time: DateTime<Local>,
|
||||
@@ -10,11 +18,7 @@ pub struct RandomHasher {
|
||||
impl RandomHasher {
|
||||
pub fn get_random_one() -> RandomHasher {
|
||||
RandomHasher {
|
||||
salt: thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(16)
|
||||
.map(char::from)
|
||||
.collect(),
|
||||
salt: random_string(16),
|
||||
start_time: Local::now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,59 @@
|
||||
use crate::rds_conn::RdsConn;
|
||||
use chrono::{offset::Local, DateTime};
|
||||
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 {
|
||||
key: String,
|
||||
@@ -7,26 +61,202 @@ pub struct Attention {
|
||||
}
|
||||
|
||||
impl Attention {
|
||||
pub fn init(namehash: &str, rconn: &RdsConn) -> Self {
|
||||
Attention {
|
||||
key: format!("hole_v2:attention:{}", namehash),
|
||||
rconn: rconn.clone(),
|
||||
}
|
||||
}
|
||||
init!(&str, "hole_v2:attention:{}");
|
||||
|
||||
pub async fn add(&mut self, pid: i32) -> RedisResult<()> {
|
||||
self.rconn.sadd(&self.key, pid).await
|
||||
}
|
||||
add!(i32);
|
||||
|
||||
has!(i32);
|
||||
|
||||
pub async fn remove(&mut self, pid: i32) -> RedisResult<()> {
|
||||
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>> {
|
||||
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;
|
||||
|
||||
@@ -27,8 +27,8 @@ def mig_post():
|
||||
r[3] = r[3] or '' # cw
|
||||
r[4] = r[4] or '' # author_title
|
||||
r[8] = r[8] or r[7] # comment_timestamp
|
||||
r[7] = datetime.fromtimestamp(r[7])
|
||||
r[8] = datetime.fromtimestamp(r[8])
|
||||
r[7] = datetime.fromtimestamp(r[7]).astimezone()
|
||||
r[8] = datetime.fromtimestamp(r[8]).astimezone()
|
||||
r[9] = bool(r[9])
|
||||
r[10] = bool(r[10] or False) # comment
|
||||
r[12] = bool(r[12])
|
||||
@@ -82,7 +82,7 @@ def mig_comment():
|
||||
for r in rs:
|
||||
r = list(r)
|
||||
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.insert(6, searchable[r[6]])
|
||||
r.insert(3, r[3].startswith('[tmp]\n'))
|
||||
|
||||
Reference in New Issue
Block a user