17
src/bot/filters/base.rs
Normal file
17
src/bot/filters/base.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use dyn_clone::DynClone;
|
||||
use grammers_client::Update;
|
||||
|
||||
pub trait Filter: DynClone + Send + Sync {
|
||||
fn filter(&self, update: &Update) -> anyhow::Result<bool>;
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(Filter);
|
||||
|
||||
impl<T> Filter for T
|
||||
where
|
||||
T: Fn(&Update) -> anyhow::Result<bool> + Clone + Send + Sync,
|
||||
{
|
||||
fn filter(&self, update: &Update) -> anyhow::Result<bool> {
|
||||
self(update)
|
||||
}
|
||||
}
|
39
src/bot/filters/chain.rs
Normal file
39
src/bot/filters/chain.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use grammers_client::Update;
|
||||
|
||||
use crate::bot::handlers::Handler;
|
||||
|
||||
use super::base::Filter;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FilteredHandler {
|
||||
filters: Vec<Box<dyn Filter>>,
|
||||
pub handler: Box<dyn Handler>,
|
||||
}
|
||||
|
||||
impl FilteredHandler {
|
||||
pub fn new<H: Handler + 'static>(handler: H) -> Self {
|
||||
Self {
|
||||
filters: vec![],
|
||||
handler: Box::new(handler),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_filter<F: Filter + 'static>(mut self, filter: F) -> Self {
|
||||
self.filters.push(Box::new(filter));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn check(&self, update: &Update) -> bool {
|
||||
for filter in &self.filters {
|
||||
match filter.filter(update) {
|
||||
Err(error) => {
|
||||
log::error!("Fitler error: {error}");
|
||||
return false;
|
||||
}
|
||||
Ok(false) => return false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
1
src/bot/filters/groups.rs
Normal file
1
src/bot/filters/groups.rs
Normal file
@ -0,0 +1 @@
|
||||
|
74
src/bot/filters/message_fitlers.rs
Normal file
74
src/bot/filters/message_fitlers.rs
Normal file
@ -0,0 +1,74 @@
|
||||
use grammers_client::Update;
|
||||
|
||||
use super::base::Filter;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum MessageDirection {
|
||||
Outgoing,
|
||||
Incoming,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum TextMatchMethod {
|
||||
IStartsWith,
|
||||
IMatches,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExcludedChatsFilter(pub Vec<i64>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MessageDirectionFilter(pub MessageDirection);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TextFilter<'a>(pub &'a [&'a str], pub TextMatchMethod);
|
||||
|
||||
impl Filter for ExcludedChatsFilter {
|
||||
fn filter(&self, update: &Update) -> anyhow::Result<bool> {
|
||||
let a = match update {
|
||||
Update::NewMessage(message) | Update::MessageEdited(message) => message.chat(),
|
||||
Update::CallbackQuery(query) => query.chat().clone(),
|
||||
_ => return Ok(false),
|
||||
};
|
||||
if self.0.contains(&a.id()) {
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl Filter for MessageDirectionFilter {
|
||||
fn filter(&self, update: &Update) -> anyhow::Result<bool> {
|
||||
let Update::NewMessage(message) = update else {return Ok(false)};
|
||||
|
||||
let res = matches!(
|
||||
(self.0, message.outgoing()),
|
||||
(MessageDirection::Outgoing, true) | (MessageDirection::Incoming, false)
|
||||
);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Filter for TextFilter<'a> {
|
||||
fn filter(&self, update: &Update) -> anyhow::Result<bool> {
|
||||
let message_text = match update {
|
||||
Update::NewMessage(message) | Update::MessageEdited(message) => message.text(),
|
||||
Update::InlineQuery(query) => query.text(),
|
||||
_ => return Ok(false),
|
||||
};
|
||||
for text in self.0 {
|
||||
let matches = match self.1 {
|
||||
TextMatchMethod::IStartsWith => message_text
|
||||
.to_lowercase()
|
||||
.starts_with(text.to_lowercase().as_str()),
|
||||
TextMatchMethod::IMatches => message_text.to_lowercase() == text.to_lowercase(),
|
||||
};
|
||||
|
||||
if matches {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
3
src/bot/filters/mod.rs
Normal file
3
src/bot/filters/mod.rs
Normal file
@ -0,0 +1,3 @@
|
||||
mod base;
|
||||
pub mod chain;
|
||||
pub mod message_fitlers;
|
10
src/bot/handlers/base.rs
Normal file
10
src/bot/handlers/base.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use async_trait::async_trait;
|
||||
use dyn_clone::DynClone;
|
||||
use grammers_client::{Client, Update};
|
||||
|
||||
#[async_trait]
|
||||
pub trait Handler: DynClone + Sync + Send {
|
||||
async fn react(&self, client: &Client, update: &Update) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
dyn_clone::clone_trait_object!(Handler);
|
17
src/bot/handlers/basic/get_chat_id.rs
Normal file
17
src/bot/handlers/basic/get_chat_id.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use grammers_client::{Client, Update};
|
||||
|
||||
use crate::{bot::handlers::Handler, utils::messages::get_message};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GetChatId;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Handler for GetChatId {
|
||||
async fn react(&self, _: &Client, update: &Update) -> anyhow::Result<()> {
|
||||
let message = get_message(update);
|
||||
if let Some(msg) = message {
|
||||
msg.reply(msg.chat().id().to_string()).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
17
src/bot/handlers/basic/help.rs
Normal file
17
src/bot/handlers/basic/help.rs
Normal file
@ -0,0 +1,17 @@
|
||||
use grammers_client::{Client, Update};
|
||||
|
||||
use crate::bot::handlers::Handler;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Help;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Handler for Help {
|
||||
async fn react(&self, _: &Client, update: &Update) -> anyhow::Result<()> {
|
||||
let Update::NewMessage(message) = update else {return Ok(())};
|
||||
|
||||
message.reply("Хелпа").await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
2
src/bot/handlers/basic/mod.rs
Normal file
2
src/bot/handlers/basic/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod get_chat_id;
|
||||
pub mod help;
|
39
src/bot/handlers/fun/blyaficator.rs
Normal file
39
src/bot/handlers/fun/blyaficator.rs
Normal file
@ -0,0 +1,39 @@
|
||||
use crate::{
|
||||
bot::handlers::Handler,
|
||||
utils::{inter_join::RandomIntersperse, messages::get_message},
|
||||
};
|
||||
use grammers_client::{Client, Update};
|
||||
|
||||
const BLYA_WORDS: &[&str] = &[", бля,", ", сука,", ", ёбаный рот,", ", охуеть конечно,"];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Blyaficator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Handler for Blyaficator {
|
||||
async fn react(&self, _: &Client, update: &Update) -> anyhow::Result<()> {
|
||||
if let Some(message) = get_message(update) {
|
||||
let maybe_blyaficated = message.text().strip_prefix(".bl").map(|stripped| {
|
||||
stripped
|
||||
// Trim string after removing prefix, to
|
||||
// remove leading spaces.
|
||||
.trim()
|
||||
// split by commas
|
||||
.split(',')
|
||||
// choose random strings from BLYA_WORDS
|
||||
// and insert them between splitted strings.
|
||||
.random_itersperse(BLYA_WORDS, &mut rand::thread_rng())
|
||||
// Collect it back to vec
|
||||
.collect::<Vec<_>>()
|
||||
// Creating one string with all words concatenated.
|
||||
.join("")
|
||||
});
|
||||
|
||||
// If the text was blyaficated we send it as a reply.
|
||||
if let Some(blyficated) = maybe_blyaficated {
|
||||
message.reply(blyficated).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
31
src/bot/handlers/fun/greeter.rs
Normal file
31
src/bot/handlers/fun/greeter.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use async_trait::async_trait;
|
||||
use grammers_client::{Client, Update};
|
||||
use rand::seq::IteratorRandom;
|
||||
|
||||
use crate::bot::handlers::base::Handler;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Greeter;
|
||||
|
||||
#[async_trait]
|
||||
impl Handler for Greeter {
|
||||
async fn react(&self, client: &Client, update: &Update) -> anyhow::Result<()> {
|
||||
let Update::NewMessage(message) = update else {return Ok(())};
|
||||
|
||||
// Check if chat has less than 100 participants.
|
||||
let participants = client.iter_participants(message.chat()).total().await?;
|
||||
if participants >= 100 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let reply_text = ["Привет!", "Добрый день!", "Здравствуйте.", "Приетствую"]
|
||||
.into_iter()
|
||||
.choose(&mut rand::thread_rng());
|
||||
|
||||
if let Some(text) = reply_text {
|
||||
message.reply(text).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
2
src/bot/handlers/fun/mod.rs
Normal file
2
src/bot/handlers/fun/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod blyaficator;
|
||||
pub mod greeter;
|
5
src/bot/handlers/mod.rs
Normal file
5
src/bot/handlers/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
pub mod base;
|
||||
pub mod basic;
|
||||
pub mod fun;
|
||||
|
||||
pub use base::Handler;
|
131
src/bot/main.rs
Normal file
131
src/bot/main.rs
Normal file
@ -0,0 +1,131 @@
|
||||
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 tokio::sync::RwLock;
|
||||
|
||||
use super::{
|
||||
filters::{
|
||||
chain::FilteredHandler,
|
||||
message_fitlers::{
|
||||
ExcludedChatsFilter, MessageDirection, MessageDirectionFilter, TextFilter,
|
||||
TextMatchMethod,
|
||||
},
|
||||
},
|
||||
handlers::{
|
||||
basic::{get_chat_id::GetChatId, help::Help},
|
||||
fun::{blyaficator::Blyaficator, greeter::Greeter},
|
||||
Handler,
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
while code.is_none() {
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
{
|
||||
code = web_code.read().await.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let signed_in = client.sign_in(&token, &code.unwrap()).await;
|
||||
match signed_in {
|
||||
Err(SignInError::PasswordRequired(password_token)) => {
|
||||
// Note: this `prompt` method will echo the password in the console.
|
||||
// Real code might want to use a better way to handle this.
|
||||
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(())
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(args: BotConfig, client: Client) {
|
||||
let handlers: Vec<FilteredHandler> = vec![
|
||||
FilteredHandler::new(Greeter)
|
||||
.add_filter(MessageDirectionFilter(MessageDirection::Incoming))
|
||||
.add_filter(TextFilter(&["привет"], TextMatchMethod::IStartsWith))
|
||||
.add_filter(ExcludedChatsFilter(args.excluded_chats)),
|
||||
FilteredHandler::new(Help).add_filter(TextFilter(&[".h"], TextMatchMethod::IMatches)),
|
||||
FilteredHandler::new(GetChatId)
|
||||
.add_filter(TextFilter(&[".cid"], TextMatchMethod::IMatches)),
|
||||
FilteredHandler::new(Blyaficator)
|
||||
.add_filter(TextFilter(&[".bl"], TextMatchMethod::IStartsWith)),
|
||||
];
|
||||
|
||||
loop {
|
||||
let update = client.next_update().await;
|
||||
if update.is_err() {
|
||||
log::error!("{}", update.unwrap_err());
|
||||
break;
|
||||
}
|
||||
if let Some(update_data) = update.unwrap() {
|
||||
let update_ref = &update_data;
|
||||
let matched_handlers = handlers
|
||||
.par_iter()
|
||||
.filter(move |val| val.check(update_ref))
|
||||
.collect::<Vec<_>>();
|
||||
for handler in matched_handlers {
|
||||
tokio::spawn(handle_with_log(
|
||||
handler.handler.clone(),
|
||||
client.clone(),
|
||||
update_data.clone(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start(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 {
|
||||
authorize(&args, &client, web_code).await?;
|
||||
client.session().save_to_file(args.session_file.as_str())?;
|
||||
}
|
||||
|
||||
run(args.clone(), client).await;
|
||||
|
||||
Ok(())
|
||||
}
|
6
src/bot/mod.rs
Normal file
6
src/bot/mod.rs
Normal file
@ -0,0 +1,6 @@
|
||||
mod filters;
|
||||
mod handlers;
|
||||
mod main;
|
||||
pub mod utils;
|
||||
|
||||
pub use main::start;
|
1
src/bot/utils.rs
Normal file
1
src/bot/utils.rs
Normal file
@ -0,0 +1 @@
|
||||
|
Reference in New Issue
Block a user