Skip to main content

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}