First commit
This commit is contained in:
parent
5efb718d83
commit
d55fa3f4fa
7 changed files with 2960 additions and 2 deletions
2460
Cargo.lock
generated
Normal file
2460
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
27
Cargo.toml
Normal file
27
Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
authors = ["TheAwiteb <awiteb@hotmail.com>"]
|
||||||
|
description = "Simple API to ping a telegram bot using superbot (mtproto)"
|
||||||
|
edition = "2021"
|
||||||
|
license = "AGPL-3.0-or-later"
|
||||||
|
name = "telepingbot"
|
||||||
|
readme = "README.md"
|
||||||
|
repository = "https://github.com/TheAwiteb/telepingbot"
|
||||||
|
rust-version = "1.68.2"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = "0.4.31"
|
||||||
|
dotenv = "0.15.0"
|
||||||
|
grammers-client = "= 0.4.0"
|
||||||
|
grammers-session = "= 0.4.0"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
log = "0.4.20"
|
||||||
|
pretty_env_logger = "0.5.0"
|
||||||
|
promptly = "0.3.1"
|
||||||
|
salvo = {version = "0.58.3", features = ["logging", "affix"]}
|
||||||
|
serde = {version = "1.0.192", features = ["derive"]}
|
||||||
|
serde_json = "1.0.108"
|
||||||
|
sha256 = "1.4.0"
|
||||||
|
tokio = {version = "1.34.0", features = ["macros", "rt-multi-thread", "signal"]}
|
186
src/api.rs
Normal file
186
src/api.rs
Normal file
|
@ -0,0 +1,186 @@
|
||||||
|
// A simple API to ping telegram bots and returns if it's online or not.
|
||||||
|
// Copyright (C) 2023 Awiteb <awitb@hotmail.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published
|
||||||
|
// by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use salvo::{catcher::Catcher, http::HeaderValue, hyper::header, logging::Logger, prelude::*};
|
||||||
|
|
||||||
|
use crate::PingList;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct AppState {
|
||||||
|
/// Clean text bot usernames
|
||||||
|
pub bots: Vec<String>,
|
||||||
|
/// Sha256 tokens
|
||||||
|
pub tokens: Vec<String>,
|
||||||
|
/// The telegram clinet
|
||||||
|
tg_client: grammers_client::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
struct MessageSchema<'a> {
|
||||||
|
message: &'a str,
|
||||||
|
status: bool,
|
||||||
|
#[serde(skip)]
|
||||||
|
status_code: StatusCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
/// Create new [`AppState`] instance from clean bots and tokens
|
||||||
|
pub(crate) fn new(
|
||||||
|
bots: Vec<String>,
|
||||||
|
tokens: Vec<String>,
|
||||||
|
client: grammers_client::Client,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
bots: bots
|
||||||
|
.into_iter()
|
||||||
|
.map(|b| b.trim_start_matches('@').trim().to_lowercase())
|
||||||
|
.collect(),
|
||||||
|
tokens: tokens
|
||||||
|
.into_iter()
|
||||||
|
.map(|t| sha256::digest(t.trim()))
|
||||||
|
.collect(),
|
||||||
|
tg_client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MessageSchema<'a> {
|
||||||
|
/// Create new [`Message`] instance with `200 OK` status
|
||||||
|
fn new(message: &'a str) -> Self {
|
||||||
|
Self {
|
||||||
|
message,
|
||||||
|
status: true,
|
||||||
|
status_code: StatusCode::OK,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the status code and status
|
||||||
|
fn code(mut self, status_code: StatusCode) -> Self {
|
||||||
|
self.status = status_code.is_success();
|
||||||
|
self.status_code = status_code;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_json_body(res: &mut Response, json_body: impl serde::Serialize) {
|
||||||
|
res.write_body(serde_json::to_string(&json_body).unwrap())
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
async fn ping(req: &Request, res: &mut Response, depot: &mut Depot) {
|
||||||
|
let bot_username = req.param::<String>("bot_username").unwrap().to_lowercase();
|
||||||
|
let app_state = depot.obtain::<Arc<AppState>>().unwrap();
|
||||||
|
|
||||||
|
let msg = if !app_state.bots.contains(&bot_username) {
|
||||||
|
MessageSchema::new("Is not authorized to check the status of this bot")
|
||||||
|
.code(StatusCode::BAD_REQUEST)
|
||||||
|
} else if let Ok(telegram_id) =
|
||||||
|
crate::superbot::send_start(&app_state.tg_client, &bot_username).await
|
||||||
|
{
|
||||||
|
if crate::PINGED_BOTS.check(telegram_id) {
|
||||||
|
MessageSchema::new("Alive")
|
||||||
|
} else {
|
||||||
|
MessageSchema::new("No response from the bot").code(StatusCode::NOT_FOUND)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
MessageSchema::new("Cant send to the bot").code(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
};
|
||||||
|
res.status_code(msg.status_code);
|
||||||
|
write_json_body(res, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
async fn handle404(res: &mut Response, ctrl: &mut FlowCtrl) {
|
||||||
|
if let Some(StatusCode::NOT_FOUND) = res.status_code {
|
||||||
|
write_json_body(
|
||||||
|
res,
|
||||||
|
MessageSchema::new("Not Found").code(StatusCode::NOT_FOUND),
|
||||||
|
);
|
||||||
|
ctrl.skip_rest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
async fn handle_server_errors(res: &mut Response, ctrl: &mut FlowCtrl) {
|
||||||
|
if matches!(res.status_code, Some(status) if status.is_server_error()) {
|
||||||
|
write_json_body(
|
||||||
|
res,
|
||||||
|
MessageSchema::new("Server Error").code(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
);
|
||||||
|
ctrl.skip_rest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
async fn auth(req: &Request, res: &mut Response, depot: &mut Depot, ctrl: &mut FlowCtrl) {
|
||||||
|
let app_state = depot.obtain::<Arc<AppState>>().unwrap();
|
||||||
|
log::info!("New auth request");
|
||||||
|
if let Some(token) = req.headers().get("Authorization") {
|
||||||
|
if let Ok(token) = token.to_str() {
|
||||||
|
if app_state.tokens.contains(&sha256::digest(token.trim())) {
|
||||||
|
log::info!("The token is authorized");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
log::info!("Unauthorized token");
|
||||||
|
write_json_body(
|
||||||
|
res,
|
||||||
|
MessageSchema::new("Unauthorized").code(StatusCode::FORBIDDEN),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::info!("Invalid token value");
|
||||||
|
write_json_body(
|
||||||
|
res,
|
||||||
|
MessageSchema::new("Invalid token value").code(StatusCode::BAD_REQUEST),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::info!("Missing `Authorization` header");
|
||||||
|
write_json_body(
|
||||||
|
res,
|
||||||
|
MessageSchema::new("Missing `Authorization` header").code(StatusCode::FORBIDDEN),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ctrl.skip_rest();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
async fn add_server_headers(res: &mut Response) {
|
||||||
|
let headers = res.headers_mut();
|
||||||
|
headers.insert(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static("application/json"),
|
||||||
|
);
|
||||||
|
// Yeah, Rusty programmer
|
||||||
|
headers.insert("X-Powered-By", HeaderValue::from_static("Rust/Salvo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn service(app_state: AppState) -> Service {
|
||||||
|
let router = Router::new()
|
||||||
|
.hoop(Logger::new())
|
||||||
|
.hoop(affix::inject(Arc::new(app_state)))
|
||||||
|
.hoop(add_server_headers)
|
||||||
|
.hoop(auth)
|
||||||
|
.push(Router::with_path("ping/@<bot_username>").get(ping));
|
||||||
|
Service::new(router).catcher(
|
||||||
|
Catcher::default()
|
||||||
|
.hoop(handle404)
|
||||||
|
.hoop(handle_server_errors),
|
||||||
|
)
|
||||||
|
}
|
176
src/main.rs
Normal file
176
src/main.rs
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
// A simple API to ping telegram bots and returns if it's online or not.
|
||||||
|
// Copyright (C) 2023 Awiteb <awitb@hotmail.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published
|
||||||
|
// by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use std::{env, fs, sync::Mutex};
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use salvo::Listener;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
mod superbot;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub(crate) struct PingedBot {
|
||||||
|
telegram_id: u64,
|
||||||
|
ping_in: i64,
|
||||||
|
is_response: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait PingList {
|
||||||
|
fn clear_outdead(&self);
|
||||||
|
fn add_new(&self, telegram_id: u64);
|
||||||
|
fn check(&self, telegram_id: u64) -> bool;
|
||||||
|
fn new_res(&self, telegram_id: u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PingList for Mutex<Vec<PingedBot>> {
|
||||||
|
fn clear_outdead(&self) {
|
||||||
|
log::info!("Clear the dead pings");
|
||||||
|
let dead_time = chrono::Utc::now().timestamp() - 60;
|
||||||
|
let mut bots = self.lock().unwrap();
|
||||||
|
*bots = bots
|
||||||
|
.iter()
|
||||||
|
.filter(|b| b.ping_in > dead_time)
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_new(&self, telegram_id: u64) {
|
||||||
|
log::debug!("Adding new bot to the list: {telegram_id}");
|
||||||
|
self.lock().unwrap().push(PingedBot::new(telegram_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(&self, telegram_id: u64) -> bool {
|
||||||
|
log::debug!("Checking the {telegram_id} if is response");
|
||||||
|
self.clear_outdead();
|
||||||
|
let result = self
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.any(|b| b.telegram_id == telegram_id && b.is_response);
|
||||||
|
log::debug!("Response status: {result}");
|
||||||
|
result
|
||||||
|
}
|
||||||
|
fn new_res(&self, telegram_id: u64) {
|
||||||
|
log::debug!("New res from: {telegram_id}");
|
||||||
|
let mut bots = self.lock().unwrap();
|
||||||
|
*bots = bots
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|b| {
|
||||||
|
if b.telegram_id == telegram_id {
|
||||||
|
log::info!("Found the sender in the list");
|
||||||
|
b.new_res()
|
||||||
|
} else {
|
||||||
|
b
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PingedBot {
|
||||||
|
pub(crate) fn new(telegram_id: u64) -> Self {
|
||||||
|
Self {
|
||||||
|
telegram_id,
|
||||||
|
ping_in: chrono::Utc::now().timestamp(),
|
||||||
|
is_response: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn new_res(mut self) -> Self {
|
||||||
|
self.is_response = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref PINGED_BOTS: Mutex<Vec<PingedBot>> = Mutex::new(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
pretty_env_logger::init();
|
||||||
|
dotenv::dotenv().ok();
|
||||||
|
log::info!("Starting the API");
|
||||||
|
|
||||||
|
let bots: Vec<String> = fs::read_to_string("bots.txt")?
|
||||||
|
.lines()
|
||||||
|
.map(|b| b.trim().to_owned())
|
||||||
|
.collect();
|
||||||
|
let tokens: Vec<String> = fs::read_to_string("tokens.txt")?
|
||||||
|
.lines()
|
||||||
|
.map(|b| b.trim().to_owned())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if bots
|
||||||
|
.iter()
|
||||||
|
.any(|b| !b.starts_with('@') || !b.to_lowercase().ends_with("bot"))
|
||||||
|
{
|
||||||
|
bots.iter().for_each(|b| {
|
||||||
|
if !b.starts_with('@') {
|
||||||
|
eprintln!("Invalid bot username `{b}`: must starts with `@`");
|
||||||
|
} else if !b.to_lowercase().ends_with("bot") {
|
||||||
|
eprintln!("Invalid bot username `{b}`: must end with `bot`");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let (client, sign_out) = superbot::login(
|
||||||
|
env::var("TELEPINGBOT_API_HASH")
|
||||||
|
.expect("`TELEPINGBOT_API_HASH` environment variable is required"),
|
||||||
|
env::var("TELEPINGBOT_API_ID")
|
||||||
|
.expect("`TELEPINGBOT_API_ID` environment variable is required")
|
||||||
|
.parse()
|
||||||
|
.expect("Invalid value for `TELEPINGBOT_API_ID` must be a number"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let host = env::var("TELEOINGBOT_HOST")
|
||||||
|
.expect("`TELEOINGBOT_HOST` environment variable must be set");
|
||||||
|
let port = env::var("TELEOINGBOT_PORT")
|
||||||
|
.expect("`TELEOINGBOT_PORT` environment variable must be set");
|
||||||
|
let app_state = api::AppState::new(bots, tokens, client.clone());
|
||||||
|
|
||||||
|
let handler_client = client.clone();
|
||||||
|
let acceptor = salvo::conn::TcpListener::new(format!("{host}:{port}"))
|
||||||
|
.bind()
|
||||||
|
.await;
|
||||||
|
let client_handler = tokio::spawn(async move { superbot::handler(handler_client).await });
|
||||||
|
let server_handler = tokio::spawn(async move {
|
||||||
|
salvo::Server::new(acceptor)
|
||||||
|
.serve_with_graceful_shutdown(
|
||||||
|
api::service(app_state),
|
||||||
|
async {
|
||||||
|
tokio::signal::ctrl_c()
|
||||||
|
.await
|
||||||
|
.expect("Faild to listen to ctrl_c event");
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
|
||||||
|
client_handler.await?;
|
||||||
|
server_handler.await?;
|
||||||
|
|
||||||
|
log::debug!("Close the API, telegram sign out status: {sign_out}");
|
||||||
|
if sign_out {
|
||||||
|
client.sign_out_disconnect().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
109
src/superbot.rs
Normal file
109
src/superbot.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// A simple API to ping telegram bots and returns if it's online or not.
|
||||||
|
// Copyright (C) 2023 Awiteb <awitb@hotmail.com>
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published
|
||||||
|
// by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use grammers_client::{Client, Config, InitParams, SignInError, Update};
|
||||||
|
use grammers_session::Session;
|
||||||
|
|
||||||
|
use crate::PingList;
|
||||||
|
|
||||||
|
const SESSION_FILE: &str = "telebotping.session";
|
||||||
|
|
||||||
|
pub(crate) async fn login(api_hash: String, api_id: i32) -> crate::Result<(Client, bool)> {
|
||||||
|
let client = Client::connect(Config {
|
||||||
|
session: Session::load_file_or_create(SESSION_FILE)?,
|
||||||
|
api_id,
|
||||||
|
api_hash: api_hash.clone(),
|
||||||
|
params: InitParams::default(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let mut sign_out = false;
|
||||||
|
|
||||||
|
if !client.is_authorized().await? {
|
||||||
|
println!("Signing in...");
|
||||||
|
let phone: String = promptly::prompt("Enter your phone number (international format)")?;
|
||||||
|
let token = client.request_login_code(&phone, api_id, &api_hash).await?;
|
||||||
|
let code: String = promptly::prompt("Enter the code you received")?;
|
||||||
|
let signed_in = client.sign_in(&token, &code).await;
|
||||||
|
match signed_in {
|
||||||
|
Err(SignInError::PasswordRequired(password_token)) => {
|
||||||
|
let hint = password_token.hint().unwrap_or("None");
|
||||||
|
let password: String =
|
||||||
|
promptly::prompt(format!("Enter the password (hint {hint})"))?;
|
||||||
|
client
|
||||||
|
.check_password(password_token, password.trim())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(_) => (),
|
||||||
|
Err(e) => panic!("{e}"),
|
||||||
|
}
|
||||||
|
let me = client.get_me().await?;
|
||||||
|
println!(
|
||||||
|
"Signed in successfully to {}",
|
||||||
|
me.username()
|
||||||
|
.map(|u| "@".to_owned() + u)
|
||||||
|
.unwrap_or_else(|| me.full_name())
|
||||||
|
);
|
||||||
|
match client.session().save_to_file(SESSION_FILE) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"NOTE: failed to save the session, will sign out when done: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
sign_out = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((client, sign_out))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_handler(upd: Update) {
|
||||||
|
if let Update::NewMessage(msg) = upd {
|
||||||
|
if let Some(sender) = msg.sender() {
|
||||||
|
crate::PINGED_BOTS.new_res(sender.id() as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn handler(client: Client) {
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
_ = tokio::signal::ctrl_c() => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(Some(update)) = client.next_update() => {
|
||||||
|
log::debug!("New update: {update:?}");
|
||||||
|
tokio::spawn(async move {
|
||||||
|
update_handler(update)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn send_start(client: &Client, bot_username: &str) -> crate::Result<u64> {
|
||||||
|
if let Some(chat) = client.resolve_username(bot_username).await? {
|
||||||
|
let telegram_id = chat.id() as u64;
|
||||||
|
crate::PINGED_BOTS.add_new(telegram_id);
|
||||||
|
client.send_message(chat, "/start").await?;
|
||||||
|
// Sleep, wating the response
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||||||
|
Ok(telegram_id)
|
||||||
|
} else {
|
||||||
|
Err(format!("Invalid username `{bot_username}`").into())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue