Compare commits

..

6 commits

Author SHA1 Message Date
2fc7e8274e
chore: Update README.md
All checks were successful
Write changelog / write-changelog (push) Successful in 24s
Rust CI / Rust CI (push) Successful in 3m23s
Signed-off-by: Awiteb <a@4rs.nl>
2024-05-31 19:06:27 +03:00
d7b2d222a0
chore: Add example config file
Signed-off-by: Awiteb <a@4rs.nl>
2024-05-31 19:06:26 +03:00
83148f848a
feat: Make config.toml hold all configrations
Signed-off-by: Awiteb <a@4rs.nl>
2024-05-31 19:06:26 +03:00
8b8056f670
chore: Replace the file with config.toml
Signed-off-by: Awiteb <a@4rs.nl>
2024-05-31 19:06:26 +03:00
01bae614e3
chore(deps): Add toml to dependencies
Signed-off-by: Awiteb <a@4rs.nl>
2024-05-31 19:06:26 +03:00
d64de3c4dc
fix: Make the bot_username parameter to lowercase
Signed-off-by: Awiteb <a@4rs.nl>
2024-05-31 19:06:26 +03:00
12 changed files with 243 additions and 87 deletions

View file

@ -1,4 +0,0 @@
TELEPINGBOT_API_HASH="" # From https://my.telegram.org/apps
TELEPINGBOT_API_ID="" # From https://my.telegram.org/apps
TELEOINGBOT_HOST="0.0.0.0" # Host to listen on
TELEOINGBOT_PORT=3939 # Port to listen on

4
.gitignore vendored
View file

@ -1,5 +1,3 @@
/target /target
tokens.txt config.toml
bots.txt
*.session *.session
.env

51
Cargo.lock generated
View file

@ -1468,7 +1468,7 @@ version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
dependencies = [ dependencies = [
"toml_edit", "toml_edit 0.21.1",
] ]
[[package]] [[package]]
@ -1997,6 +1997,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -2186,6 +2195,7 @@ dependencies = [
"serde_json", "serde_json",
"sha256", "sha256",
"tokio", "tokio",
"toml",
] ]
[[package]] [[package]]
@ -2350,11 +2360,26 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "toml"
version = "0.8.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e43f8cc456c9704c851ae29c67e17ef65d2c30017c17a9765b89c382dc8bba"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.22.13",
]
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.6" version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
@ -2364,7 +2389,20 @@ checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime",
"winnow", "winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.22.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c127785850e8c20836d49732ae6abfa47616e60bf9d9f57c43c250361a9db96c"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.9",
] ]
[[package]] [[package]]
@ -2857,6 +2895,15 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winnow"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.52.0" version = "0.52.0"

View file

@ -26,6 +26,7 @@ serde = {version = "1.0.202", features = ["derive"]}
serde_json = "1.0.117" serde_json = "1.0.117"
sha256 = "1.5.0" sha256 = "1.5.0"
tokio = {version = "1.37.0", features = ["macros", "rt-multi-thread", "signal"]} tokio = {version = "1.37.0", features = ["macros", "rt-multi-thread", "signal"]}
toml = { version = "0.8.13", default-features = false, features = ["parse"] }
[lints.rust] [lints.rust]
unsafe_code = "forbid" unsafe_code = "forbid"

View file

