feat: edit comment
This commit is contained in:
parent
800036ce49
commit
c13e172938
8 changed files with 276 additions and 61 deletions
18
Cargo.lock
generated
18
Cargo.lock
generated
|
@ -2411,18 +2411,18 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.148"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e53f64bb4ba0191d6d0676e1b141ca55047d83b74f5607e6d8eb88126c52c2dc"
|
||||
checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.148"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a55492425aa53521babf6137309e7d34c20bbfbbfcfe2c7f3a047fd1f6b92c0c"
|
||||
checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2860,9 +2860,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
|||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.22.0"
|
||||
version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d76ce4a75fb488c605c54bf610f221cea8b0dafb53333c1a67e8ee199dcd2ae3"
|
||||
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
|
@ -2873,7 +2873,7 @@ dependencies = [
|
|||
"pin-project-lite 0.2.9",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"winapi",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3011,9 +3011,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
|
|
|
@ -23,12 +23,12 @@ petname = { version = "1.1.3", optional = true, default-features = false, featur
|
|||
rand = "0.8.5"
|
||||
rand_core = { version = "0.6.4", features = ["std"] }
|
||||
rpassword = "7.2.0"
|
||||
serde = { version = "1.0.148", features = ["derive", "rc"] }
|
||||
serde = { version = "1.0.149", features = ["derive", "rc"] }
|
||||
sha2 = "0.10.6"
|
||||
sled = "0.34.7"
|
||||
tera = { version = "1.17.1", features = ["builtins", "date-locale"] }
|
||||
tide = { version = "0.16.0", default-features = false, features = ["h1-server", "cookies", "logger"] }
|
||||
tokio = { version = "1.22.0", features = ["macros", "rt-multi-thread"] }
|
||||
tokio = { version = "1.23.0", features = ["macros", "rt-multi-thread"] }
|
||||
toml_edit = { version = "0.15.0", features = ["easy"] }
|
||||
typed-sled = "0.2.0"
|
||||
unic-langid = { version = "0.9.1", features = ["macros"] }
|
||||
|
|
|
@ -18,4 +18,6 @@ comment_form-email = Your email:
|
|||
comment_form-edit_button = Edit comment
|
||||
comment_form-new_button = Post comment
|
||||
comment_form-text = Your comment:
|
||||
new_comment-success = Your comment has been published. <a href="{ $edit_link }#edit_comment-form">You can edit it with this link.</a>
|
||||
new_comment-success_pending = Your comment has been saved. It will be reviewed by an administrator before being published. <a href="{ $edit_link }#edit_comment-form">You can edit it with this link.</a>
|
||||
title = Comments
|
||||
|
|
50
src/db.rs
50
src/db.rs
|
@ -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));
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
219
src/server.rs
219
src/server.rs
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,14 @@
|
|||
<title>{{ tr(l=l,k="title")|safe }}</title>
|
||||
</head>
|
||||
<body>
|
||||
{% if log %}
|
||||
<ul id="log">
|
||||
{% for log_msg in log %}
|
||||
<li class="log-msg">{{ log_msg | safe }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% if comments_pending %}
|
||||
<div id="comments_pending">
|
||||
{% for comment in comments_pending %}
|
||||
|
@ -74,6 +82,9 @@
|
|||
</ul>
|
||||
{% endif %}
|
||||
<input type="hidden" name="id" value="{{ edit_comment | safe }}" autocomplete="off"/>
|
||||
{% if edit_comment_mutation_token %}
|
||||
<input type="hidden" name="mutation_token" value="{{ edit_comment_mutation_token | safe }}" autocomplete="off"/>
|
||||
{% endif %}
|
||||
<label for="edit_comment-author">{{ tr(l=l,k="comment_form-author")|safe }}</label>
|
||||
<input type="text" id="edit_comment-author" name="author" maxlength="{{ config.comment_author_max_len | safe }}"{% if edit_comment_author %} value="{{ edit_comment_author }}"{% endif %}/><br/>
|
||||
<label for="edit_comment-email">{{ tr(l=l,k="comment_form-email")|safe }}</label>
|
||||
|
|
Loading…
Reference in a new issue