Use Telegram bot to receive 2FA verification code (OTP) with the help of Copilot
Published: 2024-06-29 02:49:36

One week ago, I saw a post on RustDesk subreddit, https://www.reddit.com/r/rustdesk/comments/1dk4mzp/how_can_i_be_notified_track_when_someone_connects/. It gave me hint to send the notification to Telegram bot, because I had been using it to receive news and signals while I traded FX before. This also inspired to me to receive OTP through Telegram bot.


![Talk is cheap, show me the code.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/veslny7fs03famf006yb.png)



So I started to ask Copilot.

- "Please write a function to send OTP to telegram bot"

```
async fn send_otp_to_telegram(bot_token: &str, chat_id: &str, otp: &str) -> Result<()> {
    let client = Client::new();
    let url = format!("https://api.telegram.org/bot{}/sendMessage", bot_token);
    let payload = json!({
        "chat_id": chat_id,
        "text": otp
    });

    let response = client.post(&url)
        .json(&payload)
        .send()
        .await?;

    if response.status().is_success() {
        Ok(())
    } else {
        Err(anyhow!("Failed to send OTP to Telegram: {}", response.status()))
    }
}
```

- "Please write a function to send OTP to telegram bot, use reqwest"

```
use reqwest::Client;
use serde_json::json;
use std::error::Error;

async fn send_otp_to_telegram(otp: &str, bot_token: &str, chat_id: &str) -> Result<(), Box<dyn Error>> {
    let client = Client::new();
    let telegram_api_url = format!("https://api.telegram.org/bot{}/sendMessage", bot_token);
    let message = format!("Your OTP is: {}", otp);

    let response = client.post(&telegram_api_url)
        .json(&json!({
            "chat_id": chat_id,
            "text": message,
        }))
        .send()
        .await?;

    if response.status().is_success() {
        Ok(())
    } else {
        Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "Failed to send OTP")))
    }
}
```

- "How can I get telegram bot chat id with reqwest?"

```
async fn fetch_telegram_chat_id(bot_token: &str) -> Result<Vec<i64>, Box<dyn Error>> {
    let client = Client::new();
    let url = format!("https://api.telegram.org/bot{}/getUpdates", bot_token);

    let res = client.get(&url).send().await?;
    let body = res.text().await?;
    let json: Value = serde_json::from_str(&body)?;

    let mut chat_ids = Vec::new();
    if let Some(updates) = json["result"].as_array() {
        for update in updates {
            if let Some(chat_id) = update["message"]["chat"]["id"].as_i64() {
                chat_ids.push(chat_id);
            }
        }
    }

    Ok(chat_ids)
}
```

After some polish, I finished my job, 

- Rust
https://github.com/rustdesk/rustdesk/blob/9f72d05749a88bffa577784099d97374c4276ee3/src/auth_2fa.rs#L158
```
// https://gist.github.com/dideler/85de4d64f66c1966788c1b2304b9caf1
pub async fn send_2fa_code_to_telegram(text: &str, bot: TelegramBot) -> ResultType<()> {
    let url = format!("https://api.telegram.org/bot{}/sendMessage", bot.token_str);
    let params = serde_json::json!({"chat_id": bot.chat_id, "text": text});
    crate::post_request(url, params.to_string(), "").await?;
    Ok(())
}

pub fn get_chatid_telegram(bot_token: &str) -> ResultType<Option<String>> {
    let url = format!("https://api.telegram.org/bot{}/getUpdates", bot_token);
    // because caller is in tokio runtime, so we must call post_request_sync in new thread.
    let handle = std::thread::spawn(move || {
        crate::post_request_sync(url, "".to_owned(), "")
    });
    let resp = handle.join().map_err(|_| anyhow!("Thread panicked"))??;
    let value = serde_json::from_str::<serde_json::Value>(&resp).map_err(|e| anyhow!(e))?;

    // Check for an error_code in the response
    if let Some(error_code) = value.get("error_code").and_then(|code| code.as_i64()) {
        // If there's an error_code, try to use the description for the error message
        let description = value["description"]
            .as_str()
            .unwrap_or("Unknown error occurred");
        return Err(anyhow!(
            "Telegram API error: {} (error_code: {})",
            description,
            error_code
        ));
    }

    let chat_id = &value["result"][0]["message"]["chat"]["id"];
    let chat_id = if let Some(id) = chat_id.as_i64() {
        Some(id.to_string())
    } else if let Some(id) = chat_id.as_str() {
        Some(id.to_owned())
    } else {
        None
    };

    if let Some(chat_id) = chat_id.as_ref() {
        let bot = TelegramBot {
            token_str: bot_token.to_owned(),
            chat_id: chat_id.to_owned(),
            ..Default::default()
        };
        bot.save()?;
    }

    Ok(chat_id)
}
```

- Flutter UI
https://github.com/rustdesk/rustdesk/blob/a632718e80649f2be8a1a7cc1df20a6056b119b9/flutter/lib/common/widgets/dialog.dart#L1769

```
void changeBot({Function()? callback}) async {
  if (bind.mainHasValidBotSync()) {
    await bind.mainSetOption(key: "bot", value: "");
    callback?.call();
    return;
  }
  String errorText = '';
  bool loading = false;
  final controller = TextEditingController();
  gFFI.dialogManager.show((setState, close, context) {
    onVerify() async {
      final token = controller.text.trim();
      if (token == "") return;
      loading = true;
      errorText = '';
      setState(() {});
      final error = await bind.mainVerifyBot(token: token);
      if (error == "") {
        callback?.call();
        close();
      } else {
        errorText = translate(error);
        loading = false;
        setState(() {});
      }
    }

    final codeField = TextField(
      autofocus: true,
      controller: controller,
      decoration: InputDecoration(
        hintText: translate('Token'), 
      ),
    );

    return CustomAlertDialog(
      title: Text(translate("Telegram bot")),
      content: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          SelectableText(translate("enable-bot-desc"),
                  style: TextStyle(fontSize: 12))
              .marginOnly(bottom: 12),
          Row(children: [Expanded(child: codeField)]),
          if (errorText != '')
            Text(errorText, style: TextStyle(color: Colors.red))
                .marginOnly(top: 12),
        ],
      ),
      actions: [
        dialogButton("Cancel", onPressed: close, isOutline: true),
        loading
            ? CircularProgressIndicator()
            : dialogButton("OK", onPressed: onVerify),
      ],
      onCancel: close,
    );
  });
}
```


![The UI to set bot token and also get chat id implicitly](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/21d02vaxp6an9ojhumn5.png)

Thanks to Copilot.