Browse Source

feat: basic api & model for post

master
hole-thu 3 years ago
parent
commit
a9d7576ed4
  1. 3
      .gitignore
  2. 14
      Cargo.toml
  3. 5
      diesel.toml
  4. 0
      migrations/.gitkeep
  5. 2
      migrations/2022-03-11-065048_create_posts/down.sql
  6. 20
      migrations/2022-03-11-065048_create_posts/up.sql
  7. 80
      src/api/mod.rs
  8. 106
      src/api/post.rs
  9. 35
      src/main.rs
  10. 82
      src/models.rs
  11. 22
      src/random_hasher.rs
  12. 17
      src/schema.rs
  13. 37
      tools/migdb.py

3
.gitignore vendored

@ -1,3 +1,6 @@
# --> sqlite3
*.db
# ---> Rust
# Generated by Cargo
# will have compiled files and executables

14
Cargo.toml

@ -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"

5
diesel.toml

@ -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
migrations/.gitkeep

2
migrations/2022-03-11-065048_create_posts/down.sql

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE posts

20
migrations/2022-03-11-065048_create_posts/up.sql

@ -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`)

80
src/api/mod.rs

@ -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;

106
src/api/post.rs

@ -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
}))
}

35
src/main.rs

@ -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(),
}
}

82
src/models.rs

@ -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)
}
}

22
src/random_hasher.rs

@ -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)
}
}

17
src/schema.rs

@ -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,
}
}

37
tools/migdb.py

@ -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…
Cancel
Save