Api & JS client
This commit is contained in:
parent
8629bba07f
commit
5deba2fdb1
10 changed files with 399 additions and 144 deletions
28
src/db.rs
28
src/db.rs
|
|
@ -12,10 +12,10 @@ const DB_DIR: &str = "db";
|
|||
|
||||
pub type Time = u64;
|
||||
|
||||
pub const BASE64: base64::engine::fast_portable::FastPortable =
|
||||
base64::engine::fast_portable::FastPortable::from(
|
||||
pub const BASE64: base64::engine::general_purpose::GeneralPurpose =
|
||||
base64::engine::general_purpose::GeneralPurpose::new(
|
||||
&base64::alphabet::URL_SAFE,
|
||||
base64::engine::fast_portable::NO_PAD,
|
||||
base64::engine::general_purpose::NO_PAD,
|
||||
);
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -74,20 +74,13 @@ impl MutationToken {
|
|||
}
|
||||
|
||||
pub fn to_base64(&self) -> String {
|
||||
let mut buf = vec![0; 24];
|
||||
let size = BASE64.encode(&self.0, &mut buf);
|
||||
buf.truncate(size);
|
||||
String::from_utf8(buf).unwrap()
|
||||
BASE64.encode(self.0)
|
||||
}
|
||||
|
||||
pub fn from_base64(s: &str) -> Result<Self, base64::DecodeError> {
|
||||
std::panic::catch_unwind(|| {
|
||||
let mut buf = [0; 16];
|
||||
BASE64.decode(
|
||||
s.as_bytes(),
|
||||
&mut buf,
|
||||
BASE64.decoded_length_estimate(s.len()),
|
||||
)?;
|
||||
BASE64.decode_slice_unchecked(s.as_bytes(), &mut buf)?;
|
||||
Ok(Self(buf))
|
||||
})
|
||||
.map_err(|_| base64::DecodeError::InvalidLength)?
|
||||
|
|
@ -134,20 +127,13 @@ impl CommentId {
|
|||
}
|
||||
|
||||
pub fn to_base64(&self) -> String {
|
||||
let mut buf = vec![0; 24];
|
||||
let size = BASE64.encode(&self.0, &mut buf);
|
||||
buf.truncate(size);
|
||||
String::from_utf8(buf).unwrap()
|
||||
BASE64.encode(self.0)
|
||||
}
|
||||
|
||||
pub fn from_base64(s: &str) -> Result<Self, base64::DecodeError> {
|
||||
std::panic::catch_unwind(|| {
|
||||
let mut buf = [0; 16];
|
||||
BASE64.decode(
|
||||
s.as_bytes(),
|
||||
&mut buf,
|
||||
BASE64.decoded_length_estimate(s.len()),
|
||||
)?;
|
||||
BASE64.decode_slice_unchecked(s.as_bytes(), &mut buf)?;
|
||||
Ok(Self(buf))
|
||||
})
|
||||
.map_err(|_| base64::DecodeError::InvalidLength)?
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ pub async fn run_server(
|
|||
.build())
|
||||
});
|
||||
|
||||
api::init_routes(&mut app, config, dbs.clone()).await;
|
||||
api::init_routes(&mut app, config, dbs.clone(), notify_send.clone()).await;
|
||||
page::init_routes(&mut app, config, dbs, templates, locales, notify_send).await;
|
||||
|
||||
app.listen(config.listen).await.unwrap();
|
||||
|
|
|
|||
|
|
@ -1,54 +1,29 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
mod queries;
|
||||
mod resps;
|
||||
|
||||
use crate::{config::*, db::*, helpers, notify::Notification};
|
||||
|
||||
use crossbeam_channel::Sender;
|
||||
use log::{error, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
enum ApiError {
|
||||
InvalidAdminPassword,
|
||||
}
|
||||
|
||||
pub async fn init_routes(app: &mut tide::Server<()>, config: &'static Config, dbs: Dbs) {
|
||||
pub async fn init_routes(
|
||||
app: &mut tide::Server<()>,
|
||||
config: &'static Config,
|
||||
dbs: Dbs,
|
||||
notify_send: Sender<Notification>,
|
||||
) {
|
||||
// TODO pagination
|
||||
app.at(&format!("{}api/comments_by_topic", config.root_url))
|
||||
.post({
|
||||
let dbs = dbs.clone();
|
||||
move |req: tide::Request<()>| query_comments_by_topic(req, config, dbs.clone())
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CommentWithId {
|
||||
pub addr: Option<String>,
|
||||
pub author: String,
|
||||
pub editable: bool,
|
||||
pub id: String,
|
||||
pub last_edit_time: Option<Time>,
|
||||
pub status: Option<OriginalComment>,
|
||||
pub post_time: Time,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct OriginalComment {
|
||||
pub author: String,
|
||||
pub editable: bool,
|
||||
pub last_edit_time: Option<Time>,
|
||||
pub post_time: Time,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CommentsByTopicQuery {
|
||||
mutation_token: Option<String>,
|
||||
topic: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CommentsByTopicResp {
|
||||
comments: Vec<CommentWithId>,
|
||||
app.at(&format!("{}api/new_comment", config.root_url))
|
||||
.post({
|
||||
move |req: tide::Request<()>| {
|
||||
query_new_comment(req, config, dbs.clone(), notify_send.clone())
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn query_comments_by_topic(
|
||||
|
|
@ -56,11 +31,17 @@ async fn query_comments_by_topic(
|
|||
config: &Config,
|
||||
dbs: Dbs,
|
||||
) -> tide::Result<tide::Response> {
|
||||
let Ok(CommentsByTopicQuery {
|
||||
mutation_token,
|
||||
let Ok(queries::CommentsByTopic {
|
||||
mutation_token: _mutation_token,
|
||||
topic,
|
||||
}) = req.body_json().await else {
|
||||
return Err(tide::Error::from_str(400, "Invalid request"));
|
||||
return Ok(tide::Response::builder(400)
|
||||
.content_type(tide::http::mime::JSON)
|
||||
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
|
||||
.body(
|
||||
tide::Body::from_json(&resps::Error::InvalidRequest).unwrap(),
|
||||
)
|
||||
.build());
|
||||
};
|
||||
|
||||
let topic_hash = TopicHash::from_topic(&topic);
|
||||
|
|
@ -69,19 +50,21 @@ async fn query_comments_by_topic(
|
|||
.content_type(tide::http::mime::JSON)
|
||||
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
|
||||
.body(
|
||||
tide::Body::from_json(&CommentsByTopicResp {
|
||||
tide::Body::from_json(&resps::CommentsByTopic {
|
||||
comments: helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
|
||||
.map(|(comment_id, comment, _comment_status)| CommentWithId {
|
||||
addr: None,
|
||||
author: comment.author,
|
||||
editable: false,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: comment.last_edit_time,
|
||||
post_time: comment.post_time,
|
||||
status: None,
|
||||
text: comment.text,
|
||||
})
|
||||
.collect::<Vec<CommentWithId>>(),
|
||||
.map(
|
||||
|(comment_id, comment, _comment_status)| resps::CommentWithId {
|
||||
addr: None,
|
||||
author: comment.author,
|
||||
editable: false,
|
||||
id: comment_id.to_base64(),
|
||||
last_edit_time: comment.last_edit_time,
|
||||
post_time: comment.post_time,
|
||||
status: None,
|
||||
text: comment.text,
|
||||
},
|
||||
)
|
||||
.collect::<Vec<resps::CommentWithId>>(),
|
||||
})
|
||||
.map_err(|e| {
|
||||
error!("Serializing CommentsByTopicResp to json: {e:?}");
|
||||
|
|
@ -90,3 +73,125 @@ async fn query_comments_by_topic(
|
|||
)
|
||||
.build())
|
||||
}
|
||||
|
||||
async fn query_new_comment(
|
||||
mut req: tide::Request<()>,
|
||||
config: &Config,
|
||||
dbs: Dbs,
|
||||
notify_send: Sender<Notification>,
|
||||
) -> tide::Result<tide::Response> {
|
||||
let Ok(query) = req.body_json::<queries::NewComment>().await else {
|
||||
return Ok(tide::Response::builder(400)
|
||||
.content_type(tide::http::mime::JSON)
|
||||
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
|
||||
.body(
|
||||
tide::Body::from_json(&resps::Error::InvalidRequest).unwrap(),
|
||||
)
|
||||
.build());
|
||||
};
|
||||
|
||||
if query.author.len() > config.comment_author_max_len
|
||||
|| query.email.len() > config.comment_email_max_len
|
||||
|| query.text.len() > config.comment_text_max_len
|
||||
{
|
||||
return Ok(tide::Response::builder(400)
|
||||
.content_type(tide::http::mime::JSON)
|
||||
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
|
||||
.body(tide::Body::from_json(&resps::Error::IllegalContent).unwrap())
|
||||
.build());
|
||||
}
|
||||
|
||||
let client_addr = match helpers::get_client_addr(config, &req) {
|
||||
Some(Ok(addr)) => Some(addr),
|
||||
Some(Err(e)) => {
|
||||
warn!("Unable to parse client addr: {}", e);
|
||||
None
|
||||
}
|
||||
None => {
|
||||
warn!("No client addr");
|
||||
None
|
||||
}
|
||||
};
|
||||
let antispam_enabled = config.antispam_enable
|
||||
&& client_addr
|
||||
.as_ref()
|
||||
.map_or(false, |addr| !config.antispam_whitelist.contains(addr));
|
||||
|
||||
if let Some(client_addr) = &client_addr {
|
||||
if antispam_enabled {
|
||||
if let Some(antispam_timeout) =
|
||||
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
|
||||
{
|
||||
return Ok(tide::Response::builder(403)
|
||||
.content_type(tide::http::mime::JSON)
|
||||
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
|
||||
.body(
|
||||
tide::Body::from_json(&resps::Error::Antispam {
|
||||
timeout: antispam_timeout,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It's OK
|
||||
|
||||
if let Some(client_addr) = &client_addr {
|
||||
if antispam_enabled {
|
||||
helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
let topic_hash = TopicHash::from_topic(&query.topic);
|
||||
|
||||
let time = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let comment = Comment {
|
||||
topic_hash,
|
||||
author: if query.author.is_empty() {
|
||||
petname::Petnames::large().generate_one(2, " ")
|
||||
} else {
|
||||
query.author
|
||||
},
|
||||
email: if query.email.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(query.email)
|
||||
},
|
||||
last_edit_time: None,
|
||||
mutation_token: MutationToken::new(),
|
||||
post_time: time,
|
||||
text: query.text,
|
||||
};
|
||||
match helpers::new_pending_comment(&comment, client_addr, &dbs) {
|
||||
Ok(comment_id) => {
|
||||
notify_send.send(Notification { topic: query.topic }).ok();
|
||||
Ok(tide::Response::builder(200)
|
||||
.content_type(tide::http::mime::JSON)
|
||||
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
|
||||
.body(
|
||||
tide::Body::from_json(&resps::NewComment {
|
||||
id: comment_id.to_base64(),
|
||||
mutation_token: comment.mutation_token.to_base64(),
|
||||
post_time: time,
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
.build())
|
||||
}
|
||||
// TODO add message to client log and change http code
|
||||
Err(e) => {
|
||||
error!("Adding pending comment: {:?}", e);
|
||||
Ok(tide::Response::builder(500)
|
||||
.content_type(tide::http::mime::JSON)
|
||||
.header("Access-Control-Allow-Origin", &config.cors_allow_origin)
|
||||
.body(tide::Body::from_json(&resps::Error::Internal).unwrap())
|
||||
.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
15
src/server/api/queries.rs
Normal file
15
src/server/api/queries.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CommentsByTopic {
|
||||
pub mutation_token: Option<String>,
|
||||
pub topic: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NewComment {
|
||||
pub author: String,
|
||||
pub email: String,
|
||||
pub text: String,
|
||||
pub topic: String,
|
||||
}
|
||||
44
src/server/api/resps.rs
Normal file
44
src/server/api/resps.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use crate::db::*;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub enum Error {
|
||||
Antispam { timeout: Time },
|
||||
IllegalContent,
|
||||
Internal,
|
||||
InvalidRequest,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CommentsByTopic {
|
||||
pub comments: Vec<CommentWithId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct OriginalComment {
|
||||
pub author: String,
|
||||
pub editable: bool,
|
||||
pub last_edit_time: Option<Time>,
|
||||
pub post_time: Time,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct CommentWithId {
|
||||
pub addr: Option<String>,
|
||||
pub author: String,
|
||||
pub editable: bool,
|
||||
pub id: String,
|
||||
pub last_edit_time: Option<Time>,
|
||||
pub status: Option<OriginalComment>,
|
||||
pub post_time: Time,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct NewComment {
|
||||
pub id: String,
|
||||
pub mutation_token: String,
|
||||
pub post_time: Time,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue