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, dbs: &Dbs, ) -> Result { 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, 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, 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, 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, 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 + '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 + '_ { 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, 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, 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, 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( config: &Config, req: &tide::Request, ) -> Option> { 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, ) { 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); } }