use super::{Filter, FilterResult};
use crate::Request;
use regex::Regex;
use std::{borrow::Cow, convert::Infallible};
#[derive(Debug, Clone)]
pub enum PatternType {
Text(Cow<'static, str>),
Regex(Regex),
}
impl From<Cow<'static, str>> for PatternType {
#[inline]
fn from(text: Cow<'static, str>) -> Self {
Self::Text(text)
}
}
impl From<&'static str> for PatternType {
#[inline]
fn from(text: &'static str) -> Self {
Self::Text(Cow::Borrowed(text))
}
}
impl From<Regex> for PatternType {
#[inline]
fn from(regex: Regex) -> Self {
Self::Regex(regex)
}
}
#[derive(Debug, Default, Clone)]
pub struct Text {
texts: Vec<PatternType>,
contains: Vec<Cow<'static, str>>,
starts_with: Vec<Cow<'static, str>>,
ends_with: Vec<Cow<'static, str>>,
ignore_case: bool,
}
impl Text {
pub fn new<T, I1, C, I2, S, I3, E, I4>(
texts: I1,
contains: I2,
starts_with: I3,
ends_with: I4,
ignore_case: bool,
) -> Self
where
T: Into<PatternType>,
I1: IntoIterator<Item = T>,
C: Into<Cow<'static, str>>,
I2: IntoIterator<Item = C>,
S: Into<Cow<'static, str>>,
I3: IntoIterator<Item = S>,
E: Into<Cow<'static, str>>,
I4: IntoIterator<Item = E>,
{
if ignore_case {
Self {
texts: texts
.into_iter()
.map(|text| match text.into() {
PatternType::Text(text) => PatternType::Text(text.to_lowercase().into()),
PatternType::Regex(regex) => PatternType::Regex(
Regex::new(&format!("(?i){regex}"))
.expect("Failed to compile regex with (?i) flag"),
),
})
.collect(),
contains: contains
.into_iter()
.map(|val| val.into().to_lowercase().into())
.collect(),
starts_with: starts_with
.into_iter()
.map(|val| val.into().to_lowercase().into())
.collect(),
ends_with: ends_with
.into_iter()
.map(|val| val.into().to_lowercase().into())
.collect(),
ignore_case,
}
} else {
Self {
texts: texts.into_iter().map(Into::into).collect(),
contains: contains.into_iter().map(Into::into).collect(),
starts_with: starts_with.into_iter().map(Into::into).collect(),
ends_with: ends_with.into_iter().map(Into::into).collect(),
ignore_case,
}
}
}
#[inline]
#[must_use]
pub fn one(text: impl Into<PatternType>) -> Self {
Self::builder().text(text).build()
}
#[inline]
#[must_use]
pub fn many(texts: impl IntoIterator<Item = impl Into<PatternType>>) -> Self {
Self::builder().texts(texts).build()
}
#[inline]
#[must_use]
pub fn contains_single(val: impl Into<Cow<'static, str>>) -> Self {
Self::builder().contains_single(val).build()
}
#[inline]
#[must_use]
pub fn contains(val: impl IntoIterator<Item = impl Into<Cow<'static, str>>>) -> Self {
Self::builder().contains(val).build()
}
#[inline]
#[must_use]
pub fn starts_with_single(val: impl Into<Cow<'static, str>>) -> Self {
Self::builder().starts_with_single(val).build()
}
#[inline]
#[must_use]
pub fn starts_with(val: impl IntoIterator<Item = impl Into<Cow<'static, str>>>) -> Self {
Self::builder().starts_with(val).build()
}
#[inline]
#[must_use]
pub fn ends_with_single(val: impl Into<Cow<'static, str>>) -> Self {
Self::builder().ends_with_single(val).build()
}
#[inline]
#[must_use]
pub fn ends_with(val: impl IntoIterator<Item = impl Into<Cow<'static, str>>>) -> Self {
Self::builder().ends_with(val).build()
}
#[inline]
#[must_use]
pub fn builder() -> Builder {
Builder::default()
}
}
#[derive(Debug, Default, Clone)]
pub struct Builder {
texts: Vec<PatternType>,
contains: Vec<Cow<'static, str>>,
starts_with: Vec<Cow<'static, str>>,
ends_with: Vec<Cow<'static, str>>,
ignore_case: bool,
}
impl Builder {
#[must_use]
pub fn text(self, val: impl Into<PatternType>) -> Self {
Self {
texts: self.texts.into_iter().chain(Some(val.into())).collect(),
..self
}
}
#[must_use]
pub fn texts<T, I>(self, val: I) -> Self
where
T: Into<PatternType>,
I: IntoIterator<Item = T>,
{
Self {
texts: self
.texts
.into_iter()
.chain(val.into_iter().map(Into::into))
.collect(),
..self
}
}
#[must_use]
pub fn contains_single(self, val: impl Into<Cow<'static, str>>) -> Self {
Self {
contains: self.contains.into_iter().chain(Some(val.into())).collect(),
..self
}
}
#[must_use]
pub fn contains<T, I>(self, val: I) -> Self
where
T: Into<Cow<'static, str>>,
I: IntoIterator<Item = T>,
{
Self {
contains: self
.contains
.into_iter()
.chain(val.into_iter().map(Into::into))
.collect(),
..self
}
}
#[must_use]
pub fn starts_with_single(self, val: impl Into<Cow<'static, str>>) -> Self {
Self {
starts_with: self
.starts_with
.into_iter()
.chain(Some(val.into()))
.collect(),
..self
}
}
#[must_use]
pub fn starts_with<T, I>(self, starts_with: I) -> Self
where
T: Into<Cow<'static, str>>,
I: IntoIterator<Item = T>,
{
Self {
starts_with: self
.starts_with
.into_iter()
.chain(starts_with.into_iter().map(Into::into))
.collect(),
..self
}
}
#[must_use]
pub fn ends_with_single(self, val: impl Into<Cow<'static, str>>) -> Self {
Self {
ends_with: self.ends_with.into_iter().chain(Some(val.into())).collect(),
..self
}
}
#[must_use]
pub fn ends_with<T, I>(self, ends_with: I) -> Self
where
T: Into<Cow<'static, str>>,
I: IntoIterator<Item = T>,
{
Self {
ends_with: self
.ends_with
.into_iter()
.chain(ends_with.into_iter().map(Into::into))
.collect(),
..self
}
}
#[inline]
#[must_use]
pub fn ignore_case(self, ignore_case: bool) -> Self {
Self {
ignore_case,
..self
}
}
#[inline]
#[must_use]
pub fn build(self) -> Text {
Text::new(
self.texts,
self.contains,
self.starts_with,
self.ends_with,
self.ignore_case,
)
}
}
impl Text {
#[must_use]
fn prepare_text(&self, text: &str) -> Box<str> {
if self.ignore_case {
text.to_lowercase()
} else {
text.to_owned()
}
.into()
}
#[must_use]
pub fn validate_texts(&self, text: &str) -> bool {
let text = self.prepare_text(text);
let text_ref = text.as_ref();
self.texts.iter().any(|pattern| match pattern {
PatternType::Text(allowed_text) => allowed_text == text_ref,
PatternType::Regex(regex) => regex.is_match(&text),
})
}
#[must_use]
pub fn validate_contains(&self, text: &str) -> bool {
let text = self.prepare_text(text);
self.contains
.iter()
.any(|part_text| text.contains(part_text.as_ref()))
}
#[must_use]
pub fn validate_starts_with(&self, text: &str) -> bool {
let text = self.prepare_text(text);
self.starts_with
.iter()
.any(|part_text| text.starts_with(part_text.as_ref()))
}
#[must_use]
pub fn validate_ends_with(&self, text: &str) -> bool {
let text = self.prepare_text(text);
self.ends_with
.iter()
.any(|part_text| text.ends_with(part_text.as_ref()))
}
#[must_use]
pub fn validate_text(&self, text: &str) -> bool {
self.validate_texts(text)
|| self.validate_contains(text)
|| self.validate_starts_with(text)
|| self.validate_ends_with(text)
}
}
impl<Client> Filter<Client> for Text
where
Client: Send + Sync + 'static,
{
type Error = Infallible;
async fn check(&mut self, request: &mut Request<Client>) -> FilterResult<Self::Error> {
Ok(request
.update
.text()
.or(request.update.caption())
.is_some_and(|text| self.validate_text(text)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_validate_texts() {
let text = Text::builder().text("text").text("text2").build();
assert!(text.validate_texts("text"));
assert!(text.validate_texts("text2"));
assert!(!text.validate_texts("text3"));
assert!(!text.validate_texts("TEXT"));
assert!(!text.validate_texts("TEXT2"));
assert!(!text.validate_texts("TEXT3"));
let text = Text::builder()
.text("text")
.text("text2")
.ignore_case(true)
.build();
assert!(text.validate_texts("text"));
assert!(text.validate_texts("text2"));
assert!(!text.validate_texts("text3"));
assert!(text.validate_texts("TEXT"));
assert!(text.validate_texts("TEXT2"));
assert!(!text.validate_texts("TEXT3"));
}
#[test]
fn text_validate_contains() {
let text = Text::builder()
.contains_single("foo")
.contains_single("bar")
.build();
assert!(text.validate_contains("foo"));
assert!(text.validate_contains("bar"));
assert!(text.validate_contains("foobar"));
assert!(text.validate_contains("foob"));
assert!(text.validate_contains("oobar"));
assert!(!text.validate_contains("fo"));
assert!(!text.validate_contains("ba"));
assert!(!text.validate_contains("FOO"));
assert!(!text.validate_contains("BAR"));
assert!(!text.validate_contains("FOOBAR"));
assert!(!text.validate_contains("FOOB"));
assert!(!text.validate_contains("OOBAR"));
let text = Text::builder()
.contains_single("foo")
.contains_single("bar")
.ignore_case(true)
.build();
assert!(text.validate_contains("foo"));
assert!(text.validate_contains("bar"));
assert!(text.validate_contains("foobar"));
assert!(text.validate_contains("foob"));
assert!(text.validate_contains("oobar"));
assert!(!text.validate_contains("fo"));
assert!(!text.validate_contains("ba"));
assert!(text.validate_contains("FOO"));
assert!(text.validate_contains("BAR"));
assert!(text.validate_contains("FOOBAR"));
assert!(text.validate_contains("FOOB"));
assert!(text.validate_contains("OOBAR"));
}
#[test]
fn text_validate_starts_with() {
let text = Text::builder()
.starts_with_single("foo")
.starts_with_single("bar")
.build();
assert!(text.validate_starts_with("foo"));
assert!(text.validate_starts_with("bar"));
assert!(text.validate_starts_with("foobar"));
assert!(text.validate_starts_with("foob"));
assert!(!text.validate_starts_with("oobar"));
assert!(!text.validate_starts_with("fo"));
assert!(!text.validate_starts_with("ba"));
assert!(!text.validate_starts_with("FOO"));
assert!(!text.validate_starts_with("BAR"));
assert!(!text.validate_starts_with("FOOBAR"));
assert!(!text.validate_starts_with("FOOB"));
assert!(!text.validate_starts_with("OOBAR"));
let text = Text::builder()
.starts_with_single("foo")
.starts_with_single("bar")
.ignore_case(true)
.build();
assert!(text.validate_starts_with("foo"));
assert!(text.validate_starts_with("bar"));
assert!(text.validate_starts_with("foobar"));
assert!(text.validate_starts_with("foob"));
assert!(!text.validate_starts_with("oobar"));
assert!(!text.validate_starts_with("fo"));
assert!(!text.validate_starts_with("ba"));
assert!(text.validate_starts_with("FOO"));
assert!(text.validate_starts_with("BAR"));
assert!(text.validate_starts_with("FOOBAR"));
assert!(text.validate_starts_with("FOOB"));
assert!(!text.validate_starts_with("OOBAR"));
}
#[test]
fn text_validate_ends_with() {
let text = Text::builder()
.ends_with_single("foo")
.ends_with_single("bar")
.build();
assert!(text.validate_ends_with("foo"));
assert!(text.validate_ends_with("bar"));
assert!(text.validate_ends_with("foobar"));
assert!(!text.validate_ends_with("foob"));
assert!(text.validate_ends_with("oobar"));
assert!(!text.validate_ends_with("fo"));
assert!(!text.validate_ends_with("ba"));
assert!(!text.validate_ends_with("FOO"));
assert!(!text.validate_ends_with("BAR"));
assert!(!text.validate_ends_with("FOOBAR"));
assert!(!text.validate_ends_with("FOOB"));
assert!(!text.validate_ends_with("OOBAR"));
let text = Text::builder()
.ends_with_single("foo")
.ends_with_single("bar")
.ignore_case(true)
.build();
assert!(text.validate_ends_with("foo"));
assert!(text.validate_ends_with("bar"));
assert!(text.validate_ends_with("foobar"));
assert!(!text.validate_ends_with("foob"));
assert!(text.validate_ends_with("oobar"));
assert!(!text.validate_ends_with("fo"));
assert!(!text.validate_ends_with("ba"));
assert!(text.validate_ends_with("FOO"));
assert!(text.validate_ends_with("BAR"));
assert!(text.validate_ends_with("FOOBAR"));
assert!(!text.validate_ends_with("FOOB"));
assert!(text.validate_ends_with("OOBAR"));
}
}