Update internals for reply support, add error reasons, minor changes

main
Elnu 2 years ago
parent 4a39b10afb
commit 1bea87f47f

@ -1,3 +1,4 @@
<link rel="icon" href="data:,">
<meta name="soudan-content-id" content="a"> <meta name="soudan-content-id" content="a">
<link rel="stylesheet" href="https://unpkg.com/sakura.css/css/sakura-dark.css" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/sakura.css/css/sakura-dark.css" type="text/css">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">

@ -1,3 +1,4 @@
<link rel="icon" href="data:,">
<meta name="soudan-content-id" content="b"> <meta name="soudan-content-id" content="b">
<link rel="stylesheet" href="https://unpkg.com/sakura.css/css/sakura-dark.css" type="text/css"> <link rel="stylesheet" href="https://unpkg.com/sakura.css/css/sakura-dark.css" type="text/css">
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">

@ -4,6 +4,7 @@ document.getElementById("soudan").innerHTML = `<h3>Make a comment</h3>
<label for="email">Email:</label> <input type="email" name="email"> <label for="email">Email:</label> <input type="email" name="email">
<label for="text">Comment:</label> <label for="text">Comment:</label>
<textarea name="text" required></textarea> <textarea name="text" required></textarea>
<input type="hidden" name="parent">
<input type="submit"> <input type="submit">
</form> </form>
<h3 id="soudan-comments-header">Comments</h3> <h3 id="soudan-comments-header">Comments</h3>

@ -6,6 +6,8 @@ use validator::Validate;
#[derive(Serialize, Deserialize, Validate)] #[derive(Serialize, Deserialize, Validate)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Comment { pub struct Comment {
#[serde(skip_deserializing)]
pub id: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>, // None is Anonymous pub author: Option<String>, // None is Anonymous
#[serde(rename(serialize = "gravatar"))] #[serde(rename(serialize = "gravatar"))]
@ -19,7 +21,13 @@ pub struct Comment {
#[serde(with = "ts_seconds_option")] #[serde(with = "ts_seconds_option")]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<DateTime<Utc>>, pub timestamp: Option<DateTime<Utc>>,
#[serde(skip_serializing)]
pub content_id: String, pub content_id: String,
#[serde(skip_serializing)]
pub parent: Option<i64>,
#[serde(skip_serializing_if = "<[_]>::is_empty")]
#[serde(skip_deserializing)]
pub replies: Vec<Comment>,
} }
fn serialize_gravatar<S>(email: &Option<String>, s: S) -> Result<S::Ok, S::Error> fn serialize_gravatar<S>(email: &Option<String>, s: S) -> Result<S::Ok, S::Error>

@ -18,7 +18,8 @@ impl Database {
author TEXT, author TEXT,
text TEXT NOT NULL, text TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
content_id TEXT NOT NULL content_id TEXT NOT NULL,
parent INTEGER
)", )",
params![], params![],
)?; )?;
@ -27,14 +28,33 @@ impl Database {
pub fn get_comments(&self, content_id: &str) -> Result<Vec<Comment>> { pub fn get_comments(&self, content_id: &str) -> Result<Vec<Comment>> {
self.conn self.conn
.prepare(&format!("SELECT author, email, text, timestamp FROM comment WHERE content_id='{content_id}' ORDER BY timestamp DESC"))? .prepare(&format!("SELECT id, author, email, text, timestamp FROM comment WHERE content_id='{content_id}' AND parent IS NULL ORDER BY timestamp DESC"))?
.query_map([], |row| { .query_map([], |row| {
let id = row.get::<usize, Option<i64>>(0)?.unwrap();
let replies = self.conn
.prepare(&format!("SELECT id, author, email, text, timestamp FROM comment WHERE parent={id} ORDER BY timestamp DESC"))?
.query_map([], |row| {
Ok(Comment {
id: row.get(0)?,
author: row.get(1)?,
email: row.get(2)?,
text: row.get(3)?,
timestamp: row.get(4)?,
content_id: content_id.to_owned(),
parent: Some(id),
replies: Vec::new(), // no recursion
})
})?
.collect::<Result<Vec<Comment>>>()?;
Ok(Comment { Ok(Comment {
author: row.get(0)?, id: Some(id),
email: row.get(1)?, author: row.get(1)?,
text: row.get(2)?, email: row.get(2)?,
timestamp: row.get(3)?, text: row.get(3)?,
timestamp: row.get(4)?,
content_id: content_id.to_owned(), content_id: content_id.to_owned(),
parent: None,
replies,
}) })
})? })?
.collect() .collect()

@ -23,20 +23,19 @@ fn get_db<'a>(
data: &'a web::Data<AppState>, data: &'a web::Data<AppState>,
request: &HttpRequest, request: &HttpRequest,
) -> Result<MutexGuard<'a, Database>, HttpResponse> { ) -> Result<MutexGuard<'a, Database>, HttpResponse> {
// all the .into() are converting from HttpResponseBuilder to HttpResponse
let origin = match request.head().headers().get("Origin") { let origin = match request.head().headers().get("Origin") {
Some(origin) => match origin.to_str() { Some(origin) => match origin.to_str() {
Ok(origin) => origin, Ok(origin) => origin,
Err(_) => return Err(HttpResponse::BadRequest().into()), Err(_) => return Err(HttpResponse::BadRequest().reason("bad origin").finish()),
}, },
None => return Err(HttpResponse::BadRequest().into()), None => return Err(HttpResponse::BadRequest().reason("bad origin").finish()),
}; };
match data.databases.get(origin) { match data.databases.get(origin) {
Some(database) => Ok(match database.lock() { Some(database) => Ok(match database.lock() {
Ok(database) => database, Ok(database) => database,
Err(_) => return Err(HttpResponse::InternalServerError().into()), Err(_) => return Err(HttpResponse::InternalServerError().reason("database error").finish()),
}), }),
None => return Err(HttpResponse::BadRequest().into()), None => return Err(HttpResponse::BadRequest().reason("bad origin").finish()),
} }
} }
@ -69,19 +68,19 @@ async fn post_comment(
Ok(text) => { Ok(text) => {
let PostCommentsRequest { url, comment } = match serde_json::from_str(&text) { let PostCommentsRequest { url, comment } = match serde_json::from_str(&text) {
Ok(req) => req, Ok(req) => req,
Err(_) => return HttpResponse::BadRequest().into(), Err(_) => return HttpResponse::BadRequest().reason("invalid request body").finish(),
}; };
if comment.validate().is_err() { if comment.validate().is_err() {
return HttpResponse::BadRequest().into(); return HttpResponse::BadRequest().reason("invalid comment field(s)").finish();
} }
let origin = match request.head().headers().get("Origin") { let origin = match request.head().headers().get("Origin") {
Some(origin) => match origin.to_str() { Some(origin) => match origin.to_str() {
Ok(origin) => origin, Ok(origin) => origin,
// If the Origin is not valid ASCII, it is a bad request not sent from a browser // If the Origin is not valid ASCII, it is a bad request not sent from a browser
Err(_) => return HttpResponse::BadRequest().into(), Err(_) => return HttpResponse::BadRequest().reason("bad origin").finish(),
}, },
// If there is no Origin header, it is a bad request not sent from a browser // If there is no Origin header, it is a bad request not sent from a browser
None => return HttpResponse::BadRequest().into(), None => return HttpResponse::BadRequest().reason("bad origin").finish(),
}; };
// Check to see if provided URL is in scope. // Check to see if provided URL is in scope.
// This is to prevent malicious requests that try to get server to fetch external websites. // This is to prevent malicious requests that try to get server to fetch external websites.
@ -93,18 +92,18 @@ async fn post_comment(
break 'outer; break 'outer;
} }
} }
return HttpResponse::BadRequest().into(); return HttpResponse::BadRequest().reason("url out of scope").finish();
} }
match get_page_data(&url).await { match get_page_data(&url).await {
Ok(page_data_option) => match page_data_option { Ok(page_data_option) => match page_data_option {
Some(page_data) => { Some(page_data) => {
if page_data.content_id != comment.content_id { if page_data.content_id != comment.content_id {
return HttpResponse::BadRequest().into(); return HttpResponse::BadRequest().reason("content ids don't match").finish();
} }
} }
None => return HttpResponse::BadRequest().into(), None => return HttpResponse::BadRequest().reason("url invalid").finish(), // e.g. 404
}, },
Err(_) => return HttpResponse::InternalServerError().into(), Err(_) => return HttpResponse::InternalServerError().reason("failed to get page data").finish(),
}; };
let database = match get_db(&data, &request) { let database = match get_db(&data, &request) {
Ok(database) => database, Ok(database) => database,
@ -113,7 +112,7 @@ async fn post_comment(
database.create_comment(&comment).unwrap(); database.create_comment(&comment).unwrap();
HttpResponse::Ok().into() HttpResponse::Ok().into()
} }
Err(_) => HttpResponse::BadRequest().into(), Err(_) => HttpResponse::BadRequest().reason("failed to parse request body").finish(),
} }
} }

Loading…
Cancel
Save