Compare commits

..

No commits in common. "2fc7e8274e4bf97ade6668f79914a78cb3ab3a32" and "2f4f4978ffdf2bc68cd3d1bd141d349f47f62fd4" have entirely different histories.

12 changed files with 86 additions and 242 deletions

4
.env.example Normal file
View file

@ -0,0 +1,4 @@
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,3 +1,5 @@
/target
config.toml
tokens.txt
bots.txt
*.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"
checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284"
dependencies = [
"toml_edit 0.21.1",
"toml_edit",
]
[[package]]
@ -1997,15 +1997,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@ -2195,7 +2186,6 @@ dependencies = [
"serde_json",
"sha256",
"tokio",
"toml",
]
[[package]]
@ -2360,26 +2350,11 @@ dependencies = [
"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]]
name = "toml_datetime"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
@ -2389,20 +2364,7 @@ checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
dependencies = [
"indexmap",
"toml_datetime",
"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",
"winnow",
]
[[package]]
@ -2895,15 +2857,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "winnow"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86c949fede1d13936a99f14fafd3e76fd642b556dd2ce96287fbe2e0151bfac6"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.52.0"

View file

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

View file

@ -1,29 +1,32 @@
<div align="center"
# 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).
[![Forgejo CI Status](https://git.4rs.nl/awiteb/telepingbot/badges/workflows/ci.yml/badge.svg)](https://git.4rs.nl/awiteb/telepingbot)
[![Forgejo CD Status](https://git.4rs.nl/awiteb/telepingbot/badges/workflows/cd.yml/badge.svg)](https://git.4rs.nl/awiteb/telepingbot)
[![agplv3-or-later](https://www.gnu.org/graphics/agplv3-88x31.png)](https://www.gnu.org/licenses/agpl-3.0.html)
</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.
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.
> [!NOTE]
> Check out the `config.toml.example` file to see how to fill the `config.toml` file.
## `tokens.txt` file (rename `tokens.txt.example` to `tokens.txt`)
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.
> [!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`.
## CLI Arguments
- `--config`: The path to the config file. (default: `config.toml`)
## `bots.txt` file (rename `bots.txt.example` to `bots.txt`)
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.
for example:
```
@BotFather
@SomeTestBot
@SomeTestBot
```
## `.env` file (rename `.env.example` to `.env`)
You need to fill the variables in it.
## Requirements
- Rust (MSRV 1.75.0)
- Rust (MSRV 1.68.2)
- Cargo
## Build
@ -35,7 +38,7 @@ cargo build --release
```bash
cargo run --release
```
Or just run the binary file in `target/release/telepingbot`
Or just run the binary file in `target/release/telepingbot` (Not recommended because the `.env` file)
## Endpoints

3
bots.txt.example Normal file
View file

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

View file

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

View file

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

View file

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

3
tokens.txt.example Normal file
View file

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