use crate::{config::Config, db::*, locales::Locales, queries::*}; use fluent_bundle::FluentArgs; use log::error; use std::{net::IpAddr, str::FromStr}; use unic_langid::LanguageIdentifier; pub fn new_pending_comment( comment: &Comment, addr: Option, dbs: &Dbs, ) -> Result { let comment_id = CommentId::new(); dbs.comment.insert(&comment_id, comment)?; dbs.comment_pending.insert( &( comment.topic_hash.clone(), comment.post_time, comment_id.clone(), ), &addr, )?; 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), &())?; } Ok(()) } pub fn remove_comment(comment_id: CommentId, dbs: &Dbs) -> Result, sled::Error> { if let Some(comment) = dbs.comment.remove(&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, ))?; return Ok(Some(comment)); } Ok(None) } 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_by_topic_and_time: {:?}", e)) .ok()?; let comment = dbs .comment .get(&comment_id) .map_err(|e| error!("Reading comment: {:?}", e)) .ok()? .or_else(|| { error!("Comment not found"); None })?; Some((comment_id, comment, 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_id, comment)) } pub fn iter_pending_comments_by_topic( topic_hash: TopicHash, dbs: &Dbs, ) -> impl Iterator)> + '_ { iter_comments_by_topic(topic_hash, &dbs.comment_pending, dbs) } /// 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: &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(), ); } } #[cfg(test)] mod test { use super::*; #[test] fn test_comment() { let comment = Comment { topic_hash: TopicHash::from_topic("test"), author: String::from("Emmanuel Goldstein"), email: None, last_edit_time: None, post_time: 42, text: String::from("Hello world!"), }; let dbs = load_dbs(None); let comment_id = new_pending_comment(&comment, &dbs).unwrap(); let mut iter = dbs.comment.iter(); assert_eq!(iter.next(), Some(Ok((comment_id.clone(), comment.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() ), () ))) ); assert_eq!(iter.next(), None); approve_comment(comment_id.clone(), &dbs).unwrap(); 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); } }