feat: edit approval

This commit is contained in:
Pascal Engélibert 2022-12-29 22:46:21 +01:00
commit 0c3ea546fd
Signed by: tuxmain
GPG key ID: 3504BC6D362F7DCA
9 changed files with 750 additions and 185 deletions

View file

@ -1,3 +1,4 @@
use base64::engine::Engine;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{net::IpAddr, path::Path};
@ -7,11 +8,18 @@ const DB_DIR: &str = "db";
pub type Time = u64;
pub const BASE64: base64::engine::fast_portable::FastPortable =
base64::engine::fast_portable::FastPortable::from(
&base64::alphabet::URL_SAFE,
base64::engine::fast_portable::NO_PAD,
);
#[derive(Clone)]
pub struct Dbs {
pub comment: Tree<CommentId, Comment>,
pub comment: Tree<CommentId, (Comment, CommentStatus)>,
pub comment_approved: Tree<(TopicHash, Time, CommentId), ()>,
pub comment_pending: Tree<(TopicHash, Time, CommentId), Option<IpAddr>>,
/// -> (client_addr, is_edit)
pub comment_pending: Tree<(TopicHash, Time, CommentId), (Option<IpAddr>, bool)>,
/// client_addr -> (last_mutation, mutation_count)
pub client_mutation: Tree<IpAddr, (Time, u32)>,
}
@ -34,6 +42,14 @@ pub fn load_dbs(path: Option<&Path>) -> Dbs {
}
}
#[repr(u8)]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum CommentStatus {
Pending = 0,
Approved = 1,
ApprovedEdited(Comment) = 2,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Comment {
pub author: String,
@ -54,13 +70,20 @@ impl MutationToken {
}
pub fn to_base64(&self) -> String {
base64::encode_config(self.0, base64::URL_SAFE_NO_PAD)
let mut buf = vec![0; 24];
let size = BASE64.encode(&self.0, &mut buf);
buf.truncate(size);
String::from_utf8(buf).unwrap()
}
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)?;
BASE64.decode(
s.as_bytes(),
&mut buf,
BASE64.decoded_length_estimate(s.len()),
)?;
Ok(Self(buf))
})
.map_err(|_| base64::DecodeError::InvalidLength)?
@ -107,13 +130,20 @@ impl CommentId {
}
pub fn to_base64(&self) -> String {
base64::encode_config(self.0, base64::URL_SAFE_NO_PAD)
let mut buf = vec![0; 24];
let size = BASE64.encode(&self.0, &mut buf);
buf.truncate(size);
String::from_utf8(buf).unwrap()
}
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)?;
BASE64.decode(
s.as_bytes(),
&mut buf,
BASE64.decoded_length_estimate(s.len()),
)?;
Ok(Self(buf))
})
.map_err(|_| base64::DecodeError::InvalidLength)?

View file