@ -1,32 +1,29 @@
<div align="center"
# TelePingBot # TelePingBot
A simple API to ping telegram bots and returns if it's online or not. using superbot to send message to the bots (mtproto). A simple API to ping telegram bots and returns if it's online or not. using superbot to send message to the bots (mtproto).
## Why is simple? [![Forgejo CI Status](https://git.4rs.nl/awiteb/telepingbot/badges/workflows/ci.yml/badge.svg)](https://git.4rs.nl/awiteb/telepingbot)
Add your API tokens in the `tokens.txt` and add the bot usernames in the `bots.txt` and you're ready to go! No need to generate tokens or anything else. [![Forgejo CD Status](https://git.4rs.nl/awiteb/telepingbot/badges/workflows/cd.yml/badge.svg)](https://git.4rs.nl/awiteb/telepingbot)
## `tokens.txt` file (rename `tokens.txt.example` to `tokens.txt`) [![agplv3-or-later](https://www.gnu.org/graphics/agplv3-88x31.png)](https://www.gnu.org/licenses/agpl-3.0.html)
The `tokens.txt` file is where you put your API tokens. You can put as many as you want, but make sure to put one in each line. This is API access tokens, you need to put it in `Authorization` header.
</div>
## Why is simple?
Add your API tokens (`Authorization` header) and your bot usernames in the `config.toml` file, and you're ready to go.
> [!NOTE]
> Check out the `config.toml.example` file to see how to fill the `config.toml` file.
> [!WARNING] > [!WARNING]
> > Remember to keep the `config.toml` file safe, because anyone can use it to ping your bots.
> Remember to keep this file safe, because anyone can use it to ping your bots.
> Recommended to generate the tokens with `openssl rand -hex 32` or `uuidgen`.
## `bots.txt` file (rename `bots.txt.example` to `bots.txt`) ## CLI Arguments
The `bots.txt` file is where you put your bot usernames, this to make sure to ping the specifics bots only. You can put as many as you want, but make sure to put one in each line. - `--config`: The path to the config file. (default: `config.toml`)
for example:
```
@BotFather
@SomeTestBot
@SomeTestBot
```
## `.env` file (rename `.env.example` to `.env`)
You need to fill the variables in it.
## Requirements ## Requirements
- Rust (MSRV 1.68.2) - Rust (MSRV 1.75.0)
- Cargo - Cargo
## Build ## Build
@ -38,7 +35,7 @@ cargo build --release
```bash ```bash
cargo run --release cargo run --release
``` ```
Or just run the binary file in `target/release/telepingbot` (Not recommended because the `.env` file) Or just run the binary file in `target/release/telepingbot`
## Endpoints ## Endpoints

View file

@ -1,3 +0,0 @@
@FirstBot
@SecondBot
@ThirdBot

23
config.toml.example Normal file
View file

@ -0,0 +1,23 @@
# The bots that allowed to be pinged
bots = [
"@testbot",
"@someSuperBot",
"@anotherSuperBot",
]
# The tokens that will put in `Authorization` header to authenticate the request
tokens = [
"mysupertoken",
"mysecondsupertoken",
]
# Telegram MTProto API configuration
[client]
api_hash = "myhash"
api_id = 12345678
# The host and port that the server will listen on
[api]
host = "0.0.0.0"
port = 3939

View file

