feat: edit comment

This commit is contained in:
Pascal Engélibert 2022-12-07 19:06:08 +01:00
commit c13e172938
Signed by: tuxmain
GPG key ID: 3504BC6D362F7DCA
8 changed files with 276 additions and 61 deletions

View file

@ -36,12 +36,41 @@ pub fn load_dbs(path: Option<&Path>) -> Dbs {
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Comment {
pub topic_hash: TopicHash,
pub author: String,
pub email: Option<String>,
pub last_edit_time: Option<u64>,
pub mutation_token: MutationToken,
pub post_time: u64,
pub text: String,
pub topic_hash: TopicHash,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct MutationToken(pub [u8; 16]);
impl MutationToken {
pub fn new() -> Self {
Self(rand::random())
}
pub fn to_base64(&self) -> String {
base64::encode_config(self.0, base64::URL_SAFE_NO_PAD)
}
pub fn from_base64(s: &str) -> Result<Self, base64::DecodeError> {
std::panic::catch_unwind(|| {
let mut buf = [0; 16];
base64::decode_config_slice(s, base64::URL_SAFE_NO_PAD, &mut buf)?;
Ok(Self(buf))
})
.map_err(|_| base64::DecodeError::InvalidLength)?
}
}
impl AsRef<[u8]> for MutationToken {
fn as_ref(&self) -> &[u8] {
&self.0
}
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
@ -82,12 +111,12 @@ impl CommentId {
}
pub fn from_base64(s: &str) -> Result<Self, base64::DecodeError> {
let mut buf = [0; 16];
std::panic::catch_unwind(move || {
base64::decode_config_slice(s, base64::URL_SAFE_NO_PAD, &mut buf)
std::panic::catch_unwind(|| {
let mut buf = [0; 16];
base64::decode_config_slice(s, base64::URL_SAFE_NO_PAD, &mut buf)?;
Ok(Self(buf))
})
.map_err(|_| base64::DecodeError::InvalidLength)?
.map(|_| Self(buf))
}
}
@ -112,6 +141,17 @@ mod test {
assert_eq!(iter.next(), Some(Ok(((123, 456), ()))));
}
#[test]
fn test_comment_id_base64() {
for _ in 0..10 {
let comment_id = CommentId::new();
assert_eq!(
CommentId::from_base64(&comment_id.to_base64()),
Ok(comment_id)
);
}
}
#[test]
fn test_from_base64_dont_panic() {
assert_eq!(CommentId::from_base64("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"), Err(base64::DecodeError::InvalidLength));

View file

@ -217,6 +217,31 @@ pub fn check_comment(
}
}
pub fn check_can_edit_comment<'a>(
config: &Config,
comment: &Comment,
mutation_token: &MutationToken,
) -> Result<(), &'a str> {
if &comment.mutation_token != mutation_token {
return Err("bad mutation token");
}
let time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
if time
> comment
.post_time
.saturating_add(config.comment_edit_timeout)
{
return Err("mutation timeout expired");
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
@ -228,12 +253,13 @@ mod test {
author: String::from("Emmanuel Goldstein"),
email: None,
last_edit_time: None,
mutation_token: MutationToken::new(),
post_time: 42,
text: String::from("Hello world!"),
};
let dbs = load_dbs(None);
let comment_id = new_pending_comment(&comment, &dbs).unwrap();
let comment_id = new_pending_comment(&comment, None, &dbs).unwrap();
let mut iter = dbs.comment.iter();
assert_eq!(iter.next(), Some(Ok((comment_id.clone(), comment.clone()))));
@ -248,7 +274,7 @@ mod test {
comment.post_time,
comment_id.clone()
),
()
None
)))
);
assert_eq!(iter.next(), None);

View file

