rustigram_api/methods/
updates.rs1use 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#[derive(Serialize, Default)]
18pub struct GetUpdatesParams {
20 #[serde(skip_serializing_if = "Option::is_none")]
22 pub offset: Option<i64>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub limit: Option<u8>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub timeout: Option<u32>,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub allowed_updates: Option<Vec<String>>,
32}
33
34pub 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 pub fn offset(mut self, offset: i64) -> Self {
51 self.params.offset = Some(offset);
52 self
53 }
54
55 pub fn limit(mut self, limit: u8) -> Self {
57 self.params.limit = Some(limit.clamp(1, 100));
58 self
59 }
60
61 pub fn timeout(mut self, secs: u32) -> Self {
63 self.params.timeout = Some(secs);
64 self
65 }
66
67 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#[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
101pub struct SetWebhook {
103 client: BotClient,
104 params: SetWebhookParams,
105 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 pub fn certificate(mut self, certificate: InputFile) -> Self {
136 self.certificate = Some(certificate);
137 self
138 }
139 pub fn ip_address(mut self, ip: impl Into<String>) -> Self {
141 self.params.ip_address = Some(ip.into());
142 self
143 }
144 pub fn max_connections(mut self, n: u8) -> Self {
146 self.params.max_connections = Some(n.clamp(1, 100));
147 self
148 }
149 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 pub fn drop_pending_updates(mut self, v: bool) -> Self {
156 self.params.drop_pending_updates = Some(v);
157 self
158 }
159 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#[derive(Serialize, Default)]
223struct DeleteWebhookParams {
224 #[serde(skip_serializing_if = "Option::is_none")]
225 drop_pending_updates: Option<bool>,
226}
227
228pub 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 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
256pub 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 #[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 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 #[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}