@ -11,43 +11,145 @@ pub fn new_pending_comment(
dbs: &Dbs,
) -> Result<CommentId, sled::Error> {
let comment_id = CommentId::new();
dbs.comment.insert(&comment_id, comment)?;
dbs.comment
.insert(&comment_id, &(comment.clone(), CommentStatus::Pending))?;
dbs.comment_pending.insert(
&(
comment.topic_hash.clone(),
comment.post_time,
comment_id.clone(),
),
&addr,
&(addr, false),
)?;
Ok(comment_id)
}
pub fn approve_comment(comment_id: CommentId, dbs: &Dbs) -> Result<(), sled::Error> {
if let Some(comment) = dbs.comment.get(&comment_id)? {
dbs.comment_pending.remove(&(
comment.topic_hash.clone(),
comment.post_time,
comment_id.clone(),
))?;
dbs.comment_approved
.insert(&(comment.topic_hash, comment.post_time, comment_id), &())?;
// TODO when approval disabled
pub fn edit_comment(
comment_id: CommentId,
old_comment: Comment,
comment_status: CommentStatus,
edited_comment: Comment,
addr: Option<IpAddr>,
dbs: &Dbs,
) -> Result<(), sled::Error> {
match comment_status {
CommentStatus::Pending => {
dbs.comment
.insert(&comment_id, &(edited_comment, CommentStatus::Pending))?;
// TODO should we update ip address in comment_pending?
}
CommentStatus::Approved => {
dbs.comment_pending.insert(
&(
edited_comment.topic_hash.clone(),
edited_comment.post_time,
comment_id.clone(),
),
&(addr, true),
)?;
dbs.comment.insert(
&comment_id,
&(old_comment, CommentStatus::ApprovedEdited(edited_comment)),
)?;
}
CommentStatus::ApprovedEdited(_old_edited_comment) => {
dbs.comment.insert(
&comment_id,
&(old_comment, CommentStatus::ApprovedEdited(edited_comment)),
)?;
// TODO should we update ip address in comment_pending?
}
}
Ok(())
}
pub fn remove_comment(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment>, sled::Error> {
if let Some(comment) = dbs.comment.remove(&comment_id)? {
pub fn approve_comment(comment_id: CommentId, dbs: &Dbs) -> Result<(), sled::Error> {
if let Some((comment, CommentStatus::Pending)) = dbs.comment.get(&comment_id)? {
dbs.comment_pending.remove(&(
comment.topic_hash.clone(),
comment.post_time,
comment_id.clone(),
))?;
dbs.comment_approved.remove(&(
comment.topic_hash.clone(),
comment.post_time,
comment_id,
dbs.comment_approved.insert(
&(
comment.topic_hash.clone(),
comment.post_time,
comment_id.clone(),
),
&(),
)?;
dbs.comment
.insert(&comment_id, &(comment, CommentStatus::Approved))?;
}
Ok(())
}
pub fn approve_edit(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment>, sled::Error> {
if let Some((comment, CommentStatus::ApprovedEdited(edited_comment))) =
dbs.comment.get(&comment_id)?
{
dbs.comment_pending.remove(&(
edited_comment.topic_hash.clone(),
edited_comment.post_time,
comment_id.clone(),
))?;
dbs.comment
.insert(&comment_id, &(edited_comment, CommentStatus::Approved))?;
return Ok(Some(comment));
}
Ok(None)
}
pub fn remove_comment(
comment_id: CommentId,
dbs: &Dbs,
) -> Result<Option<(Comment, CommentStatus)>, sled::Error> {
if let Some((comment, edited_comment)) = dbs.comment.remove(&comment_id)? {
match &edited_comment {
CommentStatus::Pending => {
dbs.comment_pending.remove(&(
comment.topic_hash.clone(),
comment.post_time,
comment_id,
))?;
}
CommentStatus::Approved => {
dbs.comment_approved.remove(&(
comment.topic_hash.clone(),
comment.post_time,
comment_id,
))?;
}
CommentStatus::ApprovedEdited(edited_comment) => {
dbs.comment_pending.remove(&(
edited_comment.topic_hash.clone(),
edited_comment.post_time,
comment_id.clone(),
))?;
dbs.comment_approved.remove(&(
comment.topic_hash.clone(),
comment.post_time,
comment_id,
))?;
}
}
return Ok(Some((comment, edited_comment)));
}
Ok(None)
}
pub fn remove_edit(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment>, sled::Error> {
if let Some((comment, CommentStatus::ApprovedEdited(edited_comment))) =
dbs.comment.get(&comment_id)?
{
dbs.comment_pending.remove(&(
edited_comment.topic_hash.clone(),
edited_comment.post_time,
comment_id.clone(),
))?;
dbs.comment
.insert(&comment_id, &(comment.clone(), CommentStatus::Approved))?;
return Ok(Some(comment));
}
Ok(None)
@ -57,15 +159,15 @@ pub fn iter_comments_by_topic<'a, V: typed_sled::KV>(
topic_hash: TopicHash,
tree: &'a Tree<(TopicHash, Time, CommentId), V>,
dbs: &'a Dbs,
) -> impl Iterator<Item = (CommentId, Comment, V)> + 'a {
) -> impl Iterator<Item = (CommentId, Comment, CommentStatus, V)> + 'a {
tree.range(
(topic_hash.clone(), 0, CommentId::zero())..=(topic_hash, Time::MAX, CommentId::max()),
)
.filter_map(|entry| {
let ((_topic_hash, _time, comment_id), val) = entry
.map_err(|e| error!("Reading comment_by_topic_and_time: {:?}", e))
.map_err(|e| error!("Reading comment index: {:?}", e))
.ok()?;
let comment = dbs
let (comment, comment_status) = dbs
.comment
.get(&comment_id)
.map_err(|e| error!("Reading comment: {:?}", e))
@ -74,23 +176,27 @@ pub fn iter_comments_by_topic<'a, V: typed_sled::KV>(
error!("Comment not found");
None
})?;
Some((comment_id, comment, val))
Some((comment_id, comment, comment_status, val))
})
}
pub fn iter_approved_comments_by_topic(
topic_hash: TopicHash,
dbs: &Dbs,
) -> impl Iterator<Item = (CommentId, Comment)> + '_ {
) -> impl Iterator<Item = (CommentId, Comment, CommentStatus)> + '_ {
iter_comments_by_topic(topic_hash, &dbs.comment_approved, dbs)
.map(|(comment_id, comment, ())| (comment_id, comment))
.map(|(comment_id, comment, comment_status, ())| (comment_id, comment, comment_status))
}
pub fn iter_pending_comments_by_topic(
topic_hash: TopicHash,
dbs: &Dbs,
) -> impl Iterator<Item = (CommentId, Comment, Option<IpAddr>)> + '_ {
iter_comments_by_topic(topic_hash, &dbs.comment_pending, dbs)
) -> impl Iterator<Item = (CommentId, Comment, Option<IpAddr>, CommentStatus)> + '_ {
iter_comments_by_topic(topic_hash, &dbs.comment_pending, dbs).map(
|(comment_id, comment, comment_status, (addr, _is_edit))| {
(comment_id, comment, addr, comment_status)
},
)
}
/// Returns Some(time_left) if the client is banned.
@ -248,9 +354,11 @@ mod test {
#[test]
fn test_comment() {
// Post a comment
let comment = Comment {
topic_hash: TopicHash::from_topic("test"),
author: String::from("Emmanuel Goldstein"),
author: String::from("Jerry"),
email: None,
last_edit_time: None,
mutation_token: MutationToken::new(),
@ -262,7 +370,13 @@ mod test {
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()))));
assert_eq!(
iter.next(),
Some(Ok((
comment_id.clone(),
(comment.clone(), CommentStatus::Pending)
)))
);
assert_eq!(iter.next(), None);
let mut iter = dbs.comment_pending.iter();
@ -274,13 +388,265 @@ mod test {
comment.post_time,
comment_id.clone()
),
None
(None, false)
)))
);
assert_eq!(iter.next(), None);
// Edit the comment
let comment2 = Comment {
topic_hash: TopicHash::from_topic("test"),
author: String::from("Jerry Smith"),
email: Some(String::from("jerry.smith@example.tld")),
last_edit_time: Some(137),
mutation_token: comment.mutation_token.clone(),
post_time: 42,
text: String::from("Good bye world!"),
};
edit_comment(
comment_id.clone(),
comment.clone(),
CommentStatus::Pending,
comment2.clone(),
None,
&dbs,
)
.unwrap();
let mut iter = dbs.comment.iter();
assert_eq!(
iter.next(),
Some(Ok((
comment_id.clone(),
(comment2.clone(), CommentStatus::Pending)
)))
);
assert_eq!(iter.next(), None);
let mut iter = dbs.comment_pending.iter();
assert_eq!(
iter.next(),
Some(Ok((
(
comment.topic_hash.clone(),
comment.post_time,
comment_id.clone()
),
(None, false)
)))
);
assert_eq!(iter.next(), None);
// Approve the comment
approve_comment(comment_id.clone(), &dbs).unwrap();
let mut iter = dbs.comment.iter();
assert_eq!(
iter.next(),
Some(Ok((
comment_id.clone(),
(comment2.clone(), CommentStatus::Approved)
)))
);
assert_eq!(iter.next(), None);
let mut iter = dbs.comment_pending.iter();
assert_eq!(iter.next(), None);
let mut iter = dbs.comment_approved.iter();
assert_eq!(
iter.next(),
Some(Ok((
(
comment.topic_hash.clone(),
comment.post_time,
comment_id.clone()
),
()
)))
);
assert_eq!(iter.next(), None);
// Edit the approved comment
let comment3 = Comment {
topic_hash: TopicHash::from_topic("test"),
author: String::from("Jerry Smith is back"),
email: Some(String::from("jerry.smith@example.tld")),
last_edit_time: Some(666),
mutation_token: comment.mutation_token.clone(),
post_time: 42,
text: String::from("Hello again!"),
};
edit_comment(
comment_id.clone(),
comment2.clone(),
CommentStatus::Approved,
comment3.clone(),
None,
&dbs,
)
.unwrap();
let mut iter = dbs.comment.iter();
assert_eq!(
iter.next(),
Some(Ok((
comment_id.clone(),
(
comment2.clone(),
CommentStatus::ApprovedEdited(comment3.clone())
)
)))
);
assert_eq!(iter.next(), None);
let mut iter = dbs.comment_pending.iter();
assert_eq!(
iter.next(),
Some(Ok((
(
comment.topic_hash.clone(),
comment.post_time,
comment_id.clone()
),
(None, true)
)))
);
assert_eq!(iter.next(), None);
let mut iter = dbs.comment_approved.iter();
assert_eq!(
iter.next(),
Some(Ok((
(
comment.topic_hash.clone(),
comment.post_time,
comment_id.clone()
),
()
)))
);
assert_eq!(iter.next(), None);
// Edit the edited approved comment
let comment4 = Comment {
topic_hash: TopicHash::from_topic("test"),
author: String::from("Jerry Smith is still back"),
email: Some(String::from("jerry.smith@example.tld")),
last_edit_time: Some(1337),
mutation_token: comment.mutation_token.clone(),
post_time: 42,
text: String::from("Hello again one more time!"),
};
edit_comment(
comment_id.clone(),
comment2.clone(),
CommentStatus::ApprovedEdited(comment3.clone()),
comment4.clone(),
None,
&dbs,
)
.unwrap();
let mut iter = dbs.comment.iter();
assert_eq!(
iter.next(),
Some(Ok((
comment_id.clone(),
(comment2, CommentStatus::ApprovedEdited(comment4.clone()))
)))
);
assert_eq!(iter.next(), None);
let mut iter = dbs.comment_pending.iter();
assert_eq!(
iter.next(),
Some(Ok((
(
comment.topic_hash.clone(),
comment.post_time,
comment_id.clone()
),
(None, true)
)))
);
assert_eq!(iter.next(), None);
let mut iter = dbs.comment_approved.iter();
assert_eq!(
iter.next(),
Some(Ok((
(
comment.topic_hash.clone(),
comment.post_time,
comment_id.clone()
),
()
)))
);
assert_eq!(iter.next(), None);
// Approve the edit
approve_edit(comment_id.clone(), &dbs).unwrap();
let mut iter = dbs.comment.iter();
assert_eq!(
iter.next(),
Some(Ok((
comment_id.clone(),
(comment4.clone(), CommentStatus::Approved)
)))
);
assert_eq!(iter.next(), None);
let mut iter = dbs.comment_pending.iter();
assert_eq!(iter.next(), None);
let mut iter = dbs.comment_approved.iter();
assert_eq!(
iter.next(),
Some(Ok((
(
comment.topic_hash.clone(),
comment.post_time,
comment_id.clone()
),
()
)))
);
assert_eq!(iter.next(), None);
// Edit and remove the edit
edit_comment(
comment_id.clone(),
comment4.clone(),
CommentStatus::Approved,
comment.clone(),
None,
&dbs,
)
.unwrap();
remove_edit(comment_id.clone(), &dbs).unwrap();
let mut iter = dbs.comment.iter();
assert_eq!(
iter.next(),
Some(Ok((
comment_id.clone(),
(comment4.clone(), CommentStatus::Approved)
)))
);
assert_eq!(iter.next(), None);
let mut iter = dbs.comment_pending.iter();
assert_eq!(iter.next(), None);

