696 lines
16 KiB
Rust
696 lines
16 KiB
Rust
use crate::{config::Config, db::*, locales::Locales};
|
|
|
|
use fluent_bundle::FluentArgs;
|
|
use log::error;
|
|
use std::{net::IpAddr, str::FromStr};
|
|
use typed_sled::transaction::Transactional;
|
|
use unic_langid::LanguageIdentifier;
|
|
|
|
pub fn new_pending_comment(
|
|
comment: &Comment,
|
|
addr: Option<IpAddr>,
|
|
dbs: &Dbs,
|
|
) -> Result<CommentId, sled::Error> {
|
|
let comment_id = CommentId::new();
|
|
(&dbs.comment, &dbs.comment_pending)
|
|
.transaction(|(db_comment, db_comment_pending)| {
|
|
db_comment.insert(&comment_id, &(comment.clone(), CommentStatus::Pending))?;
|
|
db_comment_pending.insert(
|
|
&(
|
|
comment.topic_hash.clone(),
|
|
comment.post_time,
|
|
comment_id.clone(),
|
|
),
|
|
&(addr, false),
|
|
)?;
|
|
ConflictableTransactionResult::<_>::Ok(())
|
|
})
|
|
.unwrap();
|
|
|
|
Ok(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, &dbs.comment_pending)
|
|
.transaction(|(db_comment, db_comment_pending)| {
|
|
db_comment_pending.insert(
|
|
&(
|
|
edited_comment.topic_hash.clone(),
|
|
edited_comment.post_time,
|
|
comment_id.clone(),
|
|
),
|
|
&(addr, true),
|
|
)?;
|
|
db_comment.insert(
|
|
&comment_id,
|
|
&(
|
|
old_comment.clone(),
|
|
CommentStatus::ApprovedEdited(edited_comment.clone()),
|
|
),
|
|
)?;
|
|
ConflictableTransactionResult::<_>::Ok(())
|
|
})
|
|
.unwrap();
|
|
}
|
|
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 approve_comment(comment_id: CommentId, dbs: &Dbs) -> Result<(), sled::Error> {
|
|
(&dbs.comment, &dbs.comment_approved, &dbs.comment_pending)
|
|
.transaction(|(db_comment, db_comment_approved, db_comment_pending)| {
|
|
if let Some((comment, CommentStatus::Pending)) = db_comment.get(&comment_id)? {
|
|
db_comment_pending.remove(&(
|
|
comment.topic_hash.clone(),
|
|
comment.post_time,
|
|
comment_id.clone(),
|
|
))?;
|
|
db_comment_approved.insert(
|
|
&(
|
|
comment.topic_hash.clone(),
|
|
comment.post_time,
|
|
comment_id.clone(),
|
|
),
|
|
&(),
|
|
)?;
|
|
db_comment.insert(&comment_id, &(comment, CommentStatus::Approved))?;
|
|
}
|
|
ConflictableTransactionResult::<_>::Ok(())
|
|
})
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
pub fn approve_edit(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment>, sled::Error> {
|
|
Ok((&dbs.comment, &dbs.comment_pending)
|
|
.transaction(|(db_comment, db_comment_pending)| {
|
|
if let Some((comment, CommentStatus::ApprovedEdited(edited_comment))) =
|
|
db_comment.get(&comment_id)?
|
|
{
|
|
db_comment_pending.remove(&(
|
|
edited_comment.topic_hash.clone(),
|
|
edited_comment.post_time,
|
|
comment_id.clone(),
|
|
))?;
|
|
db_comment.insert(&comment_id, &(edited_comment, CommentStatus::Approved))?;
|
|
return ConflictableTransactionResult::<_>::Ok(Some(comment));
|
|
}
|
|
ConflictableTransactionResult::<_>::Ok(None)
|
|
})
|
|
.unwrap())
|
|
}
|
|
|
|
pub fn remove_comment(
|
|
comment_id: CommentId,
|
|
dbs: &Dbs,
|
|
) -> Result<Option<(Comment, CommentStatus)>, sled::Error> {
|
|
Ok((&dbs.comment, &dbs.comment_approved, &dbs.comment_pending)
|
|
.transaction(|(db_comment, db_comment_approved, db_comment_pending)| {
|
|
if let Some((comment, edited_comment)) = db_comment.remove(&comment_id)? {
|
|
match &edited_comment {
|
|
CommentStatus::Pending => {
|
|
db_comment_pending.remove(&(
|
|
comment.topic_hash.clone(),
|
|
comment.post_time,
|
|
comment_id.clone(),
|
|
))?;
|
|
}
|
|
CommentStatus::Approved => {
|
|
db_comment_approved.remove(&(
|
|
comment.topic_hash.clone(),
|
|
comment.post_time,
|
|
comment_id.clone(),
|
|
))?;
|
|
}
|
|
CommentStatus::ApprovedEdited(edited_comment) => {
|
|
db_comment_pending.remove(&(
|
|
edited_comment.topic_hash.clone(),
|
|
edited_comment.post_time,
|
|
comment_id.clone(),
|
|
))?;
|
|
db_comment_approved.remove(&(
|
|
comment.topic_hash.clone(),
|
|
comment.post_time,
|
|
comment_id.clone(),
|
|
))?;
|
|
}
|
|
}
|
|
return ConflictableTransactionResult::<_>::Ok(Some((comment, edited_comment)));
|
|
}
|
|
ConflictableTransactionResult::<_>::Ok(None)
|
|
})
|
|
.unwrap())
|
|
}
|
|
|
|
pub fn remove_edit(comment_id: CommentId, dbs: &Dbs) -> Result<Option<Comment>, sled::Error> {
|
|
Ok((&dbs.comment, &dbs.comment_pending)
|
|
.transaction(|(db_comment, db_comment_pending)| {
|
|
if let Some((comment, CommentStatus::ApprovedEdited(edited_comment))) =
|
|
db_comment.get(&comment_id)?
|
|
{
|
|
db_comment_pending.remove(&(
|
|
edited_comment.topic_hash.clone(),
|
|
edited_comment.post_time,
|
|
comment_id.clone(),
|
|
))?;
|
|
db_comment.insert(&comment_id, &(comment.clone(), CommentStatus::Approved))?;
|
|
return ConflictableTransactionResult::<_>::Ok(Some(comment));
|
|
}
|
|
ConflictableTransactionResult::<_>::Ok(None)
|
|
})
|
|
.unwrap())
|
|
}
|
|
|
|
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, 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 index: {:?}", e))
|
|
.ok()?;
|
|
let (comment, comment_status) = dbs
|
|
.comment
|
|
.get(&comment_id)
|
|
.map_err(|e| error!("Reading comment: {:?}", e))
|
|
.ok()?
|
|
.or_else(|| {
|
|
error!("Comment not found");
|
|
None
|
|
})?;
|
|
Some((comment_id, comment, comment_status, val))
|
|
})
|
|
}
|
|
|
|
pub fn iter_approved_comments_by_topic(
|
|
topic_hash: TopicHash,
|
|
dbs: &Dbs,
|
|
) -> impl Iterator<Item = (CommentId, Comment, CommentStatus)> + '_ {
|
|
iter_comments_by_topic(topic_hash, &dbs.comment_approved, dbs)
|
|
.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>, 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.
|
|
pub fn antispam_check_client_mutation(
|
|
addr: &IpAddr,
|
|
dbs: &Dbs,
|
|
config: &Config,
|
|
) -> Result<Option<Time>, sled::Error> {
|
|
let time = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs();
|
|
Ok(dbs
|
|
.client_mutation
|
|
.get(addr)?
|
|
.and_then(|(last_mutation, mutation_count)| {
|
|
let timeout = last_mutation + config.antispam_duration;
|
|
if timeout > time && mutation_count >= config.antispam_mutation_limit {
|
|
Some(timeout - time)
|
|
} else {
|
|
None
|
|
}
|
|
}))
|
|
}
|
|
|
|
pub fn antispam_update_client_mutation(addr: &IpAddr, dbs: &Dbs) -> Result<(), sled::Error> {
|
|
let time = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs();
|
|
dbs.client_mutation.fetch_and_update(addr, |entry| {
|
|
if let Some((_last_mutation, mutation_count)) = entry {
|
|
Some((time, mutation_count.saturating_add(1)))
|
|
} else {
|
|
Some((time, 1))
|
|
}
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
/*pub fn new_client_mutation(
|
|
addr: &IpAddr,
|
|
dbs: &Dbs,
|
|
config: &Config,
|
|
) -> Result<Option<Time>, sled::Error> {
|
|
let time = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_secs();
|
|
let mut res = None;
|
|
dbs.client_mutation.fetch_and_update(addr, |entry| {
|
|
if let Some((last_mutation, mutation_count)) = entry {
|
|
if last_mutation + config.antispam_duration > time {
|
|
if mutation_count >= config.antispam_mutation_limit {
|
|
res = Some(last_mutation + config.antispam_duration);
|
|
Some((last_mutation, mutation_count))
|
|
} else {
|
|
Some((time, mutation_count.saturating_add(1)))
|
|
}
|
|
} else {
|
|
Some((time, 1))
|
|
}
|
|
} else {
|
|
Some((time, 1))
|
|
}
|
|
})?;
|
|
Ok(res)
|
|
}*/
|
|
|
|
pub fn get_client_addr<State>(
|
|
config: &Config,
|
|
req: &tide::Request<State>,
|
|
) -> Option<Result<IpAddr, std::net::AddrParseError>> {
|
|
Some(IpAddr::from_str(
|
|
if config.reverse_proxy {
|
|
req.remote()
|
|
} else {
|
|
req.peer_addr()
|
|
}?
|
|
.rsplit_once(':')?
|
|
.0,
|
|
))
|
|
}
|
|
|
|
pub fn check_comment(
|
|
config: &Config,
|
|
locales: &Locales,
|
|
langs: &[LanguageIdentifier],
|
|
comment: &crate::server::page::queries::CommentForm,
|
|
errors: &mut Vec<String>,
|
|
) {
|
|
if comment.author.len() > config.comment_author_max_len {
|
|
let mut args = FluentArgs::new();
|
|
args.set("len", comment.author.len());
|
|
args.set("max_len", config.comment_author_max_len);
|
|
errors.push(
|
|
locales
|
|
.tr(langs, "error-comment-author_name_too_long", Some(&args))
|
|
.unwrap()
|
|
.to_string(),
|
|
);
|
|
}
|
|
if comment.email.len() > config.comment_email_max_len {
|
|
let mut args = FluentArgs::new();
|
|
args.set("len", comment.email.len());
|
|
args.set("max_len", config.comment_email_max_len);
|
|
errors.push(
|
|
locales
|
|
.tr(langs, "error-comment-email_too_long", Some(&args))
|
|
.unwrap()
|
|
.to_string(),
|
|
);
|
|
}
|
|
if comment.text.len() > config.comment_text_max_len {
|
|
let mut args = FluentArgs::new();
|
|
args.set("len", comment.text.len());
|
|
args.set("max_len", config.comment_text_max_len);
|
|
errors.push(
|
|
locales
|
|
.tr(langs, "error-comment-text_too_long", Some(&args))
|
|
.unwrap()
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
|
|
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::*;
|
|
|
|
#[test]
|
|
fn test_comment() {
|
|
// Post a comment
|
|
|
|
let comment = Comment {
|
|
topic_hash: TopicHash::from_topic("test"),
|
|
author: String::from("Jerry"),
|
|
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, None, &dbs).unwrap();
|
|
|
|
let mut iter = dbs.comment.iter();
|
|
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();
|
|
assert_eq!(
|
|
iter.next(),
|
|
Some(Ok((
|
|
(
|
|
comment.topic_hash.clone(),
|
|
comment.post_time,
|
|
comment_id.clone()
|
|
),
|
|
(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);
|
|
|
|
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);
|
|
}
|
|
}
|