Skip to main content

rustigram_api/methods/
updates.rs

1use std::future::{Future, IntoFuture};
2use std::pin::Pin;
3
4use reqwest::multipart::{Form, Part};
5use serde::Serialize;
6
7use rustigram_types::update::Update;
8use rustigram_types::webhook::WebhookInfo;
9
10use rustigram_types::file::InputFile;
11
12use crate::client::BotClient;
13use crate::error::Result;
14
15// ─── getUpdates ───────────────────────────────────────────────────────────────
16
17#[derive(Serialize, Default)]
18/// Parameters sent with a `getUpdates` request.
19pub struct GetUpdatesParams {
20    /// Identifier of the first update to return. Confirms all updates before this ID.
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub offset: Option<i64>,
23    /// Maximum number of updates to return (1–100).
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub limit: Option<u8>,
26    /// Timeout in seconds for long polling. `0` for short polling.
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub timeout: Option<u32>,
29    /// List of update types to receive. All types received if omitted.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub allowed_updates: Option<Vec<String>>,
32}
33
34/// Builder for the [`getUpdates`](https://core.telegram.org/bots/api#getupdates) method.
35pub struct GetUpdates {
36    client: BotClient,
37    params: GetUpdatesParams,
38}
39
40impl GetUpdates {
41    pub(crate) fn new(client: BotClient) -> Self {
42        Self {
43            client,
44            params: GetUpdatesParams::default(),
45        }
46    }
47
48    /// Sets the update offset — all updates with `update_id < offset` are
49    /// acknowledged and will not be returned again.
50    pub fn offset(mut self, offset: i64) -> Self {
51        self.params.offset = Some(offset);
52        self
53    }
54
55    /// Limits the number of updates returned (1–100, default 100).
56    pub fn limit(mut self, limit: u8) -> Self {
57        self.params.limit = Some(limit.clamp(1, 100));
58        self
59    }
60
61    /// Sets the long-poll server-side timeout in seconds. Use 0 for short polling.
62    pub fn timeout(mut self, secs: u32) -> Self {
63        self.params.timeout = Some(secs);
64        self
65    }
66
67    /// Restricts which update types are returned (e.g. `["message", "callback_query"]`).
68    pub fn allowed_updates(mut self, types: Vec<impl Into<String>>) -> Self {
69        self.params.allowed_updates = Some(types.into_iter().map(Into::into).collect());
70        self
71    }
72}
73
74impl IntoFuture for GetUpdates {
75    type Output = Result<Vec<Update>>;
76    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
77
78    fn into_future(self) -> Self::IntoFuture {
79        Box::pin(async move { self.client.post_json("getUpdates", &self.params).await })
80    }
81}
82
83// ─── setWebhook ───────────────────────────────────────────────────────────────
84
85/// Parameters for a `setWebhook` request.
86#[derive(Serialize)]
87pub struct SetWebhookParams {
88    url: String,
89    #[serde(skip_serializing_if = "Option::is_none")]
90    ip_address: Option<String>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    max_connections: Option<u8>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    allowed_updates: Option<Vec<String>>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    drop_pending_updates: Option<bool>,
97    #[serde(skip_serializing_if = "Option::is_none")]
98    secret_token: Option<String>,
99}
100
101/// Builder for the [`setWebhook`](https://core.telegram.org/bots/api#setwebhook) method.
102pub struct SetWebhook {
103    client: BotClient,
104    params: SetWebhookParams,
105    /// Public key certificate for a self-signed setup.
106    ///
107    /// Held outside `params` because uploading one switches the request from
108    /// JSON to multipart, and an `InputFile` is not meaningfully serialisable
109    /// into the JSON body.
110    certificate: Option<InputFile>,
111}
112
113impl SetWebhook {
114    pub(crate) fn new(client: BotClient, url: impl Into<String>) -> Self {
115        Self {
116            client,
117            params: SetWebhookParams {
118                url: url.into(),
119                ip_address: None,
120                max_connections: None,
121                allowed_updates: None,
122                drop_pending_updates: None,
123                secret_token: None,
124            },
125            certificate: None,
126        }
127    }
128
129    /// Uploads a public key certificate so Telegram can verify a self-signed
130    /// setup.
131    ///
132    /// Supplying one switches the request to multipart. Telegram requires the
133    /// certificate to be uploaded as a file, so `InputFile::Url` and
134    /// `InputFile::FileId` are not accepted here — use `InputFile::Bytes`.
135    pub fn certificate(mut self, certificate: InputFile) -> Self {
136        self.certificate = Some(certificate);
137        self
138    }
139    /// Overrides the resolved IP address of the webhook server.
140    pub fn ip_address(mut self, ip: impl Into<String>) -> Self {
141        self.params.ip_address = Some(ip.into());
142        self
143    }
144    /// Sets the maximum number of concurrent HTTPS connections (1–100, default 40).
145    pub fn max_connections(mut self, n: u8) -> Self {
146        self.params.max_connections = Some(n.clamp(1, 100));
147        self
148    }
149    /// List of update types to receive. All types received if omitted.
150    pub fn allowed_updates(mut self, types: Vec<impl Into<String>>) -> Self {
151        self.params.allowed_updates = Some(types.into_iter().map(Into::into).collect());
152        self
153    }
154    /// If `true`, the webhook server will remove all pending updates.
155    pub fn drop_pending_updates(mut self, v: bool) -> Self {
156        self.params.drop_pending_updates = Some(v);
157        self
158    }
159    /// Sets the secret token Telegram sends in `X-Telegram-Bot-Api-Secret-Token`
160    /// on every webhook request.
161    pub fn secret_token(mut self, token: impl Into<String>) -> Self {
162        self.params.secret_token = Some(token.into());
163        self
164    }
165}
166
167impl IntoFuture for SetWebhook {
168    type Output = Result<bool>;
169    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
170    fn into_future(self) -> Self::IntoFuture {
171        Box::pin(async move {
172            let Some(certificate) = self.certificate else {
173                return self.client.post_json("setWebhook", &self.params).await;
174            };
175
176            let InputFile::Bytes {
177                filename,
178                data,
179                mime_type,
180            } = certificate
181            else {
182                return Err(crate::error::Error::MissingParam(
183                    "setWebhook certificate must be InputFile::Bytes — Telegram \
184                     requires the certificate to be uploaded, not referenced",
185                ));
186            };
187
188            let part = Part::bytes(data)
189                .file_name(filename)
190                .mime_str(&mime_type)
191                .map_err(|e| crate::error::Error::Decode(e.to_string()))?;
192
193            let p = &self.params;
194            let mut form = Form::new()
195                .part("certificate", part)
196                .text("url", p.url.clone());
197            if let Some(v) = &p.ip_address {
198                form = form.text("ip_address", v.clone());
199            }
200            if let Some(v) = p.max_connections {
201                form = form.text("max_connections", v.to_string());
202            }
203            if let Some(v) = &p.allowed_updates {
204                if let Ok(json) = serde_json::to_string(v) {
205                    form = form.text("allowed_updates", json);
206                }
207            }
208            if let Some(v) = p.drop_pending_updates {
209                form = form.text("drop_pending_updates", v.to_string());
210            }
211            if let Some(v) = &p.secret_token {
212                form = form.text("secret_token", v.clone());
213            }
214
215            self.client.post_multipart("setWebhook", form).await
216        })
217    }
218}
219
220// ─── deleteWebhook ────────────────────────────────────────────────────────────
221
222#[derive(Serialize, Default)]
223struct DeleteWebhookParams {
224    #[serde(skip_serializing_if = "Option::is_none")]
225    drop_pending_updates: Option<bool>,
226}
227
228/// Builder for the [`deleteWebhook`](https://core.telegram.org/bots/api#deletewebhook) method.
229pub struct DeleteWebhook {
230    client: BotClient,
231    params: DeleteWebhookParams,
232}
233
234impl DeleteWebhook {
235    pub(crate) fn new(client: BotClient) -> Self {
236        Self {
237            client,
238            params: DeleteWebhookParams::default(),
239        }
240    }
241    /// If `true`, the webhook server will remove all pending updates.
242    pub fn drop_pending_updates(mut self, v: bool) -> Self {
243        self.params.drop_pending_updates = Some(v);
244        self
245    }
246}
247
248impl IntoFuture for DeleteWebhook {
249    type Output = Result<bool>;
250    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
251    fn into_future(self) -> Self::IntoFuture {
252        Box::pin(async move { self.client.post_json("deleteWebhook", &self.params).await })
253    }
254}
255
256// ─── getWebhookInfo ───────────────────────────────────────────────────────────
257
258/// Builder for the [`getWebhookInfo`](https://core.telegram.org/bots/api#getwebhookinfo) method.
259pub struct GetWebhookInfo {
260    client: BotClient,
261}
262
263impl GetWebhookInfo {
264    pub(crate) fn new(client: BotClient) -> Self {
265        Self { client }
266    }
267}
268
269impl IntoFuture for GetWebhookInfo {
270    type Output = Result<WebhookInfo>;
271    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
272    fn into_future(self) -> Self::IntoFuture {
273        Box::pin(async move {
274            self.client
275                .post_json("getWebhookInfo", &serde_json::json!({}))
276                .await
277        })
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284    use crate::client::BotClient;
285
286    fn client() -> BotClient {
287        BotClient::from_token("123456:test-token-for-unit-tests").unwrap()
288    }
289
290    /// Without a certificate the request must stay on the JSON path, unchanged.
291    #[test]
292    fn without_certificate_the_params_serialize_as_before() {
293        let w = SetWebhook::new(client(), "https://example.com")
294            .secret_token("s3cret")
295            .max_connections(40);
296        let json = serde_json::to_value(&w.params).unwrap();
297        assert_eq!(json["url"], "https://example.com");
298        assert_eq!(json["secret_token"], "s3cret");
299        assert_eq!(json["max_connections"], 40);
300        assert!(w.certificate.is_none());
301        // The certificate is deliberately not part of the JSON body.
302        assert!(json.get("certificate").is_none());
303    }
304
305    #[test]
306    fn certificate_is_held_outside_the_json_params() {
307        let w = SetWebhook::new(client(), "https://example.com").certificate(InputFile::Bytes {
308            filename: "cert.pem".to_owned(),
309            data: b"-----BEGIN CERTIFICATE-----".to_vec(),
310            mime_type: "application/x-pem-file".to_owned(),
311        });
312        assert!(w.certificate.is_some());
313        assert!(serde_json::to_value(&w.params)
314            .unwrap()
315            .get("certificate")
316            .is_none());
317    }
318
319    /// Telegram requires the certificate to be uploaded, so a URL or file_id
320    /// reference is rejected with a clear error rather than silently ignored.
321    #[tokio::test]
322    async fn non_uploaded_certificate_is_rejected() {
323        let err = SetWebhook::new(client(), "https://example.com")
324            .certificate(InputFile::Url("https://example.com/cert.pem".to_owned()))
325            .await
326            .unwrap_err();
327        assert!(
328            matches!(err, crate::error::Error::MissingParam(m) if m.contains("must be InputFile::Bytes")),
329            "unexpected error: {err:?}"
330        );
331    }
332}