teloxide_ng/dispatching/dialogue.rs
1//! Support for user dialogues.
2//!
3//! The main type is (surprise!) [`Dialogue`]. Under the hood, it is just a
4//! wrapper over [`Storage`] and a chat ID. All it does is provides convenient
5//! method for manipulating the dialogue state. [`Storage`] is where all
6//! dialogue states are stored; it can be either [`InMemStorage`], which is a
7//! simple hash map from [`std::collections`], or an advanced database wrapper
8//! such as [`SqliteStorage`]. In the latter case, your dialogues are
9//! _persistent_, meaning that you can safely restart your bot and all ongoing
10//! dialogues will remain in the database -- this is a preferred method for
11//! production bots.
12//!
13//! [`examples/dialogue.rs`] clearly demonstrates the typical usage of
14//! dialogues. Your dialogue state can be represented as an enumeration:
15//!
16//! ```no_run
17//! #[derive(Clone, Default)]
18//! pub enum State {
19//! #[default]
20//! Start,
21//! ReceiveFullName,
22//! ReceiveAge {
23//! full_name: String,
24//! },
25//! ReceiveLocation {
26//! full_name: String,
27//! age: u8,
28//! },
29//! }
30//! ```
31//!
32//! Each state is associated with its respective handler: e.g., when a dialogue
33//! state is `ReceiveAge`, `receive_age` is invoked:
34//!
35//! ```no_run
36//! # use teloxide_ng::{dispatching::dialogue::InMemStorage, prelude::*};
37//! # type MyDialogue = Dialogue<State, InMemStorage<State>>;
38//! # type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
39//! # #[derive(Clone, Debug)] enum State { ReceiveLocation { full_name: String, age: u8 } }
40//! async fn receive_age(
41//! bot: Bot,
42//! dialogue: MyDialogue,
43//! full_name: String, // Available from `State::ReceiveAge`.
44//! msg: Message,
45//! ) -> HandlerResult {
46//! match msg.text().map(|text| text.parse::<u8>()) {
47//! Some(Ok(age)) => {
48//! bot.send_message(msg.chat.id, "What's your location?").await?;
49//! dialogue.update(State::ReceiveLocation { full_name, age }).await?;
50//! }
51//! _ => {
52//! bot.send_message(msg.chat.id, "Send me a number.").await?;
53//! }
54//! }
55//!
56//! Ok(())
57//! }
58//! ```
59//!
60//! Variant's fields are passed to state handlers as single arguments like
61//! `full_name: String` or tuples in case of two or more variant parameters (see
62//! below). Using [`Dialogue::update`], you can update the dialogue with a new
63//! state, in our case -- `State::ReceiveLocation { full_name, age }`. To exit
64//! the dialogue, just call [`Dialogue::exit`] and it will be removed from the
65//! underlying storage:
66//!
67//! ```no_run
68//! # use teloxide_ng::{dispatching::dialogue::InMemStorage, prelude::*};
69//! # type MyDialogue = Dialogue<State, InMemStorage<State>>;
70//! # type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>;
71//! # #[derive(Clone, Debug)] enum State {}
72//! async fn receive_location(
73//! bot: Bot,
74//! dialogue: MyDialogue,
75//! (full_name, age): (String, u8), // Available from `State::ReceiveLocation`.
76//! msg: Message,
77//! ) -> HandlerResult {
78//! match msg.text() {
79//! Some(location) => {
80//! let message =
81//! format!("Full name: {}\nAge: {}\nLocation: {}", full_name, age, location);
82//! bot.send_message(msg.chat.id, message).await?;
83//! dialogue.exit().await?;
84//! }
85//! None => {
86//! bot.send_message(msg.chat.id, "Send me a text message.").await?;
87//! }
88//! }
89//!
90//! Ok(())
91//! }
92//! ```
93//!
94//! [`examples/dialogue.rs`]: https://github.com/teloxide/teloxide/blob/master/crates/teloxide-ng/examples/dialogue.rs
95
96#[cfg(feature = "redis-storage")]
97pub use self::{RedisStorage, RedisStorageError};
98
99#[cfg(any(feature = "sqlite-storage-nativetls", feature = "sqlite-storage-rustls"))]
100pub use self::{SqliteStorage, SqliteStorageError};
101
102#[cfg(any(feature = "postgres-storage-nativetls", feature = "postgres-storage-rustls"))]
103pub use self::{PostgresStorage, PostgresStorageError};
104
105pub use get_chat_id::GetChatId;
106pub use storage::*;
107
108use dptree::Handler;
109use teloxide_core_ng::types::ChatId;
110
111use std::{fmt::Debug, marker::PhantomData, sync::Arc};
112
113use super::DpHandlerDescription;
114
115mod get_chat_id;
116mod storage;
117
118const TELOXIDE_DIALOGUE_BEHAVIOUR: &str = "TELOXIDE_DIALOGUE_BEHAVIOUR";
119
120/// A handle for controlling dialogue state.
121#[derive(Debug)]
122pub struct Dialogue<D, S>
123where
124 S: ?Sized,
125{
126 storage: Arc<S>,
127 chat_id: ChatId,
128 _phantom: PhantomData<D>,
129}
130
131// `#[derive]` requires generics to implement `Clone`, but `S` is wrapped around
132// `Arc`, and `D` is wrapped around PhantomData.
133impl<D, S> Clone for Dialogue<D, S>
134where
135 S: ?Sized,
136{
137 fn clone(&self) -> Self {
138 Dialogue { storage: self.storage.clone(), chat_id: self.chat_id, _phantom: PhantomData }
139 }
140}
141
142impl<D, S> Dialogue<D, S>
143where
144 D: Send + 'static,
145 S: Storage<D> + ?Sized,
146{
147 /// Constructs a new dialogue with `storage` (where dialogues are stored)
148 /// and `chat_id` of a current dialogue.
149 #[must_use]
150 pub fn new(storage: Arc<S>, chat_id: ChatId) -> Self {
151 Self { storage, chat_id, _phantom: PhantomData }
152 }
153
154 /// Returns a chat ID associated with this dialogue.
155 #[must_use]
156 pub fn chat_id(&self) -> ChatId {
157 self.chat_id
158 }
159
160 /// Retrieves the current state of the dialogue or `None` if there is no
161 /// dialogue.
162 pub async fn get(&self) -> Result<Option<D>, S::Error> {
163 self.storage.clone().get_dialogue(self.chat_id).await
164 }
165
166 /// Like [`Dialogue::get`] but returns a default value if there is no
167 /// dialogue.
168 pub async fn get_or_default(&self) -> Result<D, S::Error>
169 where
170 D: Default,
171 {
172 match self.get().await? {
173 Some(d) => Ok(d),
174 None => {
175 self.storage.clone().update_dialogue(self.chat_id, D::default()).await?;
176 Ok(D::default())
177 }
178 }
179 }
180
181 /// Updates the dialogue state.
182 ///
183 /// The dialogue type `D` must implement `From<State>` to allow implicit
184 /// conversion from `State` to `D`.
185 pub async fn update<State>(&self, state: State) -> Result<(), S::Error>
186 where
187 D: From<State>,
188 {
189 let new_dialogue = state.into();
190 self.storage.clone().update_dialogue(self.chat_id, new_dialogue).await?;
191 Ok(())
192 }
193
194 /// Updates the dialogue with a default value.
195 pub async fn reset(&self) -> Result<(), S::Error>
196 where
197 D: Default,
198 {
199 self.update(D::default()).await
200 }
201
202 /// Removes the dialogue from the storage provided to [`Dialogue::new`].
203 pub async fn exit(&self) -> Result<(), S::Error> {
204 self.storage.clone().remove_dialogue(self.chat_id).await
205 }
206}
207
208/// Enters a dialogue context.
209///
210/// If `TELOXIDE_DIALOGUE_BEHAVIOUR` environmental variable exists and is equal
211/// to "default", this function will not panic if it can't get the dialogue (if,
212/// for example, the state enum was updated). Setting the value to "panic" will
213/// return the initial behaviour.
214///
215/// A call to this function is the same as `dptree::entry().enter_dialogue()`.
216///
217/// See [`HandlerExt::enter_dialogue`].
218///
219/// ## Dependency requirements
220///
221/// - `Arc<S>`
222/// - `Upd`
223///
224/// [`HandlerExt::enter_dialogue`]: super::HandlerExt::enter_dialogue
225#[must_use]
226pub fn enter<Upd, S, D, Output>() -> Handler<'static, Output, DpHandlerDescription>
227where
228 S: Storage<D> + ?Sized + Send + Sync + 'static,
229 <S as Storage<D>>::Error: Debug + Send,
230 D: Default + Clone + Send + Sync + 'static,
231 Upd: GetChatId + Clone + Send + Sync + 'static,
232 Output: Send + Sync + 'static,
233{
234 dptree::filter_map(|storage: Arc<S>, upd: Upd| {
235 let chat_id = upd.chat_id()?;
236 Some(Dialogue::new(storage, chat_id))
237 })
238 .filter_map_async(|dialogue: Dialogue<D, S>| async move {
239 match dialogue.get_or_default().await {
240 Ok(dialogue) => Some(dialogue),
241 Err(err) => match std::env::var(TELOXIDE_DIALOGUE_BEHAVIOUR).as_deref() {
242 Ok("default") => {
243 let default = D::default();
244 dialogue.update(default.clone()).await.ok()?;
245 Some(default)
246 }
247 Ok("panic") | Err(_) => {
248 log::error!("dialogue.get_or_default() failed: {err:?}");
249 None
250 }
251 Ok(_) => {
252 panic!(
253 "`TELOXIDE_DIALOGUE_BEHAVIOUR` env variable should be one of: \
254 default/panic"
255 )
256 }
257 },
258 }
259 })
260}