use std::future::{Future, IntoFuture};
use std::pin::Pin;
use reqwest::multipart::{Form, Part};
use serde::Serialize;
use rustigram_types::update::Update;
use rustigram_types::webhook::WebhookInfo;
use rustigram_types::file::InputFile;
use crate::client::BotClient;
use crate::error::Result;
#[derive(Serialize, Default)]
pub struct GetUpdatesParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allowed_updates: Option<Vec<String>>,
}
pub struct GetUpdates {
client: BotClient,
params: GetUpdatesParams,
}
impl GetUpdates {
pub(crate) fn new(client: BotClient) -> Self {
Self {
client,
params: GetUpdatesParams::default(),
}
}
pub fn offset(mut self, offset: i64) -> Self {
self.params.offset = Some(offset);
self
}
pub fn limit(mut self, limit: u8) -> Self {
self.params.limit = Some(limit.clamp(1, 100));
self
}
pub fn timeout(mut self, secs: u32) -> Self {
self.params.timeout = Some(secs);
self
}
pub fn allowed_updates(mut self, types: Vec<impl Into<String>>) -> Self {
self.params.allowed_updates = Some(types.into_iter().map(Into::into).collect());
self
}
}
impl IntoFuture for GetUpdates {
type Output = Result<Vec<Update>>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move { self.client.post_json("getUpdates", &self.params).await })
}
}
#[derive(Serialize)]
pub struct SetWebhookParams {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
ip_address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
max_connections: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
allowed_updates: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
drop_pending_updates: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
secret_token: Option<String>,
}
pub struct SetWebhook {
client: BotClient,
params: SetWebhookParams,
certificate: Option<InputFile>,
}
impl SetWebhook {
pub(crate) fn new(client: BotClient, url: impl Into<String>) -> Self {
Self {
client,
params: SetWebhookParams {
url: url.into(),
ip_address: None,
max_connections: None,
allowed_updates: None,
drop_pending_updates: None,
secret_token: None,
},
certificate: None,
}
}
pub fn certificate(mut self, certificate: InputFile) -> Self {
self.certificate = Some(certificate);
self
}
pub fn ip_address(mut self, ip: impl Into<String>) -> Self {
self.params.ip_address = Some(ip.into());
self
}
pub fn max_connections(mut self, n: u8) -> Self {
self.params.max_connections = Some(n.clamp(1, 100));
self
}
pub fn allowed_updates(mut self, types: Vec<impl Into<String>>) -> Self {
self.params.allowed_updates = Some(types.into_iter().map(Into::into).collect());
self
}
pub fn drop_pending_updates(mut self, v: bool) -> Self {
self.params.drop_pending_updates = Some(v);
self
}
pub fn secret_token(mut self, token: impl Into<String>) -> Self {
self.params.secret_token = Some(token.into());
self
}
}
impl IntoFuture for SetWebhook {
type Output = Result<bool>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let Some(certificate) = self.certificate else {
return self.client.post_json("setWebhook", &self.params).await;
};
let InputFile::Bytes {
filename,
data,
mime_type,
} = certificate
else {
return Err(crate::error::Error::MissingParam(
"setWebhook certificate must be InputFile::Bytes — Telegram \
requires the certificate to be uploaded, not referenced",
));
};
let part = Part::bytes(data)
.file_name(filename)
.mime_str(&mime_type)
.map_err(|e| crate::error::Error::Decode(e.to_string()))?;
let p = &self.params;
let mut form = Form::new()
.part("certificate", part)
.text("url", p.url.clone());
if let Some(v) = &p.ip_address {
form = form.text("ip_address", v.clone());
}
if let Some(v) = p.max_connections {
form = form.text("max_connections", v.to_string());
}
if let Some(v) = &p.allowed_updates {
if let Ok(json) = serde_json::to_string(v) {
form = form.text("allowed_updates", json);
}
}
if let Some(v) = p.drop_pending_updates {
form = form.text("drop_pending_updates", v.to_string());
}
if let Some(v) = &p.secret_token {
form = form.text("secret_token", v.clone());
}
self.client.post_multipart("setWebhook", form).await
})
}
}
#[derive(Serialize, Default)]
struct DeleteWebhookParams {
#[serde(skip_serializing_if = "Option::is_none")]
drop_pending_updates: Option<bool>,
}
pub struct DeleteWebhook {
client: BotClient,
params: DeleteWebhookParams,
}
impl DeleteWebhook {
pub(crate) fn new(client: BotClient) -> Self {
Self {
client,
params: DeleteWebhookParams::default(),
}
}
pub fn drop_pending_updates(mut self, v: bool) -> Self {
self.params.drop_pending_updates = Some(v);
self
}
}
impl IntoFuture for DeleteWebhook {
type Output = Result<bool>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move { self.client.post_json("deleteWebhook", &self.params).await })
}
}
pub struct GetWebhookInfo {
client: BotClient,
}
impl GetWebhookInfo {
pub(crate) fn new(client: BotClient) -> Self {
Self { client }
}
}
impl IntoFuture for GetWebhookInfo {
type Output = Result<WebhookInfo>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
self.client
.post_json("getWebhookInfo", &serde_json::json!({}))
.await
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::BotClient;
fn client() -> BotClient {
BotClient::from_token("123456:test-token-for-unit-tests").unwrap()
}
#[test]
fn without_certificate_the_params_serialize_as_before() {
let w = SetWebhook::new(client(), "https://example.com")
.secret_token("s3cret")
.max_connections(40);
let json = serde_json::to_value(&w.params).unwrap();
assert_eq!(json["url"], "https://example.com");
assert_eq!(json["secret_token"], "s3cret");
assert_eq!(json["max_connections"], 40);
assert!(w.certificate.is_none());
assert!(json.get("certificate").is_none());
}
#[test]
fn certificate_is_held_outside_the_json_params() {
let w = SetWebhook::new(client(), "https://example.com").certificate(InputFile::Bytes {
filename: "cert.pem".to_owned(),
data: b"-----BEGIN CERTIFICATE-----".to_vec(),
mime_type: "application/x-pem-file".to_owned(),
});
assert!(w.certificate.is_some());
assert!(serde_json::to_value(&w.params)
.unwrap()
.get("certificate")
.is_none());
}
#[tokio::test]
async fn non_uploaded_certificate_is_rejected() {
let err = SetWebhook::new(client(), "https://example.com")
.certificate(InputFile::Url("https://example.com/cert.pem".to_owned()))
.await
.unwrap_err();
assert!(
matches!(err, crate::error::Error::MissingParam(m) if m.contains("must be InputFile::Bytes")),
"unexpected error: {err:?}"
);
}
}