232 lines
8.1 KiB
Rust
232 lines
8.1 KiB
Rust
use std::{sync::Arc, time::Duration};
|
|
|
|
use crate::args::BotConfig;
|
|
use grammers_client::{Client, Config, SignInError, Update};
|
|
use grammers_session::Session;
|
|
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
|
|
use regex::Regex;
|
|
use tokio::sync::RwLock;
|
|
|
|
use super::{
|
|
filters::{
|
|
filtered_handler::FilteredHandler,
|
|
message_fitlers::{
|
|
ExcludedChatsFilter, MessageDirection, MessageDirectionFilter, RegexFilter,
|
|
SilentFilter, TextFilter, TextMatchMethod, UpdateType, UpdateTypeFilter,
|
|
},
|
|
},
|
|
handlers::{
|
|
basic::{
|
|
currency_converter::{CurrencyConverter, CurrencyTextFilter},
|
|
get_chat_id::GetChatId,
|
|
help::Help,
|
|
weather_forecaster::WeatherForecaster,
|
|
},
|
|
fun::{blyaficator::Blyaficator, greeter::Greeter, repeator::Repeator, rotator::Rotator},
|
|
Handler,
|
|
},
|
|
middlewares::members_count::MembersCount,
|
|
};
|
|
|
|
/// Authorization function.
|
|
///
|
|
/// This function asks for login code and
|
|
/// waits for it to become available.
|
|
///
|
|
/// Also it validates two-factor authentication
|
|
/// password if it was supplied.
|
|
async fn authorize(
|
|
args: &BotConfig,
|
|
client: &Client,
|
|
web_code: Arc<RwLock<Option<String>>>,
|
|
) -> anyhow::Result<()> {
|
|
log::info!("Requesting login code.");
|
|
let token = client
|
|
.request_login_code(&args.phone, args.app_id, &args.api_hash)
|
|
.await?;
|
|
let mut code = None;
|
|
|
|
// Check for code to becom available every second.
|
|
while code.is_none() {
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
{
|
|
code = web_code.read().await.clone();
|
|
}
|
|
}
|
|
// Acutal signing in.
|
|
let signed_in = client.sign_in(&token, &code.unwrap()).await;
|
|
match signed_in {
|
|
// If signing i
|
|
Err(SignInError::PasswordRequired(password_token)) => {
|
|
// If the password was not supplied, we use the hint in panic.
|
|
let hint = password_token.hint().unwrap_or("None");
|
|
let password = args
|
|
.tfa_password
|
|
.as_ref()
|
|
.unwrap_or_else(|| panic!("2FA password is required. Hint for password: `{hint}`"));
|
|
log::info!("Checking client's password.");
|
|
client
|
|
.check_password(password_token, password.trim())
|
|
.await?;
|
|
}
|
|
Ok(_) => (),
|
|
Err(e) => panic!("{}", e),
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// This little function is used to execute handlers on updates and print errors
|
|
/// if something bad happens.
|
|
///
|
|
/// The reason, I created a separate function is simple. I spawn every handler as a
|
|
/// separate task and I don't care if fails.
|
|
async fn handle_with_log(handler: Box<dyn Handler>, client: Client, update_data: Update) {
|
|
if let Err(err) = handler.react(&client, &update_data).await {
|
|
log::error!("{err}");
|
|
}
|
|
}
|
|
|
|
/// Acutal logic on handling updates.
|
|
///
|
|
/// This function handles every update we get from telegram
|
|
/// and spawns correcsponding handlers.
|
|
///
|
|
/// Also, every available handler is defined here.
|
|
async fn run(args: BotConfig, client: Client) -> anyhow::Result<()> {
|
|
let me = client.get_me().await?;
|
|
let handlers: Vec<FilteredHandler> = vec![
|
|
// Printing help.
|
|
FilteredHandler::new(Help).add_filter(TextFilter(&[".h"], TextMatchMethod::IMatches)),
|
|
// Greeting my fellow humans.
|
|
FilteredHandler::new(Greeter)
|
|
.add_filter(UpdateTypeFilter(&[UpdateType::New]))
|
|
.add_filter(MessageDirectionFilter(MessageDirection::Incoming))
|
|
.add_filter(SilentFilter)
|
|
.add_filter(ExcludedChatsFilter(vec![me.id()]))
|
|
.add_filter(TextFilter(&["привет"], TextMatchMethod::IStartsWith))
|
|
.add_filter(ExcludedChatsFilter(args.excluded_chats))
|
|
.add_middleware::<MembersCount<100>>(),
|
|
// Getting chat id.
|
|
FilteredHandler::new(GetChatId)
|
|
.add_filter(TextFilter(&[".cid"], TextMatchMethod::IMatches)),
|
|
// Make бля fun again.
|
|
FilteredHandler::new(Blyaficator)
|
|
.add_filter(UpdateTypeFilter(&[UpdateType::New]))
|
|
.add_filter(SilentFilter)
|
|
.add_filter(TextFilter(&[".bl"], TextMatchMethod::IStartsWith)),
|
|
// Handler for converting currecies.
|
|
FilteredHandler::new(CurrencyConverter::new()?)
|
|
.add_filter(UpdateTypeFilter(&[UpdateType::New]))
|
|
.add_filter(SilentFilter)
|
|
.add_filter(ExcludedChatsFilter(args.currency_excluded_chats))
|
|
.add_filter(CurrencyTextFilter)
|
|
.add_middleware::<MembersCount<100>>(),
|
|
// Simlpe rotator.
|
|
FilteredHandler::new(Rotator)
|
|
.add_filter(UpdateTypeFilter(&[UpdateType::New]))
|
|
.add_filter(SilentFilter)
|
|
.add_filter(TextFilter(&[".rl"], TextMatchMethod::IStartsWith)),
|
|
// Weather forecast.
|
|
FilteredHandler::new(WeatherForecaster::new()?)
|
|
.add_filter(UpdateTypeFilter(&[UpdateType::New]))
|
|
.add_filter(SilentFilter)
|
|
.add_filter(TextFilter(&[".w"], TextMatchMethod::IStartsWith)),
|
|
// Smiley repeator.
|
|
FilteredHandler::new(Repeator)
|
|
.add_filter(UpdateTypeFilter(&[UpdateType::New]))
|
|
.add_filter(MessageDirectionFilter(MessageDirection::Incoming))
|
|
.add_filter(SilentFilter)
|
|
.add_filter(ExcludedChatsFilter(vec![me.id()]))
|
|
.add_filter(RegexFilter(Regex::new("^[)0]+$")?))
|
|
.add_middleware::<MembersCount<100>>(),
|
|
];
|
|
|
|
let mut errors_count = 0;
|
|
|
|
loop {
|
|
// Get new update
|
|
let update = client.next_update().await;
|
|
if update.is_err() {
|
|
log::error!("{}", update.unwrap_err());
|
|
errors_count += 1;
|
|
if errors_count > 10 {
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
// We get update if there's no error
|
|
let Some(update_data) = update.ok().and_then(|inner|inner) else{
|
|
log::warn!("Empty update is found.");
|
|
continue;
|
|
};
|
|
// A reference to update, so we can easily move it.
|
|
let update_ref = &update_data;
|
|
let filtered = handlers
|
|
// A parralel iterator over matchers.
|
|
.par_iter()
|
|
// Here we get all handlers that match filters.
|
|
.filter(move |val| val.check(update_ref))
|
|
// For each matched handler we spawn a new task.
|
|
.collect::<Vec<_>>();
|
|
for handler in filtered {
|
|
tokio::spawn(handle_with_log(
|
|
handler.handler.clone(),
|
|
client.clone(),
|
|
update_data.clone(),
|
|
));
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
pub async fn bot_life(
|
|
args: BotConfig,
|
|
web_code: Arc<RwLock<Option<String>>>,
|
|
) -> anyhow::Result<()> {
|
|
log::info!("Connecting to Telegram...");
|
|
let client = Client::connect(Config {
|
|
session: Session::load_file_or_create(args.session_file.as_str())?,
|
|
api_id: args.app_id,
|
|
api_hash: args.api_hash.clone(),
|
|
params: grammers_client::InitParams {
|
|
device_model: String::from("MEME_MACHINE"),
|
|
catch_up: false,
|
|
..Default::default()
|
|
},
|
|
})
|
|
.await?;
|
|
|
|
log::info!("Connected!");
|
|
|
|
if client.is_authorized().await? {
|
|
// If we already authrized, we write random token, so web won't update it.
|
|
let mut code_writer = web_code.write().await;
|
|
*code_writer = Some(String::new());
|
|
} else {
|
|
// If we don't have token, wait for it.
|
|
authorize(&args, &client, web_code).await?;
|
|
client.session().save_to_file(args.session_file.as_str())?;
|
|
}
|
|
|
|
run(args.clone(), client).await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// The main entrypoint for bot.
|
|
///
|
|
/// This function starts bot, performs login and
|
|
/// starts endless loop.
|
|
pub async fn start(args: BotConfig, web_code: Arc<RwLock<Option<String>>>) -> anyhow::Result<()> {
|
|
loop {
|
|
match bot_life(args.clone(), web_code.clone()).await {
|
|
Err(err) => {
|
|
log::error!("{err}");
|
|
}
|
|
Ok(_) => {
|
|
log::info!("Lol, messages are ended.");
|
|
}
|
|
}
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
}
|
|
}
|