Skip to main content

ferobot/
client_sync.rs

1// Copyright (c) Ankit Chaubey <ankitchaubey.dev@gmail.com>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3//
4// ferobot: async Telegram Bot API framework written in Rust
5// Repository: https://github.com/ankit-chaubey/ferobot
6//
7// Ferobot provides a fast and ergonomic framework for building Telegram bots
8// using the official Telegram Bot API.
9//
10// Author: Ankit Chaubey
11//
12// If you use or modify this code, keep this notice at the top of your file
13// and include the LICENSE-MIT or LICENSE-APACHE file from this repository.
14
15//! Synchronous `ureq`-backed HTTP client (item 23).
16//!
17//! Enable with the `client-ureq` Cargo feature:
18//!
19//! ```toml
20//! ferobot = { version = "0.3", features = ["client-ureq"] }
21//! ```
22//!
23//! # Example
24//!
25//! ```rust,no_run
26//! use ferobot::client_sync::SyncBot;
27//!
28//! fn main() {
29//!     let bot = SyncBot::new("YOUR_TOKEN");
30//!     let me = bot.call_sync("getMe", serde_json::Value::Null).unwrap();
31//!     println!("{me}");
32//! }
33//! ```
34
35use serde_json::Value;
36use ureq::Agent;
37
38use crate::BotError;
39
40/// A synchronous, blocking Telegram bot client backed by `ureq`.
41///
42/// This wraps the raw JSON API; all methods are blocking and return
43/// `Result<serde_json::Value, BotError>`. Use the async [`Bot`](crate::Bot)
44/// for production bots; `SyncBot` is best for scripts and CLI tools.
45#[derive(Debug, Clone)]
46pub struct SyncBot {
47    api_url: String,
48    agent: Agent,
49}
50
51impl SyncBot {
52    /// Create a `SyncBot` using the default Telegram API URL.
53    pub fn new(token: &str) -> Self {
54        Self::with_url(token, "https://api.telegram.org")
55    }
56
57    /// Create a `SyncBot` with a custom API URL (e.g. a local Bot API server).
58    pub fn with_url(token: &str, base_url: &str) -> Self {
59        let agent = ureq::AgentBuilder::new()
60            .timeout(std::time::Duration::from_secs(60))
61            .build();
62        Self {
63            api_url: format!("{}/bot{}", base_url.trim_end_matches('/'), token),
64            agent,
65        }
66    }
67
68    /// Call any Telegram Bot API method synchronously.
69    ///
70    /// - `method`: e.g. `"sendMessage"`.
71    /// - `params`: a JSON value; pass `Value::Null` for methods with no params.
72    pub fn call_sync(&self, method: &str, params: Value) -> Result<Value, BotError> {
73        let url = format!("{}/{}", self.api_url, method);
74
75        let response: Value = if params.is_null() {
76            self.agent
77                .post(&url)
78                .call()
79                .map_err(|e| BotError::Other(e.to_string()))?
80                .into_json()
81                .map_err(|e| BotError::Other(e.to_string()))?
82        } else {
83            self.agent
84                .post(&url)
85                .send_json(params)
86                .map_err(|e| BotError::Other(e.to_string()))?
87                .into_json()
88                .map_err(|e| BotError::Other(e.to_string()))?
89        };
90
91        if response["ok"].as_bool().unwrap_or(false) {
92            Ok(response["result"].clone())
93        } else {
94            let code = response["error_code"].as_i64().unwrap_or(0);
95            let description = response["description"]
96                .as_str()
97                .unwrap_or("unknown error")
98                .to_string();
99            let retry_after = response["parameters"]["retry_after"].as_i64();
100            let migrate_to_chat_id = response["parameters"]["migrate_to_chat_id"].as_i64();
101            Err(BotError::Api {
102                code,
103                description,
104                retry_after,
105                migrate_to_chat_id,
106            })
107        }
108    }
109}