13 changed files with 423 additions and 0 deletions
@ -0,0 +1,14 @@
|
||||
[package] |
||||
name = "hole-thu" |
||||
version = "0.1.0" |
||||
edition = "2021" |
||||
license = "AGPL-3.0" |
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||
|
||||
[dependencies] |
||||
rocket = { version = "0.5.0-rc.1", features = ["json"] } |
||||
diesel = { version = "1.4.8", features= ["sqlite", "chrono"] } |
||||
chrono = { version="0.4", features=["serde"] } |
||||
rand = "0.8.5" |
||||
dotenv = "0.15.0" |
@ -0,0 +1,5 @@
|
||||
# For documentation on how to configure this file, |
||||
# see diesel.rs/guides/configuring-diesel-cli |
||||
|
||||
[print_schema] |
||||
file = "src/schema.rs" |
@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql` |
||||
DROP TABLE posts |
@ -0,0 +1,20 @@
|
||||
-- Your SQL goes here |
||||
|
||||
CREATE TABLE posts ( |
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
||||
author_hash VARCHAR NOT NULL, |
||||
content TEXT NOT NULL, |
||||
cw VARCHAR NOT NULL DEFAULT '', |
||||
author_title VARCHAR NOT NULL DEFAULT '', |
||||
n_likes INTEGER NOT NULL DEFAULT 0, |
||||
n_comments INTEGER NOT NULL DEFAULT 0, |
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||
last_comment_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, |
||||
is_deleted BOOLEAN NOT NULL DEFAULT FALSE, |
||||
is_reported BOOLEAN NOT NULL DEFAULT FALSE, |
||||
hot_score INTEGER NOT NULL DEFAULT 0, |
||||
allow_search BOOLEAN NOT NULL DEFAULT '' |
||||
); |
||||
CREATE INDEX posts_last_comment_time_idx ON posts (`last_comment_time`); |
||||
CREATE INDEX posts_hot_idx ON posts (`hot_score`) |
||||
|
@ -0,0 +1,80 @@
|
||||
use crate::random_hasher::RandomHasher; |
||||
use rocket::http::Status; |
||||
use rocket::request::{self, FromRequest, Request}; |
||||
use rocket::serde::json::{Value, json}; |
||||
use rocket::response::{self, Responder}; |
||||
|
||||
#[catch(401)] |
||||
pub fn catch_401_error() -> Value { |
||||
json!({ |
||||
"code": -1, |
||||
"msg": "未登录或token过期" |
||||
}) |
||||
} |
||||
|
||||
pub struct CurrentUser { |
||||
namehash: String, |
||||
is_admin: bool, |
||||
} |
||||
|
||||
#[rocket::async_trait] |
||||
impl<'r> FromRequest<'r> for CurrentUser { |
||||
type Error = (); |
||||
async fn from_request(request: &'r Request<'_>) -> request::Outcome<Self, Self::Error> { |
||||
let token = request.headers().get_one("User-Token"); |
||||
match token { |
||||
Some(t) => request::Outcome::Success(CurrentUser { |
||||
namehash: request |
||||
.rocket() |
||||
.state::<RandomHasher>() |
||||
.unwrap() |
||||
.hash_with_salt(t), |
||||
is_admin: t == "admin", // TODO
|
||||
}), |
||||
None => request::Outcome::Failure((Status::Unauthorized, ())), |
||||
} |
||||
} |
||||
} |
||||
|
||||
pub enum PolicyError { |
||||
IsReported, |
||||
IsDeleted, |
||||
NotAllowed |
||||
} |
||||
|
||||
|
||||
pub enum APIError { |
||||
DbError(diesel::result::Error), |
||||
PcError(PolicyError) |
||||
} |
||||
|
||||
impl APIError { |
||||
fn from_db(err: diesel::result::Error) -> APIError { |
||||
APIError::DbError(err) |
||||
} |
||||
} |
||||
|
||||
impl<'r> Responder<'r, 'static> for APIError { |
||||
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { |
||||
match self { |
||||
APIError::DbError(e) => |
||||
json!({ |
||||
"code": -1, |
||||
"msg": e.to_string() |
||||
}).respond_to(req), |
||||
APIError::PcError(e) => |
||||
json!({ |
||||
"code": -1, |
||||
"msg": match e { |
||||
PolicyError::IsReported => "内容被举报,处理中", |
||||
PolicyError::IsDeleted => "内容被删除", |
||||
PolicyError::NotAllowed => "不允许的操作", |
||||
} |
||||
}).respond_to(req), |
||||
} |
||||
} |
||||
} |
||||
|
||||
pub type API<T> = Result<T, APIError>; |
||||
|
||||
pub mod post; |
@ -0,0 +1,106 @@
|
||||
use crate::api::{APIError, CurrentUser, PolicyError::*, API}; |
||||
use crate::models::*; |
||||
use chrono::NaiveDateTime; |
||||
use rocket::serde::{ |
||||
json::{json, Json, Value}, |
||||
Deserialize, Serialize, |
||||
}; |
||||
|
||||
#[derive(Deserialize)] |
||||
#[serde(crate = "rocket::serde")] |
||||
pub struct PostInput<'r> { |
||||
content: &'r str, |
||||
cw: &'r str, |
||||
} |
||||
|
||||
#[derive(Serialize)] |
||||
#[serde(crate = "rocket::serde")] |
||||
pub struct PostOutput { |
||||
id: i32, |
||||
content: String, |
||||
cw: String, |
||||
author_title: String, |
||||
n_likes: i32, |
||||
n_comments: i32, |
||||
create_time: NaiveDateTime, |
||||
last_comment_time: NaiveDateTime, |
||||
allow_search: bool, |
||||
is_reported: Option<bool>, |
||||
// for old version frontend
|
||||
timestamp: NaiveDateTime, |
||||
} |
||||
|
||||
fn p2output(p: &Post, user: &CurrentUser) -> PostOutput { |
||||
PostOutput { |
||||
id: p.id, |
||||
content: p.content.to_string(), |
||||
cw: p.cw.to_string(), |
||||
author_title: p.author_title.to_string(), |
||||
n_likes: p.n_likes, |
||||
n_comments: p.n_comments, |
||||
create_time: p.create_time, |
||||
last_comment_time: p.last_comment_time, |
||||
allow_search: p.allow_search, |
||||
is_reported: if user.is_admin { |
||||
Some(p.is_reported) |
||||
} else { |
||||
None |
||||
}, |
||||
// for old version frontend
|
||||
timestamp: p.create_time, |
||||
} |
||||
} |
||||
|
||||
#[get("/post/<pid>")] |
||||
pub fn get_one(pid: i32, user: CurrentUser) -> API<Value> { |
||||
let conn = establish_connection(); |
||||
let p = Post::get(&conn, pid).map_err(APIError::from_db)?; |
||||
if !user.is_admin { |
||||
if p.is_reported { |
||||
return Err(APIError::PcError(IsReported)); |
||||
} |
||||
if p.is_deleted { |
||||
return Err(APIError::PcError(IsDeleted)); |
||||
} |
||||
} |
||||
Ok(json!({ |
||||
"data": p2output(&p, &user), |
||||
"code": 0, |
||||
})) |
||||
} |
||||
|
||||
#[get("/getlist?<p>&<order_mode>")] |
||||
pub fn get_list(p: Option<u32>, order_mode: u8, user: CurrentUser) -> API<Value> { |
||||
let page = p.unwrap_or(1); |
||||
let conn = establish_connection(); |
||||
let ps = Post::gets_by_page(&conn, order_mode, page, 25, user.is_admin) |
||||
.map_err(APIError::from_db)?; |
||||
let ps_data = ps |
||||
.iter() |
||||
.map(|p| p2output(p, &user)) |
||||
.collect::<Vec<PostOutput>>(); |
||||
Ok(json!({ |
||||
"data": ps_data, |
||||
"count": ps_data.len(), |
||||
"code": 0 |
||||
})) |
||||
} |
||||
|
||||
#[post("/dopost", format = "json", data = "<poi>")] |
||||
pub fn publish_post(poi: Json<PostInput>, user: CurrentUser) -> API<Value> { |
||||
let conn = establish_connection(); |
||||
let r = Post::create( |
||||
&conn, |
||||
NewPost { |
||||
content: &poi.content, |
||||
cw: &poi.cw, |
||||
author_hash: &user.namehash, |
||||
author_title: "", |
||||
}, |
||||
) |
||||
.map_err(APIError::from_db)?; |
||||
Ok(json!({ |
||||
"data": r, |
||||
"code": 0 |
||||
})) |
||||
} |
@ -0,0 +1,35 @@
|
||||
#[macro_use] |
||||
extern crate rocket; |
||||
|
||||
#[macro_use] |
||||
extern crate diesel; |
||||
|
||||
mod random_hasher; |
||||
mod models; |
||||
mod schema; |
||||
mod api; |
||||
|
||||
use random_hasher::RandomHasher; |
||||
|
||||
#[launch] |
||||
fn rocket() -> _ { |
||||
load_env(); |
||||
rocket::build() |
||||
.mount("/_api", routes![ |
||||
api::post::get_list, |
||||
api::post::get_one,
|
||||
api::post::publish_post |
||||
]) |
||||
.register("/_api", catchers![ |
||||
api::catch_401_error |
||||
]) |
||||
.manage(RandomHasher::get_random_one()) |
||||
} |
||||
|
||||
fn load_env() { |
||||
match dotenv::dotenv() { |
||||
Ok(path) => eprintln!("Configuration read from {}", path.display()), |
||||
Err(ref e) if e.not_found() => eprintln!("Warning: no .env was found"), |
||||
e => e.map(|_| ()).unwrap(), |
||||
} |
||||
} |
@ -0,0 +1,82 @@
|
||||
#![allow(clippy::all)] |
||||
|
||||
use chrono::NaiveDateTime; |
||||
use diesel::{insert_into, Connection, ExpressionMethods, QueryDsl, RunQueryDsl, SqliteConnection}; |
||||
use std::env; |
||||
|
||||
use crate::schema::posts; |
||||
|
||||
type MR<T> = Result<T, diesel::result::Error>; |
||||
|
||||
no_arg_sql_function!(RANDOM, (), "Represents the sql RANDOM() function"); |
||||
|
||||
pub fn establish_connection() -> SqliteConnection { |
||||
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); |
||||
SqliteConnection::establish(&database_url) |
||||
.expect(&format!("Error connecting to {}", database_url)) |
||||
} |
||||
|
||||
#[derive(Queryable, Debug)] |
||||
pub struct Post { |
||||
pub id: i32, |
||||
pub author_hash: String, |
||||
pub content: String, |
||||
pub cw: String, |
||||
pub author_title: String, |
||||
pub n_likes: i32, |
||||
pub n_comments: i32, |
||||
pub create_time: NaiveDateTime, |
||||
pub last_comment_time: NaiveDateTime, |
||||
pub is_deleted: bool, |
||||
pub is_reported: bool, |
||||
pub hot_score: i32, |
||||
pub allow_search: bool, |
||||
} |
||||
|
||||
#[derive(Insertable)] |
||||
#[table_name = "posts"] |
||||
pub struct NewPost<'a> { |
||||
pub content: &'a str, |
||||
pub cw: &'a str, |
||||
pub author_hash: &'a str, |
||||
pub author_title: &'a str, |
||||
// TODO: tags
|
||||
} |
||||
|
||||
impl Post { |
||||
pub fn get(conn: &SqliteConnection, id: i32) -> MR<Self> { |
||||
posts::table.find(id).first(conn) |
||||
} |
||||
|
||||
pub fn gets_by_page( |
||||
conn: &SqliteConnection, |
||||
order_mode: u8, |
||||
page: u32, |
||||
page_size: u32, |
||||
is_admin: bool, |
||||
) -> MR<Vec<Self>> { |
||||
let mut query = posts::table.into_boxed(); |
||||
if !is_admin { |
||||
query = query.filter(posts::is_deleted.eq(false)); |
||||
if order_mode > 0 { |
||||
query = query.filter(posts::is_reported.eq(false)) |
||||
} |
||||
} |
||||
|
||||
match order_mode { |
||||
1 => query = query.order(posts::last_comment_time.desc()), |
||||
2 => query = query.order(posts::hot_score.desc()), |
||||
3 => query = query.order(RANDOM), |
||||
_ => query = query.order(posts::id.desc()), |
||||
} |
||||
query |
||||
.offset(((page - 1) * page_size).into()) |
||||
.limit(page_size.into()) |
||||
.load(conn) |
||||
} |
||||
|
||||
pub fn create(conn: &SqliteConnection, new_post: NewPost) -> MR<usize> { |
||||
// TODO: tags
|
||||
insert_into(posts::table).values(&new_post).execute(conn) |
||||
} |
||||
} |
@ -0,0 +1,22 @@
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng}; |
||||
|
||||
pub struct RandomHasher { |
||||
salt: String, |
||||
} |
||||
|
||||
impl RandomHasher { |
||||
pub fn get_random_one() -> RandomHasher { |
||||
RandomHasher { |
||||
salt: thread_rng() |
||||
.sample_iter(&Alphanumeric) |
||||
.take(16) |
||||
.map(char::from) |
||||
.collect(), |
||||
} |
||||
} |
||||
|
||||
pub fn hash_with_salt(&self, text: &str) -> String { |
||||
// TODO
|
||||
format!("hash({}+{})", self.salt, text) |
||||
} |
||||
} |
@ -0,0 +1,17 @@
|
||||
table! { |
||||
posts (id) { |
||||
id -> Integer, |
||||
author_hash -> Text, |
||||
content -> Text, |
||||
cw -> Text, |
||||
author_title -> Text, |
||||
n_likes -> Integer, |
||||
n_comments -> Integer, |
||||
create_time -> Timestamp, |
||||
last_comment_time -> Timestamp, |
||||
is_deleted -> Bool, |
||||
is_reported -> Bool, |
||||
hot_score -> Integer, |
||||
allow_search -> Bool, |
||||
} |
||||
} |
@ -0,0 +1,37 @@
|
||||
import sqlite3 |
||||
from datetime import datetime |
||||
|
||||
def mig_post(db_old, db_new): |
||||
c_old = db_old.cursor() |
||||
c_new = db_new.cursor() |
||||
rs = c_old.execute( |
||||
'SELECT id, name_hash, content, cw, author_title, ' |
||||
'likenum, n_comments, timestamp, comment_timestamp, ' |
||||
'deleted, is_reported, hot_score, allow_search ' |
||||
'FROM post WHERE deleted = false' |
||||
) |
||||
|
||||
for r in rs: |
||||
r = list(r) |
||||
r[3] = r[3] or '' # cw |
||||
r[4] = r[4] or '' # author_title |
||||
r[8] = r[8] or r[7] # comment_timestamp |
||||
r[7] = datetime.fromtimestamp(r[7]) |
||||
r[8] = datetime.fromtimestamp(r[8]) |
||||
r[10] = r[10] or False # comment |
||||
c_new.execute( |
||||
'INSERT OR REPLACE INTO posts VALUES({})'.format(','.join(['?'] * 13)), |
||||
r |
||||
) |
||||
db_new.commit() |
||||
|
||||
c_old.close() |
||||
c_new.close() |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
db_old = sqlite3.connect('hole.db') |
||||
db_new = sqlite3.connect('hole_v2.db') |
||||
|
||||
mig_post(db_old, db_new) |
||||
|
Loading…
Reference in new issue