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, NotFilter, OnlyFromId, RegexFilter, SilentFilter, TextFilter, TextMatchMethod, UpdateType, UpdateTypeFilter, }, }, handlers::{ basic::{ currency_converter::{CurrencyConverter, CurrencyTextFilter}, get_chat_id::GetChatId, get_user_id::GetUserId, help::Help, notify_all::NotifyAll, time_converter::TimeConverter, weather_forecaster::WeatherForecaster, }, fun::{ blyaficator::Blyaficator, chooser::Chooser, greeter::Greeter, magic_ball::MagicBall, 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>>, ) -> anyhow::Result<()> { log::info!("Requesting login code."); let token = client.request_login_code(&args.phone).await?; // args.app_id, &args.api_hash 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, 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. #[allow(clippy::too_many_lines)] async fn run(args: BotConfig, client: Client) -> anyhow::Result<()> { let me = client.get_me().await?; let handlers: Vec = vec![ // Printing help. FilteredHandler::new(Help) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(TextFilter(&[".h"], TextMatchMethod::Matches)), // Greeting my fellow humans. FilteredHandler::new(Greeter) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(MessageDirectionFilter(MessageDirection::Incoming)) .add_filter(SilentFilter) .add_filter(ExcludedChatsFilter(vec![me.id()])) .add_filter(TextFilter( &["привет", "здравствуйте"], TextMatchMethod::StartsWith, )) .add_middleware::>(), // Getting chat id. FilteredHandler::new(GetChatId) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(TextFilter(&[".cid"], TextMatchMethod::Matches)) .add_filter(OnlyFromId(me.id())), // Get user id. FilteredHandler::new(GetUserId) .add_filter(SilentFilter) .add_filter(MessageDirectionFilter(MessageDirection::Outgoing)) .add_filter(TextFilter(&[".uid"], TextMatchMethod::Matches)), // Make бля fun again. FilteredHandler::new(Blyaficator) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(SilentFilter) .add_filter(TextFilter(&[".bl"], TextMatchMethod::StartsWith)), // Handler for converting currecies. FilteredHandler::new(CurrencyConverter::new()?) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(ExcludedChatsFilter(args.currency_excluded_chats)) .add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(SilentFilter) .add_filter(CurrencyTextFilter), // Simlpe rotator. FilteredHandler::new(Rotator) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(SilentFilter) .add_filter(TextFilter(&[".rl"], TextMatchMethod::StartsWith)), // Weather forecast. FilteredHandler::new(WeatherForecaster::new()?) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(SilentFilter) .add_filter(TextFilter(&[".w"], TextMatchMethod::StartsWith)), // Smiley repeator. FilteredHandler::new(Repeator) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .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::>(), // The magic balls. FilteredHandler::new(MagicBall) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(SilentFilter) .add_filter(TextFilter(&[".mb"], TextMatchMethod::StartsWith)), // Random chooser. FilteredHandler::new(Chooser) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(SilentFilter) .add_filter(TextFilter(&[".c"], TextMatchMethod::StartsWith)) .add_filter(NotFilter(TextFilter( &[".cid"], TextMatchMethod::StartsWith, ))), FilteredHandler::new(NotifyAll) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(SilentFilter) .add_filter(TextFilter(&["@all"], TextMatchMethod::Contains)) .add_middleware::>(), // Time conversion utils FilteredHandler::new(TimeConverter) .add_filter(ExcludedChatsFilter(args.excluded_chats.clone())) .add_filter(UpdateTypeFilter(&[UpdateType::New])) .add_filter(SilentFilter) .add_filter(TextFilter(&[".t"], TextMatchMethod::StartsWith)), ]; 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() 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::>(); 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>>, ) -> 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>>) -> 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; } }