Skip to main content

rustigram_bot/
context.rs

1use std::sync::Arc;
2
3use rustigram_api::BotClient;
4use rustigram_types::inline::InlineQuery;
5use rustigram_types::message::Message;
6use rustigram_types::update::CallbackQuery;
7use rustigram_types::update::{Update, UpdateKind};
8use rustigram_types::user::ChatId;
9
10/// The context object passed to every handler.
11///
12/// Contains the incoming [`Update`], the [`BotClient`] (ready to make API
13/// calls), and a reference to any shared bot-level state.
14#[derive(Clone)]
15/// The context object passed to every handler.
16///
17/// `Context` bundles the incoming [`Update`] with the [`BotClient`] so
18/// handlers have everything they need in a single value.
19///
20/// # Accessing the update
21///
22/// ```rust,ignore
23/// async fn handler(ctx: Context) -> BotResult<()> {
24///     // Convenience accessors
25///     let text    = ctx.text();       // message text or caption
26///     let cmd     = ctx.command();    // "/start" → Some("start")
27///     let chat_id = ctx.chat_id();
28///     let user_id = ctx.from_id();
29///
30///     // Raw update for anything not covered by the helpers
31///     let update = &ctx.update;
32///     Ok(())
33/// }
34/// ```
35///
36/// # Sending replies
37///
38/// `ctx.reply(text)` is a shortcut that sends a message to the current chat
39/// and automatically sets `reply_to_message_id`. It returns `None` when the
40/// update has no associated chat (e.g. inline queries).
41///
42/// ```rust,ignore
43/// if let Some(reply) = ctx.reply("Got it!") {
44///     reply.parse_mode(ParseMode::HTML).await?;
45/// }
46/// ```
47pub struct Context {
48    /// The incoming update that triggered this handler.
49    pub update: Arc<Update>,
50
51    /// The API client — call any Bot API method directly.
52    pub bot: BotClient,
53}
54
55impl Context {
56    /// Creates a new `Context`.
57    #[must_use]
58    pub fn new(update: Update, bot: BotClient) -> Self {
59        Self {
60            update: Arc::new(update),
61            bot,
62        }
63    }
64
65    /// Returns the update ID.
66    #[must_use]
67    pub fn update_id(&self) -> i64 {
68        self.update.update_id
69    }
70
71    /// Returns the [`Message`] from a message, edited message, channel post,
72    /// or callback query update. Returns `None` for all other update types.
73    #[must_use]
74    pub fn message(&self) -> Option<&Message> {
75        match &self.update.kind {
76            UpdateKind::Message(m)
77            | UpdateKind::EditedMessage(m)
78            | UpdateKind::ChannelPost(m)
79            | UpdateKind::EditedChannelPost(m)
80            | UpdateKind::BusinessMessage(m)
81            | UpdateKind::EditedBusinessMessage(m) => Some(m),
82            UpdateKind::CallbackQuery(q) => q.message.as_ref(),
83            _ => None,
84        }
85    }
86
87    /// Returns the chat ID from the current update as a [`ChatId`], if available.
88    #[must_use]
89    pub fn chat_id(&self) -> Option<ChatId> {
90        self.update.chat_id().map(ChatId::Id)
91    }
92
93    /// Returns the sender's user ID, if available.
94    #[must_use]
95    pub fn from_id(&self) -> Option<i64> {
96        self.update.from().map(|u| u.id)
97    }
98
99    /// Returns the [`CallbackQuery`] if this is a callback query update.
100    #[must_use]
101    pub fn callback_query(&self) -> Option<&CallbackQuery> {
102        match &self.update.kind {
103            UpdateKind::CallbackQuery(q) => Some(q),
104            _ => None,
105        }
106    }
107
108    /// Returns the [`InlineQuery`] if this is an inline query update.
109    #[must_use]
110    pub fn inline_query(&self) -> Option<&InlineQuery> {
111        match &self.update.kind {
112            UpdateKind::InlineQuery(q) => Some(q),
113            _ => None,
114        }
115    }
116
117    /// Returns the effective text of the message — [`Message::text`] if present,
118    /// falling back to [`Message::caption`].
119    #[must_use]
120    pub fn text(&self) -> Option<&str> {
121        self.message().and_then(|m| m.effective_text())
122    }
123
124    /// Returns the command name if the message starts with a bot command entity.
125    ///
126    /// The leading `/` and optional `@BotName` suffix are stripped automatically.
127    /// `/start@mybot` returns `Some("start")`.
128    #[must_use]
129    pub fn command(&self) -> Option<&str> {
130        self.message().and_then(|m| m.command())
131    }
132
133    /// Returns `true` if the current update's message is an ephemeral message.
134    #[must_use]
135    pub fn is_ephemeral(&self) -> bool {
136        self.message()
137            .is_some_and(|m| m.ephemeral_message_id.is_some())
138    }
139
140    /// Returns the ephemeral message identifier, if the current update's
141    /// message is an ephemeral message.
142    ///
143    /// Note that this is unrelated to [`Context::reply`] — replying to an
144    /// ephemeral message still requires calling [`BotClient::send_message`]
145    /// directly with `.receiver_user_id(...)` (and, within 15 seconds of the
146    /// triggering action, `.callback_query_id(...)` or
147    /// [`ReplyParameters::reply_to_ephemeral`](rustigram_types::message::ReplyParameters::reply_to_ephemeral)),
148    /// since `reply()`'s signature has no room for the extra targeting
149    /// parameters without a breaking change.
150    #[must_use]
151    pub fn ephemeral_message_id(&self) -> Option<i64> {
152        self.message()?.ephemeral_message_id
153    }
154
155    /// Sends a text reply to the current chat, automatically setting
156    /// `reply_to_message_id` to the incoming message.
157    ///
158    /// Returns `None` when the update has no associated chat.
159    pub fn reply(
160        &self,
161        text: impl Into<String>,
162    ) -> Option<rustigram_api::methods::sending::SendMessage> {
163        let chat_id = self.chat_id()?;
164        let mut builder = self.bot.send_message(chat_id, text);
165        if let Some(msg) = self.message() {
166            builder = builder.reply_to(msg.message_id);
167        }
168        Some(builder)
169    }
170
171    /// Returns the Web App data from this update's message, if present.
172    ///
173    /// Populated when a user taps a `web_app` keyboard button that sends data
174    /// directly to the bot — as opposed to launching a full TMA session.
175    /// Contains the raw `data` string and the `button_text` that triggered it.
176    ///
177    /// For TMA session-based `initData` validation use
178    /// [`rustigram_miniapp::validate_hmac`] or
179    /// [`rustigram_miniapp::validate_ed25519`] on the server side.
180    ///
181    /// # Example
182    ///
183    /// ```rust,ignore
184    /// async fn handle_tma(ctx: Context) -> BotResult<()> {
185    ///     if let Some(data) = ctx.tma_data() {
186    ///         println!("button: {}, payload: {}", data.button_text, data.data);
187    ///     }
188    ///     Ok(())
189    /// }
190    /// ```
191    #[cfg(feature = "tma")]
192    #[must_use]
193    pub fn tma_data(&self) -> Option<&rustigram_types::message::WebAppData> {
194        self.message()?.web_app_data.as_ref()
195    }
196}