webcomment/src/helpers.rs

302 lines
7.1 KiB
Rust

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<IpAddr>,
dbs: &Dbs,
) -> Result<CommentId, sled::Error> {
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<Option<Comment>, 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<Item = (CommentId, Comment, 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))
.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<Item = (CommentId, Comment)> + '_ {
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<Item = (CommentId, Comment, Option<IpAddr>)> + '_ {
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<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: &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() {
let comment = Comment {
topic_hash: TopicHash::from_topic("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, None, &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()
),
None
)))
);
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);
}
}