|
5 | 5 |
|
6 | 6 | //! Default and useful filters. |
7 | 7 |
|
| 8 | +use std::collections::HashMap; |
| 9 | + |
8 | 10 | use grammers::{Client, media::Media, peer::Peer, tl, update::Update}; |
9 | 11 |
|
10 | 12 | use super::{AsyncMarker, FilterExt, Flow, IntoFilter, SyncMarker}; |
| 13 | +use crate::router::CommandParams; |
11 | 14 |
|
12 | 15 | /// Always passing filter, it doesn't check for anything. |
13 | 16 | pub fn always(_: Client, _: Update) -> bool { |
@@ -41,45 +44,162 @@ pub fn not<Marker>(filter: impl IntoFilter<Marker>) -> impl IntoFilter<AsyncMark |
41 | 44 | } |
42 | 45 |
|
43 | 46 | /// Pass if the message text contains the specified pattern. |
44 | | -pub fn text(pat: &'static str) -> impl IntoFilter<SyncMarker> { |
| 47 | +pub fn text(pattern: &'static str) -> impl IntoFilter<SyncMarker> { |
45 | 48 | move |_, update| match update { |
46 | 49 | Update::NewMessage(message) | Update::MessageEdited(message) => { |
47 | | - message.text().contains(pat) |
| 50 | + message.text().contains(pattern) |
48 | 51 | } |
49 | 52 | _ => false, |
50 | 53 | } |
51 | 54 | } |
52 | 55 |
|
53 | 56 | /// Pass if the message text or query data matches the specified pattern. |
54 | | -pub fn regex(pat: &'static str) -> impl IntoFilter<SyncMarker> { |
| 57 | +pub fn regex(pattern: &'static str) -> impl IntoFilter<SyncMarker> { |
55 | 58 | move |_, update| match update { |
56 | 59 | Update::NewMessage(message) | Update::MessageEdited(message) => { |
57 | | - let re = regex::Regex::new(pat).unwrap(); |
58 | | - |
| 60 | + let re = regex::Regex::new(pattern).unwrap(); |
59 | 61 | re.is_match(message.text()) |
60 | 62 | } |
61 | 63 | Update::CallbackQuery(query) => { |
62 | | - let re = regex::bytes::Regex::new(pat).unwrap(); |
63 | | - |
| 64 | + let re = regex::bytes::Regex::new(pattern).unwrap(); |
64 | 65 | re.is_match(query.data()) |
65 | 66 | } |
66 | 67 | Update::InlineQuery(query) => { |
67 | | - let re = regex::Regex::new(pat).unwrap(); |
68 | | - |
| 68 | + let re = regex::Regex::new(pattern).unwrap(); |
69 | 69 | re.is_match(query.text()) |
70 | 70 | } |
71 | 71 | _ => false, |
72 | 72 | } |
73 | 73 | } |
74 | 74 |
|
| 75 | +/// Pass if the message text matches the specified command pattern. |
| 76 | +/// |
| 77 | +/// It supports parameters, which are: |
| 78 | +/// - `:param`: a one-word required parameter. |
| 79 | +/// - `:param?`: a one-word optional parameter. |
| 80 | +/// - `*param`: a multiple-word required parameter. |
| 81 | +/// - `*param?`: a multiple-word optional parameter. |
| 82 | +/// |
| 83 | +/// Note: optional parameters (those ending in `?`) can only be added at the |
| 84 | +/// end of patterns, which also means that required parameters cannot follow |
| 85 | +/// optional parameters, otherwise it'll panic. |
| 86 | +/// |
| 87 | +/// # Injects: |
| 88 | +/// * [`CommandParams`]: extracted params. |
| 89 | +/// |
| 90 | +/// # Examples |
| 91 | +/// |
| 92 | +/// ``` |
| 93 | +/// use ferogram::prelude::*; |
| 94 | +/// handler::new_message(filter::command("/profile :id?")) |
| 95 | +/// ``` |
| 96 | +pub fn command(pattern: &'static str) -> impl IntoFilter<SyncMarker> { |
| 97 | + let parts = pattern.split_whitespace().collect::<Vec<_>>(); |
| 98 | + if parts.is_empty() { |
| 99 | + panic!("Invalid pattern '{pattern}': it needs to have at least one word"); |
| 100 | + } |
| 101 | + |
| 102 | + let mut pat = format!("^{}", regex::escape(parts[0])); |
| 103 | + |
| 104 | + let mut seen_multiple = false; |
| 105 | + let mut seen_optional = false; |
| 106 | + |
| 107 | + for part in &parts[1..] { |
| 108 | + let is_optional = part.ends_with('?'); |
| 109 | + |
| 110 | + if seen_multiple { |
| 111 | + panic!( |
| 112 | + "Invalid pattern '{pattern}': parameter '{part}' cannot follow a multiple parameter" |
| 113 | + ); |
| 114 | + } |
| 115 | + |
| 116 | + if let Some(stripped) = part.strip_prefix(':') { |
| 117 | + if is_optional { |
| 118 | + seen_optional = true; |
| 119 | + |
| 120 | + let name = &stripped[..stripped.len() - 1]; |
| 121 | + pat.push_str(&format!(r"(?:\s+(?P<{name}>\S+))?")); |
| 122 | + } else { |
| 123 | + if seen_optional { |
| 124 | + panic!( |
| 125 | + "Invalid pattern '{pattern}': mandatory parameter '{part}' cannot follow an optional parameter" |
| 126 | + ); |
| 127 | + } |
| 128 | + |
| 129 | + let name = stripped; |
| 130 | + pat.push_str(&format!(r"\s+(?P<{name}>\S+)")); |
| 131 | + } |
| 132 | + } else if let Some(stripped) = part.strip_prefix('*') { |
| 133 | + seen_multiple = true; |
| 134 | + |
| 135 | + if is_optional { |
| 136 | + seen_optional = true; |
| 137 | + |
| 138 | + let name = &stripped[..stripped.len() - 1]; |
| 139 | + pat.push_str(&format!(r"(?:\s+(?P<{name}>.*))?")); |
| 140 | + } else { |
| 141 | + if seen_optional { |
| 142 | + panic!( |
| 143 | + "Invalid pattern '{pattern}': mandatory parameter '{part}' cannot follow an optional parameter" |
| 144 | + ); |
| 145 | + } |
| 146 | + |
| 147 | + let name = stripped; |
| 148 | + pat.push_str(&format!(r"\s+(?P<{name}>.*)")); |
| 149 | + } |
| 150 | + } else { |
| 151 | + if seen_optional { |
| 152 | + panic!( |
| 153 | + "Invalid pattern '{pattern}': literal word '{part}' cannot follow an optional parameter" |
| 154 | + ); |
| 155 | + } |
| 156 | + |
| 157 | + pat.push_str(&format!(r"\s+{}", regex::escape(part))); |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | + let re = regex::Regex::new(&pat).unwrap(); |
| 162 | + |
| 163 | + fn extract_params(text: &str, re: ®ex::Regex) -> Flow { |
| 164 | + if let Some(captures) = re.captures(text) { |
| 165 | + let mut params = HashMap::new(); |
| 166 | + for name in re.capture_names().flatten() { |
| 167 | + if let Some(m) = captures.name(name) { |
| 168 | + params.insert(name.to_string(), m.as_str().to_string()); |
| 169 | + } |
| 170 | + } |
| 171 | + |
| 172 | + super::proceed_with(CommandParams(params)) |
| 173 | + } else if re.is_match(text) { |
| 174 | + super::proceed() |
| 175 | + } else { |
| 176 | + super::stop() |
| 177 | + } |
| 178 | + } |
| 179 | + |
| 180 | + move |_, update| match update { |
| 181 | + Update::NewMessage(message) => { |
| 182 | + let text = message.text(); |
| 183 | + |
| 184 | + extract_params(text, &re) |
| 185 | + } |
| 186 | + Update::CallbackQuery(query) => { |
| 187 | + let data = String::from_utf8_lossy(query.data()).to_string(); |
| 188 | + |
| 189 | + extract_params(&data, &re) |
| 190 | + } |
| 191 | + _ => super::stop(), |
| 192 | + } |
| 193 | +} |
| 194 | + |
75 | 195 | /// Pass if the message has a URL. |
76 | 196 | /// |
77 | 197 | /// It extracts URLs directly from message's format entities, [`tl::enums::MessageEntity::Url`]. |
78 | 198 | /// |
79 | 199 | /// Note: if the `url` feature is enabled, it will also extracts URLs that weren't converted to format |
80 | 200 | /// entities, which can happen when using a broken client. |
81 | 201 | /// |
82 | | -/// Injects: |
| 202 | +/// # Injects: |
83 | 203 | /// * `Vec<String>`: extracted urls. |
84 | 204 | pub fn has_url(_: Client, update: Update) -> Flow { |
85 | 205 | match update { |
@@ -128,7 +248,7 @@ pub fn has_url(_: Client, update: Update) -> Flow { |
128 | 248 |
|
129 | 249 | /// Pass if the message has a dice attached to it. |
130 | 250 | /// |
131 | | -/// Injects: |
| 251 | +/// # Injects: |
132 | 252 | /// * [`grammers::message::Dice`][]: dice. |
133 | 253 | pub fn has_dice(_: Client, update: Update) -> Flow { |
134 | 254 | match update { |
|
0 commit comments