Building a Telegram bot with Rust.
Published: 2023-02-02 17:21:57

---
title: Building a Telegram bot with Rust.
published: true
date: 2022-12-30 17:00:45 UTC
tags: 
canonical_url: https://carlosmv.hashnode.dev/building-a-telegram-bot-with-rust
---

For this article, we are going to build a Telegram bot that saves URLs address with Teloxide. The idea is a little bit silly and probably uncomfortable to use, but it will be useful for learning purposes.

This is not a comprehensive tutorial about Teloxide, we will just build a bot that saves and retrieves URLs from a Hashmap.

I'm a little fan of Telegram bots because they help me to build some backends projects ideas without the need to think about a UI.

What I want to achieve with this article is to continue learning Rust and to learn a little bit about Teloxide by building this bot.

Prerequisites:

- Rust basic knowledge

- Ngrok installed

We will use ngrok to be able to use webhook from our local machine. If you don't have ngrok, you can download it from [here](https://ngrok.com/).

### What's Teloxide?

According to its [documentation](https://docs.rs/crate/teloxide/latest):

> A full-featured framework that empowers you to easily build Telegram bots using Rust. It handles all the difficult stuff so you can focus only on your business logic.
> 
> ### **Highlights:**
> 
> - **Declarative design.** `teloxide` is based upon `dptree`, a functional [chain of responsibility](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern) pattern that allows you to express pipelines of message processing in a highly declarative and extensible style.
> 
> - **Feature-rich.** You can use both long polling and webhooks, configure an underlying HTTPS client, set a custom URL of a Telegram API server, do graceful shutdown, and much more.
> 
> - **Simple dialogues.** Our dialogues subsystem is simple and easy-to-use, and, furthermore, is agnostic of how/where dialogues are stored. For example, you can just replace a one line to achieve [persistence](https://en.wikipedia.org/wiki/Persistence_(computer_science)). Out-of-the-box storages include [Redis](https://redis.io/), [RocksDB](https://rocksdb.org/) and [Sqlite](https://www.sqlite.org).
> 
> - **Strongly typed commands.** Define bot commands as an `enum` and teloxide will parse them automatically just like JSON structures in `serde-json` and command-line arguments in `structopt`.

### Cargo.toml

```
[dependencies]
teloxide = { version = "0.11", features = ["macros", "webhooks-axum"] }
log = "0.4"
pretty_env_logger = "0.4"
tokio = { version = "1.8", features = ["rt-multi-thread", "macros"] }
nanoid = "0.4"

```

### BotFather

To create a Telegram bot, we need a token.

Go to the Telegram App and enter @BotFather in the search bar.

![Screenshot-2022-12-16-092357](https://www.freecodecamp.org/news/content/images/2022/12/Screenshot-2022-12-16-092357.png)

Select `/start` to activate the bot. Then we select `/newbot` and follow the instructions to create a bot.

![Screenshot-2022-12-16-093337](https://www.freecodecamp.org/news/content/images/2022/12/Screenshot-2022-12-16-093337.png)

We have to choose a name that the users will see and a username. Then the bot will send us a message with our token in it.

### main.rs

```
use teloxide::{dispatching::update_listeners::webhooks, prelude::*, utils::command::BotCommands};

#[tokio::main]
async fn main() {
    pretty_env_logger::init();
    log::info!("Starting command bot...");

    let bot = Bot::from_env();

    let addr = ([127, 0, 0, 1], 8000).into();
    let url = "Your HTTPS ngrok URL here. Get it by `ngrok http 8000`".parse().unwrap();
    let listener = webhooks::axum(bot.clone(), webhooks::Options::new(addr, url))
        .await
        .expect("Couldn't setup webhook");

    Command::repl_with_listener(bot, answer, listener).await;

}

```

We are going to use webhooks and set three commands for this bot.

We create a `Bot` instance, it allows sending requests to the Telegram Bot API. We use `from_env()`function to read the Telegram token from the environment.

Then we define the port for our server. The value for the variable `url` will be the address we get when we initialize ngrok or if we want to deploy the bot, the address we get from the cloud service.

To initialize ngrok, we type the following command where ngrok is located, or if it is added to our PATH it will work too.

```
ngrok http 8000

```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672416554884/c1fbd3be-cc6c-47a7-bbaf-6298c21b22b9.png)

If we go to localhost:4040, we will have our web interface.

The address shows it as the Forwarding HTTPS address is what we have to type as a value for the variable `url`.

We create a webhook instance and we pass `addr` and `url` as arguments.

We are going to use commands, so we use `repl_with_listener` and pass `bot`, `answer`, and `listener` as arguments; `answer` is a handler that we will define later.

This is what the [documentation](https://docs.rs/teloxide/0.11.0/teloxide/fn.commands_repl_with_listener.html) says about `commands_repl_with_listener` :

> A [REPL](https://en.wikipedia.org/wiki/Read-eval-print_loop) for commands, with a custom [`UpdateListener`](https://docs.rs/teloxide/0.11.0/teloxide/dispatching/update_listeners/trait.UpdateListener.html).
> 
> REPLs are meant only for simple bots and rapid prototyping. If you need to supply dependencies or describe more complex dispatch logic, please use [`Dispatcher`](https://docs.rs/teloxide/0.11.0/teloxide/dispatching/struct.Dispatcher.html). See also: [Dispatching or REPLs?](https://docs.rs/teloxide/0.11.0/teloxide/dispatching/index.html#dispatching-or-repls).
> 
> All errors from the handler and update listener will be logged.
> 
> ### [**Signature**](https://docs.rs/teloxide/0.11.0/teloxide/fn.commands_repl_with_listener.html#signature)
> 
> Dont be scared by many trait bounds in the signature, in essence they require:
> 
> 1. `bot` is a bot, client for the Telegram bot API. It is represented via the [`Requester`](https://docs.rs/teloxide/0.11.0/teloxide/requests/trait.Requester.html) trait.
> 
> 2. `handler` is an `async` function that takes arguments from [`DependencyMap`](https://docs.rs/teloxide/0.11.0/teloxide/prelude/struct.DependencyMap.html) (see below) and returns [`ResponseResult`](https://docs.rs/teloxide/0.11.0/teloxide/requests/type.ResponseResult.html).
> 
> 3. `listener` is something that takes updates from a Telegram server and implements [`UpdateListener`](https://docs.rs/teloxide/0.11.0/teloxide/dispatching/update_listeners/trait.UpdateListener.html).
> 
> 4. `cmd` is a type hint for your command enumeration `MyCommand`: just write `MyCommand::ty()`. Note that `MyCommand` must implement the [`BotCommands`](https://docs.rs/teloxide/0.11.0/teloxide/utils/command/trait.BotCommands.html) trait, typically via `#[derive(BotCommands)]`.
> 
> All the other requirements are about thread safety and data validity and can be ignored for most of the time.

```
#[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase", description = "These commands are supported:")]
enum Command {
    #[command(description = "display this text.")]
    Help,
    #[command(description = "Use this command to save a URL")]
    Save(String),
    #[command(description = "handle user's chat ID")]
    ChatId,

}

```

Here we define our bot's commands. The `Help` command will display the list of commands. The `Save` command will save a URL, and the `ChatId` command returns the user's chat ID.

```
async fn answer(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> {
    let chat_id = msg.chat.id;

    match cmd {
        Command::Help => bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?,
        Command::ChatId => {
            bot.send_message(msg.chat.id, format!("Your chat ID is {chat_id}")).await?
        }
        Command::Save(text) => {
            bot.send_message(msg.chat.id, format!("The URL you want to save is: {text}")).await?
        }
    };

    Ok(())
}

```

The `answer` function will handle the answers for every command. If the `/help` command is received, it will send the description of the commands. If `/chatid` is received it will send the message: "Your chat Id is {chat\_id}". If `/url` is received, it will send the message: "The URL you want to save is: {url}".

According to its documentation:

> ### [**Handler arguments**](https://docs.rs/teloxide/0.11.0/teloxide/fn.commands_repl_with_listener.html#handler-arguments)
> 
> `teloxide` provides the following types to the `handler`:
> 
> - [`Message`](https://docs.rs/teloxide/0.11.0/teloxide/types/struct.Message.html)
> 
> - `R` (type of the `bot`)
> 
> - `Cmd` (type of the parsed command)
> 
> - [`Me`](https://docs.rs/teloxide/0.11.0/teloxide/types/struct.Me.html)
> 
> Each of these types can be accepted as a handler parameter. Note that they arent all required at the same time: e.g., you can take only the bot and the command without [`Me`](https://docs.rs/teloxide/0.11.0/teloxide/types/struct.Me.html) and [`Message`](https://docs.rs/teloxide/0.11.0/teloxide/types/struct.Message.html).

It will look like this when we start our bot:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672089250300/843c3d11-1793-4ce9-ab07-0c539b155bda.png)

Before we start our bot, we need to set the token:

```
# Unix-like
$ export TELOXIDE_TOKEN=<Your token here>

# Windows command line
$ set TELOXIDE_TOKEN=<Your token here>

# Windows PowerShell
$ $env:TELOXIDE_TOKEN=<Your token here>

```

Then we type in our command line:

```
cargo run

```

Now, we will implement the handlers to save and retrieve the URLs.

```
fn save_url(url: String) -> String {
    let new_id = &nanoid::nanoid!(6).to_string();

    let _url = url;

    format!("URL saved, the ID is {}", new_id)

}

```

Here the function receives a URL and generates an `id` string. Then it returns the `id` .

We need to add the `save_url` handler to the `answer` function.

```
async fn answer(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> {
    let chat_id = msg.chat.id;

    match cmd {
        Command::Help => bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?,
        Command::ChatId => {
            bot.send_message(msg.chat.id, format!("Your chat ID is {chat_id}")).await?
        }
        Command::Save(text) => {
            bot.send_message(msg.chat.id, save_url(text).to_string()).await?
        }
    };

    Ok(())
}

```

Now when we use the `/save` command and provide a URL, it will return an ID.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672277758727/8c8b1b88-f4e5-41f0-a79e-abfc705f1fa3.png)

But this is not enough. We need something to store the URLs to retrieve them later.

To make it simple, we will store the URLs in a HashMap. We create a new file to define a model and the HashMap.

data.rs

```
use once_cell::sync::Lazy;

use std::sync::Mutex;
use std::collections::HashMap;

//use serde_json::json;

#[derive(Debug,Clone, Eq, Hash, PartialEq)]
pub struct StoredURL {
    pub id: String,
    pub https_address: String,
}

impl std::fmt::Display for StoredURL {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(f, "{}", self.https_address)
    }
}

pub static DATA: Lazy<Mutex<HashMap<String, String>>> = Lazy::new(|| Mutex::new(
    HashMap::from([

    ])
));

```

Here we define StoredURL with the attributes `id` and `https_addres`. We implement Display for StoredURL to use the `format!` macro. Then we create a HashMap to store our URLs and be able to retrieve them by their ID.

Now we change our `save_url` handler to store the URLs.

```
fn save_url(url: String) -> String {
    let new_id = &nanoid::nanoid!(6).to_string();  

    let new_url= StoredURL{id:new_id.clone(), https_address:url};

    let mut data = DATA.lock().unwrap();

    data.insert(new_url.id, new_url.https_address);

    format!("URL saved, the ID is {}", new_id)

}

```

Now we add a command to retrieve a URL stored, we go back to the `main.rs` file.

```
#[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase", description = "These commands are supported:")]
enum Command {
    #[command(description = "display this text.")]
    Help,
    #[command(description = "Use this command to save a URL")]
    Save(String),
    #[command(description = "Use this command to retrieve a URL with its ID")]
    Get(String),
    #[command(description = "handle user's chat ID")]
    ChatId,

}

```

We will use the `/get` command when we want to retrieve a URL. Now we need a handler to do that job.

```
pub fn get_url(id:String) -> String {  

    let data = DATA.lock().unwrap();

    let url = data.get(&id);

     format!("{:?}", url)   
}

```

With `get_url` we will be able to retrieve any URL stored in `DATA` by its ID.

Now we add `get_url` handler to the `answer` function.

```
async fn answer(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> {
    let chat_id = msg.chat.id;

    match cmd {
        Command::Help => bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?,
        Command::ChatId => {
            bot.send_message(msg.chat.id, format!("Your chat ID is {chat_id}")).await?
        }
        Command::Save(text) => {
            bot.send_message(msg.chat.id, save_url(text)).await?
        }
        Command::Get(text) => {
            bot.send_message(msg.chat.id, get_url(text)).await?
        }
    };

    Ok(())
}

```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672278364286/1971c9ca-834b-46a5-9640-608a4b261779.png)

Let's change the code a little bit so the handler only returns the URL address.

```
pub fn get_url(id:String) -> String {

    let data = DATA.lock().unwrap()

    match data.get(&id) {
        Some(value) => format!("{}",value.to_string()),
        None => format!("There is not URL with this ID")

}

```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672325579135/9d49203a-2b64-464c-9470-457b1d9b2b9d.png)

### Conclusion

It was interesting and fun for me to build this bot. At first, I felt overwhelmed when trying to use Teloxide, it has a lot of features, but the documentation and the examples on its Github page were really helpful.

I tried to make this bot as simple as possible. But it will be good to add a database and a way to verify that the string passed is a URL before the bot saves it. A command to retrieve all the URLs saved and a command to delete a URL by its ID.

Thank you for taking the time to read this article.

If you have any recommendations about other packages, architectures, how to improve my code, my English, or anything; please leave a comment or contact me through [**Twitter**](https://twitter.com/Carlos_marcv), or [**LinkedIn**](https://www.linkedin.com/in/carlos-marcano-a2135a134/).

The source code is [**here**](https://github.com/carlosm27/url_shortener_bot)

### References

[Teloxide's GitHub Page](https://github.com/teloxide/teloxide)

[Teloxide Documentation](https://docs.rs/crate/teloxide/latest)

[ngrok Documentation](https://ngrok.com/docs)

[Once\_cell Documentation](https://crates.io/crates/once_cell)

[How to Create a Telegram Bot using Python](https://www.freecodecamp.org/news/how-to-create-a-telegram-bot-using-python/)