Compare commits
No commits in common. "2fc7e8274e4bf97ade6668f79914a78cb3ab3a32" and "2f4f4978ffdf2bc68cd3d1bd141d349f47f62fd4" have entirely different histories.
2fc7e8274e
...
2f4f4978ff
12 changed files with 86 additions and 242 deletions
4
.env.example
Normal file
4
.env.example
Normal 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
4
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
|||
/target
|
||||
config.toml
|
||||
tokens.txt
|
||||
bots.txt
|
||||
*.session
|
||||
.env
|
||||
|
|
51
Cargo.lock
generated
51
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
37
README.md
37
README.md
|
@ -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
3
bots.txt.example
Normal file
|
@ -0,0 +1,3 @@
|
|||
@FirstBot
|
||||
@SecondBot
|
||||
@ThirdBot
|
|
@ -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
|
||||
|
|
@ -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");
|
||||
|
|
122
src/config.rs
122
src/config.rs
|
@ -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
|
||||
}
|
|
@ -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}")]
|
||||
|
|
55
src/main.rs
55
src/main.rs
|
@ -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,33 +67,60 @@ 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();
|
||||
|
||||
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 = TcpListener::new(format!("{}:{}", config.api.host, config.api.port))
|
||||
.bind()
|
||||
.await;
|
||||
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::info!("Bind the API to {}:{}", config.api.host, config.api.port);
|
||||
|
||||
tokio::select! {
|
||||
_ = client_handler => {},
|
||||
_ = server_handler=> {},
|
||||
_ = signal::ctrl_c() => {},
|
||||
}
|
||||
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
3
tokens.txt.example
Normal file
|
@ -0,0 +1,3 @@
|
|||
FirstToken
|
||||
SecondToken
|
||||
ThirdToken
|
Loading…
Reference in a new issue