commit
7e328e5ff2
@ -0,0 +1 @@
|
|||||||
|
/target
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "tbotsend-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
authors = ["Aaron Johnson <amjohnson@skyfall.tech>"]
|
||||||
|
description = "Telegram bot message sender"
|
||||||
|
license = "MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_yaml = "0.9"
|
||||||
|
serde_json = "1.0"
|
||||||
|
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||||
|
regex = "1"
|
||||||
|
chrono = "0.4"
|
||||||
|
dirs = "5"
|
||||||
|
|
@ -0,0 +1,144 @@
|
|||||||
|
use clap::{Arg, ArgAction, Command};
|
||||||
|
use regex::Regex;
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Config {
|
||||||
|
chat_id: String,
|
||||||
|
token: String,
|
||||||
|
timeout: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TelegramResponse {
|
||||||
|
ok: bool,
|
||||||
|
description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
let matches = Command::new("tbotsend")
|
||||||
|
.version("development")
|
||||||
|
.arg(Arg::new("config").short('f').long("config").value_name("FILE"))
|
||||||
|
.arg(Arg::new("chatid").short('c').long("chatid").value_name("chat_id"))
|
||||||
|
.arg(Arg::new("token").short('t').long("token").value_name("token"))
|
||||||
|
.arg(Arg::new("parse").short('p').long("parse").default_value("MarkdownV2").value_name("MODE"))
|
||||||
|
.arg(Arg::new("timeout").long("timeout").value_parser(clap::value_parser!(u8)).default_value("10").value_name("SECONDS"))
|
||||||
|
.arg(Arg::new("silent").short('s').long("silent").action(ArgAction::SetTrue))
|
||||||
|
.arg(Arg::new("version").short('v').long("version").action(ArgAction::SetTrue))
|
||||||
|
.arg(Arg::new("help").short('h').long("help").action(ArgAction::Help))
|
||||||
|
.arg(Arg::new("message").num_args(1..).required(false))
|
||||||
|
.get_matches();
|
||||||
|
|
||||||
|
let message: String = matches
|
||||||
|
.get_many::<String>("message")
|
||||||
|
.map(|vals| vals.map(String::as_str).collect::<Vec<_>>().join(" "))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
println!(" -- {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"));
|
||||||
|
|
||||||
|
let config_used;
|
||||||
|
let config = if let (Some(chatid), Some(token)) = (matches.get_one::<String>("chatid"), matches.get_one::<String>("token")) {
|
||||||
|
if matches.get_one::<String>("config").is_some() {
|
||||||
|
print_error("Cannot use both configuration file and configuration flags");
|
||||||
|
process::exit(2);
|
||||||
|
}
|
||||||
|
config_used = "MANUAL".to_string();
|
||||||
|
Config {
|
||||||
|
chat_id: chatid.clone(),
|
||||||
|
token: token.clone(),
|
||||||
|
timeout: matches.get_one::<u8>("timeout").copied(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let config_path = if let Some(cfg) = matches.get_one::<String>("config") {
|
||||||
|
PathBuf::from(cfg)
|
||||||
|
} else {
|
||||||
|
dirs::home_dir()
|
||||||
|
.map(|p| p.join(".config/tbotsend.yaml"))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut config_data = fs::read_to_string(&config_path).or_else(|_| {
|
||||||
|
let fallback = dirs::home_dir().unwrap().join(".tbotsend.yaml");
|
||||||
|
fs::read_to_string(&fallback)
|
||||||
|
}).map_err(|e| {
|
||||||
|
print_error(&format!("Failed to read configuration file: {}", e));
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
|
||||||
|
config_used = config_path.to_string_lossy().to_string();
|
||||||
|
serde_yaml::from_str::<Config>(&mut config_data)?
|
||||||
|
};
|
||||||
|
|
||||||
|
if config.chat_id.is_empty() || config.token.is_empty() {
|
||||||
|
print_error("Configuration is missing ChatID or Token");
|
||||||
|
process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if message.trim().is_empty() {
|
||||||
|
print_error("No message provided");
|
||||||
|
process::exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parse_mode = matches.get_one::<String>("parse").unwrap();
|
||||||
|
let silent = matches.get_flag("silent");
|
||||||
|
let timeout_secs = config.timeout.unwrap_or(10);
|
||||||
|
|
||||||
|
let url = format!("https://api.telegram.org/bot{}/sendMessage", config.token);
|
||||||
|
let client = Client::builder()
|
||||||
|
.timeout(Duration::from_secs(timeout_secs as u64))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
let mut resp = client
|
||||||
|
.post(&url)
|
||||||
|
.form(&[
|
||||||
|
("chat_id", config.chat_id.as_str()),
|
||||||
|
("disable_web_page_preview", "1"),
|
||||||
|
("disable_notification", if silent { "true" } else { "false" }),
|
||||||
|
("text", &message),
|
||||||
|
("parse_mode", parse_mode),
|
||||||
|
])
|
||||||
|
.send()?;
|
||||||
|
|
||||||
|
let mut body = String::new();
|
||||||
|
resp.read_to_string(&mut body)?;
|
||||||
|
let telegram_response: TelegramResponse = serde_json::from_str(&body)?;
|
||||||
|
|
||||||
|
if !telegram_response.ok {
|
||||||
|
print_failure(telegram_response.description.as_deref().unwrap_or("Unknown error"));
|
||||||
|
process::exit(13);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\x1b[1mConfig Used:\x1b[0m {}", config_used);
|
||||||
|
println!("\x1b[1mChat ID:\x1b[0m {}", config.chat_id);
|
||||||
|
println!("\x1b[1mSilent:\x1b[0m {}", silent);
|
||||||
|
println!("\x1b[1mMessage:\x1b[0m {}", message);
|
||||||
|
print_success();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_error(msg: &str) {
|
||||||
|
eprintln!("[\x1b[1;31mERROR\x1b[0m] {}", msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_failure(msg: &str) {
|
||||||
|
eprintln!("\x1b[1mResult:\x1b[0m [\x1b[1;31mFAILURE\x1b[0m]: {}\n", msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_success() {
|
||||||
|
println!("\x1b[1mResult:\x1b[0m [\x1b[1;32mSUCCESS\x1b[0m]\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn strip_ansi(input: &str) -> String {
|
||||||
|
let ansi_regex = Regex::new(r"\x1B\[[0-9;]*[mK]").unwrap();
|
||||||
|
ansi_regex.replace_all(input, "").to_string()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in new issue