Skip to main content

rustigram_bot/
filter.rs

1use rustigram_types::update::UpdateKind;
2
3use crate::context::Context;
4
5/// A predicate evaluated against an incoming [`Context`].
6///
7/// Filters are `Send + Sync + 'static` and cheap to clone, making them safe
8/// to share across tasks. Combine filters with [`FilterExt::and`],
9/// [`FilterExt::or`], and [`FilterExt::not`].
10///
11/// Implement this trait to create custom filters:
12///
13/// ```rust,ignore
14/// use rustigram_bot::filter::Filter;
15/// use rustigram_bot::Context;
16///
17/// #[derive(Clone)]
18/// struct HasPhotoFilter;
19///
20/// impl Filter for HasPhotoFilter {
21///     fn check(&self, ctx: &Context) -> bool {
22///         ctx.message().and_then(|m| m.photo.as_ref()).is_some()
23///     }
24/// }
25/// ```
26pub trait Filter: Send + Sync + 'static {
27    /// Returns `true` if this filter matches the given context.
28    fn check(&self, ctx: &Context) -> bool;
29}
30
31/// Extension methods for composing [`Filter`] values.
32///
33/// Automatically implemented for every type that implements [`Filter`].
34pub trait FilterExt: Filter + Sized + Clone {
35    /// Passes only when both `self` and `other` match.
36    fn and<F: Filter + Clone>(self, other: F) -> And<Self, F> {
37        And {
38            left: self,
39            right: other,
40        }
41    }
42
43    /// Passes when `self` or `other` (or both) match.
44    fn or<F: Filter + Clone>(self, other: F) -> Or<Self, F> {
45        Or {
46            left: self,
47            right: other,
48        }
49    }
50
51    /// Inverts this filter.
52    fn not(self) -> Not<Self> {
53        Not { inner: self }
54    }
55}
56
57impl<F: Filter + Clone> FilterExt for F {}
58
59// ─── Combinators ─────────────────────────────────────────────────────────────
60
61/// Combines two filters with logical AND: passes only if both filters pass.
62#[derive(Clone)]
63pub struct And<L, R> {
64    left: L,
65    right: R,
66}
67impl<L: Filter, R: Filter> Filter for And<L, R> {
68    fn check(&self, ctx: &Context) -> bool {
69        self.left.check(ctx) && self.right.check(ctx)
70    }
71}
72
73/// Combines two filters with logical OR: passes if either filter (or both) pass.
74#[derive(Clone)]
75pub struct Or<L, R> {
76    left: L,
77    right: R,
78}
79impl<L: Filter, R: Filter> Filter for Or<L, R> {
80    fn check(&self, ctx: &Context) -> bool {
81        self.left.check(ctx) || self.right.check(ctx)
82    }
83}
84
85/// Inverts a filter: passes when the inner filter fails, and vice versa.
86#[derive(Clone)]
87pub struct Not<F> {
88    inner: F,
89}
90impl<F: Filter> Filter for Not<F> {
91    fn check(&self, ctx: &Context) -> bool {
92        !self.inner.check(ctx)
93    }
94}
95
96// ─── Function filter ──────────────────────────────────────────────────────────
97
98#[derive(Clone)]
99/// Wraps a plain function or closure as a [`Filter`].
100pub struct FnFilter<F>(pub F);
101
102impl<F: Fn(&Context) -> bool + Send + Sync + Clone + 'static> Filter for FnFilter<F> {
103    fn check(&self, ctx: &Context) -> bool {
104        (self.0)(ctx)
105    }
106}
107
108/// Creates a [`Filter`] from any closure with signature `fn(&Context) -> bool`.
109pub fn filter_fn<F>(f: F) -> FnFilter<F>
110where
111    F: Fn(&Context) -> bool + Send + Sync + Clone + 'static,
112{
113    FnFilter(f)
114}
115
116// ─── Built-in filters ─────────────────────────────────────────────────────────
117
118#[derive(Clone, Copy)]
119/// Passes only for [`Message`](rustigram_types::update::UpdateKind::Message) updates.
120pub struct MessageFilter;
121impl Filter for MessageFilter {
122    fn check(&self, ctx: &Context) -> bool {
123        matches!(ctx.update.kind, UpdateKind::Message(_))
124    }
125}
126
127/// Passes only for `EditedMessage` updates.
128#[derive(Clone, Copy)]
129/// Passes only for [`EditedMessage`](rustigram_types::update::UpdateKind::EditedMessage) updates.
130pub struct EditedMessageFilter;
131impl Filter for EditedMessageFilter {
132    fn check(&self, ctx: &Context) -> bool {
133        matches!(ctx.update.kind, UpdateKind::EditedMessage(_))
134    }
135}
136
137/// Passes only for `CallbackQuery` updates.
138#[derive(Clone, Copy)]
139/// Passes only for [`CallbackQuery`](rustigram_types::update::UpdateKind::CallbackQuery) updates.
140pub struct CallbackQueryFilter;
141impl Filter for CallbackQueryFilter {
142    fn check(&self, ctx: &Context) -> bool {
143        matches!(ctx.update.kind, UpdateKind::CallbackQuery(_))
144    }
145}
146
147/// Passes only for `InlineQuery` updates.
148#[derive(Clone, Copy)]
149/// Passes only for [`InlineQuery`](rustigram_types::update::UpdateKind::InlineQuery) updates.
150pub struct InlineQueryFilter;
151impl Filter for InlineQueryFilter {
152    fn check(&self, ctx: &Context) -> bool {
153        matches!(ctx.update.kind, UpdateKind::InlineQuery(_))
154    }
155}
156
157#[derive(Clone)]
158/// Passes when the message is a bot command matching `command`.
159///
160/// The check is case-insensitive and strips the leading `/` and any
161/// `@BotName` suffix automatically.
162pub struct CommandFilter {
163    command: String,
164}
165
166impl CommandFilter {
167    /// Creates a new filter matching the command `command`.
168    pub fn new(command: impl Into<String>) -> Self {
169        Self {
170            command: command.into(),
171        }
172    }
173}
174
175impl Filter for CommandFilter {
176    fn check(&self, ctx: &Context) -> bool {
177        ctx.command()
178            .map_or(false, |cmd| cmd.eq_ignore_ascii_case(&self.command))
179    }
180}
181
182#[derive(Clone)]
183/// Passes when the message text exactly equals `text`.
184pub struct TextFilter {
185    text: String,
186}
187
188impl TextFilter {
189    /// Creates a new filter matching the exact text `text`.
190    pub fn new(text: impl Into<String>) -> Self {
191        Self { text: text.into() }
192    }
193}
194
195impl Filter for TextFilter {
196    fn check(&self, ctx: &Context) -> bool {
197        ctx.text().map_or(false, |t| t == self.text)
198    }
199}
200
201#[derive(Clone)]
202/// Passes when the message text contains `needle` as a substring.
203pub struct TextContainsFilter {
204    needle: String,
205}
206
207impl TextContainsFilter {
208    /// Creates a new filter matching text containing `needle`.
209    pub fn new(needle: impl Into<String>) -> Self {
210        Self {
211            needle: needle.into(),
212        }
213    }
214}
215
216impl Filter for TextContainsFilter {
217    fn check(&self, ctx: &Context) -> bool {
218        ctx.text()
219            .map_or(false, |t| t.contains(self.needle.as_str()))
220    }
221}
222
223#[derive(Clone)]
224/// Passes when the callback query data exactly equals `data`.
225pub struct CallbackDataFilter {
226    data: String,
227}
228
229impl CallbackDataFilter {
230    /// Creates a new filter matching the callback data `data`.
231    pub fn new(data: impl Into<String>) -> Self {
232        Self { data: data.into() }
233    }
234}
235
236impl Filter for CallbackDataFilter {
237    fn check(&self, ctx: &Context) -> bool {
238        ctx.callback_query()
239            .and_then(|q| q.data.as_deref())
240            .map_or(false, |d| d == self.data)
241    }
242}
243
244#[derive(Clone)]
245/// Passes when the callback query data starts with `prefix`.
246pub struct CallbackDataPrefixFilter {
247    prefix: String,
248}
249
250impl CallbackDataPrefixFilter {
251    /// Creates a new filter matching callback data starting with `prefix`.
252    pub fn new(prefix: impl Into<String>) -> Self {
253        Self {
254            prefix: prefix.into(),
255        }
256    }
257}
258
259impl Filter for CallbackDataPrefixFilter {
260    fn check(&self, ctx: &Context) -> bool {
261        ctx.callback_query()
262            .and_then(|q| q.data.as_deref())
263            .map_or(false, |d| d.starts_with(self.prefix.as_str()))
264    }
265}
266
267#[derive(Clone, Copy)]
268/// Passes only for messages in private chats.
269pub struct PrivateChatFilter;
270impl Filter for PrivateChatFilter {
271    fn check(&self, ctx: &Context) -> bool {
272        ctx.message().map_or(false, |m| {
273            matches!(m.chat.kind, rustigram_types::chat::ChatType::Private)
274        })
275    }
276}
277
278#[derive(Clone, Copy)]
279/// Passes only for messages in group and supergroup chats.
280pub struct GroupFilter;
281impl Filter for GroupFilter {
282    fn check(&self, ctx: &Context) -> bool {
283        ctx.message().map_or(false, |m| {
284            matches!(
285                m.chat.kind,
286                rustigram_types::chat::ChatType::Group
287                    | rustigram_types::chat::ChatType::Supergroup
288            )
289        })
290    }
291}
292
293/// Convenience constructors for all built-in filters.
294///
295/// Import this module and call functions to create filters:
296///
297/// ```rust,ignore
298/// use rustigram_bot::filter::filters;
299/// use rustigram_bot::filter::FilterExt;
300///
301/// let f = filters::command("start")
302///             .and(filters::private());
303/// ```
304pub mod filters {
305    use super::*;
306
307    /// Passes for any `Message` update.
308    pub fn message() -> MessageFilter {
309        MessageFilter
310    }
311    /// Passes for any `EditedMessage` update.
312    pub fn edited_message() -> EditedMessageFilter {
313        EditedMessageFilter
314    }
315    /// Passes for any `CallbackQuery` update.
316    pub fn callback_query() -> CallbackQueryFilter {
317        CallbackQueryFilter
318    }
319    /// Passes for any `InlineQuery` update.
320    pub fn inline_query() -> InlineQueryFilter {
321        InlineQueryFilter
322    }
323    /// Passes when the message is the given bot command (case-insensitive).
324    pub fn command(cmd: impl Into<String>) -> CommandFilter {
325        CommandFilter::new(cmd)
326    }
327    /// Passes when the message text exactly equals `t`.
328    pub fn text(t: impl Into<String>) -> TextFilter {
329        TextFilter::new(t)
330    }
331    /// Passes when the message text contains `needle` as a substring.
332    pub fn text_contains(needle: impl Into<String>) -> TextContainsFilter {
333        TextContainsFilter::new(needle)
334    }
335    /// Passes when the callback query data exactly equals `data`.
336    pub fn callback_data(data: impl Into<String>) -> CallbackDataFilter {
337        CallbackDataFilter::new(data)
338    }
339    /// Passes when the callback query data starts with `prefix`.
340    pub fn callback_data_prefix(prefix: impl Into<String>) -> CallbackDataPrefixFilter {
341        CallbackDataPrefixFilter::new(prefix)
342    }
343    /// Passes for messages in private chats.
344    pub fn private() -> PrivateChatFilter {
345        PrivateChatFilter
346    }
347    /// Passes for messages in group and supergroup chats.
348    pub fn group() -> GroupFilter {
349        GroupFilter
350    }
351    /// Always passes — useful as a catch-all fallback route.
352    pub fn any() -> FnFilter<fn(&Context) -> bool> {
353        FnFilter(|_| true)
354    }
355}