@ -86,7 +86,8 @@ fn write_json_body(res: &mut Response, json_body: impl serde::Serialize) {
async fn ping(req: &Request, res: &mut Response, depot: &mut Depot) { async fn ping(req: &Request, res: &mut Response, depot: &mut Depot) {
let bot_username = req let bot_username = req
.param::<String>("bot_username") .param::<String>("bot_username")
.expect("The path param is required"); .expect("The path param is required")
.to_lowercase();
let app_state = depot let app_state = depot
.obtain::<Arc<AppState>>() .obtain::<Arc<AppState>>()
.expect("The app state is injected"); .expect("The app state is injected");

122
src/config.rs Normal file
View file

@ -0,0 +1,122 @@
// A simple API to ping telegram bots and returns if it's online or not.
// Copyright (C) 2023-2024 Awiteb <a@4rs.nl>
//
// 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/agpl-3.0>.
use std::{fs, path::Path};
use serde::{de::Error as DeError, Deserialize};
use crate::{ServerError, ServerResult};
/// The config of telegram client
#[derive(Deserialize, Debug)]
pub(crate) struct TelegramClient {
pub api_hash: String,
pub api_id: i32,
}
/// The config of the api
#[derive(Deserialize, Debug)]
pub(crate) struct ApiConfig {
#[serde(deserialize_with = "host_deserialize")]
#[serde(default = "default_host")]
pub host: String,
#[serde(default = "default_port")]
pub port: u16,
}
/// The config struct
#[derive(Deserialize, Debug)]
pub(crate) struct Config {
pub client: TelegramClient,
pub api: ApiConfig,
#[serde(deserialize_with = "bots_deserialize")]
pub bots: Vec<String>,
#[serde(deserialize_with = "one_or_more_string")]
pub tokens: Vec<String>,
}
impl Config {
/// Initialize the config from toml file
pub fn from_toml_file(file_path: impl AsRef<Path>) -> ServerResult<Self> {
toml::from_str(
&fs::read_to_string(file_path).map_err(|err| ServerError::Config(err.to_string()))?,
)
.map_err(|err| ServerError::Config(err.to_string()))
}
}
/// A function to deserialize the host, make sure it's a valid host to band
fn host_deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result<String, D::Error> {
let host = String::deserialize(d)?;
if host == "localhost" {
return Ok(host);
}
let octets = host
.split(".")
.map(|octet| octet.parse::<u8>())
.collect::<Result<Vec<_>, _>>();
if let Ok(octets) = octets {
if octets.len() != 4 {
return Err(DeError::custom("There is more then 4 octets"));
}
} else {
return Err(DeError::custom("Contain invalid number"));
}
Ok(host)
}
fn bots_deserialize<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Vec<String>, D::Error> {
let bots = Vec::<String>::deserialize(d)?;
if bots.is_empty() {
return Err(DeError::custom("There must be one or more bots"));
}
for bot in &bots {
if !bot.starts_with('@') {
return Err(DeError::custom(format!(
"Invalid bot username `{bot}`: must starts with `@`"
)));
} else if !bot.to_lowercase().ends_with("bot") {
return Err(DeError::custom(format!(
"Invalid bot username `{bot}`: must end with `bot`"
)));
}
}
Ok(bots.iter().map(|s| s.trim().to_owned()).collect())
}
fn one_or_more_string<'de, D>(d: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let str_vec = Vec::<String>::deserialize(d)?;
if str_vec.is_empty() {
return Err(DeError::custom("There is must be one at least"));
}
Ok(str_vec.iter().map(|s| s.trim().to_owned()).collect())
}
/// The default host `0.0.0.0`
fn default_host() -> String {
"0.0.0.0".to_owned()
}
/// The default port `3939`
const fn default_port() -> u16 {
3939
}

View file

@ -28,6 +28,8 @@ pub(crate) type Result<T> = std::result::Result<T, Error>;
pub(crate) enum Error { pub(crate) enum Error {
#[error("Cli Error: {0}")] #[error("Cli Error: {0}")]
CliParse(String), CliParse(String),
#[error("Config Error: {0}")]
Config(String),
#[error("IO Error: {0}")] #[error("IO Error: {0}")]
Io(#[from] IoError), Io(#[from] IoError),
#[error("Thread Error: {0}")] #[error("Thread Error: {0}")]

View file

@ -16,18 +16,20 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
use std::{env, fs, process::ExitCode, sync::Mutex}; use std::{process::ExitCode, sync::Mutex};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use salvo::{conn::TcpListener, Listener}; use salvo::{conn::TcpListener, Listener};
mod api; mod api;
mod cli_parser; mod cli_parser;
mod config;
mod errors; mod errors;
mod superbot; mod superbot;
mod traits; mod traits;
pub(crate) use errors::{Error as ServerError, Result as ServerResult}; pub(crate) use errors::{Error as ServerError, Result as ServerResult};
use tokio::signal;
pub(crate) use traits::PingList; pub(crate) use traits::PingList;
#[derive(Default, Clone)] #[derive(Default, Clone)]
@ -67,60 +69,33 @@ async fn try_main() -> ServerResult<()> {
return Ok(()); return Ok(());
} }
let config = config::Config::from_toml_file(&cli_args.config_file)?;
let bots: Vec<String> = fs::read_to_string("bots.txt")? let (client, sign_out) = superbot::login(config.client.api_hash, config.client.api_id).await?;
.lines() let app_state = api::AppState::new(config.bots, config.tokens, client.clone());
.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 handler_client = client.clone();
let acceptor = TcpListener::new(format!("{host}:{port}")).bind().await; let acceptor = TcpListener::new(format!("{}:{}", config.api.host, config.api.port))
.bind()
.await;
let client_handler = tokio::spawn(async move { superbot::handler(handler_client).await }); let client_handler = tokio::spawn(async move { superbot::handler(handler_client).await });
let server_handler = tokio::spawn(async move { let server_handler = tokio::spawn(async move {
salvo::Server::new(acceptor) salvo::Server::new(acceptor)
.serve(api::service(app_state)) .serve(api::service(app_state))
.await .await
}); });
log::info!("Bind the API to {}:{}", config.api.host, config.api.port);
client_handler.await?; tokio::select! {
server_handler.await?; _ = client_handler => {},
_ = server_handler=> {},
_ = signal::ctrl_c() => {},
}
log::debug!("Close the API, telegram sign out status: {sign_out}"); log::debug!("Close the API, telegram sign out status: {sign_out}");
if sign_out { if sign_out {
client.sign_out_disconnect().await?; client.sign_out_disconnect().await?;
} }
}
Ok(()) Ok(())
} }

View file

@ -1,3 +0,0 @@
FirstToken
SecondToken
ThirdToken