Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9003cefbc4 | |||
| 0487427342 | |||
| 8e386d98d0 | |||
| fae30ed97a | |||
| bdb3bc49a6 | |||
| d3d9f30b2a | |||
| 84943a3965 | |||
| 7621f56b34 | |||
| 289a19d4da | |||
| 50df8eda36 | |||
| 01f56ea0a6 | |||
| bbed041253 | |||
| 959e6caa1d | |||
| 6911038f56 | |||
| 104ccb030d | |||
| 54fa0e1cbd | |||
| f5005faedc | |||
| e531f0fed8 | |||
| dd88bbb868 | |||
| 0a5af9f35f | |||
| 1a04c41846 | |||
| 553b46b504 | |||
| b70b6b2e7f | |||
| 3b8efc0c0b | |||
| fcf6b33aa7 | |||
| d98e7fb653 | |||
| 04a0d1084f | |||
| e0d6720433 | |||
| 8fabca785c | |||
| cbf24d6eec | |||
| 503a8a3b9b | |||
| 7ab60c9975 | |||
| 791d1f526d | |||
| 2bae66099d | |||
| d5a3c3d7d7 | |||
| 8639ed0c88 | |||
| dd110be8c7 | |||
| 86b736dadb |
@@ -1,6 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
rust-toolchain
|
|
||||||
/user_files
|
/user_files
|
||||||
*.db
|
*.db
|
||||||
.env
|
.env
|
||||||
|
|||||||
18
Cargo.toml
18
Cargo.toml
@@ -11,15 +11,11 @@ default = ["mastlogin"]
|
|||||||
mastlogin = ["reqwest"]
|
mastlogin = ["reqwest"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { version = "=0.5.0-rc.1", features = ["json"] }
|
rocket = { version = "0.5.0", features = ["json"] }
|
||||||
rocket_codegen = "=0.5.0-rc.1"
|
rocket_sync_db_pools = { version = "0.1.0", features = ["diesel_postgres_pool"] }
|
||||||
rocket_http = "=0.5.0-rc.1"
|
diesel = { version = "2.1", features = ["postgres", "chrono"] }
|
||||||
rocket_sync_db_pools_codegen = "=0.1.0-rc.1"
|
diesel_migrations = "2.1"
|
||||||
rocket_sync_db_pools = { version = "=0.1.0-rc.1", features = ["diesel_postgres_pool"] }
|
redis = { version="0.24.0", features = ["aio", "tokio-comp"] }
|
||||||
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.4.19", features = ["serde"] }
|
chrono = { version="0.4.19", features = ["serde"] }
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
@@ -28,5 +24,9 @@ log = "0.4.16"
|
|||||||
env_logger = "0.9.0"
|
env_logger = "0.9.0"
|
||||||
web-push = "0.9.2"
|
web-push = "0.9.2"
|
||||||
url = "2.2.2"
|
url = "2.2.2"
|
||||||
|
futures = "0.3.24"
|
||||||
|
futures-util = "0.3.24"
|
||||||
|
lru = "0.11"
|
||||||
|
|
||||||
reqwest = { version = "0.11.10", features = ["json"], optional = true }
|
reqwest = { version = "0.11.10", features = ["json"], optional = true }
|
||||||
|
moka = { version = "0.12.15", features = ["future"] }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
FROM rust:1-bullseye as builder
|
FROM rust:1-bookworm as builder
|
||||||
WORKDIR /usr/src/
|
WORKDIR /usr/src/
|
||||||
RUN cargo new myapp --vcs none
|
RUN cargo new myapp --vcs none
|
||||||
WORKDIR /usr/src/myapp
|
WORKDIR /usr/src/myapp
|
||||||
COPY Cargo.toml ./
|
COPY Cargo.toml ./
|
||||||
|
COPY rust-toolchain ./
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
# 为了充分利用docker的缓存
|
# 为了充分利用docker的缓存
|
||||||
@@ -11,7 +12,7 @@ COPY migrations ./migrations
|
|||||||
RUN touch src/main.rs && cargo build --release
|
RUN touch src/main.rs && cargo build --release
|
||||||
|
|
||||||
|
|
||||||
FROM debian:bullseye-slim
|
FROM debian:bookworm-slim
|
||||||
RUN apt-get update && apt-get install libpq5 -y
|
RUN apt-get update && apt-get install libpq5 -y
|
||||||
COPY --from=builder /usr/src/myapp/target/release/hole-thu /usr/local/bin/hole-thu
|
COPY --from=builder /usr/src/myapp/target/release/hole-thu /usr/local/bin/hole-thu
|
||||||
COPY Rocket.toml /usr/local/bin/
|
COPY Rocket.toml /usr/local/bin/
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ clone 代码 (略)
|
|||||||
|
|
||||||
安装postgresql (略)
|
安装postgresql (略)
|
||||||
|
|
||||||
安装redis (略)
|
安装redis/valkey (略)
|
||||||
|
|
||||||
#### 准备数据库
|
#### 准备数据库
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
ALTER TABLE posts
|
||||||
|
DROP COLUMN up_votes,
|
||||||
|
DROP COLUMN down_votes
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
ALTER TABLE posts
|
||||||
|
ADD COLUMN up_votes INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN down_votes INTEGER NOT NULL DEFAULT 0
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP INDEX posts_attention_idx
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
CREATE INDEX posts_attention_idx ON posts (n_attentions)
|
||||||
@@ -1,12 +1,9 @@
|
|||||||
use crate::api::post::ps2outputs;
|
use crate::api::post::ps2outputs;
|
||||||
use crate::api::{CurrentUser, JsonApi, PolicyError::*, 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;
|
use rocket::serde::json::json;
|
||||||
use rocket::serde::json::serde_json;
|
use rocket::serde::json::serde_json;
|
||||||
@@ -34,7 +31,7 @@ pub async fn attention_post(
|
|||||||
// 临时用户不允许手动关注
|
// 临时用户不允许手动关注
|
||||||
user.id.ok_or(YouAreTmp)?;
|
user.id.ok_or(YouAreTmp)?;
|
||||||
|
|
||||||
let mut p = Post::get(&db, &rconn, ai.pid).await?;
|
let mut p = Post::get(&db, 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;
|
||||||
@@ -62,7 +59,7 @@ pub async fn attention_post(
|
|||||||
if switch_to && user.is_admin {
|
if switch_to && user.is_admin {
|
||||||
update!(p, posts, &db, { is_reported, to false });
|
update!(p, posts, &db, { is_reported, to false });
|
||||||
}
|
}
|
||||||
p.refresh_cache(&rconn, false).await;
|
p.refresh_cache(false).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
@@ -78,7 +75,14 @@ pub async fn attention_post(
|
|||||||
pub async fn get_attention(user: CurrentUser, db: Db, rconn: RdsConn) -> JsonApi {
|
pub async fn get_attention(user: CurrentUser, db: Db, rconn: RdsConn) -> JsonApi {
|
||||||
let mut ids = Attention::init(&user.namehash, &rconn).all().await?;
|
let mut ids = Attention::init(&user.namehash, &rconn).all().await?;
|
||||||
ids.sort_by_key(|x| -x);
|
ids.sort_by_key(|x| -x);
|
||||||
let ps = Post::get_multi(&db, &rconn, &ids).await?;
|
let ps: Vec<Post> = Post::get_multi(&db, &ids)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|post| {
|
||||||
|
!post.get_is_private()
|
||||||
|
|| chrono::offset::Utc::now() - post.create_time < chrono::Duration::days(30)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let ps_data = ps2outputs(&ps, &user, &db, &rconn).await?;
|
let ps_data = ps2outputs(&ps, &user, &db, &rconn).await?;
|
||||||
|
|
||||||
code0!(ps_data)
|
code0!(ps_data)
|
||||||
@@ -108,7 +112,7 @@ pub async fn set_notification(pid: i32, ni: Form<NotificatinInput>, _user: Curre
|
|||||||
.ok_or(UnknownPushEndpoint)?
|
.ok_or(UnknownPushEndpoint)?
|
||||||
.to_string();
|
.to_string();
|
||||||
(url_host.ends_with("googleapis.com") || url_host.ends_with("mozilla.com"))
|
(url_host.ends_with("googleapis.com") || url_host.ends_with("mozilla.com"))
|
||||||
.then(|| ())
|
.then_some(())
|
||||||
.ok_or(UnknownPushEndpoint)?;
|
.ok_or(UnknownPushEndpoint)?;
|
||||||
|
|
||||||
if ni.enable {
|
if ni.enable {
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
use crate::api::{ApiError, CurrentUser, JsonApi, PolicyError::*, Ugc};
|
use crate::api::{ApiError, CurrentUser, JsonApi, PolicyError::*, Ugc};
|
||||||
use crate::cache::BlockDictCache;
|
use crate::cache::BlockDictCache;
|
||||||
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 futures::{future, join};
|
||||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
|
||||||
use rocket::form::Form;
|
use rocket::form::Form;
|
||||||
use rocket::futures::future;
|
|
||||||
use rocket::futures::join;
|
|
||||||
use rocket::serde::{json::json, Serialize};
|
use rocket::serde::{json::json, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -31,18 +27,18 @@ pub struct CommentOutput {
|
|||||||
is_tmp: bool,
|
is_tmp: bool,
|
||||||
create_time: i64,
|
create_time: i64,
|
||||||
is_blocked: bool,
|
is_blocked: bool,
|
||||||
blocked_count: Option<i32>,
|
//blocked_count: Option<i32>,
|
||||||
// for old version frontend
|
// for old version frontend
|
||||||
timestamp: i64,
|
timestamp: i64,
|
||||||
blocked: bool,
|
blocked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn c2output<'r>(
|
pub async fn c2output(
|
||||||
p: &'r Post,
|
p: &Post,
|
||||||
cs: &[Comment],
|
cs: &[Comment],
|
||||||
user: &CurrentUser,
|
user: &CurrentUser,
|
||||||
cached_block_dict: &HashMap<String, bool>,
|
cached_block_dict: &HashMap<String, bool>,
|
||||||
rconn: &RdsConn,
|
//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)]);
|
||||||
let name_ids_iter = cs.iter().map(|c| match hash2id.get(&c.author_hash) {
|
let name_ids_iter = cs.iter().map(|c| match hash2id.get(&c.author_hash) {
|
||||||
@@ -69,6 +65,7 @@ pub async fn c2output<'r>(
|
|||||||
is_tmp: c.is_tmp,
|
is_tmp: c.is_tmp,
|
||||||
create_time: c.create_time.timestamp(),
|
create_time: c.create_time.timestamp(),
|
||||||
is_blocked,
|
is_blocked,
|
||||||
|
/*
|
||||||
blocked_count: if user.is_admin {
|
blocked_count: if user.is_admin {
|
||||||
BlockCounter::get_count(rconn, &c.author_hash)
|
BlockCounter::get_count(rconn, &c.author_hash)
|
||||||
.await
|
.await
|
||||||
@@ -77,6 +74,7 @@ pub async fn c2output<'r>(
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
timestamp: c.create_time.timestamp(),
|
timestamp: c.create_time.timestamp(),
|
||||||
blocked: is_blocked,
|
blocked: is_blocked,
|
||||||
})
|
})
|
||||||
@@ -90,16 +88,16 @@ pub async fn c2output<'r>(
|
|||||||
|
|
||||||
#[get("/getcomment?<pid>")]
|
#[get("/getcomment?<pid>")]
|
||||||
pub async fn get_comment(pid: i32, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonApi {
|
pub async fn get_comment(pid: i32, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonApi {
|
||||||
let p = Post::get(&db, &rconn, pid).await?;
|
let p = Post::get(&db, pid).await?;
|
||||||
if p.is_deleted {
|
if p.is_deleted {
|
||||||
return Err(ApiError::Pc(IsDeleted));
|
return Err(ApiError::Pc(IsDeleted));
|
||||||
}
|
}
|
||||||
let cs = p.get_comments(&db, &rconn).await?;
|
let cs = p.get_comments(&db).await?;
|
||||||
let hash_list = cs.iter().map(|c| &c.author_hash).collect::<Vec<_>>();
|
let hash_list = cs.iter().map(|c| &c.author_hash).collect::<Vec<_>>();
|
||||||
let cached_block_dict = BlockDictCache::init(&user.namehash, p.id, &rconn)
|
let cached_block_dict = BlockDictCache::init(&user.namehash, p.id)
|
||||||
.get_or_create(&user, &hash_list)
|
.get_or_create(&user, &hash_list, &rconn)
|
||||||
.await?;
|
.await?;
|
||||||
let data = c2output(&p, &cs, &user, &cached_block_dict, &rconn).await;
|
let data = c2output(&p, &cs, &user, &cached_block_dict).await;
|
||||||
|
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"code": 0,
|
"code": 0,
|
||||||
@@ -111,11 +109,6 @@ pub async fn get_comment(pid: i32, user: CurrentUser, db: Db, rconn: RdsConn) ->
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/docomment")]
|
|
||||||
pub async fn old_add_comment() -> ApiError {
|
|
||||||
OldApi.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/post/<pid>/comment", data = "<ci>")]
|
#[post("/post/<pid>/comment", data = "<ci>")]
|
||||||
pub async fn add_comment(
|
pub async fn add_comment(
|
||||||
pid: i32,
|
pid: i32,
|
||||||
@@ -124,17 +117,19 @@ pub async fn add_comment(
|
|||||||
db: Db,
|
db: Db,
|
||||||
rconn: RdsConn,
|
rconn: RdsConn,
|
||||||
) -> JsonApi {
|
) -> JsonApi {
|
||||||
let mut p = Post::get(&db, &rconn, pid).await?;
|
let mut p = Post::get(&db, pid).await?;
|
||||||
|
if p.author_hash != user.namehash {
|
||||||
|
user.id.ok_or(YouAreTmp)?;
|
||||||
|
}
|
||||||
|
let use_title = ci.use_title.is_some() || user.is_admin || user.is_candidate;
|
||||||
let c = 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: (if ci.use_title.is_some() {
|
author_title: user
|
||||||
CustomTitle::get(&rconn, &user.namehash).await?
|
.custom_title
|
||||||
} else {
|
.and_then(|title| use_title.then_some(title))
|
||||||
None
|
|
||||||
})
|
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
is_tmp: user.id.is_none(),
|
is_tmp: user.id.is_none(),
|
||||||
post_id: pid,
|
post_id: pid,
|
||||||
@@ -165,10 +160,7 @@ pub async fn add_comment(
|
|||||||
{ hot_score, add hs_delta }
|
{ hot_score, add hs_delta }
|
||||||
);
|
);
|
||||||
|
|
||||||
join!(
|
join!(p.refresh_cache(false), p.clear_comments_cache(),);
|
||||||
p.refresh_cache(&rconn, false),
|
|
||||||
p.clear_comments_cache(&rconn),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"code": 0
|
"code": 0
|
||||||
|
|||||||
124
src/api/mod.rs
124
src/api/mod.rs
@@ -1,12 +1,12 @@
|
|||||||
|
#![allow(clippy::unnecessary_lazy_evaluations)]
|
||||||
|
|
||||||
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::rate_limit::MainLimiters;
|
||||||
use crate::rds_conn::RdsConn;
|
use crate::rds_conn::RdsConn;
|
||||||
use crate::rds_models::*;
|
use crate::rds_models::*;
|
||||||
use crate::schema;
|
use rocket::http::{Method, Status};
|
||||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
|
||||||
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};
|
||||||
@@ -53,59 +53,88 @@ pub fn catch_403_error() -> &'static str {
|
|||||||
"可能被封禁了,等下次重置吧"
|
"可能被封禁了,等下次重置吧"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[catch(404)]
|
||||||
|
pub fn catch_404_error() -> &'static str {
|
||||||
|
"请更新前端版本"
|
||||||
|
}
|
||||||
|
|
||||||
pub struct CurrentUser {
|
pub struct CurrentUser {
|
||||||
pub id: Option<i32>, // tmp user has no id, only for block
|
pub id: Option<i32>, // tmp user has no id, only for block
|
||||||
namehash: String,
|
namehash: String,
|
||||||
is_admin: bool,
|
is_admin: bool,
|
||||||
custom_title: String,
|
is_candidate: bool,
|
||||||
|
custom_title: Option<String>,
|
||||||
|
title_secret: Option<String>,
|
||||||
pub auto_block_rank: u8,
|
pub auto_block_rank: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CurrentUser {
|
||||||
|
pub async fn from_hash(rconn: &RdsConn, namehash: String) -> Self {
|
||||||
|
let (custom_title, title_secret) = CustomTitle::get(rconn, &namehash)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.unwrap_or((None, None));
|
||||||
|
Self {
|
||||||
|
id: None,
|
||||||
|
is_admin: false,
|
||||||
|
is_candidate: false,
|
||||||
|
custom_title,
|
||||||
|
title_secret,
|
||||||
|
auto_block_rank: AutoBlockRank::get(rconn, &namehash).await.unwrap_or(2),
|
||||||
|
namehash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
impl<'r> FromRequest<'r> for CurrentUser {
|
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 rconn = try_outcome!(request.guard::<RdsConn>().await);
|
let rconn = try_outcome!(request.guard::<RdsConn>().await);
|
||||||
|
let limiters = request.rocket().state::<MainLimiters>().unwrap();
|
||||||
|
|
||||||
let mut id = None;
|
if let Some(user) = {
|
||||||
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() {
|
||||||
namehash = Some(rh.hash_with_salt(sp[1]));
|
Some(CurrentUser::from_hash(&rconn, rh.hash_with_salt(sp[1])).await)
|
||||||
id = None;
|
|
||||||
is_admin = false;
|
|
||||||
} else {
|
} else {
|
||||||
let db = try_outcome!(request.guard::<Db>().await);
|
let db = try_outcome!(request.guard::<Db>().await);
|
||||||
if let Some(u) = User::get_by_token(&db, &rconn, token).await {
|
if let Some(u) = User::get_by_token(&db, token).await {
|
||||||
id = Some(u.id);
|
let namehash = rh.hash_with_salt(&u.name);
|
||||||
namehash = Some(rh.hash_with_salt(&u.name));
|
let user_base = CurrentUser::from_hash(&rconn, namehash).await;
|
||||||
is_admin = u.is_admin;
|
Some(CurrentUser {
|
||||||
}
|
id: Some(u.id),
|
||||||
}
|
is_admin: u.is_admin
|
||||||
}
|
|| is_elected_admin(&rconn, &user_base.custom_title)
|
||||||
match namehash {
|
|
||||||
Some(nh) => {
|
|
||||||
if BannedUsers::has(&rconn, &nh).await.unwrap() {
|
|
||||||
Outcome::Failure((Status::Forbidden, ()))
|
|
||||||
} else {
|
|
||||||
Outcome::Success(CurrentUser {
|
|
||||||
id,
|
|
||||||
custom_title: CustomTitle::get(&rconn, &nh)
|
|
||||||
.await
|
.await
|
||||||
.ok()
|
.unwrap(),
|
||||||
.flatten()
|
is_candidate: is_elected_candidate(&rconn, &user_base.custom_title)
|
||||||
.unwrap_or_default(),
|
.await
|
||||||
auto_block_rank: AutoBlockRank::get(&rconn, &nh).await.unwrap_or(2),
|
.unwrap(),
|
||||||
namehash: nh,
|
..user_base
|
||||||
is_admin,
|
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Outcome::Failure((Status::Unauthorized, ())),
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} {
|
||||||
|
if BannedUsers::has(&rconn, &user.namehash).await.unwrap() {
|
||||||
|
Outcome::Error((Status::Forbidden, ()))
|
||||||
|
} else if !limiters.check(
|
||||||
|
request.method() == Method::Post,
|
||||||
|
user.id.unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
Outcome::Error((Status::TooManyRequests, ()))
|
||||||
|
} else {
|
||||||
|
Outcome::Success(user)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Outcome::Error((Status::Unauthorized, ()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,12 +142,14 @@ impl<'r> FromRequest<'r> for CurrentUser {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum PolicyError {
|
pub enum PolicyError {
|
||||||
IsReported,
|
IsReported,
|
||||||
|
IsPrivate,
|
||||||
IsDeleted,
|
IsDeleted,
|
||||||
NotAllowed,
|
NotAllowed,
|
||||||
TitleUsed,
|
TitleUsed,
|
||||||
|
TitleProtected,
|
||||||
|
InvalidTitle,
|
||||||
YouAreTmp,
|
YouAreTmp,
|
||||||
NoReason,
|
NoReason,
|
||||||
OldApi,
|
|
||||||
UnknownPushEndpoint,
|
UnknownPushEndpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,12 +173,14 @@ impl<'r> Responder<'r, 'static> for ApiError {
|
|||||||
"code": -1,
|
"code": -1,
|
||||||
"msg": match e {
|
"msg": match e {
|
||||||
PolicyError::IsReported => "内容被举报,处理中",
|
PolicyError::IsReported => "内容被举报,处理中",
|
||||||
|
PolicyError::IsPrivate => "未被设置为公开",
|
||||||
PolicyError::IsDeleted => "内容被删除",
|
PolicyError::IsDeleted => "内容被删除",
|
||||||
PolicyError::NotAllowed => "不允许的操作",
|
PolicyError::NotAllowed => "不允许的操作",
|
||||||
PolicyError::TitleUsed => "头衔已被使用",
|
PolicyError::TitleUsed => "头衔已被使用",
|
||||||
PolicyError::YouAreTmp => "临时用户只可发布内容和进入单个洞",
|
PolicyError::TitleProtected => "头衔处于保护期",
|
||||||
|
PolicyError::InvalidTitle => "头衔包含不允许的符号",
|
||||||
|
PolicyError::YouAreTmp => "临时用户只可发布内容",
|
||||||
PolicyError::NoReason => "未填写理由",
|
PolicyError::NoReason => "未填写理由",
|
||||||
PolicyError::OldApi => "请使用最新版前端地址并检查更新",
|
|
||||||
PolicyError::UnknownPushEndpoint => "未知的浏览器推送地址",
|
PolicyError::UnknownPushEndpoint => "未知的浏览器推送地址",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -194,18 +227,22 @@ pub trait Ugc {
|
|||||||
fn get_author_hash(&self) -> &str;
|
fn get_author_hash(&self) -> &str;
|
||||||
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 get_is_private(&self) -> bool;
|
||||||
fn extra_delete_condition(&self) -> bool;
|
fn extra_delete_condition(&self) -> bool;
|
||||||
async fn do_set_deleted(&mut self, db: &Db) -> Api<()>;
|
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 {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if mode.contains('r') && self.get_is_deleted() {
|
if mode.contains('r') && self.get_is_deleted() {
|
||||||
return Err(ApiError::Pc(PolicyError::IsDeleted));
|
return Err(ApiError::Pc(PolicyError::IsDeleted));
|
||||||
}
|
}
|
||||||
|
if user.is_admin {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
if mode.contains('o') && self.get_is_reported() {
|
if mode.contains('o') && self.get_is_reported() {
|
||||||
return Err(ApiError::Pc(PolicyError::IsReported));
|
return Err(ApiError::Pc(PolicyError::IsReported));
|
||||||
}
|
}
|
||||||
|
if mode.contains('o') && self.get_is_private() {
|
||||||
|
return Err(ApiError::Pc(PolicyError::IsPrivate));
|
||||||
|
}
|
||||||
if mode.contains('w') && self.get_author_hash() != user.namehash {
|
if mode.contains('w') && self.get_author_hash() != user.namehash {
|
||||||
return Err(ApiError::Pc(PolicyError::NotAllowed));
|
return Err(ApiError::Pc(PolicyError::NotAllowed));
|
||||||
}
|
}
|
||||||
@@ -231,6 +268,9 @@ impl Ugc for Post {
|
|||||||
fn get_is_reported(&self) -> bool {
|
fn get_is_reported(&self) -> bool {
|
||||||
self.is_reported
|
self.is_reported
|
||||||
}
|
}
|
||||||
|
fn get_is_private(&self) -> bool {
|
||||||
|
!(self.allow_search || self.n_attentions > 20)
|
||||||
|
}
|
||||||
fn get_is_deleted(&self) -> bool {
|
fn get_is_deleted(&self) -> bool {
|
||||||
self.is_deleted
|
self.is_deleted
|
||||||
}
|
}
|
||||||
@@ -251,6 +291,9 @@ impl Ugc for Comment {
|
|||||||
fn get_is_reported(&self) -> bool {
|
fn get_is_reported(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
fn get_is_private(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
fn get_is_deleted(&self) -> bool {
|
fn get_is_deleted(&self) -> bool {
|
||||||
self.is_deleted
|
self.is_deleted
|
||||||
}
|
}
|
||||||
@@ -273,6 +316,7 @@ pub mod attention;
|
|||||||
pub mod comment;
|
pub mod comment;
|
||||||
pub mod operation;
|
pub mod operation;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
|
pub mod reaction;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod systemlog;
|
pub mod systemlog;
|
||||||
pub mod upload;
|
pub mod upload;
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
use crate::api::{ApiError, CurrentUser, JsonApi, PolicyError::*, Ugc};
|
use crate::api::{ApiError, CurrentUser, JsonApi, PolicyError::*, Ugc};
|
||||||
use crate::cache::*;
|
use crate::cache::*;
|
||||||
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::Local;
|
use chrono::offset::Local;
|
||||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
|
||||||
use rocket::form::Form;
|
use rocket::form::Form;
|
||||||
use rocket::serde::json::json;
|
use rocket::serde::json::json;
|
||||||
|
|
||||||
@@ -25,7 +22,7 @@ pub async fn delete(di: Form<DeleteInput>, user: CurrentUser, db: Db, rconn: Rds
|
|||||||
"cid" => {
|
"cid" => {
|
||||||
let mut 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, &rconn, c.post_id).await?;
|
let mut p = Post::get(&db, c.post_id).await?;
|
||||||
update!(
|
update!(
|
||||||
p,
|
p,
|
||||||
posts,
|
posts,
|
||||||
@@ -34,13 +31,13 @@ pub async fn delete(di: Form<DeleteInput>, user: CurrentUser, db: Db, rconn: Rds
|
|||||||
{ hot_score, add -1 }
|
{ hot_score, add -1 }
|
||||||
);
|
);
|
||||||
|
|
||||||
p.refresh_cache(&rconn, false).await;
|
p.refresh_cache(false).await;
|
||||||
p.clear_comments_cache(&rconn).await;
|
p.clear_comments_cache().await;
|
||||||
|
|
||||||
(c.author_hash.clone(), p)
|
(c.author_hash.clone(), p)
|
||||||
}
|
}
|
||||||
"pid" => {
|
"pid" => {
|
||||||
let mut p = Post::get(&db, &rconn, di.id).await?;
|
let mut p = Post::get(&db, di.id).await?;
|
||||||
|
|
||||||
// 有评论:清空主楼而非删除
|
// 有评论:清空主楼而非删除
|
||||||
if p.author_hash == user.namehash && p.n_comments > 0 {
|
if p.author_hash == user.namehash && p.n_comments > 0 {
|
||||||
@@ -55,7 +52,7 @@ pub async fn delete(di: Form<DeleteInput>, user: CurrentUser, db: Db, rconn: Rds
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 如果是删除,需要也从0号缓存队列中去掉
|
// 如果是删除,需要也从0号缓存队列中去掉
|
||||||
p.refresh_cache(&rconn, true).await;
|
p.refresh_cache(true).await;
|
||||||
|
|
||||||
(p.author_hash.clone(), p)
|
(p.author_hash.clone(), p)
|
||||||
}
|
}
|
||||||
@@ -64,7 +61,7 @@ pub async fn delete(di: Form<DeleteInput>, user: CurrentUser, db: Db, rconn: Rds
|
|||||||
|
|
||||||
if user.is_admin && !user.namehash.eq(&author_hash) {
|
if user.is_admin && !user.namehash.eq(&author_hash) {
|
||||||
Systemlog {
|
Systemlog {
|
||||||
user_hash: user.namehash.clone(),
|
user_hash: user.custom_title.clone().unwrap_or(look!(user.namehash)),
|
||||||
action_type: LogType::AdminDelete,
|
action_type: LogType::AdminDelete,
|
||||||
target: format!("#{}, {}={}", p.id, di.id_type, di.id),
|
target: format!("#{}, {}={}", p.id, di.id_type, di.id),
|
||||||
detail: di.note.clone(),
|
detail: di.note.clone(),
|
||||||
@@ -75,7 +72,7 @@ pub async fn delete(di: Form<DeleteInput>, user: CurrentUser, db: Db, rconn: Rds
|
|||||||
|
|
||||||
if di.note.starts_with("!ban ") {
|
if di.note.starts_with("!ban ") {
|
||||||
Systemlog {
|
Systemlog {
|
||||||
user_hash: user.namehash.clone(),
|
user_hash: user.custom_title.unwrap_or(look!(user.namehash)),
|
||||||
action_type: LogType::Ban,
|
action_type: LogType::Ban,
|
||||||
target: look!(author_hash),
|
target: look!(author_hash),
|
||||||
detail: di.note.clone(),
|
detail: di.note.clone(),
|
||||||
@@ -108,19 +105,19 @@ pub async fn report(ri: Form<ReportInput>, user: CurrentUser, db: Db, rconn: Rds
|
|||||||
.await?
|
.await?
|
||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
< 10)
|
< 10)
|
||||||
.then(|| ())
|
.then_some(())
|
||||||
.ok_or(NotAllowed)?;
|
.ok_or(NotAllowed)?;
|
||||||
|
|
||||||
(!ri.reason.is_empty()).then(|| ()).ok_or(NoReason)?;
|
(!ri.reason.is_empty()).then_some(()).ok_or(NoReason)?;
|
||||||
|
|
||||||
let mut p = Post::get(&db, &rconn, ri.pid).await?;
|
let mut p = Post::get(&db, ri.pid).await?;
|
||||||
if ri.should_hide.is_some() {
|
if ri.should_hide.is_some() {
|
||||||
update!(p, posts, &db, { is_reported, to true });
|
update!(p, posts, &db, { is_reported, to true });
|
||||||
p.refresh_cache(&rconn, false).await;
|
p.refresh_cache(false).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Systemlog {
|
Systemlog {
|
||||||
user_hash: user.namehash.to_string(),
|
user_hash: look!(user.namehash),
|
||||||
action_type: LogType::Report,
|
action_type: LogType::Report,
|
||||||
target: format!("#{}", ri.pid),
|
target: format!("#{}", ri.pid),
|
||||||
detail: ri.reason.clone(),
|
detail: ri.reason.clone(),
|
||||||
@@ -135,7 +132,7 @@ pub async fn report(ri: Form<ReportInput>, user: CurrentUser, db: Db, rconn: Rds
|
|||||||
NewPost {
|
NewPost {
|
||||||
content: format!("[系统自动代发]\n我举报了 #{}\n理由: {}", &p.id, &ri.reason),
|
content: format!("[系统自动代发]\n我举报了 #{}\n理由: {}", &p.id, &ri.reason),
|
||||||
cw: "举报".to_string(),
|
cw: "举报".to_string(),
|
||||||
author_hash: user.namehash.to_string(),
|
author_hash: user.namehash.clone(),
|
||||||
author_title: String::default(),
|
author_title: String::default(),
|
||||||
is_tmp: false,
|
is_tmp: false,
|
||||||
n_attentions: 1,
|
n_attentions: 1,
|
||||||
@@ -145,7 +142,7 @@ pub async fn report(ri: Form<ReportInput>, user: CurrentUser, db: Db, rconn: Rds
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Attention::init(&user.namehash, &rconn).add(p.id).await?;
|
Attention::init(&user.namehash, &rconn).add(p.id).await?;
|
||||||
p.refresh_cache(&rconn, true).await;
|
p.refresh_cache(true).await;
|
||||||
|
|
||||||
code0!()
|
code0!()
|
||||||
}
|
}
|
||||||
@@ -166,7 +163,7 @@ pub async fn block(bi: Form<BlockInput>, user: CurrentUser, db: Db, rconn: RdsCo
|
|||||||
let pid;
|
let pid;
|
||||||
let nh_to_block = match bi.content_type.as_str() {
|
let nh_to_block = match bi.content_type.as_str() {
|
||||||
"post" => {
|
"post" => {
|
||||||
let p = Post::get(&db, &rconn, bi.id).await?;
|
let p = Post::get(&db, bi.id).await?;
|
||||||
pid = p.id;
|
pid = p.id;
|
||||||
p.author_hash
|
p.author_hash
|
||||||
}
|
}
|
||||||
@@ -185,12 +182,12 @@ pub async fn block(bi: Form<BlockInput>, user: CurrentUser, db: Db, rconn: RdsCo
|
|||||||
let curr = if blk.add(&nh_to_block).await? > 0 {
|
let curr = if blk.add(&nh_to_block).await? > 0 {
|
||||||
BlockCounter::count_incr(&rconn, &nh_to_block).await?
|
BlockCounter::count_incr(&rconn, &nh_to_block).await?
|
||||||
} else {
|
} else {
|
||||||
114514
|
BlockCounter::get_count(&rconn, &nh_to_block)
|
||||||
|
.await?
|
||||||
|
.unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
BlockDictCache::init(&user.namehash, pid, &rconn)
|
BlockDictCache::init(&user.namehash, pid).clear().await;
|
||||||
.clear()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"code": 0,
|
"code": 0,
|
||||||
@@ -204,15 +201,24 @@ pub async fn block(bi: Form<BlockInput>, user: CurrentUser, db: Db, rconn: RdsCo
|
|||||||
pub struct TitleInput {
|
pub struct TitleInput {
|
||||||
#[field(validate = len(1..31))]
|
#[field(validate = len(1..31))]
|
||||||
title: String,
|
title: String,
|
||||||
|
secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/title", data = "<ti>")]
|
#[post("/set-title", data = "<ti>")]
|
||||||
pub async fn set_title(ti: Form<TitleInput>, user: CurrentUser, rconn: RdsConn) -> JsonApi {
|
pub async fn set_title(ti: Form<TitleInput>, user: CurrentUser, rconn: RdsConn) -> JsonApi {
|
||||||
if CustomTitle::set(&rconn, &user.namehash, &ti.title).await? {
|
if ti.title.is_empty() {
|
||||||
code0!()
|
Err(InvalidTitle)?
|
||||||
} else {
|
|
||||||
Err(TitleUsed)?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ti.title
|
||||||
|
.chars()
|
||||||
|
.map(|c| c.is_alphanumeric().then_some(()).ok_or(InvalidTitle))
|
||||||
|
.collect::<Result<Vec<()>, PolicyError>>()?;
|
||||||
|
*/
|
||||||
|
|
||||||
|
let secret = CustomTitle::set(&rconn, &user.namehash, &ti.title, &ti.secret).await?;
|
||||||
|
code0!(secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromForm)]
|
#[derive(FromForm)]
|
||||||
|
|||||||
100
src/api/post.rs
100
src/api/post.rs
@@ -3,12 +3,9 @@ use crate::api::vote::get_poll_dict;
|
|||||||
use crate::api::{Api, CurrentUser, JsonApi, PolicyError::*, Ugc};
|
use crate::api::{Api, CurrentUser, JsonApi, PolicyError::*, Ugc};
|
||||||
use crate::cache::*;
|
use crate::cache::*;
|
||||||
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::futures::future::{self, OptionFuture};
|
use rocket::futures::future::{self, OptionFuture};
|
||||||
use rocket::serde::{
|
use rocket::serde::{
|
||||||
@@ -49,8 +46,11 @@ pub struct PostOutput {
|
|||||||
attention: bool,
|
attention: bool,
|
||||||
hot_score: Option<i32>,
|
hot_score: Option<i32>,
|
||||||
is_blocked: bool,
|
is_blocked: bool,
|
||||||
blocked_count: Option<i32>,
|
//blocked_count: Option<i32>,
|
||||||
poll: Option<Value>,
|
poll: Option<Value>,
|
||||||
|
up_votes: i32,
|
||||||
|
down_votes: i32,
|
||||||
|
reaction_status: i32, // -1, 0, 1
|
||||||
// for old version frontend
|
// for old version frontend
|
||||||
timestamp: i64,
|
timestamp: i64,
|
||||||
likenum: i32,
|
likenum: i32,
|
||||||
@@ -67,7 +67,7 @@ pub struct CwInput {
|
|||||||
|
|
||||||
async fn p2output(p: &Post, user: &CurrentUser, db: &Db, rconn: &RdsConn) -> Api<PostOutput> {
|
async fn p2output(p: &Post, user: &CurrentUser, db: &Db, rconn: &RdsConn) -> Api<PostOutput> {
|
||||||
let comments: Option<Vec<Comment>> = if p.n_comments < 5 {
|
let comments: Option<Vec<Comment>> = if p.n_comments < 5 {
|
||||||
Some(p.get_comments(db, rconn).await?)
|
Some(p.get_comments(db).await?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -78,8 +78,8 @@ async fn p2output(p: &Post, user: &CurrentUser, db: &Db, rconn: &RdsConn) -> Api
|
|||||||
.chain(std::iter::once(&p.author_hash))
|
.chain(std::iter::once(&p.author_hash))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
//dbg!(&hash_list);
|
//dbg!(&hash_list);
|
||||||
let cached_block_dict = BlockDictCache::init(&user.namehash, p.id, rconn)
|
let cached_block_dict = BlockDictCache::init(&user.namehash, p.id)
|
||||||
.get_or_create(user, &hash_list)
|
.get_or_create(user, &hash_list, rconn)
|
||||||
.await?;
|
.await?;
|
||||||
let is_blocked = cached_block_dict[&p.author_hash];
|
let is_blocked = cached_block_dict[&p.author_hash];
|
||||||
let can_view =
|
let can_view =
|
||||||
@@ -87,35 +87,43 @@ async fn p2output(p: &Post, user: &CurrentUser, db: &Db, rconn: &RdsConn) -> Api
|
|||||||
Ok(PostOutput {
|
Ok(PostOutput {
|
||||||
pid: p.id,
|
pid: p.id,
|
||||||
room_id: p.room_id,
|
room_id: p.room_id,
|
||||||
text: can_view.then(|| p.content.clone()).unwrap_or_default(),
|
text: if can_view {
|
||||||
cw: (!p.cw.is_empty()).then(|| p.cw.clone()),
|
p.content.clone()
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
},
|
||||||
|
cw: (!p.cw.is_empty()).then_some(p.cw.clone()),
|
||||||
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.timestamp(),
|
create_time: p.create_time.timestamp(),
|
||||||
last_comment_time: p.last_comment_time.timestamp(),
|
last_comment_time: p.last_comment_time.timestamp(),
|
||||||
allow_search: p.allow_search,
|
allow_search: p.allow_search,
|
||||||
author_title: (!p.author_title.is_empty()).then(|| p.author_title.clone()),
|
author_title: (!p.author_title.is_empty()).then_some(p.author_title.clone()),
|
||||||
is_tmp: p.is_tmp,
|
is_tmp: p.is_tmp,
|
||||||
is_reported: user.is_admin.then(|| p.is_reported),
|
is_reported: user.is_admin.then_some(p.is_reported),
|
||||||
comments: OptionFuture::from(
|
comments: OptionFuture::from(
|
||||||
comments
|
comments.map(|cs| async move { c2output(p, &cs, user, &cached_block_dict).await }),
|
||||||
.map(|cs| async move { c2output(p, &cs, user, &cached_block_dict, rconn).await }),
|
|
||||||
)
|
)
|
||||||
.await,
|
.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).has(p.id).await?,
|
attention: Attention::init(&user.namehash, rconn).has(p.id).await?,
|
||||||
hot_score: user.is_admin.then(|| p.hot_score),
|
hot_score: user.is_admin.then_some(p.hot_score),
|
||||||
is_blocked,
|
is_blocked,
|
||||||
|
/*
|
||||||
blocked_count: if user.is_admin {
|
blocked_count: if user.is_admin {
|
||||||
BlockCounter::get_count(rconn, &p.author_hash).await?
|
BlockCounter::get_count(rconn, &p.author_hash).await?
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
poll: if can_view {
|
poll: if can_view {
|
||||||
get_poll_dict(p.id, rconn, &user.namehash).await
|
get_poll_dict(p.id, rconn, &user.namehash).await
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
up_votes: p.up_votes,
|
||||||
|
down_votes: p.down_votes,
|
||||||
|
reaction_status: get_user_post_reaction_status(rconn, p.id, &user.namehash).await?,
|
||||||
// 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,
|
||||||
@@ -139,7 +147,8 @@ 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, &rconn, pid).await?;
|
user.id.ok_or(YouAreTmp)?;
|
||||||
|
let p = Post::get(&db, 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?,
|
||||||
@@ -160,21 +169,24 @@ pub async fn get_list(
|
|||||||
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(
|
let ps: Vec<Post> =
|
||||||
&db,
|
Post::gets_by_page(&db, room_id, order_mode, start as usize, page_size as usize)
|
||||||
&rconn,
|
.await?
|
||||||
room_id,
|
.into_iter()
|
||||||
order_mode,
|
.filter(|post| page < 40 || !post.get_is_private())
|
||||||
start.into(),
|
.collect();
|
||||||
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": user.custom_title,
|
"custom_title": user.custom_title,
|
||||||
|
"title_secret": user.title_secret,
|
||||||
|
"is_admin": user.is_admin,
|
||||||
|
"is_candidate": user.is_candidate,
|
||||||
"auto_block_rank": user.auto_block_rank,
|
"auto_block_rank": user.auto_block_rank,
|
||||||
|
"announcement": get_announcement(&rconn).await?,
|
||||||
"code": 0
|
"code": 0
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -186,31 +198,34 @@ pub async fn publish_post(
|
|||||||
db: Db,
|
db: Db,
|
||||||
rconn: RdsConn,
|
rconn: RdsConn,
|
||||||
) -> JsonApi {
|
) -> JsonApi {
|
||||||
let text = if poi.room_id.is_none() {
|
let use_title = poi.use_title.is_some() || user.is_admin || user.is_candidate;
|
||||||
format!(
|
|
||||||
"{}\n\n---\n\n\\* 无效分区或来自旧版前端,已默认归档到0区。分区管理说明见 #100426,建议尽快更新前端(点击ⓘ ,点击“立即更新”)。",
|
let is_tmp = user.id.is_none();
|
||||||
&poi.text
|
let room_id = if is_tmp {
|
||||||
)
|
0
|
||||||
} else {
|
} else {
|
||||||
poi.text.to_string()
|
poi.room_id.unwrap_or_default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let p = Post::create(
|
let p = Post::create(
|
||||||
&db,
|
&db,
|
||||||
NewPost {
|
NewPost {
|
||||||
content: text,
|
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: poi.use_title.map(|_| user.custom_title).unwrap_or_default(),
|
author_title: user
|
||||||
is_tmp: user.id.is_none(),
|
.custom_title
|
||||||
|
.and_then(|title| use_title.then_some(title))
|
||||||
|
.unwrap_or_default(),
|
||||||
|
is_tmp,
|
||||||
n_attentions: 1,
|
n_attentions: 1,
|
||||||
allow_search: poi.allow_search.is_some(),
|
allow_search: poi.allow_search.is_some(),
|
||||||
room_id: poi.room_id.unwrap_or_default(),
|
room_id,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Attention::init(&user.namehash, &rconn).add(p.id).await?;
|
Attention::init(&user.namehash, &rconn).add(p.id).await?;
|
||||||
p.refresh_cache(&rconn, true).await;
|
p.refresh_cache(true).await;
|
||||||
|
|
||||||
if !poi.poll_options.is_empty() {
|
if !poi.poll_options.is_empty() {
|
||||||
PollOption::init(p.id, &rconn)
|
PollOption::init(p.id, &rconn)
|
||||||
@@ -221,18 +236,25 @@ pub async fn publish_post(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/editcw", data = "<cwi>")]
|
#[post("/editcw", data = "<cwi>")]
|
||||||
pub async fn edit_cw(cwi: Form<CwInput>, user: CurrentUser, db: Db, rconn: RdsConn) -> JsonApi {
|
pub async fn edit_cw(cwi: Form<CwInput>, user: CurrentUser, db: Db) -> JsonApi {
|
||||||
let mut p = Post::get(&db, &rconn, cwi.pid).await?;
|
let mut p = Post::get(&db, cwi.pid).await?;
|
||||||
p.check_permission(&user, "w")?;
|
p.check_permission(&user, "w")?;
|
||||||
update!(p, posts, &db, { cw, to cwi.cw.to_string() });
|
update!(p, posts, &db, { cw, to cwi.cw.to_string() });
|
||||||
p.refresh_cache(&rconn, false).await;
|
p.refresh_cache(false).await;
|
||||||
code0!()
|
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 {
|
||||||
user.id.ok_or(YouAreTmp)?;
|
user.id.ok_or(YouAreTmp)?;
|
||||||
let ps = Post::get_multi(&db, &rconn, &pids).await?;
|
let ps: Vec<Post> = Post::get_multi(&db, &pids)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter(|post| {
|
||||||
|
!post.get_is_private()
|
||||||
|
|| chrono::offset::Utc::now() - post.create_time < chrono::Duration::days(30)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let ps_data = ps2outputs(&ps, &user, &db, &rconn).await?;
|
let ps_data = ps2outputs(&ps, &user, &db, &rconn).await?;
|
||||||
|
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
|
|||||||
65
src/api/reaction.rs
Normal file
65
src/api/reaction.rs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
use crate::api::{CurrentUser, JsonApi, PolicyError::*, Ugc};
|
||||||
|
use crate::db_conn::Db;
|
||||||
|
use crate::models::*;
|
||||||
|
use crate::rds_conn::RdsConn;
|
||||||
|
use crate::rds_models::*;
|
||||||
|
use rocket::form::Form;
|
||||||
|
use rocket::serde::json::json;
|
||||||
|
|
||||||
|
#[derive(FromForm)]
|
||||||
|
pub struct ReactionInput {
|
||||||
|
#[field(validate = range(-1..2))]
|
||||||
|
status: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/post/<pid>/reaction", data = "<ri>")]
|
||||||
|
pub async fn reaction(
|
||||||
|
pid: i32,
|
||||||
|
ri: Form<ReactionInput>,
|
||||||
|
user: CurrentUser,
|
||||||
|
db: Db,
|
||||||
|
rconn: RdsConn,
|
||||||
|
) -> JsonApi {
|
||||||
|
user.id.ok_or(YouAreTmp)?;
|
||||||
|
|
||||||
|
let mut p = Post::get(&db, pid).await?;
|
||||||
|
p.check_permission(&user, "r")?;
|
||||||
|
let mut r_up = Reaction::init(pid, 1, &rconn);
|
||||||
|
let mut r_down = Reaction::init(pid, -1, &rconn);
|
||||||
|
|
||||||
|
let (delta_up, delta_down): (i32, i32) = match ri.status {
|
||||||
|
1 => (
|
||||||
|
r_up.add(&user.namehash).await? as i32,
|
||||||
|
-(r_down.rem(&user.namehash).await? as i32),
|
||||||
|
),
|
||||||
|
-1 => (
|
||||||
|
-(r_up.rem(&user.namehash).await? as i32),
|
||||||
|
r_down.add(&user.namehash).await? as i32,
|
||||||
|
),
|
||||||
|
_ => (
|
||||||
|
-(r_up.rem(&user.namehash).await? as i32),
|
||||||
|
-(r_down.rem(&user.namehash).await? as i32),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if delta_up != 0 || delta_down != 0 {
|
||||||
|
update!(
|
||||||
|
p,
|
||||||
|
posts,
|
||||||
|
&db,
|
||||||
|
{ up_votes, add delta_up },
|
||||||
|
{ down_votes, add delta_down }
|
||||||
|
);
|
||||||
|
|
||||||
|
p.refresh_cache(false).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(json!({
|
||||||
|
"code": 0,
|
||||||
|
"data": {
|
||||||
|
"up_votes": p.up_votes,
|
||||||
|
"down_votes": p.down_votes,
|
||||||
|
"reaction_status": ri.status,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
@@ -25,7 +25,6 @@ pub async fn search(
|
|||||||
} else {
|
} else {
|
||||||
Post::search(
|
Post::search(
|
||||||
&db,
|
&db,
|
||||||
&rconn,
|
|
||||||
room_id,
|
room_id,
|
||||||
search_mode,
|
search_mode,
|
||||||
keywords.to_string(),
|
keywords.to_string(),
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
use crate::api::{CurrentUser, JsonApi};
|
use crate::api::{CurrentUser, JsonApi};
|
||||||
|
use crate::cache::cached_user_count;
|
||||||
|
use crate::db_conn::Db;
|
||||||
use crate::random_hasher::RandomHasher;
|
use crate::random_hasher::RandomHasher;
|
||||||
use crate::rds_conn::RdsConn;
|
use crate::rds_conn::RdsConn;
|
||||||
use crate::rds_models::Systemlog;
|
use crate::rds_models::{get_admin_list, get_candidate_list, Systemlog};
|
||||||
use rocket::serde::json::{json, Value};
|
use rocket::serde::json::{json, Value};
|
||||||
use rocket::State;
|
use rocket::State;
|
||||||
|
|
||||||
#[get("/systemlog")]
|
#[get("/systemlog")]
|
||||||
pub async fn get_systemlog(user: CurrentUser, rh: &State<RandomHasher>, rconn: RdsConn) -> JsonApi {
|
pub async fn get_systemlog(
|
||||||
|
user: CurrentUser,
|
||||||
|
rh: &State<RandomHasher>,
|
||||||
|
db: Db,
|
||||||
|
rconn: RdsConn,
|
||||||
|
) -> JsonApi {
|
||||||
let logs = Systemlog::get_list(&rconn, 50).await?;
|
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(),
|
||||||
|
"user_count": cached_user_count(&db).await?,
|
||||||
"custom_title": user.custom_title,
|
"custom_title": user.custom_title,
|
||||||
|
"admin_list": get_admin_list(&rconn).await?,
|
||||||
|
"candidate_list": get_candidate_list(&rconn).await?,
|
||||||
"data": logs.into_iter().map(|log|
|
"data": logs.into_iter().map(|log|
|
||||||
json!({
|
json!({
|
||||||
"type": log.action_type,
|
"type": log.action_type,
|
||||||
"user": look!(log.user_hash),
|
"user": log.user_hash,
|
||||||
"timestamp": log.time.timestamp(),
|
"timestamp": log.time.timestamp(),
|
||||||
"detail": format!("{}\n{}", &log.target, &log.detail),
|
"detail": format!("{}\n{}", &log.target, &log.detail),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
use super::PolicyError::OldApi;
|
use super::{CurrentUser, JsonApi};
|
||||||
use super::{ApiError, CurrentUser, JsonApi};
|
|
||||||
use rocket::fs::TempFile;
|
use rocket::fs::TempFile;
|
||||||
use rocket::serde::json::json;
|
use rocket::serde::json::json;
|
||||||
use std::env::var;
|
use std::env::var;
|
||||||
|
|
||||||
#[post("/upload")]
|
|
||||||
pub async fn ipfs_upload() -> ApiError {
|
|
||||||
OldApi.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/upload", data = "<file>")]
|
#[post("/upload", data = "<file>")]
|
||||||
pub async fn local_upload(_user: CurrentUser, mut file: TempFile<'_>) -> JsonApi {
|
pub async fn local_upload(_user: CurrentUser, mut file: TempFile<'_>) -> JsonApi {
|
||||||
let filename: String = format!(
|
let filename: String = format!(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub async fn get_poll_dict(pid: i32, rconn: &RdsConn, namehash: &str) -> Option<
|
|||||||
.has(namehash)
|
.has(namehash)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.then(|| opt)
|
.then_some(opt)
|
||||||
}))
|
}))
|
||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
484
src/cache.rs
484
src/cache.rs
@@ -1,228 +1,164 @@
|
|||||||
use crate::api::CurrentUser;
|
use crate::api::{Api, CurrentUser};
|
||||||
|
use crate::db_conn::Db;
|
||||||
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, BlockedUsers};
|
use crate::rds_models::BlockedUsers;
|
||||||
|
use diesel::result::{Error as DieselError, QueryResult};
|
||||||
|
use moka::future::Cache;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use redis::{AsyncCommands, RedisError, RedisResult};
|
use redis::RedisResult;
|
||||||
use rocket::serde::json::serde_json;
|
|
||||||
// can use rocket::serde::json::to_string in master version
|
|
||||||
use rocket::futures::future;
|
use rocket::futures::future;
|
||||||
|
use rocket::tokio::sync::RwLock;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::io;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
const INSTANCE_EXPIRE_TIME: usize = 60 * 60;
|
const USER_COUNT_EXPIRE_TIME: u64 = 60;
|
||||||
|
const INSTANCE_EXPIRE_TIME: u64 = 60 * 60;
|
||||||
|
|
||||||
const MIN_LENGTH: isize = 200;
|
// Global cache getters using OnceLock
|
||||||
const MAX_LENGTH: isize = 900;
|
fn post_cache() -> &'static Cache<i32, Post> {
|
||||||
const CUT_LENGTH: isize = 100;
|
static CACHE: OnceLock<Cache<i32, Post>> = OnceLock::new();
|
||||||
|
CACHE.get_or_init(|| Cache::builder().max_capacity(10_000).build())
|
||||||
macro_rules! post_cache_key {
|
|
||||||
($id: expr) => {
|
|
||||||
format!("hole_v2:cache:post:{}", $id)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PostCache {
|
fn post_comment_cache() -> &'static Cache<String, Vec<Comment>> {
|
||||||
rconn: RdsConn,
|
static CACHE: OnceLock<Cache<String, Vec<Comment>>> = OnceLock::new();
|
||||||
|
CACHE.get_or_init(|| {
|
||||||
|
Cache::builder()
|
||||||
|
.time_to_idle(Duration::from_secs(INSTANCE_EXPIRE_TIME))
|
||||||
|
.build()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Each list in post_list_cache, keyed by room_id and mode, is a sorted list. The element is a pair of numbers. The first one is the weight used to sort, the second one is the post id.
|
||||||
|
fn post_list_cache() -> &'static Cache<String, Arc<RwLock<Vec<(i64, i32)>>>> {
|
||||||
|
static CACHE: OnceLock<Cache<String, Arc<RwLock<Vec<(i64, i32)>>>>> = OnceLock::new();
|
||||||
|
CACHE.get_or_init(|| Cache::builder().build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_cache() -> &'static Cache<String, User> {
|
||||||
|
static CACHE: OnceLock<Cache<String, User>> = OnceLock::new();
|
||||||
|
CACHE.get_or_init(|| {
|
||||||
|
Cache::builder()
|
||||||
|
.time_to_idle(Duration::from_secs(INSTANCE_EXPIRE_TIME))
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn block_dict_cache() -> &'static Cache<String, Arc<RwLock<HashMap<String, bool>>>> {
|
||||||
|
static CACHE: OnceLock<Cache<String, Arc<RwLock<HashMap<String, bool>>>>> = OnceLock::new();
|
||||||
|
CACHE.get_or_init(|| {
|
||||||
|
Cache::builder()
|
||||||
|
.time_to_idle(Duration::from_secs(INSTANCE_EXPIRE_TIME))
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn user_count_cache() -> &'static Cache<String, i64> {
|
||||||
|
static CACHE: OnceLock<Cache<String, i64>> = OnceLock::new();
|
||||||
|
CACHE.get_or_init(|| {
|
||||||
|
Cache::builder()
|
||||||
|
.time_to_live(Duration::from_secs(USER_COUNT_EXPIRE_TIME))
|
||||||
|
.build()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_shared_diesel_error(err: Arc<DieselError>) -> DieselError {
|
||||||
|
match err.as_ref() {
|
||||||
|
DieselError::NotFound => DieselError::NotFound,
|
||||||
|
DieselError::RollbackTransaction => DieselError::RollbackTransaction,
|
||||||
|
DieselError::AlreadyInTransaction => DieselError::AlreadyInTransaction,
|
||||||
|
DieselError::NotInTransaction => DieselError::NotInTransaction,
|
||||||
|
DieselError::BrokenTransactionManager => DieselError::BrokenTransactionManager,
|
||||||
|
_ => DieselError::QueryBuilderError(Box::new(io::Error::other(err.to_string()))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PostCache;
|
||||||
|
|
||||||
impl PostCache {
|
impl PostCache {
|
||||||
init!();
|
pub async fn sets(ps: &[&Post]) {
|
||||||
|
|
||||||
pub async fn sets(&mut self, ps: &[&Post]) {
|
|
||||||
if ps.is_empty() {
|
if ps.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let kvs: Vec<(String, String)> = ps
|
for p in ps {
|
||||||
.iter()
|
post_cache().insert(p.id, (*p).clone()).await;
|
||||||
.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> {
|
pub async fn get(pid: &i32) -> Option<Post> {
|
||||||
let key = post_cache_key!(pid);
|
post_cache().get(pid).await
|
||||||
let rds_result: Option<String> = self
|
}
|
||||||
.rconn
|
|
||||||
.get::<String, Option<String>>(key)
|
pub async fn get_with<F>(pid: i32, init: F) -> QueryResult<Post>
|
||||||
|
where
|
||||||
|
F: Future<Output = QueryResult<Post>>,
|
||||||
|
{
|
||||||
|
post_cache()
|
||||||
|
.try_get_with(pid, init)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e| {
|
.map_err(map_shared_diesel_error)
|
||||||
warn!("try to get post cache, connect rds failed, {}", e);
|
|
||||||
None
|
|
||||||
});
|
|
||||||
|
|
||||||
rds_result.and_then(|s| {
|
|
||||||
serde_json::from_str(&s).unwrap_or_else(|e| {
|
|
||||||
warn!("get post cache, decode failed {}, {}", e, s);
|
|
||||||
None
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn gets(&mut self, pids: &[i32]) -> Vec<Option<Post>> {
|
pub async fn gets(pids: &[i32]) -> Vec<Option<Post>> {
|
||||||
// 长度为1时会走GET而非MGET,返回值格式不兼容。愚蠢的设计。
|
future::join_all(pids.iter().map(Self::get)).await
|
||||||
match pids.len() {
|
|
||||||
0 => vec![],
|
|
||||||
1 => vec![self.get(&pids[0]).await],
|
|
||||||
_ => {
|
|
||||||
let ks: Vec<String> = pids.iter().map(|pid| post_cache_key!(pid)).collect();
|
|
||||||
// dbg!(&ks);
|
|
||||||
// Vec is single arg, while &Vec is not. Seems a bug.
|
|
||||||
let rds_result: Vec<Option<String>> = self
|
|
||||||
.rconn
|
|
||||||
.get::<Vec<String>, Vec<Option<String>>>(ks)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
warn!("try to get posts cache, connect rds failed, {}", e);
|
|
||||||
vec![None; pids.len()]
|
|
||||||
});
|
|
||||||
// dbg!(&rds_result);
|
|
||||||
|
|
||||||
// 定期热度衰减的时候会清空缓存,这里设不设置过期时间影响不大
|
|
||||||
|
|
||||||
rds_result
|
|
||||||
.into_iter()
|
|
||||||
.map(|x| {
|
|
||||||
// dbg!(&x);
|
|
||||||
x.and_then(|s| {
|
|
||||||
serde_json::from_str(&s).unwrap_or_else(|e| {
|
|
||||||
warn!("get post cache, decode failed {}, {}", e, s);
|
|
||||||
None
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clear_all(&mut self) {
|
pub async fn clear_all() {
|
||||||
let mut keys = self
|
post_cache().invalidate_all();
|
||||||
.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 {
|
pub struct PostCommentCache {
|
||||||
key: String,
|
key: String,
|
||||||
rconn: RdsConn,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostCommentCache {
|
impl PostCommentCache {
|
||||||
init!(i32, "hole_v2:cache:post_comments:{}");
|
pub fn init(post_id: i32) -> Self {
|
||||||
|
Self {
|
||||||
pub async fn set(&mut self, cs: &[Comment]) {
|
key: format!("hole_v2:cache:post_comments:{}", post_id),
|
||||||
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>> {
|
pub async fn get_with<F>(&self, init: F) -> QueryResult<Vec<Comment>>
|
||||||
let rds_result = self.rconn.get::<&String, String>(&self.key).await;
|
where
|
||||||
// dbg!(&rds_result);
|
F: Future<Output = QueryResult<Vec<Comment>>>,
|
||||||
if let Ok(s) = rds_result {
|
{
|
||||||
self.rconn
|
post_comment_cache()
|
||||||
.expire::<&String, bool>(&self.key, INSTANCE_EXPIRE_TIME)
|
.try_get_with(self.key.clone(), init)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e| {
|
.map_err(map_shared_diesel_error)
|
||||||
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) {
|
pub async fn clear(&mut self) {
|
||||||
self.rconn.del(&self.key).await.unwrap_or_else(|e| {
|
post_comment_cache().invalidate(&self.key).await;
|
||||||
warn!("clear commenrs cache fail, {}", e);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PostListCache {
|
pub struct PostListCache {
|
||||||
key: String,
|
key: String,
|
||||||
mode: u8,
|
mode: u8,
|
||||||
rconn: RdsConn,
|
|
||||||
length: isize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostListCache {
|
impl PostListCache {
|
||||||
pub fn init(room_id: Option<i32>, mode: u8, rconn: &RdsConn) -> Self {
|
pub const MAX_LENGTH: usize = 900;
|
||||||
|
// pub const MIN_LENGTH: usize = 200;
|
||||||
|
pub const CUT_LENGTH: usize = 100;
|
||||||
|
pub fn init(room_id: Option<i32>, mode: u8) -> Self {
|
||||||
Self {
|
Self {
|
||||||
key: format!(
|
key: format!(
|
||||||
"hole_v2:cache:post_list:{}:{}",
|
"hole_v2:cache:post_list:{}:{}",
|
||||||
match room_id {
|
room_id.map_or_else(String::new, |i| i.to_string()),
|
||||||
Some(i) => i.to_string(),
|
|
||||||
None => "".to_owned(),
|
|
||||||
},
|
|
||||||
&mode
|
&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) {
|
fn p2pair(&self, p: &Post) -> (i64, i32) {
|
||||||
(
|
(
|
||||||
match self.mode {
|
match self.mode {
|
||||||
@@ -230,152 +166,162 @@ impl PostListCache {
|
|||||||
1 => -p.last_comment_time.timestamp(),
|
1 => -p.last_comment_time.timestamp(),
|
||||||
2 => (-p.hot_score).into(),
|
2 => (-p.hot_score).into(),
|
||||||
3 => rand::thread_rng().gen_range(0..i64::MAX),
|
3 => rand::thread_rng().gen_range(0..i64::MAX),
|
||||||
|
4 => (-p.n_attentions).into(),
|
||||||
_ => panic!("wrong mode"),
|
_ => panic!("wrong mode"),
|
||||||
},
|
},
|
||||||
p.id,
|
p.id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fill(&mut self, ps: &[Post]) {
|
pub async fn fill_with<F>(&mut self, query_posts: F) -> QueryResult<usize>
|
||||||
let items: Vec<(i64, i32)> = ps.iter().map(|p| self.p2pair(p)).collect();
|
where
|
||||||
self.rconn
|
F: Future<Output = QueryResult<Vec<Post>>>,
|
||||||
.zadd_multiple(&self.key, &items)
|
{
|
||||||
|
let list_ref = post_list_cache()
|
||||||
|
.try_get_with(self.key.clone(), async {
|
||||||
|
let mut items: Vec<(i64, i32)> =
|
||||||
|
query_posts.await?.iter().map(|p| self.p2pair(p)).collect();
|
||||||
|
items.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
Ok(Arc::new(RwLock::new(items)))
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e| {
|
.map_err(map_shared_diesel_error)?;
|
||||||
warn!("fill list cache failed, {} {}", e, &self.key);
|
let list = list_ref.read().await;
|
||||||
});
|
|
||||||
|
|
||||||
self.set_and_check_length().await;
|
// Double-Checked Locking
|
||||||
|
if list.len() <= Self::MAX_LENGTH {
|
||||||
|
return Ok(list.len());
|
||||||
|
}
|
||||||
|
drop(list);
|
||||||
|
let mut list = list_ref.write().await;
|
||||||
|
|
||||||
|
if list.len() <= Self::MAX_LENGTH {
|
||||||
|
Ok(list.len())
|
||||||
|
} else {
|
||||||
|
list.truncate(Self::MAX_LENGTH - Self::CUT_LENGTH);
|
||||||
|
Ok(list.len())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn put(&mut self, p: &Post) {
|
pub async fn put(&mut self, p: &Post) {
|
||||||
// 其他都是加到最前面的,但热榜不是。可能导致MIN_LENGTH到MAX_LENGTH之间的数据不可靠
|
// Don't put is there is no cache. Let fill_with handle it.
|
||||||
// 影响不大,先不管了
|
if let Some(list_ref) = post_list_cache().get(&self.key).await {
|
||||||
|
let mut list = list_ref.write().await;
|
||||||
|
// Remove any existing entry for this post_id
|
||||||
|
if let Some(pos) = list.iter().position(|(_, pid)| *pid == p.id) {
|
||||||
|
list.remove(pos);
|
||||||
|
}
|
||||||
if p.is_deleted || (self.mode > 0 && p.is_reported) {
|
if p.is_deleted || (self.mode > 0 && p.is_reported) {
|
||||||
self.rconn.zrem(&self.key, p.id).await.unwrap_or_else(|e| {
|
return;
|
||||||
warn!(
|
}
|
||||||
"remove from list cache failed, {} {} {}",
|
list.push(self.p2pair(p));
|
||||||
e, &self.key, p.id
|
list.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
);
|
|
||||||
});
|
|
||||||
} 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> {
|
pub async fn get_pids(&mut self, start: usize, limit: usize) -> Vec<i32> {
|
||||||
self.rconn
|
if let Some(list_ref) = post_list_cache().get(&self.key).await {
|
||||||
.zrange(
|
let list = list_ref.read().await;
|
||||||
&self.key,
|
list.iter()
|
||||||
start.try_into().unwrap(),
|
.skip(start)
|
||||||
(start + limit - 1).try_into().unwrap(),
|
.take(limit)
|
||||||
)
|
.map(|(_, pid)| *pid)
|
||||||
.await
|
.collect()
|
||||||
.unwrap()
|
} else {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clear(&mut self) {
|
pub async fn clear(&mut self) {
|
||||||
self.rconn.del(&self.key).await.unwrap_or_else(|e| {
|
post_list_cache().invalidate(&self.key).await;
|
||||||
warn!("clear post list cache failed, {}", e);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct UserCache {
|
pub struct UserCache {
|
||||||
key: String,
|
key: String,
|
||||||
rconn: RdsConn,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UserCache {
|
impl UserCache {
|
||||||
init!(&str, "hole_v2:cache:user:{}");
|
pub fn init(user_id: &str) -> Self {
|
||||||
|
Self {
|
||||||
pub async fn set(&mut self, u: &User) {
|
key: format!("hole_v2:cache:user:{}", user_id),
|
||||||
self.rconn
|
}
|
||||||
.set_ex(
|
|
||||||
&self.key,
|
|
||||||
serde_json::to_string(u).unwrap(),
|
|
||||||
INSTANCE_EXPIRE_TIME,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
warn!("set user cache failed: {}", e);
|
|
||||||
dbg!(u);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(&mut self) -> Option<User> {
|
// No need to use get_with for User. Just check and set separately.
|
||||||
let rds_result = self.rconn.get::<&String, String>(&self.key).await;
|
pub async fn set(&self, u: &User) {
|
||||||
if let Ok(s) = rds_result {
|
user_cache().insert(self.key.clone(), u.clone()).await;
|
||||||
self.rconn
|
|
||||||
.expire::<&String, bool>(&self.key, INSTANCE_EXPIRE_TIME)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
warn!(
|
|
||||||
"get user cache, set new expire failed: {}, {}, {} ",
|
|
||||||
e, &self.key, &s
|
|
||||||
);
|
|
||||||
false
|
|
||||||
});
|
|
||||||
serde_json::from_str(&s).unwrap_or_else(|e| {
|
|
||||||
warn!("get user cache, decode failed {}, {}", e, s);
|
|
||||||
None
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get(&self) -> Option<User> {
|
||||||
|
user_cache().get(&self.key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_all() {
|
||||||
|
user_cache().invalidate_all();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BlockDictCache {
|
pub struct BlockDictCache {
|
||||||
key: String,
|
key: String,
|
||||||
rconn: RdsConn,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlockDictCache {
|
impl BlockDictCache {
|
||||||
// namehash, pid
|
pub fn init(namehash: &str, post_id: i32) -> Self {
|
||||||
init!(&str, i32, "hole_v2:cache:block_dict:{}:{}");
|
Self {
|
||||||
|
key: format!("hole_v2:cache:block_dict:{}:{}", namehash, post_id),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_or_create(
|
pub async fn get_or_create(
|
||||||
&mut self,
|
&mut self,
|
||||||
user: &CurrentUser,
|
user: &CurrentUser,
|
||||||
hash_list: &[&String],
|
hash_list: &[&String],
|
||||||
|
rconn: &RdsConn,
|
||||||
) -> RedisResult<HashMap<String, bool>> {
|
) -> RedisResult<HashMap<String, bool>> {
|
||||||
let mut block_dict = self
|
let dict_ref = block_dict_cache()
|
||||||
.rconn
|
.get_with(self.key.clone(), async move {
|
||||||
.hgetall::<&String, HashMap<String, bool>>(&self.key)
|
Arc::new(RwLock::new(HashMap::new()))
|
||||||
.await?;
|
|
||||||
|
|
||||||
//dbg!(&self.key, &block_dict);
|
|
||||||
|
|
||||||
let missing: Vec<(String, bool)> =
|
|
||||||
future::try_join_all(hash_list.iter().filter_map(|hash| {
|
|
||||||
(!block_dict.contains_key(&hash.to_string())).then(|| async {
|
|
||||||
Ok::<(String, bool), RedisError>((
|
|
||||||
hash.to_string(),
|
|
||||||
BlockedUsers::check_if_block(&self.rconn, user, hash).await?,
|
|
||||||
))
|
|
||||||
})
|
})
|
||||||
}))
|
.await;
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !missing.is_empty() {
|
// Find missing hashes
|
||||||
self.rconn.hset_multiple(&self.key, &missing).await?;
|
let mut missing_keys: Vec<String> = Vec::new();
|
||||||
self.rconn.expire(&self.key, INSTANCE_EXPIRE_TIME).await?;
|
{
|
||||||
block_dict.extend(missing.into_iter());
|
let block_dict = dict_ref.read().await;
|
||||||
|
for hash in hash_list {
|
||||||
|
if !block_dict.contains_key(hash.as_str()) {
|
||||||
|
missing_keys.push((*hash).clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//dbg!(&block_dict);
|
if !missing_keys.is_empty() {
|
||||||
|
let mut missing: Vec<(String, bool)> = Vec::with_capacity(missing_keys.len());
|
||||||
Ok(block_dict)
|
for hash in missing_keys {
|
||||||
|
let is_blocked = BlockedUsers::check_if_block(rconn, user, &hash).await?;
|
||||||
|
missing.push((hash, is_blocked));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clear(&mut self) -> RedisResult<()> {
|
let mut block_dict = dict_ref.write().await;
|
||||||
self.rconn.del(&self.key).await
|
for (hash, is_blocked) in missing {
|
||||||
|
block_dict.entry(hash).or_insert(is_blocked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = dict_ref.read().await.clone();
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear(&mut self) {
|
||||||
|
block_dict_cache().invalidate(&self.key).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn cached_user_count(db: &Db) -> Api<i64> {
|
||||||
|
let key = "hole_v2:cache:user_count";
|
||||||
|
Ok(user_count_cache()
|
||||||
|
.try_get_with(key.to_string(), async { User::get_count(db).await })
|
||||||
|
.await
|
||||||
|
.map_err(map_shared_diesel_error)?)
|
||||||
|
}
|
||||||
|
|||||||
10
src/cors.rs
10
src/cors.rs
@@ -19,11 +19,11 @@ impl Fairing for Cors {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
|
async fn on_response<'r>(&self, request: &'r Request<'_>, response: &mut Response<'r>) {
|
||||||
if let Some(origin) = request
|
if let Some(origin) = request.headers().get_one("Origin").and_then(|origin| {
|
||||||
.headers()
|
self.whitelist
|
||||||
.get_one("Origin")
|
.contains(&origin.to_string())
|
||||||
.and_then(|origin| self.whitelist.contains(&origin.to_string()).then(|| origin))
|
.then_some(origin)
|
||||||
{
|
}) {
|
||||||
response.set_header(Header::new("Access-Control-Allow-Origin", origin));
|
response.set_header(Header::new("Access-Control-Allow-Origin", origin));
|
||||||
response.set_header(Header::new(
|
response.set_header(Header::new(
|
||||||
"Access-Control-Allow-Methods",
|
"Access-Control-Allow-Methods",
|
||||||
|
|||||||
@@ -1,148 +0,0 @@
|
|||||||
/*
|
|
||||||
* from https://github.com/shssoichiro/diesel-logger
|
|
||||||
* change Connection to &mut Connection
|
|
||||||
*/
|
|
||||||
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
use diesel::backend::{Backend, UsesAnsiSavepointSyntax};
|
|
||||||
use diesel::connection::{AnsiTransactionManager, SimpleConnection};
|
|
||||||
use diesel::debug_query;
|
|
||||||
use diesel::deserialize::QueryableByName;
|
|
||||||
use diesel::prelude::*;
|
|
||||||
use diesel::query_builder::{AsQuery, QueryFragment, QueryId};
|
|
||||||
use diesel::sql_types::HasSqlType;
|
|
||||||
|
|
||||||
/// Wraps a diesel `Connection` to time and log each query using
|
|
||||||
/// the configured logger for the `log` crate.
|
|
||||||
///
|
|
||||||
/// Currently, this produces a `debug` log on every query,
|
|
||||||
/// an `info` on queries that take longer than 1 second,
|
|
||||||
/// and a `warn`ing on queries that take longer than 5 seconds.
|
|
||||||
/// These thresholds will be configurable in a future version.
|
|
||||||
pub struct LoggingConnection<'r, C: Connection>(&'r mut C);
|
|
||||||
|
|
||||||
impl<'r, C: Connection> LoggingConnection<'r, C> {
|
|
||||||
pub fn new(conn: &'r mut C) -> Self {
|
|
||||||
LoggingConnection(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'r, C: Connection> Deref for LoggingConnection<'r, C> {
|
|
||||||
type Target = C;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'r, C> SimpleConnection for LoggingConnection<'r, C>
|
|
||||||
where
|
|
||||||
C: Connection + Send + 'static,
|
|
||||||
{
|
|
||||||
fn batch_execute(&self, query: &str) -> QueryResult<()> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
let result = self.0.batch_execute(query);
|
|
||||||
let duration = start_time.elapsed();
|
|
||||||
log_query(query, duration);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C: Connection> Connection for LoggingConnection<'_, C>
|
|
||||||
where
|
|
||||||
C: Connection<TransactionManager = AnsiTransactionManager> + Send + 'static,
|
|
||||||
C::Backend: UsesAnsiSavepointSyntax,
|
|
||||||
<C::Backend as Backend>::QueryBuilder: Default,
|
|
||||||
{
|
|
||||||
type Backend = C::Backend;
|
|
||||||
type TransactionManager = C::TransactionManager;
|
|
||||||
|
|
||||||
fn establish(_: &str) -> ConnectionResult<Self> {
|
|
||||||
Err(ConnectionError::__Nonexhaustive)
|
|
||||||
//Ok(LoggingConnection(C::establish(database_url)?))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn execute(&self, query: &str) -> QueryResult<usize> {
|
|
||||||
let start_time = Instant::now();
|
|
||||||
let result = self.0.execute(query);
|
|
||||||
let duration = start_time.elapsed();
|
|
||||||
log_query(query, duration);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn query_by_index<T, U>(&self, source: T) -> QueryResult<Vec<U>>
|
|
||||||
where
|
|
||||||
T: AsQuery,
|
|
||||||
T::Query: QueryFragment<Self::Backend> + QueryId,
|
|
||||||
Self::Backend: HasSqlType<T::SqlType>,
|
|
||||||
U: Queryable<T::SqlType, Self::Backend>,
|
|
||||||
{
|
|
||||||
let query = source.as_query();
|
|
||||||
let debug_query = debug_query::<Self::Backend, _>(&query).to_string();
|
|
||||||
let start_time = Instant::now();
|
|
||||||
let result = self.0.query_by_index(query);
|
|
||||||
let duration = start_time.elapsed();
|
|
||||||
log_query(&debug_query, duration);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn query_by_name<T, U>(&self, source: &T) -> QueryResult<Vec<U>>
|
|
||||||
where
|
|
||||||
T: QueryFragment<Self::Backend> + QueryId,
|
|
||||||
U: QueryableByName<Self::Backend>,
|
|
||||||
{
|
|
||||||
let debug_query = debug_query::<Self::Backend, _>(&source).to_string();
|
|
||||||
let start_time = Instant::now();
|
|
||||||
let result = self.0.query_by_name(source);
|
|
||||||
let duration = start_time.elapsed();
|
|
||||||
log_query(&debug_query, duration);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn execute_returning_count<T>(&self, source: &T) -> QueryResult<usize>
|
|
||||||
where
|
|
||||||
T: QueryFragment<Self::Backend> + QueryId,
|
|
||||||
{
|
|
||||||
let debug_query = debug_query::<Self::Backend, _>(&source).to_string();
|
|
||||||
let start_time = Instant::now();
|
|
||||||
let result = self.0.execute_returning_count(source);
|
|
||||||
let duration = start_time.elapsed();
|
|
||||||
log_query(&debug_query, duration);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transaction_manager(&self) -> &Self::TransactionManager {
|
|
||||||
self.0.transaction_manager()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn log_query(query: &str, duration: Duration) {
|
|
||||||
if duration.as_secs() >= 5 {
|
|
||||||
warn!(
|
|
||||||
"Slow query ran in {:.2} seconds: {}",
|
|
||||||
duration_to_secs(duration),
|
|
||||||
query
|
|
||||||
);
|
|
||||||
} else if duration.as_secs() >= 1 {
|
|
||||||
info!(
|
|
||||||
"Slow query ran in {:.2} seconds: {}",
|
|
||||||
duration_to_secs(duration),
|
|
||||||
query
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
debug!("Query ran in {:.1} ms: {}", duration_to_ms(duration), query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const NANOS_PER_MILLI: u32 = 1_000_000;
|
|
||||||
const MILLIS_PER_SEC: u32 = 1_000;
|
|
||||||
|
|
||||||
fn duration_to_secs(duration: Duration) -> f32 {
|
|
||||||
duration_to_ms(duration) / MILLIS_PER_SEC as f32
|
|
||||||
}
|
|
||||||
|
|
||||||
fn duration_to_ms(duration: Duration) -> f32 {
|
|
||||||
(duration.as_secs() as u32 * 1000) as f32
|
|
||||||
+ (duration.subsec_nanos() as f32 / NANOS_PER_MILLI as f32)
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pub mod diesel_logger;
|
|
||||||
179
src/login.rs
179
src/login.rs
@@ -1,35 +1,57 @@
|
|||||||
|
#![allow(clippy::unused_unit)]
|
||||||
|
|
||||||
use crate::db_conn::Db;
|
use crate::db_conn::Db;
|
||||||
use crate::models::User;
|
use crate::models::User;
|
||||||
|
use crate::random_hasher::RandomHasher;
|
||||||
use rocket::request::{FromRequest, Outcome, Request};
|
use rocket::request::{FromRequest, Outcome, Request};
|
||||||
use rocket::response::Redirect;
|
use rocket::response::Redirect;
|
||||||
use rocket::serde::Deserialize;
|
use rocket::serde::Deserialize;
|
||||||
|
use rocket::State;
|
||||||
use std::env;
|
use std::env;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub struct RefHeader(pub String);
|
#[derive(Debug)]
|
||||||
|
pub struct FrontendAddr(pub String);
|
||||||
|
|
||||||
#[rocket::async_trait]
|
#[rocket::async_trait]
|
||||||
impl<'r> FromRequest<'r> for RefHeader {
|
impl<'r> FromRequest<'r> for FrontendAddr {
|
||||||
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> {
|
||||||
match request.headers().get_one("Referer") {
|
Outcome::Success(Self(
|
||||||
Some(h) => Outcome::Success(RefHeader(h.to_string())),
|
request
|
||||||
None => Outcome::Forward(()),
|
.headers()
|
||||||
|
.get_one("Referer")
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.unwrap_or_else(|| env::var("DEFAULT_FRONTEND").unwrap()),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BackendAddr(pub String);
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for BackendAddr {
|
||||||
|
type Error = ();
|
||||||
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
Outcome::Success(Self(
|
||||||
|
request
|
||||||
|
.headers()
|
||||||
|
.get_one("Host")
|
||||||
|
.map(|s| format!("https://{}", s))
|
||||||
|
.unwrap_or_else(|| env::var("DEFAULT_BACKEND").unwrap()),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/?p=cs")]
|
#[get("/?p=cs")]
|
||||||
pub fn cs_login(r: RefHeader) -> Redirect {
|
pub fn cs_login(r: FrontendAddr, h: BackendAddr) -> Redirect {
|
||||||
let mast_url = env::var("MAST_BASE_URL").unwrap();
|
let mast_url = env::var("MAST_BASE_URL").unwrap();
|
||||||
let mast_cli = env::var("MAST_CLIENT").unwrap();
|
let mast_cli = env::var("MAST_CLIENT").unwrap();
|
||||||
let mast_scope = env::var("MAST_SCOPE").unwrap();
|
let mast_scope = env::var("MAST_SCOPE").unwrap();
|
||||||
|
|
||||||
let jump_to_url = Url::parse(&r.0).unwrap();
|
let jump_to_url = Url::parse(&r.0).unwrap();
|
||||||
|
let mut redirect_url = Url::parse(&h.0).unwrap();
|
||||||
let mut redirect_url = env::var("AUTH_BACKEND_URL")
|
|
||||||
.map(|url| Url::parse(&url).unwrap())
|
|
||||||
.unwrap_or_else(|_| jump_to_url.clone());
|
|
||||||
redirect_url.set_path("/_login/cs/auth");
|
redirect_url.set_path("/_login/cs/auth");
|
||||||
|
|
||||||
redirect_url = Url::parse_with_params(
|
redirect_url = Url::parse_with_params(
|
||||||
@@ -67,7 +89,21 @@ struct Account {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
}
|
}
|
||||||
#[get("/cs/auth?<code>&<redirect_url>&<jump_to_url>")]
|
#[get("/cs/auth?<code>&<redirect_url>&<jump_to_url>")]
|
||||||
pub async fn cs_auth(code: String, redirect_url: String, jump_to_url: String, db: Db) -> Redirect {
|
pub async fn cs_auth(
|
||||||
|
code: String,
|
||||||
|
redirect_url: String,
|
||||||
|
jump_to_url: String,
|
||||||
|
db: Db,
|
||||||
|
rh: &State<RandomHasher>,
|
||||||
|
) -> Result<Redirect, &'static str> {
|
||||||
|
if !env::var("FRONTEND_WHITELIST")
|
||||||
|
.unwrap_or_default()
|
||||||
|
.split(',')
|
||||||
|
.any(|url| jump_to_url.starts_with(url))
|
||||||
|
{
|
||||||
|
return Err("前端地址不在白名单内");
|
||||||
|
}
|
||||||
|
|
||||||
let mast_url = env::var("MAST_BASE_URL").unwrap();
|
let mast_url = env::var("MAST_BASE_URL").unwrap();
|
||||||
let mast_cli = env::var("MAST_CLIENT").unwrap();
|
let mast_cli = env::var("MAST_CLIENT").unwrap();
|
||||||
let mast_sec = env::var("MAST_SECRET").unwrap();
|
let mast_sec = env::var("MAST_SECRET").unwrap();
|
||||||
@@ -115,23 +151,124 @@ pub async fn cs_auth(code: String, redirect_url: String, jump_to_url: String, db
|
|||||||
|
|
||||||
//dbg!(&account);
|
//dbg!(&account);
|
||||||
|
|
||||||
let tk = User::find_or_create_token(&db, &format!("cs_{}", &account.id), false)
|
let tk = User::find_or_create_token(
|
||||||
|
&db,
|
||||||
|
&rh.hash_with_salt(&format!("cs_{}", &account.id)),
|
||||||
|
false,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
Redirect::to(format!(
|
Ok(Redirect::to(format!("{}?token={}", &jump_to_url, &tk)))
|
||||||
"{}?token={}",
|
}
|
||||||
{
|
|
||||||
if env::var("FRONTEND_WHITELIST")
|
#[get("/gh")]
|
||||||
|
pub fn gh_login(r: FrontendAddr, h: BackendAddr) -> Redirect {
|
||||||
|
let gh_url = "https://github.com/login/oauth/authorize";
|
||||||
|
let gh_cli = env::var("GH_CLIENT").unwrap();
|
||||||
|
let gh_scope = "user:email";
|
||||||
|
|
||||||
|
let jump_to_url = Url::parse(&r.0).unwrap();
|
||||||
|
let mut redirect_url = Url::parse(&h.0).unwrap();
|
||||||
|
redirect_url.set_path("/_login/gh/auth");
|
||||||
|
|
||||||
|
redirect_url = Url::parse_with_params(
|
||||||
|
redirect_url.as_str(),
|
||||||
|
&[("jump_to_url", jump_to_url.as_str())],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let url = Url::parse_with_params(
|
||||||
|
gh_url,
|
||||||
|
&[
|
||||||
|
("redirect_uri", redirect_url.as_str()),
|
||||||
|
("client_id", &gh_cli),
|
||||||
|
("scope", gh_scope),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Redirect::to(url.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(crate = "rocket::serde")]
|
||||||
|
struct GithubEmail {
|
||||||
|
pub email: String,
|
||||||
|
pub verified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/gh/auth?<code>&<jump_to_url>")]
|
||||||
|
pub async fn gh_auth(
|
||||||
|
code: String,
|
||||||
|
jump_to_url: String,
|
||||||
|
db: Db,
|
||||||
|
rh: &State<RandomHasher>,
|
||||||
|
) -> Result<Redirect, &'static str> {
|
||||||
|
if !env::var("FRONTEND_WHITELIST")
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.split(',')
|
.split(',')
|
||||||
.any(|url| jump_to_url.starts_with(url))
|
.any(|url| jump_to_url.starts_with(url))
|
||||||
{
|
{
|
||||||
&jump_to_url
|
return Err("前端地址不在白名单内");
|
||||||
} else {
|
|
||||||
"/"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let gh_cli = env::var("GH_CLIENT").unwrap();
|
||||||
|
let gh_sec = env::var("GH_SECRET").unwrap();
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let r = client
|
||||||
|
.post("https://github.com/login/oauth/access_token")
|
||||||
|
.header(reqwest::header::ACCEPT, "application/json")
|
||||||
|
.form(&[
|
||||||
|
("client_id", gh_cli.as_str()),
|
||||||
|
("client_secret", gh_sec.as_str()),
|
||||||
|
("code", code.as_str()),
|
||||||
|
])
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
//let token: rocket::serde::json::Value = r.json().await.unwrap();
|
||||||
|
let token: Token = r.json().await.unwrap();
|
||||||
|
|
||||||
|
dbg!(&token);
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let r = client
|
||||||
|
.get("https://api.github.com/user/emails")
|
||||||
|
.bearer_auth(token.access_token)
|
||||||
|
.header(reqwest::header::USER_AGENT, "hole_thu LoginBot")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
// dbg!(&r);
|
||||||
|
let emails = r
|
||||||
|
.json::<Vec<GithubEmail>>()
|
||||||
|
//.json::<rocket::serde::json::Value>()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
//dbg!(&emails);
|
||||||
|
|
||||||
|
let name = emails
|
||||||
|
.iter()
|
||||||
|
.filter(|email| email.verified)
|
||||||
|
.find_map(
|
||||||
|
|email| match email.email.split('@').collect::<Vec<&str>>()[..] {
|
||||||
|
[name, "mails.tsinghua.edu.cn"] | [name, "tsinghua.org.cn"] => Some(name),
|
||||||
|
_ => None,
|
||||||
},
|
},
|
||||||
&tk
|
);
|
||||||
))
|
|
||||||
|
if let Some(name) = name {
|
||||||
|
let tk =
|
||||||
|
User::find_or_create_token(&db, &rh.hash_with_salt(&format!("email_{}", name)), false)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok(Redirect::to(format!("{}?token={}", &jump_to_url, &tk)))
|
||||||
|
} else {
|
||||||
|
Err("没有找到已验证的清华邮箱/校友邮箱")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
59
src/main.rs
59
src/main.rs
@@ -14,59 +14,64 @@ mod api;
|
|||||||
mod cache;
|
mod cache;
|
||||||
mod cors;
|
mod cors;
|
||||||
mod db_conn;
|
mod db_conn;
|
||||||
mod libs;
|
|
||||||
#[cfg(feature = "mastlogin")]
|
#[cfg(feature = "mastlogin")]
|
||||||
mod login;
|
mod login;
|
||||||
mod models;
|
mod models;
|
||||||
mod random_hasher;
|
mod random_hasher;
|
||||||
|
mod rate_limit;
|
||||||
mod rds_conn;
|
mod rds_conn;
|
||||||
mod rds_models;
|
mod rds_models;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
|
||||||
use db_conn::{establish_connection, Conn, Db};
|
use std::env;
|
||||||
|
|
||||||
use diesel::Connection;
|
use diesel::Connection;
|
||||||
|
use diesel_migrations::{EmbeddedMigrations, MigrationHarness};
|
||||||
|
use rocket::tokio;
|
||||||
|
use rocket::tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
use db_conn::{establish_connection, Conn, Db};
|
||||||
use random_hasher::RandomHasher;
|
use random_hasher::RandomHasher;
|
||||||
|
use rate_limit::MainLimiters;
|
||||||
use rds_conn::{init_rds_client, RdsConn};
|
use rds_conn::{init_rds_client, RdsConn};
|
||||||
use rds_models::clear_outdate_redis_data;
|
use rds_models::clear_outdate_redis_data;
|
||||||
use std::env;
|
|
||||||
use tokio::time::{sleep, Duration};
|
|
||||||
|
|
||||||
embed_migrations!("migrations/postgres");
|
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations/postgres");
|
||||||
|
|
||||||
#[rocket::main]
|
#[rocket::main]
|
||||||
async fn main() -> Result<(), rocket::Error> {
|
async fn main() {
|
||||||
load_env();
|
load_env();
|
||||||
if env::args().any(|arg| arg.eq("--init-database")) {
|
if env::args().any(|arg| arg.eq("--init-database")) {
|
||||||
init_database();
|
init_database();
|
||||||
return Ok(());
|
return;
|
||||||
}
|
}
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let rmc = init_rds_client().await;
|
let rmc = init_rds_client().await;
|
||||||
let rconn = RdsConn(rmc.clone());
|
let mut rconn = RdsConn(rmc.clone());
|
||||||
clear_outdate_redis_data(&rconn.clone()).await;
|
let mut c_start = establish_connection();
|
||||||
|
models::User::clear_non_admin_users(&mut c_start).await;
|
||||||
|
clear_outdate_redis_data(&mut rconn).await;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
sleep(Duration::from_secs(3 * 60 * 60)).await;
|
sleep(Duration::from_secs(3 * 60 * 60)).await;
|
||||||
models::Post::annealing(establish_connection(), &rconn).await;
|
models::Post::annealing(&mut c_start).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let rconn = RdsConn(rmc.clone());
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
for room_id in (0..5).map(Some).chain([None, Some(42)]) {
|
for room_id in (0..5).map(Some).chain([None, Some(42)]) {
|
||||||
cache::PostListCache::init(room_id, 3, &rconn).clear().await;
|
cache::PostListCache::init(room_id, 3).clear().await;
|
||||||
}
|
}
|
||||||
sleep(Duration::from_secs(5 * 60)).await;
|
sleep(Duration::from_secs(5 * 60)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
rocket::build()
|
let _ = rocket::build()
|
||||||
.mount(
|
.mount(
|
||||||
"/_api/v1",
|
"/_api/v1",
|
||||||
routes![
|
routes![
|
||||||
api::comment::get_comment,
|
api::comment::get_comment,
|
||||||
api::comment::old_add_comment,
|
|
||||||
api::post::get_list,
|
api::post::get_list,
|
||||||
api::post::get_one,
|
api::post::get_one,
|
||||||
api::post::publish_post,
|
api::post::publish_post,
|
||||||
@@ -78,36 +83,46 @@ async fn main() -> Result<(), rocket::Error> {
|
|||||||
api::systemlog::get_systemlog,
|
api::systemlog::get_systemlog,
|
||||||
api::operation::delete,
|
api::operation::delete,
|
||||||
api::operation::report,
|
api::operation::report,
|
||||||
api::operation::set_title,
|
|
||||||
api::operation::block,
|
api::operation::block,
|
||||||
api::operation::set_auto_block,
|
api::operation::set_auto_block,
|
||||||
api::vote::vote,
|
api::vote::vote,
|
||||||
api::upload::ipfs_upload,
|
|
||||||
cors::options_handler,
|
cors::options_handler,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/_api/v2",
|
"/_api/v2",
|
||||||
routes![
|
routes![
|
||||||
|
api::attention::set_notification,
|
||||||
|
api::reaction::reaction,
|
||||||
api::comment::add_comment,
|
api::comment::add_comment,
|
||||||
|
api::operation::set_title,
|
||||||
api::upload::local_upload,
|
api::upload::local_upload,
|
||||||
cors::options_handler,
|
cors::options_handler,
|
||||||
api::attention::set_notification,
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.mount(
|
.mount(
|
||||||
"/_login",
|
"/_login",
|
||||||
[
|
[
|
||||||
#[cfg(feature = "mastlogin")]
|
#[cfg(feature = "mastlogin")]
|
||||||
routes![login::cs_login, login::cs_auth],
|
routes![
|
||||||
|
login::cs_login,
|
||||||
|
login::cs_auth,
|
||||||
|
login::gh_login,
|
||||||
|
login::gh_auth
|
||||||
|
],
|
||||||
routes![],
|
routes![],
|
||||||
]
|
]
|
||||||
.concat(),
|
.concat(),
|
||||||
)
|
)
|
||||||
.register(
|
.register(
|
||||||
"/_api",
|
"/_api",
|
||||||
catchers![api::catch_401_error, api::catch_403_error,],
|
catchers![
|
||||||
|
api::catch_401_error,
|
||||||
|
api::catch_403_error,
|
||||||
|
api::catch_404_error
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
.manage(MainLimiters::init())
|
||||||
.manage(RandomHasher::get_random_one())
|
.manage(RandomHasher::get_random_one())
|
||||||
.manage(rmc)
|
.manage(rmc)
|
||||||
.attach(Db::fairing())
|
.attach(Db::fairing())
|
||||||
@@ -119,7 +134,7 @@ async fn main() -> Result<(), rocket::Error> {
|
|||||||
.collect::<Vec<String>>(),
|
.collect::<Vec<String>>(),
|
||||||
})
|
})
|
||||||
.launch()
|
.launch()
|
||||||
.await
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_env() {
|
fn load_env() {
|
||||||
@@ -132,6 +147,6 @@ fn load_env() {
|
|||||||
|
|
||||||
fn init_database() {
|
fn init_database() {
|
||||||
let database_url = env::var("DATABASE_URL").unwrap();
|
let database_url = env::var("DATABASE_URL").unwrap();
|
||||||
let conn = Conn::establish(&database_url).unwrap();
|
let mut conn = Conn::establish(&database_url).unwrap();
|
||||||
embedded_migrations::run(&conn).unwrap();
|
conn.run_pending_migrations(MIGRATIONS).unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
180
src/models.rs
180
src/models.rs
@@ -1,13 +1,10 @@
|
|||||||
#![allow(clippy::all)]
|
// #![allow(clippy::all)]
|
||||||
|
|
||||||
use crate::cache::*;
|
use crate::cache::*;
|
||||||
use crate::db_conn::{Conn, Db};
|
use crate::db_conn::{Conn, Db};
|
||||||
use crate::libs::diesel_logger::LoggingConnection;
|
|
||||||
use crate::random_hasher::random_string;
|
use crate::random_hasher::random_string;
|
||||||
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::sql_types::*;
|
use diesel::sql_types::*;
|
||||||
use diesel::{
|
use diesel::{
|
||||||
insert_into, BoolExpressionMethods, ExpressionMethods, QueryDsl, QueryResult, RunQueryDsl,
|
insert_into, BoolExpressionMethods, ExpressionMethods, QueryDsl, QueryResult, RunQueryDsl,
|
||||||
@@ -15,11 +12,15 @@ use diesel::{
|
|||||||
};
|
};
|
||||||
use rocket::futures::{future, join};
|
use rocket::futures::{future, join};
|
||||||
use rocket::serde::{Deserialize, Serialize};
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
no_arg_sql_function!(RANDOM, (), "Represents the sql RANDOM() function");
|
#[declare_sql_function]
|
||||||
sql_function!(fn floor(x: Float) -> Int4);
|
extern "SQL" {
|
||||||
sql_function!(fn float4(x: Int4) -> Float);
|
fn random() -> Text;
|
||||||
|
fn floor(x: Float) -> Int4;
|
||||||
|
fn float4(x: Int4) -> Float;
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! _get {
|
macro_rules! _get {
|
||||||
($table:ident) => {
|
($table:ident) => {
|
||||||
@@ -40,7 +41,7 @@ macro_rules! _get_multi {
|
|||||||
// eq(any()) is only for postgres
|
// eq(any()) is only for postgres
|
||||||
db.run(move |c| {
|
db.run(move |c| {
|
||||||
$table::table
|
$table::table
|
||||||
.filter($table::id.eq(any(ids)))
|
.filter($table::id.eq_any(ids))
|
||||||
.filter($table::is_deleted.eq(false))
|
.filter($table::is_deleted.eq(false))
|
||||||
.load(with_log!(c))
|
.load(with_log!(c))
|
||||||
})
|
})
|
||||||
@@ -60,6 +61,8 @@ macro_rules! op_to_col_expr {
|
|||||||
|
|
||||||
macro_rules! update {
|
macro_rules! update {
|
||||||
($obj:expr, $table:ident, $db:expr, $({ $col:ident, $op:ident $v:expr }), + ) => {{
|
($obj:expr, $table:ident, $db:expr, $({ $col:ident, $op:ident $v:expr }), + ) => {{
|
||||||
|
use crate::schema;
|
||||||
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
|
||||||
let id = $obj.id;
|
let id = $obj.id;
|
||||||
$obj = $db
|
$obj = $db
|
||||||
.run(move |c| {
|
.run(move |c| {
|
||||||
@@ -82,13 +85,14 @@ macro_rules! base_query {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: log sql query
|
||||||
macro_rules! with_log {
|
macro_rules! with_log {
|
||||||
($c: expr) => {
|
($c: expr) => {
|
||||||
&LoggingConnection::new($c)
|
$c
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Insertable, Serialize, Deserialize, Debug)]
|
#[derive(Queryable, Insertable, Serialize, Deserialize, Debug, Clone)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct Comment {
|
pub struct Comment {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@@ -102,7 +106,7 @@ pub struct Comment {
|
|||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Insertable, Serialize, Deserialize, Debug)]
|
#[derive(Queryable, Insertable, Serialize, Deserialize, Debug, Clone)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@@ -120,9 +124,11 @@ pub struct Post {
|
|||||||
pub hot_score: i32,
|
pub hot_score: i32,
|
||||||
pub allow_search: bool,
|
pub allow_search: bool,
|
||||||
pub room_id: i32,
|
pub room_id: i32,
|
||||||
|
pub up_votes: i32,
|
||||||
|
pub down_votes: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Queryable, Insertable, Serialize, Deserialize, Debug)]
|
#[derive(Queryable, Insertable, Serialize, Deserialize, Debug, Clone)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
@@ -132,7 +138,7 @@ pub struct User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
#[table_name = "posts"]
|
#[diesel(table_name = posts)]
|
||||||
pub struct NewPost {
|
pub struct NewPost {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub cw: String,
|
pub cw: String,
|
||||||
@@ -149,9 +155,8 @@ impl Post {
|
|||||||
|
|
||||||
_get_multi!(posts);
|
_get_multi!(posts);
|
||||||
|
|
||||||
pub async fn get_multi(db: &Db, rconn: &RdsConn, ids: &Vec<i32>) -> QueryResult<Vec<Self>> {
|
pub async fn get_multi(db: &Db, ids: &[i32]) -> QueryResult<Vec<Self>> {
|
||||||
let mut cacher = PostCache::init(&rconn);
|
let mut cached_posts = PostCache::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();
|
||||||
|
|
||||||
// dbg!(&cached_posts);
|
// dbg!(&cached_posts);
|
||||||
@@ -161,7 +166,7 @@ impl Post {
|
|||||||
.zip(cached_posts.iter_mut())
|
.zip(cached_posts.iter_mut())
|
||||||
.filter_map(|(pid, p)| match p {
|
.filter_map(|(pid, p)| match p {
|
||||||
None => {
|
None => {
|
||||||
id2po.insert(pid.clone(), p);
|
id2po.insert(*pid, p);
|
||||||
Some(pid)
|
Some(pid)
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
@@ -173,7 +178,7 @@ impl Post {
|
|||||||
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().collect::<Vec<_>>()).await;
|
PostCache::sets(&missing_ps.iter().collect::<Vec<_>>()).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) {
|
||||||
@@ -187,56 +192,52 @@ impl Post {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(db: &Db, rconn: &RdsConn, id: i32) -> QueryResult<Self> {
|
pub async fn get(db: &Db, id: i32) -> QueryResult<Self> {
|
||||||
// 注意即使is_deleted也应该缓存和返回
|
// 注意即使is_deleted也应该缓存和返回
|
||||||
let mut cacher = PostCache::init(&rconn);
|
PostCache::get_with(id, async move { Self::_get(db, id).await }).await
|
||||||
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>> {
|
pub async fn get_comments(&self, db: &Db) -> QueryResult<Vec<Comment>> {
|
||||||
let mut cacher = PostCommentCache::init(self.id, rconn);
|
let cacher = PostCommentCache::init(self.id);
|
||||||
if let Some(cs) = cacher.get().await {
|
cacher
|
||||||
Ok(cs)
|
.get_with(async move { Comment::gets_by_post_id(db, self.id).await })
|
||||||
} else {
|
.await
|
||||||
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) {
|
pub async fn clear_comments_cache(&self) {
|
||||||
PostCommentCache::init(self.id, rconn).clear().await;
|
PostCommentCache::init(self.id).clear().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn gets_by_page(
|
pub async fn gets_by_page(
|
||||||
db: &Db,
|
db: &Db,
|
||||||
rconn: &RdsConn,
|
|
||||||
room_id: Option<i32>,
|
room_id: Option<i32>,
|
||||||
order_mode: u8,
|
order_mode: u8,
|
||||||
start: i64,
|
start: usize,
|
||||||
limit: i64,
|
limit: usize,
|
||||||
) -> QueryResult<Vec<Self>> {
|
) -> QueryResult<Vec<Self>> {
|
||||||
let mut cacher = PostListCache::init(room_id, order_mode, &rconn);
|
let mut cacher = PostListCache::init(room_id, order_mode);
|
||||||
if cacher.need_fill().await {
|
|
||||||
let pids =
|
let current_len = cacher
|
||||||
Self::_get_ids_by_page(db, room_id, order_mode.clone(), 0, cacher.i64_minlen())
|
.fill_with(async move {
|
||||||
|
let pids = Self::_get_ids_by_page(
|
||||||
|
db,
|
||||||
|
room_id,
|
||||||
|
order_mode,
|
||||||
|
0,
|
||||||
|
PostListCache::MAX_LENGTH as i64,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let ps = Self::get_multi(db, rconn, &pids).await?;
|
Self::get_multi(db, &pids).await
|
||||||
cacher.fill(&ps).await;
|
})
|
||||||
}
|
.await?;
|
||||||
let pids = if start + limit > cacher.i64_len() {
|
|
||||||
Self::_get_ids_by_page(db, room_id, order_mode, start, limit).await?
|
let pids = if start + limit > current_len {
|
||||||
|
Self::_get_ids_by_page(db, room_id, order_mode, start as i64, limit as i64).await?
|
||||||
} else {
|
} else {
|
||||||
cacher.get_pids(start, limit).await
|
cacher.get_pids(start, limit).await
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::get_multi(db, rconn, &pids).await
|
Self::get_multi(db, &pids).await
|
||||||
}
|
}
|
||||||
async fn _get_ids_by_page(
|
async fn _get_ids_by_page(
|
||||||
db: &Db,
|
db: &Db,
|
||||||
@@ -251,6 +252,10 @@ impl Post {
|
|||||||
query = query.filter(posts::is_reported.eq(false));
|
query = query.filter(posts::is_reported.eq(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if order_mode == 1 {
|
||||||
|
query = query.filter(posts::n_comments.gt(0));
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(ri) = room_id {
|
if let Some(ri) = room_id {
|
||||||
query = query.filter(posts::room_id.eq(ri));
|
query = query.filter(posts::room_id.eq(ri));
|
||||||
}
|
}
|
||||||
@@ -259,7 +264,8 @@ impl Post {
|
|||||||
0 => query.order(posts::id.desc()),
|
0 => query.order(posts::id.desc()),
|
||||||
1 => query.order(posts::last_comment_time.desc()),
|
1 => query.order(posts::last_comment_time.desc()),
|
||||||
2 => query.order(posts::hot_score.desc()),
|
2 => query.order(posts::hot_score.desc()),
|
||||||
3 => query.order(RANDOM),
|
3 => query.order(random()),
|
||||||
|
4 => query.order(posts::n_attentions.desc()),
|
||||||
_ => panic!("Wrong order mode!"),
|
_ => panic!("Wrong order mode!"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -270,14 +276,13 @@ impl Post {
|
|||||||
|
|
||||||
pub async fn search(
|
pub async fn search(
|
||||||
db: &Db,
|
db: &Db,
|
||||||
rconn: &RdsConn,
|
|
||||||
room_id: Option<i32>,
|
room_id: Option<i32>,
|
||||||
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('%', "\\%");
|
||||||
let pids = db
|
let pids = db
|
||||||
.run(move |c| {
|
.run(move |c| {
|
||||||
let pat;
|
let pat;
|
||||||
@@ -304,7 +309,7 @@ impl Post {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
1 => {
|
1 => {
|
||||||
pat = format!("%{}%", search_text2.replace(" ", "%"));
|
pat = format!("%{}%", search_text2.replace(' ', "%"));
|
||||||
query
|
query
|
||||||
.filter(
|
.filter(
|
||||||
posts::content.like(&pat).or(comments::content
|
posts::content.like(&pat).or(comments::content
|
||||||
@@ -328,7 +333,7 @@ impl Post {
|
|||||||
.load(with_log!(c))
|
.load(with_log!(c))
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
Self::get_multi(db, rconn, &pids).await
|
Self::get_multi(db, &pids).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create(db: &Db, new_post: NewPost) -> QueryResult<Self> {
|
pub async fn create(db: &Db, new_post: NewPost) -> QueryResult<Self> {
|
||||||
@@ -340,33 +345,33 @@ impl Post {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_instance_cache(&self, rconn: &RdsConn) {
|
pub async fn set_instance_cache(&self) {
|
||||||
PostCache::init(rconn).sets(&vec![self]).await;
|
PostCache::sets(&[self]).await;
|
||||||
}
|
}
|
||||||
pub async fn refresh_cache(&self, rconn: &RdsConn, is_new: bool) {
|
pub async fn refresh_cache(&self, is_new: bool) {
|
||||||
join!(
|
join!(
|
||||||
self.set_instance_cache(rconn),
|
self.set_instance_cache(),
|
||||||
future::join_all((if is_new { 0..4 } else { 1..4 }).map(|mode| async move {
|
future::join_all((if is_new { [0, 2, 3, 4] } else { [1, 2, 3, 4] }).map(
|
||||||
PostListCache::init(None, mode, &rconn.clone())
|
|mode| async move {
|
||||||
|
PostListCache::init(None, mode).put(self).await;
|
||||||
|
PostListCache::init(Some(self.room_id), mode)
|
||||||
.put(self)
|
.put(self)
|
||||||
.await;
|
.await;
|
||||||
PostListCache::init(Some(self.room_id), mode, &rconn.clone())
|
}
|
||||||
.put(self)
|
)),
|
||||||
.await;
|
|
||||||
})),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn annealing(mut c: Conn, rconn: &RdsConn) {
|
pub async fn annealing(c: &mut Conn) {
|
||||||
info!("Time for annealing!");
|
info!("Time for annealing!");
|
||||||
diesel::update(posts::table.filter(posts::hot_score.gt(10)))
|
diesel::update(posts::table.filter(posts::hot_score.gt(10)))
|
||||||
.set(posts::hot_score.eq(floor(float4(posts::hot_score) * 0.9)))
|
.set(posts::hot_score.eq(floor(float4(posts::hot_score) * 0.9)))
|
||||||
.execute(with_log!(&mut c))
|
.execute(with_log!(c))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
PostCache::init(&rconn).clear_all().await;
|
PostCache::clear_all().await;
|
||||||
for room_id in (0..5).map(Some).chain([None, Some(42)]) {
|
for room_id in (0..5).map(Some).chain([None, Some(42)]) {
|
||||||
PostListCache::init(room_id, 2, rconn).clear().await;
|
PostListCache::init(room_id, 2).clear().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,8 +388,25 @@ impl User {
|
|||||||
.ok()
|
.ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_by_token(db: &Db, rconn: &RdsConn, token: &str) -> Option<Self> {
|
pub async fn get_by_token(db: &Db, token: &str) -> Option<Self> {
|
||||||
let mut cacher = UserCache::init(token, &rconn);
|
let cacher = UserCache::init(token);
|
||||||
|
if let Some(u) = cacher.get().await {
|
||||||
|
return Some(u);
|
||||||
|
}
|
||||||
|
let real_token;
|
||||||
|
|
||||||
|
let token = match &token.split(':').collect::<Vec<&str>>()[..] {
|
||||||
|
["sha256", tk] => {
|
||||||
|
let mut h = Sha256::new();
|
||||||
|
h.update(tk);
|
||||||
|
h.update("hole");
|
||||||
|
real_token = format!("{:x}", h.finalize())[0..16].to_string();
|
||||||
|
&real_token
|
||||||
|
}
|
||||||
|
_ => token,
|
||||||
|
};
|
||||||
|
// dbg!(token);
|
||||||
|
let cacher = UserCache::init(token);
|
||||||
if let Some(u) = cacher.get().await {
|
if let Some(u) = cacher.get().await {
|
||||||
Some(u)
|
Some(u)
|
||||||
} else {
|
} else {
|
||||||
@@ -425,10 +447,22 @@ impl User {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn clear_non_admin_users(c: &mut Conn) {
|
||||||
|
diesel::delete(users::table.filter(users::is_admin.eq(false)))
|
||||||
|
.execute(c)
|
||||||
|
.unwrap();
|
||||||
|
UserCache::clear_all().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_count(db: &Db) -> QueryResult<i64> {
|
||||||
|
db.run(move |c| users::table.count().get_result(with_log!(c)))
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable)]
|
#[derive(Insertable)]
|
||||||
#[table_name = "comments"]
|
#[diesel(table_name = comments)]
|
||||||
pub struct NewComment {
|
pub struct NewComment {
|
||||||
pub content: String,
|
pub content: String,
|
||||||
pub author_hash: String,
|
pub author_hash: String,
|
||||||
|
|||||||
67
src/rate_limit.rs
Normal file
67
src/rate_limit.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use std::iter;
|
||||||
|
use std::num::NonZeroUsize;
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use lru::LruCache;
|
||||||
|
|
||||||
|
pub struct Limiter {
|
||||||
|
record: Mutex<LruCache<i32, Vec<u64>>>,
|
||||||
|
amount: u64,
|
||||||
|
interval: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Limiter {
|
||||||
|
pub fn init(amount: u64, interval: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
record: Mutex::new(LruCache::new(NonZeroUsize::new(2000).unwrap())),
|
||||||
|
amount,
|
||||||
|
interval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check(&self, uid: i32) -> bool {
|
||||||
|
let t = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_secs();
|
||||||
|
let mut r = self.record.lock().unwrap();
|
||||||
|
if let Some(ts) = r.pop(&uid) {
|
||||||
|
let new_ts: Vec<u64> = ts
|
||||||
|
.into_iter()
|
||||||
|
.chain(iter::once(t))
|
||||||
|
.filter(|&tt| tt + self.interval > t)
|
||||||
|
.collect();
|
||||||
|
let len = new_ts.len() as u64;
|
||||||
|
r.put(uid, new_ts);
|
||||||
|
len < self.amount
|
||||||
|
} else {
|
||||||
|
r.put(uid, vec![t]);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MainLimiters {
|
||||||
|
post_min: Limiter,
|
||||||
|
post_hour: Limiter,
|
||||||
|
get_hour: Limiter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MainLimiters {
|
||||||
|
pub fn init() -> Self {
|
||||||
|
Self {
|
||||||
|
post_min: Limiter::init(6, 60),
|
||||||
|
post_hour: Limiter::init(50, 3600),
|
||||||
|
get_hour: Limiter::init(1000, 3600),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check(&self, is_post: bool, uid: i32) -> bool {
|
||||||
|
if is_post {
|
||||||
|
self.post_hour.check(uid) && self.post_min.check(uid)
|
||||||
|
} else {
|
||||||
|
self.get_hour.check(uid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::api::CurrentUser;
|
use crate::api::{Api, CurrentUser, PolicyError};
|
||||||
|
use crate::random_hasher::random_string;
|
||||||
use crate::rds_conn::RdsConn;
|
use crate::rds_conn::RdsConn;
|
||||||
use chrono::{offset::Local, DateTime};
|
use chrono::{offset::Local, DateTime};
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
use redis::{AsyncCommands, RedisResult};
|
use redis::{AsyncCommands, RedisResult};
|
||||||
use rocket::serde::json::serde_json;
|
use rocket::serde::json::serde_json;
|
||||||
use rocket::serde::{Deserialize, Serialize};
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
@@ -13,7 +15,7 @@ macro_rules! init {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
($ktype:ty, $formatter:expr) => {
|
($ktype:ty, $formatter:literal) => {
|
||||||
pub fn init(k: $ktype, rconn: &RdsConn) -> Self {
|
pub fn init(k: $ktype, rconn: &RdsConn) -> Self {
|
||||||
Self {
|
Self {
|
||||||
key: format!($formatter, k),
|
key: format!($formatter, k),
|
||||||
@@ -21,7 +23,7 @@ macro_rules! init {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
($k1type:ty, $k2type:ty, $formatter:expr) => {
|
($k1type:ty, $k2type:ty, $formatter:literal) => {
|
||||||
pub fn init(k1: $k1type, k2: $k2type, rconn: &RdsConn) -> Self {
|
pub fn init(k1: $k1type, k2: $k2type, rconn: &RdsConn) -> Self {
|
||||||
Self {
|
Self {
|
||||||
key: format!($formatter, k1, k2),
|
key: format!($formatter, k1, k2),
|
||||||
@@ -47,11 +49,46 @@ macro_rules! add {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
macro_rules! rem {
|
||||||
|
($vtype:ty) => {
|
||||||
|
pub async fn rem(&mut self, v: $vtype) -> RedisResult<usize> {
|
||||||
|
self.rconn.srem(&self.key, v).await
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! clear_all {
|
||||||
|
($pattern:literal) => {
|
||||||
|
pub async fn clear_all(rconn: &mut RdsConn) {
|
||||||
|
let keys: Vec<String> = rconn
|
||||||
|
.scan_match::<&str, String>($pattern)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
rconn
|
||||||
|
.del(keys)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|e| warn!("clear all fail, pattern: {} , {}", $pattern, e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const KEY_SYSTEMLOG: &str = "hole_v2:systemlog_list";
|
const KEY_SYSTEMLOG: &str = "hole_v2:systemlog_list";
|
||||||
const KEY_BANNED_USERS: &str = "hole_v2:banned_user_hash_list";
|
const KEY_BANNED_USERS: &str = "hole_v2:banned_user_hash_list";
|
||||||
const KEY_BLOCKED_COUNTER: &str = "hole_v2:blocked_counter";
|
const KEY_BLOCKED_COUNTER: &str = "hole_v2:blocked_counter";
|
||||||
const KEY_CUSTOM_TITLE: &str = "hole_v2:title";
|
const KEY_CUSTOM_TITLE: &str = "hole_v2:title";
|
||||||
|
const CUSTOM_TITLE_KEEP_TIME: u64 = 7 * 24 * 60 * 60;
|
||||||
|
macro_rules! KEY_TITLE_SECRET {
|
||||||
|
($title: expr) => {
|
||||||
|
format!("hole_v2:title_secret:{}", $title)
|
||||||
|
};
|
||||||
|
}
|
||||||
const KEY_AUTO_BLOCK_RANK: &str = "hole_v2:auto_block_rank"; // rank * 5: 自动过滤的拉黑数阈值
|
const KEY_AUTO_BLOCK_RANK: &str = "hole_v2:auto_block_rank"; // rank * 5: 自动过滤的拉黑数阈值
|
||||||
|
const KEY_ANNOUNCEMENT: &str = "hole_v2:announcement";
|
||||||
|
const KEY_CANDIDATE: &str = "hole_v2:candidate";
|
||||||
|
const KEY_ADMIN: &str = "hole_v2:admin";
|
||||||
|
|
||||||
const SYSTEMLOG_MAX_LEN: isize = 1000;
|
const SYSTEMLOG_MAX_LEN: isize = 1000;
|
||||||
|
|
||||||
@@ -67,6 +104,8 @@ impl Attention {
|
|||||||
|
|
||||||
has!(i32);
|
has!(i32);
|
||||||
|
|
||||||
|
clear_all!("hole_v2:attention:*");
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -74,26 +113,34 @@ impl Attention {
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn clear_all(rconn: &RdsConn) {
|
pub struct Reaction {
|
||||||
let mut rconn = rconn.clone();
|
key: String,
|
||||||
let mut keys = rconn
|
rconn: RdsConn,
|
||||||
.scan_match::<&str, String>("hole_v2:attention:*")
|
}
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let mut ks_for_del = Vec::new();
|
impl Reaction {
|
||||||
while let Some(key) = keys.next_item().await {
|
init!(i32, i32, "hole_v2:reaction:{}:{}");
|
||||||
ks_for_del.push(key);
|
|
||||||
|
add!(&str);
|
||||||
|
|
||||||
|
rem!(&str);
|
||||||
|
|
||||||
|
has!(&str);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_post_reaction_status(
|
||||||
|
rconn: &RdsConn,
|
||||||
|
pid: i32,
|
||||||
|
namehash: &str,
|
||||||
|
) -> RedisResult<i32> {
|
||||||
|
for rt in [-1, 1] {
|
||||||
|
if Reaction::init(pid, rt, rconn).has(namehash).await? {
|
||||||
|
return Ok(rt);
|
||||||
}
|
}
|
||||||
if ks_for_del.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
rconn
|
|
||||||
.del(ks_for_del)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|e| warn!("clear all post cache fail, {}", e));
|
|
||||||
}
|
}
|
||||||
|
Ok(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
@@ -129,7 +176,9 @@ impl Systemlog {
|
|||||||
pub async fn create(&self, rconn: &RdsConn) -> RedisResult<()> {
|
pub async fn create(&self, rconn: &RdsConn) -> RedisResult<()> {
|
||||||
let mut rconn = rconn.clone();
|
let mut rconn = rconn.clone();
|
||||||
if rconn.llen::<&str, isize>(KEY_SYSTEMLOG).await? > SYSTEMLOG_MAX_LEN {
|
if rconn.llen::<&str, isize>(KEY_SYSTEMLOG).await? > SYSTEMLOG_MAX_LEN {
|
||||||
rconn.ltrim(KEY_SYSTEMLOG, 0, SYSTEMLOG_MAX_LEN - 1).await?;
|
rconn
|
||||||
|
.ltrim::<_, ()>(KEY_SYSTEMLOG, 0, SYSTEMLOG_MAX_LEN - 1)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
rconn
|
rconn
|
||||||
.lpush(KEY_SYSTEMLOG, serde_json::to_string(&self).unwrap())
|
.lpush(KEY_SYSTEMLOG, serde_json::to_string(&self).unwrap())
|
||||||
@@ -162,8 +211,8 @@ impl BannedUsers {
|
|||||||
rconn.clone().sismember(KEY_BANNED_USERS, namehash).await
|
rconn.clone().sismember(KEY_BANNED_USERS, namehash).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clear(rconn: &RdsConn) -> RedisResult<()> {
|
pub async fn clear(rconn: &mut RdsConn) -> RedisResult<()> {
|
||||||
rconn.clone().del(KEY_BANNED_USERS).await
|
rconn.del(KEY_BANNED_USERS).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,6 +228,8 @@ impl BlockedUsers {
|
|||||||
|
|
||||||
has!(&str);
|
has!(&str);
|
||||||
|
|
||||||
|
clear_all!("hole_v2:blocked_users:*");
|
||||||
|
|
||||||
pub async fn check_if_block(
|
pub async fn check_if_block(
|
||||||
rconn: &RdsConn,
|
rconn: &RdsConn,
|
||||||
user: &CurrentUser,
|
user: &CurrentUser,
|
||||||
@@ -195,7 +246,7 @@ impl BlockedUsers {
|
|||||||
pub struct BlockCounter;
|
pub struct BlockCounter;
|
||||||
|
|
||||||
impl BlockCounter {
|
impl BlockCounter {
|
||||||
pub async fn count_incr(rconn: &RdsConn, namehash: &str) -> RedisResult<usize> {
|
pub async fn count_incr(rconn: &RdsConn, namehash: &str) -> RedisResult<i32> {
|
||||||
rconn.clone().hincr(KEY_BLOCKED_COUNTER, namehash, 1).await
|
rconn.clone().hincr(KEY_BLOCKED_COUNTER, namehash, 1).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,24 +258,63 @@ impl BlockCounter {
|
|||||||
pub struct CustomTitle;
|
pub struct CustomTitle;
|
||||||
|
|
||||||
impl CustomTitle {
|
impl CustomTitle {
|
||||||
|
async fn gen_and_set_secret(rconn: &RdsConn, title: &str) -> RedisResult<String> {
|
||||||
|
let secret = random_string(8);
|
||||||
|
() = rconn
|
||||||
|
.clone()
|
||||||
|
.set_ex::<_, _, ()>(KEY_TITLE_SECRET!(&title), &secret, CUSTOM_TITLE_KEEP_TIME)
|
||||||
|
.await?;
|
||||||
|
Ok(secret)
|
||||||
|
}
|
||||||
|
|
||||||
// return false if title exits
|
// return false if title exits
|
||||||
pub async fn set(rconn: &RdsConn, namehash: &str, title: &str) -> RedisResult<bool> {
|
pub async fn set(rconn: &RdsConn, namehash: &str, title: &str, secret: &str) -> Api<String> {
|
||||||
let mut rconn = rconn.clone();
|
let mut rconn = rconn.clone();
|
||||||
if rconn.hexists(KEY_CUSTOM_TITLE, title).await? {
|
if rconn.hexists(KEY_CUSTOM_TITLE, title).await? {
|
||||||
Ok(false)
|
Err(PolicyError::TitleUsed)?
|
||||||
} else {
|
} else {
|
||||||
rconn.hset(KEY_CUSTOM_TITLE, namehash, title).await?;
|
let ori_secret: Option<String> = rconn.get(KEY_TITLE_SECRET!(title)).await?;
|
||||||
rconn.hset(KEY_CUSTOM_TITLE, title, namehash).await?;
|
if ori_secret.is_none() {
|
||||||
Ok(true)
|
clear_title_from_admins(&rconn, title).await?;
|
||||||
|
}
|
||||||
|
ori_secret
|
||||||
|
.map_or(Some(()), |s| (s.eq(&secret).then_some(())))
|
||||||
|
.ok_or(PolicyError::TitleProtected)?;
|
||||||
|
|
||||||
|
let old_title: Option<String> = rconn.hget(KEY_CUSTOM_TITLE, namehash).await?;
|
||||||
|
if let Some(t) = old_title {
|
||||||
|
clear_title_from_admins(&rconn, &t).await?;
|
||||||
|
}
|
||||||
|
() = rconn.hset(KEY_CUSTOM_TITLE, namehash, title).await?;
|
||||||
|
() = rconn.hset(KEY_CUSTOM_TITLE, title, namehash).await?;
|
||||||
|
Ok(Self::gen_and_set_secret(&rconn, title).await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(rconn: &RdsConn, namehash: &str) -> RedisResult<Option<String>> {
|
pub async fn get(
|
||||||
rconn.clone().hget(KEY_CUSTOM_TITLE, namehash).await
|
rconn: &RdsConn,
|
||||||
|
namehash: &str,
|
||||||
|
) -> RedisResult<(Option<String>, Option<String>)> {
|
||||||
|
let t: Option<String> = rconn.clone().hget(KEY_CUSTOM_TITLE, namehash).await?;
|
||||||
|
Ok(if let Some(title) = t {
|
||||||
|
let s: Option<String> = rconn.clone().get(KEY_TITLE_SECRET!(title)).await?;
|
||||||
|
let secret = if let Some(ss) = s {
|
||||||
|
() = rconn
|
||||||
|
.clone()
|
||||||
|
.expire(KEY_TITLE_SECRET!(title), CUSTOM_TITLE_KEEP_TIME as i64)
|
||||||
|
.await?;
|
||||||
|
ss
|
||||||
|
} else {
|
||||||
|
Self::gen_and_set_secret(rconn, &title).await?
|
||||||
|
};
|
||||||
|
(Some(title), Some(secret))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clear(rconn: &RdsConn) -> RedisResult<()> {
|
pub async fn clear(rconn: &mut RdsConn) -> RedisResult<()> {
|
||||||
rconn.clone().del(KEY_CUSTOM_TITLE).await
|
rconn.del(KEY_CUSTOM_TITLE).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,8 +333,8 @@ impl AutoBlockRank {
|
|||||||
Ok(rank.unwrap_or(4))
|
Ok(rank.unwrap_or(4))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clear(rconn: &RdsConn) -> RedisResult<()> {
|
pub async fn clear(rconn: &mut RdsConn) -> RedisResult<()> {
|
||||||
rconn.clone().del(KEY_AUTO_BLOCK_RANK).await
|
rconn.del(KEY_AUTO_BLOCK_RANK).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,7 +347,7 @@ impl PollOption {
|
|||||||
init!(i32, "hole_thu:poll_opts:{}");
|
init!(i32, "hole_thu:poll_opts:{}");
|
||||||
|
|
||||||
pub async fn set_list(&mut self, v: &Vec<String>) -> RedisResult<()> {
|
pub async fn set_list(&mut self, v: &Vec<String>) -> RedisResult<()> {
|
||||||
self.rconn.del(&self.key).await?;
|
() = self.rconn.del(&self.key).await?;
|
||||||
self.rconn.rpush(&self.key, v).await
|
self.rconn.rpush(&self.key, v).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,11 +373,47 @@ impl PollVote {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn clear_outdate_redis_data(rconn: &RdsConn) {
|
pub async fn clear_outdate_redis_data(rconn: &mut RdsConn) {
|
||||||
BannedUsers::clear(rconn).await.unwrap();
|
BannedUsers::clear(rconn).await.unwrap();
|
||||||
CustomTitle::clear(rconn).await.unwrap();
|
CustomTitle::clear(rconn).await.unwrap();
|
||||||
AutoBlockRank::clear(rconn).await.unwrap();
|
AutoBlockRank::clear(rconn).await.unwrap();
|
||||||
Attention::clear_all(rconn).await;
|
Attention::clear_all(rconn).await;
|
||||||
|
BlockedUsers::clear_all(rconn).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_announcement(rconn: &RdsConn) -> RedisResult<Option<String>> {
|
||||||
|
rconn.clone().get(KEY_ANNOUNCEMENT).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_elected_candidate(rconn: &RdsConn, title: &Option<String>) -> RedisResult<bool> {
|
||||||
|
if let Some(t) = title {
|
||||||
|
rconn.clone().sismember(KEY_CANDIDATE, t).await
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_elected_admin(rconn: &RdsConn, title: &Option<String>) -> RedisResult<bool> {
|
||||||
|
if let Some(t) = title {
|
||||||
|
rconn.clone().sismember(KEY_ADMIN, t).await
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_admin_list(rconn: &RdsConn) -> RedisResult<Vec<String>> {
|
||||||
|
rconn.clone().smembers(KEY_ADMIN).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_candidate_list(rconn: &RdsConn) -> RedisResult<Vec<String>> {
|
||||||
|
rconn.clone().smembers(KEY_CANDIDATE).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_title_from_admins(rconn: &RdsConn, title: &str) -> RedisResult<()> {
|
||||||
|
let mut rconn = rconn.clone();
|
||||||
|
() = rconn.srem(KEY_CANDIDATE, title).await?;
|
||||||
|
rconn.srem(KEY_ADMIN, title).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use clear_all;
|
||||||
pub(crate) use init;
|
pub(crate) use init;
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ table! {
|
|||||||
hot_score -> Int4,
|
hot_score -> Int4,
|
||||||
allow_search -> Bool,
|
allow_search -> Bool,
|
||||||
room_id -> Int4,
|
room_id -> Int4,
|
||||||
|
up_votes -> Int4,
|
||||||
|
down_votes -> Int4,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user