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}