View file

@ -65,11 +65,21 @@ pub struct ApproveQuery {
pub approve: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct ApproveEditQuery {
pub approve_edit: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct RemoveQuery {
pub remove: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct RemoveEditQuery {
pub remove_edit: String,
}
#[derive(Clone, Debug, Deserialize)]
pub struct EditQuery {
pub edit: String,

View file

@ -119,7 +119,7 @@ async fn serve_edit_comment<'a>(
return serve_comments(req, config, templates, dbs, client_langs, context, 400).await;
};
let Some(comment) = dbs.comment.get(&comment_id).unwrap() else {
let Some((comment, _edited_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;
};
@ -189,6 +189,13 @@ async fn serve_comments<'a>(
.ok();
}
}
if let Ok(query) = req.query::<ApproveEditQuery>() {
if let Ok(comment_id) = CommentId::from_base64(&query.approve_edit) {
helpers::approve_edit(comment_id, &dbs)
.map_err(|e| error!("Approving edit: {:?}", e))
.ok();
}
}
if let Ok(query) = req.query::<RemoveQuery>() {
if let Ok(comment_id) = CommentId::from_base64(&query.remove) {
helpers::remove_comment(comment_id, &dbs)
@ -196,9 +203,16 @@ async fn serve_comments<'a>(
.ok();
}
}
if let Ok(query) = req.query::<RemoveEditQuery>() {
if let Ok(comment_id) = CommentId::from_base64(&query.remove_edit) {
helpers::remove_edit(comment_id, &dbs)
.map_err(|e| error!("Removing edit: {:?}", e))
.ok();
}
}
if let Ok(query) = req.query::<EditQuery>() {
if let Ok(comment_id) = CommentId::from_base64(&query.edit) {
if let Some(comment) = dbs.comment.get(&comment_id).unwrap() {
if let Some((comment, _comment_status)) = dbs.comment.get(&comment_id).unwrap() {
context.insert("edit_comment", &comment_id.to_base64());
context.insert("edit_comment_author", &comment.author);
context.insert("edit_comment_email", &comment.email);
@ -210,14 +224,38 @@ async fn serve_comments<'a>(
context.insert(
"comments_pending",
&helpers::iter_pending_comments_by_topic(topic_hash.clone(), &dbs)
.map(|(comment_id, comment, addr)| CommentWithId {
addr: addr.map(|addr| addr.to_string()),
author: comment.author,
editable: admin,
id: comment_id.to_base64(),
needs_approval: true,
post_time: comment.post_time,
text: comment.text,
.map(|(comment_id, comment, addr, comment_status)| {
if let CommentStatus::ApprovedEdited(edited_comment) = comment_status {
CommentWithId {
addr: addr.map(|addr| addr.to_string()),
author: edited_comment.author,
editable: true,
id: comment_id.to_base64(),
last_edit_time: edited_comment.last_edit_time,
needs_approval: true,
original: Some(OriginalComment {
author: comment.author,
editable: true,
last_edit_time: comment.last_edit_time,
post_time: comment.post_time,
text: comment.text,
}),
post_time: edited_comment.post_time,
text: edited_comment.text,
}
} else {
CommentWithId {
addr: addr.map(|addr| addr.to_string()),
author: comment.author,
editable: true,
id: comment_id.to_base64(),
last_edit_time: comment.last_edit_time,
needs_approval: true,
original: None,
post_time: comment.post_time,
text: comment.text,
}
}
})
.collect::<Vec<CommentWithId>>(),
);
@ -226,12 +264,14 @@ async fn serve_comments<'a>(
context.insert(
"comments",
&helpers::iter_approved_comments_by_topic(topic_hash, &dbs)
.map(|(comment_id, comment)| CommentWithId {
.map(|(comment_id, comment, _comment_status)| CommentWithId {
addr: None,
author: comment.author,
editable: admin,
id: comment_id.to_base64(),
last_edit_time: comment.last_edit_time,
needs_approval: false,
original: None,
post_time: comment.post_time,
text: comment.text,
})
@ -272,10 +312,10 @@ async fn serve_admin<'a>(
&dbs.comment_pending
.iter()
.filter_map(|entry| {
let ((_topic_hash, _time, comment_id), addr) = entry
let ((_topic_hash, _time, comment_id), (addr, _is_edit)) = entry
.map_err(|e| error!("Reading comment_pending: {:?}", e))
.ok()?;
let comment = dbs
let (comment, comment_status) = dbs
.comment
.get(&comment_id)
.map_err(|e| error!("Reading comment: {:?}", e))
@ -284,15 +324,37 @@ async fn serve_admin<'a>(
error!("Comment not found");
None
})?;
Some(CommentWithId {
addr: addr.map(|addr| addr.to_string()),
author: comment.author,
editable: true,
id: comment_id.to_base64(),
needs_approval: true,
post_time: comment.post_time,
text: comment.text,
})
if let CommentStatus::ApprovedEdited(edited_comment) = comment_status {
Some(CommentWithId {
addr: addr.map(|addr| addr.to_string()),
author: edited_comment.author,
editable: true,
id: comment_id.to_base64(),
last_edit_time: edited_comment.last_edit_time,
needs_approval: true,
original: Some(OriginalComment {
author: comment.author,
editable: true,
last_edit_time: comment.last_edit_time,
post_time: comment.post_time,
text: comment.text,
}),
post_time: edited_comment.post_time,
text: edited_comment.text,
})
} else {
Some(CommentWithId {
addr: addr.map(|addr| addr.to_string()),
author: comment.author,
editable: true,
id: comment_id.to_base64(),
last_edit_time: comment.last_edit_time,
needs_approval: true,
original: None,
post_time: comment.post_time,
text: comment.text,
})
}
})
.collect::<Vec<CommentWithId>>(),
);
@ -456,6 +518,7 @@ async fn handle_post_comments(
.unwrap()],
);
}
// TODO add message to client log and change http code
Err(e) => error!("Adding pending comment: {:?}", e),
}
} else {
@ -466,16 +529,20 @@ async fn handle_post_comments(
context.insert("new_comment_errors", &errors);
}
CommentQuery::EditComment(query) => {
helpers::check_comment(config, locales, &client_langs, &query.comment, &mut errors);
let Ok(topic) = req.param("topic") else {
return Err(tide::Error::from_str(404, "No topic"))
};
let Ok(comment_id) = CommentId::from_base64(&query.id) else {
return Err(tide::Error::from_str(400, "Invalid comment id"));
};
let Some(mut comment) = dbs.comment.get(&comment_id).unwrap() else {
let Some((old_comment, old_edited_comment)) = dbs.comment.get(&comment_id).unwrap() else {
return Err(tide::Error::from_str(404, "Not found"));
};
helpers::check_comment(config, locales, &client_langs, &query.comment, &mut errors);
let mutation_token = if admin {
None
} else {
@ -491,7 +558,7 @@ async fn handle_post_comments(
};
if let Err(e) =
helpers::check_can_edit_comment(config, &comment, &mutation_token)
helpers::check_can_edit_comment(config, &old_comment, &mutation_token)
{
errors.push(e.to_string());
}
@ -535,6 +602,8 @@ async fn handle_post_comments(
.unwrap()
.as_secs();
let mut comment = old_comment.clone();
comment.author = if query.comment.author.is_empty() {
petname::Petnames::large().generate_one(2, " ")
} else {
@ -548,7 +617,42 @@ async fn handle_post_comments(
comment.text = query.comment.text;
comment.last_edit_time = Some(time);
dbs.comment.insert(&comment_id, &comment).unwrap();
match helpers::edit_comment(
comment_id.clone(),
old_comment,
old_edited_comment,
comment.clone(),
client_addr,
&dbs,
) {
Ok(()) => {
context.insert(
"log",
&[locales
.tr(
&client_langs,
if config.comment_approve {
"edit_comment-success_pending"
} else {
"edit_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()],
);
}
// TODO add message to client log and change http code
Err(e) => error!("Editing comment: {:?}", e),
}
} else {
context.insert("edit_comment", &comment_id.to_base64());
if let Some(mutation_token) = &mutation_token {

View file

@ -41,7 +41,18 @@ pub struct CommentWithId {
pub author: String,
pub editable: bool,
pub id: String,
pub last_edit_time: Option<Time>,
pub needs_approval: bool,
pub original: 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,
}