@ -28,6 +28,8 @@ impl Locales {
.iter()
.map(|(lang, raw)| {
let mut bundle = FluentBundle::new_concurrent(vec![lang.clone()]);
// We don't want dangerous zero-width bidi chars everywhere! (issue #26)
bundle.set_use_isolating(false);
bundle
.add_resource(
FluentResource::try_new(raw.to_string()).unwrap_or_else(|e| {
@ -68,7 +70,6 @@ impl Locales {
NegotiationStrategy::Filtering,
) {
if let Some(bundle) = self.bundles.get(prefered_lang.as_ref()) {
println!("got bundle");
if let Some(message) = bundle.get_message(key) {
let mut errors = Vec::new();
let ret = bundle.format_pattern(message.value().unwrap(), args, &mut errors);

View file

@ -39,6 +39,43 @@ pub async fn run_server(
}
});
app.at(&format!("{}t/:topic", config.root_url)).post({
let dbs = dbs.clone();
let notify_send = notify_send.clone();
move |req: tide::Request<()>| {
handle_post_comments(
req,
config,
templates,
dbs.clone(),
locales,
notify_send.clone(),
)
}
});
app.at(&format!(
"{}t/:topic/edit/:comment_id/:mutation_token",
config.root_url
))
.get({
let dbs = dbs.clone();
move |req: tide::Request<()>| {
let client_langs = get_client_langs(&req);
serve_edit_comment(
req,
config,
templates,
dbs.clone(),
client_langs,
Context::new(),
200,
)
}
});
app.at(&format!(
"{}t/:topic/edit/:comment_id/:mutation_token",
config.root_url
))
.post({
let dbs = dbs.clone();
move |req: tide::Request<()>| {
handle_post_comments(
@ -63,6 +100,53 @@ pub async fn run_server(
app.listen(config.listen).await.unwrap();
}
async fn serve_edit_comment<'a>(
req: tide::Request<()>,
config: &Config,
templates: &Templates,
dbs: Dbs,
client_langs: Vec<LanguageIdentifier>,
mut context: Context,
status_code: u16,
) -> tide::Result<tide::Response> {
let (Ok(comment_id_str), Ok(mutation_token_str)) = (req.param("comment_id"), req.param("mutation_token")) else {
context.insert("log", &["no comment id or no token"]);
return serve_comments(req, config, templates, dbs, client_langs, context, 400).await;
};
let (Ok(comment_id), Ok(mutation_token)) = (CommentId::from_base64(comment_id_str), MutationToken::from_base64(mutation_token_str)) else {
context.insert("log", &["badly encoded comment id or token"]);
return serve_comments(req, config, templates, dbs, client_langs, context, 400).await;
};
let Some(comment) = dbs.comment.get(&comment_id).unwrap() else {
context.insert("log", &["not found comment"]);
return serve_comments(req, config, templates, dbs, client_langs, context, 404).await;
};
if let Err(e) = helpers::check_can_edit_comment(config, &comment, &mutation_token) {
context.insert("log", &[e]);
return serve_comments(req, config, templates, dbs, client_langs, context, 403).await;
}
context.insert("edit_comment", &comment_id.to_base64());
context.insert("edit_comment_mutation_token", &mutation_token.to_base64());
context.insert("edit_comment_author", &comment.author);
context.insert("edit_comment_email", &comment.email);
context.insert("edit_comment_text", &comment.text);
serve_comments(
req,
config,
templates,
dbs,
client_langs,
context,
status_code,
)
.await
}
async fn serve_comments<'a>(
req: tide::Request<()>,
config: &Config,
@ -337,17 +421,43 @@ async fn handle_post_comments(
Some(query.comment.email)
},
last_edit_time: None,
mutation_token: MutationToken::new(),
post_time: time,
text: query.comment.text,
};
helpers::new_pending_comment(&comment, client_addr, &dbs)
.map_err(|e| error!("Adding pending comment: {:?}", e))
.ok();
notify_send
.send(Notification {
topic: topic.to_string(),
})
.ok();
match helpers::new_pending_comment(&comment, client_addr, &dbs) {
Ok(comment_id) => {
notify_send
.send(Notification {
topic: topic.to_string(),
})
.ok();
context.insert(
"log",
&[locales
.tr(
&client_langs,
if config.comment_approve {
"new_comment-success_pending"
} else {
"new_comment-success"
},
Some(&FluentArgs::from_iter([(
"edit_link",
format!(
"{}t/{}/edit/{}/{}",
&config.root_url,
topic,
comment_id.to_base64(),
comment.mutation_token.to_base64(),
),
)])),
)
.unwrap()],
);
}
Err(e) => error!("Adding pending comment: {:?}", e),
}
} else {
context.insert("new_comment_author", &query.comment.author);
context.insert("new_comment_email", &query.comment.email);
@ -356,51 +466,69 @@ async fn handle_post_comments(
context.insert("new_comment_errors", &errors);
}
CommentQuery::EditComment(query) => {
if !admin {
return Err(tide::Error::from_str(403, "Forbidden"));
}
helpers::check_comment(config, locales, &client_langs, &query.comment, &mut errors);
let comment_id = if let Ok(comment_id) = CommentId::from_base64(&query.id) {
comment_id
} else {
let Ok(comment_id) = CommentId::from_base64(&query.id) else {
return Err(tide::Error::from_str(400, "Invalid comment id"));
};
let mut comment = if let Some(comment) = dbs.comment.get(&comment_id).unwrap() {
comment
} else {
let Some(mut comment) = dbs.comment.get(&comment_id).unwrap() else {
return Err(tide::Error::from_str(404, "Not found"));
};
// We're admin
/*if let Some(client_addr) = &client_addr {
if let Some(antispam_timeout) =
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
{
let client_langs = get_client_langs(&req);
errors.push(
locales
.tr(
&client_langs,
"error-antispam",
Some(&FluentArgs::from_iter([(
"antispam_timeout",
antispam_timeout,
)])),
)
.unwrap()
.into_owned(),
);
let mutation_token = if admin {
None
} else {
'mutation_token: {
let Ok(mutation_token_str) = req.param("mutation_token") else {
errors.push("no mutation token".into());
break 'mutation_token None;
};
let Ok(mutation_token) = MutationToken::from_base64(mutation_token_str) else {
errors.push("badly encoded token".into());
break 'mutation_token None;
};
if let Err(e) =
helpers::check_can_edit_comment(config, &comment, &mutation_token)
{
errors.push(e.to_string());
}
Some(mutation_token)
}
}*/
};
if !admin {
if let Some(client_addr) = &client_addr {
if let Some(antispam_timeout) =
helpers::antispam_check_client_mutation(client_addr, &dbs, config).unwrap()
{
let client_langs = get_client_langs(&req);
errors.push(
locales
.tr(
&client_langs,
"error-antispam",
Some(&FluentArgs::from_iter([(
"antispam_timeout",
antispam_timeout,
)])),
)
.unwrap()
.into_owned(),
);
}
}
}
if errors.is_empty() {
// We're admin
/*if let Some(client_addr) = &client_addr {
helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
}*/
if !admin {
if let Some(client_addr) = &client_addr {
helpers::antispam_update_client_mutation(client_addr, &dbs).unwrap();
}
}
let time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
@ -423,9 +551,16 @@ async fn handle_post_comments(
dbs.comment.insert(&comment_id, &comment).unwrap();
} else {
context.insert("edit_comment", &comment_id.to_base64());
if let Some(mutation_token) = &mutation_token {
context.insert("edit_comment_mutation_token", &mutation_token.to_base64());
}
context.insert("edit_comment_author", &query.comment.author);
context.insert("edit_comment_email", &query.comment.email);
context.insert("edit_comment_text", &query.comment.text);
context.insert("edit_comment_errors", &errors);
return serve_edit_comment(req, config, templates, dbs, client_langs, context, 400)
.await;
}
context.insert("edit_comment_errors", &errors);
}