diff --git a/src/dispatching/filters/command.rs b/src/dispatching/filters/command.rs deleted file mode 100644 index 1fd5bab5..00000000 --- a/src/dispatching/filters/command.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::{dispatching::Filter, types::Message}; - -/// Filter which find command in message text -/// -/// *NB:* filter compare only text of message, not caption of media message -/// -/// Examples: -/// ``` -/// use teloxide::dispatching::filters::CommandFilter; -/// CommandFilter::new("start"); // filter will return true if text message starts with "/start" -/// CommandFilter::with_prefix("!", "ban"); // filter will return true if text message starts with "!ban" -/// ``` -pub struct CommandFilter { - command: String, -} - -impl Filter for CommandFilter { - fn test(&self, value: &Message) -> bool { - match value.text() { - Some(text) => text.starts_with(&self.command), - None => false, - } - } -} - -impl CommandFilter { - pub fn new(command: T) -> Self - where - T: Into, - { - Self { - command: '/'.to_string() + &command.into(), - } - } - pub fn with_prefix(prefix: U, command: T) -> Self - where - T: Into, - U: Into, - { - Self { - command: prefix.into() + &command.into(), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::types::{ - Chat, ChatKind, ForwardKind, MediaKind, MessageKind, Sender, User, - }; - - #[test] - fn commands_are_equal() { - let filter = CommandFilter::new("command"); - let message = create_message_with_text("/command".to_string()); - assert!(filter.test(&message)); - } - - #[test] - fn commands_are_not_equal() { - let filter = CommandFilter::new("command"); - let message = - create_message_with_text("/not_equal_command".to_string()); - assert_eq!(filter.test(&message), false); - } - - #[test] - fn command_have_args() { - let filter = CommandFilter::new("command"); - let message = - create_message_with_text("/command arg1 arg2".to_string()); - assert!(filter.test(&message)); - } - - #[test] - fn message_have_only_whitespace() { - let filter = CommandFilter::new("command"); - let message = create_message_with_text(" ".to_string()); - assert_eq!(filter.test(&message), false); - } - - #[test] - fn another_prefix() { - let filter = CommandFilter::with_prefix("command", "!"); - let message = create_message_with_text("!command".to_string()); - assert!(filter.test(&message)); - } - - fn create_message_with_text(text: String) -> Message { - Message { - id: 0, - date: 0, - chat: Chat { - id: 0, - kind: ChatKind::Private { - type_: (), - username: None, - first_name: None, - last_name: None, - }, - photo: None, - }, - kind: MessageKind::Common { - from: Sender::User(User { - id: 0, - is_bot: false, - first_name: "".to_string(), - last_name: None, - username: None, - language_code: None, - }), - forward_kind: ForwardKind::Origin { - reply_to_message: None, - }, - edit_date: None, - media_kind: MediaKind::Text { - text, - entities: vec![], - }, - reply_markup: None, - }, - } - } -} diff --git a/src/dispatching/filters/main.rs b/src/dispatching/filters/main.rs deleted file mode 100644 index 1a3b9b19..00000000 --- a/src/dispatching/filters/main.rs +++ /dev/null @@ -1,379 +0,0 @@ -/// Filter that determines that particular event -/// is suitable for particular handler. -pub trait Filter { - /// Passes (return true) if event is suitable (otherwise return false) - fn test(&self, value: &T) -> bool; -} - -/// ``` -/// use teloxide::dispatching::filters::Filter; -/// -/// let closure = |i: &i32| -> bool { *i >= 42 }; -/// assert!(closure.test(&42)); -/// assert!(closure.test(&100)); -/// -/// assert_eq!(closure.test(&41), false); -/// assert_eq!(closure.test(&0), false); -/// ``` -impl bool> Filter for F { - fn test(&self, value: &T) -> bool { - (self)(value) - } -} - -/// ``` -/// use teloxide::dispatching::filters::Filter; -/// -/// assert!(true.test(&())); -/// assert_eq!(false.test(&()), false); -/// ``` -impl Filter for bool { - fn test(&self, _: &T) -> bool { - *self - } -} - -/// And filter. -/// -/// Passes if both underlying filters pass. -/// -/// **NOTE**: if one of filters don't pass -/// it is **not** guaranteed that other will be executed. -/// -/// ## Examples -/// ``` -/// use teloxide::dispatching::filters::{And, Filter}; -/// -/// // Note: bool can be treated as `Filter` that always return self. -/// assert_eq!(And::new(true, false).test(&()), false); -/// assert_eq!(And::new(true, false).test(&()), false); -/// assert!(And::new(true, true).test(&())); -/// assert!(And::new(true, And::new(|_: &()| true, true)).test(&())); -/// ``` -#[derive(Debug, Clone, Copy)] -pub struct And(A, B); - -impl And { - pub fn new(a: A, b: B) -> Self { - And(a, b) - } -} - -impl Filter for And -where - A: Filter, - B: Filter, -{ - fn test(&self, value: &T) -> bool { - self.0.test(value) && self.1.test(value) - } -} - -/// Alias for [`And::new`] -/// -/// ## Examples -/// ``` -/// use teloxide::dispatching::filters::{and, Filter}; -/// -/// assert!(and(true, true).test(&())); -/// assert_eq!(and(true, false).test(&()), false); -/// ``` -/// -/// [`And::new`]: crate::dispatching::filters::And::new -pub fn and(a: A, b: B) -> And { - And::new(a, b) -} - -/// Or filter. -/// -/// Passes if at least one underlying filters passes. -/// -/// **NOTE**: if one of filters passes -/// it is **not** guaranteed that other will be executed. -/// -/// ## Examples -/// ``` -/// use teloxide::dispatching::filters::{Filter, Or}; -/// -/// // Note: bool can be treated as `Filter` that always return self. -/// assert!(Or::new(true, false).test(&())); -/// assert!(Or::new(false, true).test(&())); -/// assert!(Or::new(false, Or::new(|_: &()| true, false)).test(&())); -/// assert_eq!(Or::new(false, false).test(&()), false); -/// ``` -#[derive(Debug, Clone, Copy)] -pub struct Or(A, B); - -impl Or { - pub fn new(a: A, b: B) -> Self { - Or(a, b) - } -} - -impl Filter for Or -where - A: Filter, - B: Filter, -{ - fn test(&self, value: &T) -> bool { - self.0.test(value) || self.1.test(value) - } -} - -/// Alias for [`Or::new`] -/// -/// ## Examples -/// ``` -/// use teloxide::dispatching::filters::{or, Filter}; -/// -/// assert!(or(true, false).test(&())); -/// assert_eq!(or(false, false).test(&()), false); -/// ``` -/// -/// [`Or::new`]: crate::dispatching::filters::Or::new -pub fn or(a: A, b: B) -> Or { - Or::new(a, b) -} - -/// Not filter. -/// -/// Passes if underlying filter don't pass. -/// -/// ## Examples -/// ``` -/// use teloxide::dispatching::filters::{Filter, Not}; -/// -/// // Note: bool can be treated as `Filter` that always return self. -/// assert!(Not::new(false).test(&())); -/// assert_eq!(Not::new(true).test(&()), false); -/// ``` -#[derive(Debug, Clone, Copy)] -pub struct Not(A); - -impl Not { - pub fn new(a: A) -> Self { - Not(a) - } -} - -impl Filter for Not -where - A: Filter, -{ - fn test(&self, value: &T) -> bool { - !self.0.test(value) - } -} - -/// Alias for [`Not::new`] -/// -/// ## Examples -/// ``` -/// use teloxide::dispatching::filters::{not, Filter}; -/// -/// assert!(not(false).test(&())); -/// assert_eq!(not(true).test(&()), false); -/// ``` -/// -/// [`Not::new`]: crate::dispatching::filters::Not::new -pub fn not(a: A) -> Not { - Not::new(a) -} - -/// Return [filter] that passes if and only if all of the given filters passes. -/// -/// **NOTE**: if one of filters don't pass -/// it is **not** guaranteed that other will be executed. -/// -/// ## Examples -/// ``` -/// use teloxide::{all, dispatching::filters::Filter}; -/// -/// assert!(all![true].test(&())); -/// assert!(all![true, true].test(&())); -/// assert!(all![true, true, true].test(&())); -/// -/// assert_eq!(all![false].test(&()), false); -/// assert_eq!(all![true, false].test(&()), false); -/// assert_eq!(all![false, true].test(&()), false); -/// assert_eq!(all![false, false].test(&()), false); -/// ``` -/// -/// [filter]: crate::dispatching::filters::Filter -#[macro_export] -macro_rules! all { - ($one:expr) => { $one }; - ($head:expr, $($tail:tt)+) => { - $crate::dispatching::filters::And::new( - $head, - $crate::all!($($tail)+) - ) - }; -} - -/// Return [filter] that passes if any of the given filters passes. -/// -/// **NOTE**: if one of filters passes -/// it is **not** guaranteed that other will be executed. -/// -/// ## Examples -/// ``` -/// use teloxide::{any, dispatching::filters::Filter}; -/// -/// assert!(any![true].test(&())); -/// assert!(any![true, true].test(&())); -/// assert!(any![false, true].test(&())); -/// assert!(any![true, false, true].test(&())); -/// -/// assert_eq!(any![false].test(&()), false); -/// assert_eq!(any![false, false].test(&()), false); -/// assert_eq!(any![false, false, false].test(&()), false); -/// ``` -/// -/// [filter]: crate::dispatching::filters::Filter -#[macro_export] -macro_rules! any { - ($one:expr) => { $one }; - ($head:expr, $($tail:tt)+) => { - $crate::dispatching::filters::Or::new( - $head, - $crate::all!($($tail)+) - ) - }; -} - -/// Simple wrapper around `Filter` that adds `|` and `&` operators. -/// -/// ## Examples -/// ``` -/// use teloxide::dispatching::filters::{f, And, Filter, Or, F}; -/// -/// let flt1 = |i: &i32| -> bool { *i > 17 }; -/// let flt2 = |i: &i32| -> bool { *i < 42 }; -/// let flt3 = |i: &i32| -> bool { *i % 2 == 0 }; -/// -/// let and = f(flt1) & flt2; -/// assert!(and.test(&19)); // both filters pass -/// -/// assert_eq!(and.test(&50), false); // `flt2` doesn't pass -/// assert_eq!(and.test(&16), false); // `flt1` doesn't pass -/// -/// let or = f(flt1) | flt3; -/// assert!(or.test(&19)); // `flt1` passes -/// assert!(or.test(&16)); // `flt2` passes -/// assert!(or.test(&20)); // both pass -/// -/// assert_eq!(or.test(&17), false); // both don't pass -/// -/// // Note: only first filter in chain should be wrapped in `f(...)` -/// let complicated: F, _>> = f(flt1) & flt2 | flt3; -/// assert!(complicated.test(&2)); // `flt3` passes -/// assert!(complicated.test(&21)); // `flt1` and `flt2` pass -/// -/// assert_eq!(complicated.test(&15), false); // `flt1` and `flt3` don't pass -/// assert_eq!(complicated.test(&43), false); // `flt2` and `flt3` don't pass -/// ``` -pub struct F(A); - -/// Constructor fn for [F] -/// -/// [F]: crate::dispatching::filters::F; -pub fn f(a: A) -> F { - F(a) -} - -impl Filter for F -where - A: Filter, -{ - fn test(&self, value: &T) -> bool { - self.0.test(value) - } -} - -impl std::ops::BitAnd for F { - type Output = F>; - - fn bitand(self, other: B) -> Self::Output { - f(and(self.0, other)) - } -} - -impl std::ops::BitOr for F { - type Output = F>; - - fn bitor(self, other: B) -> Self::Output { - f(or(self.0, other)) - } -} - -/* workaround for `E0207` compiler error */ -/// Extensions for filters -pub trait FilterExt { - /// Alias for [`Not::new`] - /// - /// ## Examples - /// ``` - /// use teloxide::dispatching::filters::{Filter, FilterExt}; - /// - /// let flt = |i: &i32| -> bool { *i > 0 }; - /// let flt = flt.not(); - /// assert!(flt.test(&-1)); - /// assert_eq!(flt.test(&1), false); - /// ``` - /// - /// [`Not::new`]: crate::dispatching::filters::Not::new - fn not(self) -> Not - where - Self: Sized, - { - Not::new(self) - } - - /// Alias for [`And::new`] - /// - /// ## Examples - /// ``` - /// use teloxide::dispatching::filters::{Filter, FilterExt}; - /// - /// let flt = |i: &i32| -> bool { *i > 0 }; - /// let flt = flt.and(|i: &i32| *i < 42); - /// - /// assert!(flt.test(&1)); - /// assert_eq!(flt.test(&-1), false); - /// assert_eq!(flt.test(&43), false); - /// ``` - /// - /// [`Not::new`]: crate::dispatching::filters::And::new - fn and(self, other: B) -> And - where - Self: Sized, - { - And::new(self, other) - } - - /// Alias for [`Or::new`] - /// - /// ## Examples - /// ``` - /// use teloxide::dispatching::filters::{Filter, FilterExt}; - /// - /// let flt = |i: &i32| -> bool { *i < 0 }; - /// let flt = flt.or(|i: &i32| *i > 42); - /// - /// assert!(flt.test(&-1)); - /// assert!(flt.test(&43)); - /// assert_eq!(flt.test(&17), false); - /// ``` - /// - /// [`Not::new`]: crate::dispatching::filters::Or::new - fn or(self, other: B) -> Or - where - Self: Sized, - { - Or::new(self, other) - } -} - -// All methods implemented via defaults -impl FilterExt for F where F: Filter {} diff --git a/src/dispatching/filters/message_caption.rs b/src/dispatching/filters/message_caption.rs deleted file mode 100644 index 02255bc6..00000000 --- a/src/dispatching/filters/message_caption.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::{dispatching::Filter, types::Message}; - -/// Filter which compare caption of media with another text. -/// Returns true if the caption of media is equal to another text, otherwise -/// false. -/// -/// NOTE: filter compares only caption of media, does not compare text of -/// message! -/// -/// If you want to compare text of message use -/// [MessageTextFilter] -/// -/// If you want to compare text and caption use -/// [MessageTextCaptionFilter] -/// -/// [MessageTextFilter]: crate::dispatching::filters::MessageTextFilter -/// [MessageTextCaptionFilter]: -/// crate::dispatching::filters::MessageTextCaptionFilter -pub struct MessageCaptionFilter { - text: String, -} - -impl Filter for MessageCaptionFilter { - fn test(&self, value: &Message) -> bool { - match value.caption() { - Some(caption) => self.text == caption, - None => false, - } - } -} - -impl MessageCaptionFilter { - pub fn new(text: T) -> Self - where - T: Into, - { - Self { text: text.into() } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::types::{ - Chat, ChatKind, ForwardKind, MediaKind, MessageKind, Sender, User, - }; - - #[test] - fn captions_are_equal() { - let filter = MessageCaptionFilter::new("caption".to_string()); - let message = create_message_with_caption("caption".to_string()); - assert!(filter.test(&message)); - } - - #[test] - fn captions_are_not_equal() { - let filter = MessageCaptionFilter::new("caption".to_string()); - let message = - create_message_with_caption("not equal caption".to_string()); - assert_eq!(filter.test(&message), false); - } - - fn create_message_with_caption(caption: String) -> Message { - Message { - id: 0, - date: 0, - chat: Chat { - id: 0, - kind: ChatKind::Private { - type_: (), - username: None, - first_name: None, - last_name: None, - }, - photo: None, - }, - kind: MessageKind::Common { - from: Sender::User(User { - id: 0, - is_bot: false, - first_name: "".to_string(), - last_name: None, - username: None, - language_code: None, - }), - forward_kind: ForwardKind::Origin { - reply_to_message: None, - }, - edit_date: None, - media_kind: MediaKind::Photo { - photo: vec![], - caption: Some(caption), - caption_entities: vec![], - media_group_id: None, - }, - reply_markup: None, - }, - } - } -} diff --git a/src/dispatching/filters/message_text.rs b/src/dispatching/filters/message_text.rs deleted file mode 100644 index e713d9bc..00000000 --- a/src/dispatching/filters/message_text.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::{dispatching::Filter, types::Message}; - -/// Filter which compare message text with another text. -/// Returns true if the message text is equal to another text, otherwise false. -/// -/// NOTE: filter compares only text message, does not compare caption of media! -/// -/// If you want to compare caption use -/// [MessageCaptionFilter] -/// -/// If you want to compare text and caption use -/// [MessageTextCaptionFilter] -/// -/// [MessageCaptionFilter]: crate::dispatching::filters::MessageCaptionFilter -/// [MessageTextCaptionFilter]: -/// crate::dispatching::filters::MessageTextCaptionFilter -pub struct MessageTextFilter { - text: String, -} - -impl Filter for MessageTextFilter { - fn test(&self, value: &Message) -> bool { - match value.text() { - Some(text) => self.text == text, - None => false, - } - } -} - -impl MessageTextFilter { - pub fn new(text: T) -> Self - where - T: Into, - { - Self { text: text.into() } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::types::{ - Chat, ChatKind, ForwardKind, MediaKind, MessageKind, Sender, User, - }; - - #[test] - fn texts_are_equal() { - let filter = MessageTextFilter::new("text"); - let message = create_message_with_text("text".to_string()); - assert!(filter.test(&message)); - } - - #[test] - fn texts_are_not_equal() { - let filter = MessageTextFilter::new("text"); - let message = create_message_with_text("not equal text".to_string()); - assert_eq!(filter.test(&message), false); - } - - fn create_message_with_text(text: String) -> Message { - Message { - id: 0, - date: 0, - chat: Chat { - id: 0, - kind: ChatKind::Private { - type_: (), - username: None, - first_name: None, - last_name: None, - }, - photo: None, - }, - kind: MessageKind::Common { - from: Sender::User(User { - id: 0, - is_bot: false, - first_name: "".to_string(), - last_name: None, - username: None, - language_code: None, - }), - forward_kind: ForwardKind::Origin { - reply_to_message: None, - }, - edit_date: None, - media_kind: MediaKind::Text { - text, - entities: vec![], - }, - reply_markup: None, - }, - } - } -} diff --git a/src/dispatching/filters/message_text_caption.rs b/src/dispatching/filters/message_text_caption.rs deleted file mode 100644 index eabcfb71..00000000 --- a/src/dispatching/filters/message_text_caption.rs +++ /dev/null @@ -1,152 +0,0 @@ -use crate::{dispatching::Filter, types::Message}; - -/// Filter which compare message text or caption of media with another text. -/// Returns true if the message text or caption of media is equal to another -/// text, otherwise false. -/// -/// NOTE: filter compares text of message or if it is not exists, compares -/// caption of the message! -/// -/// If you want to compare only caption use -/// [MessageCaptionFilter] -/// -/// If you want to compare only text use -/// [MessageTextFilter] -/// -/// [MessageCaptionFilter]: crate::dispatching::filters::MessageCaptionFilter -/// [MessageTextFilter]: crate::dispatching::filters::MessageTextFilter -pub struct MessageTextCaptionFilter { - text: String, -} - -impl Filter for MessageTextCaptionFilter { - fn test(&self, value: &Message) -> bool { - match value.text() { - Some(text) => self.text == text, - None => match value.caption() { - Some(caption) => self.text == caption, - None => false, - }, - } - } -} - -impl MessageTextCaptionFilter { - pub fn new(text: T) -> Self - where - T: Into, - { - Self { text: text.into() } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::types::{ - Chat, ChatKind, ForwardKind, MediaKind, MessageKind, Sender, User, - }; - - #[test] - fn texts_are_equal() { - let filter = MessageTextCaptionFilter::new("text"); - let message = create_message_with_text("text".to_string()); - assert!(filter.test(&message)); - } - - #[test] - fn texts_are_not_equal() { - let filter = MessageTextCaptionFilter::new("text"); - let message = create_message_with_text("not equal text".to_string()); - assert_eq!(filter.test(&message), false); - } - - fn create_message_with_text(text: String) -> Message { - Message { - id: 0, - date: 0, - chat: Chat { - id: 0, - kind: ChatKind::Private { - type_: (), - username: None, - first_name: None, - last_name: None, - }, - photo: None, - }, - kind: MessageKind::Common { - from: Sender::User(User { - id: 0, - is_bot: false, - first_name: "".to_string(), - last_name: None, - username: None, - language_code: None, - }), - forward_kind: ForwardKind::Origin { - reply_to_message: None, - }, - edit_date: None, - media_kind: MediaKind::Text { - text, - entities: vec![], - }, - reply_markup: None, - }, - } - } - - #[test] - fn captions_are_equal() { - let filter = MessageTextCaptionFilter::new("caption".to_string()); - let message = create_message_with_caption("caption".to_string()); - assert!(filter.test(&message)); - } - - #[test] - fn captions_are_not_equal() { - let filter = MessageTextCaptionFilter::new("caption".to_string()); - let message = - create_message_with_caption("not equal caption".to_string()); - assert_eq!(filter.test(&message), false); - } - - fn create_message_with_caption(caption: String) -> Message { - Message { - id: 0, - date: 0, - chat: Chat { - id: 0, - kind: ChatKind::Private { - type_: (), - username: None, - first_name: None, - last_name: None, - }, - photo: None, - }, - kind: MessageKind::Common { - from: Sender::User(User { - id: 0, - is_bot: false, - first_name: "".to_string(), - last_name: None, - username: None, - language_code: None, - }), - forward_kind: ForwardKind::Origin { - reply_to_message: None, - }, - edit_date: None, - media_kind: MediaKind::Photo { - photo: vec![], - caption: Some(caption), - caption_entities: vec![], - media_group_id: None, - }, - reply_markup: None, - }, - } - } -} diff --git a/src/dispatching/filters/mod.rs b/src/dispatching/filters/mod.rs deleted file mode 100644 index c8d62294..00000000 --- a/src/dispatching/filters/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! Filters of messages. - -pub use main::*; - -pub use command::*; -pub use message_caption::*; -pub use message_text::*; -pub use message_text_caption::*; -#[cfg(feature = "regex_filter")] -pub use regex_filter::*; - -mod main; - -mod command; -mod message_caption; -mod message_text; -mod message_text_caption; -#[cfg(feature = "regex_filter")] -mod regex_filter; diff --git a/src/dispatching/filters/regex_filter.rs b/src/dispatching/filters/regex_filter.rs deleted file mode 100644 index 71ce4d3f..00000000 --- a/src/dispatching/filters/regex_filter.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::{dispatching::Filter, types::Message}; -use regex::Regex; - -// TODO: docs -pub struct RegexFilter { - regexp: Regex, -} - -impl Filter for RegexFilter { - fn test(&self, value: &Message) -> bool { - match value.text() { - Some(text) => self.regexp.is_match(text), - None => false, - } - } -} - -impl RegexFilter { - pub fn new(regexp: Regex) -> Self { - Self { regexp } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::types::{ - Chat, ChatKind, ForwardKind, MediaKind, MessageKind, Sender, User, - }; - - #[test] - fn match_true() { - let filter = RegexFilter::new(Regex::new(r"\w+").unwrap()); - let message = create_message_with_text("text".to_string()); - assert!(filter.test(&message)); - } - - fn create_message_with_text(text: String) -> Message { - Message { - id: 0, - date: 0, - chat: Chat { - id: 0, - kind: ChatKind::Private { - type_: (), - username: None, - first_name: None, - last_name: None, - }, - photo: None, - }, - kind: MessageKind::Common { - from: Sender::User(User { - id: 0, - is_bot: false, - first_name: "".to_string(), - last_name: None, - username: None, - language_code: None, - }), - forward_kind: ForwardKind::Origin { - reply_to_message: None, - }, - edit_date: None, - media_kind: MediaKind::Text { - text, - entities: vec![], - }, - reply_markup: None, - }, - } - } -} diff --git a/src/dispatching/mod.rs b/src/dispatching/mod.rs index 8819a0ea..b1099d8f 100644 --- a/src/dispatching/mod.rs +++ b/src/dispatching/mod.rs @@ -2,12 +2,10 @@ //mod dispatchers; pub mod error_handlers; -pub mod filters; mod handler; pub mod updaters; //pub use dispatchers::filter::FilterDispatcher; pub use error_handlers::ErrorHandler; -pub use filters::Filter; pub use handler::Handler; pub use updaters::Updater;