diff --git a/migrations/2021-03-07-131037_add_filter_words_to_subscriptions/down.sql b/migrations/2021-03-07-131037_add_filter_words_to_subscriptions/down.sql new file mode 100644 index 00000000..86b44e25 --- /dev/null +++ b/migrations/2021-03-07-131037_add_filter_words_to_subscriptions/down.sql @@ -0,0 +1 @@ +ALTER TABLE telegram_subscriptions DROP COLUMN filter_words; diff --git a/migrations/2021-03-07-131037_add_filter_words_to_subscriptions/up.sql b/migrations/2021-03-07-131037_add_filter_words_to_subscriptions/up.sql new file mode 100644 index 00000000..841c65c4 --- /dev/null +++ b/migrations/2021-03-07-131037_add_filter_words_to_subscriptions/up.sql @@ -0,0 +1 @@ +ALTER TABLE telegram_subscriptions ADD COLUMN filter_words text[]; diff --git a/src/bot/api.rs b/src/bot/api.rs index 2bf1589c..78f652f2 100644 --- a/src/bot/api.rs +++ b/src/bot/api.rs @@ -17,6 +17,9 @@ static SET_TIMEZONE: &str = "/set_timezone"; static GET_TIMEZONE: &str = "/get_timezone"; static SET_TEMPLATE: &str = "/set_template"; static GET_TEMPLATE: &str = "/get_template"; +static SET_FILTER: &str = "/set_filter"; +static GET_FILTER: &str = "/get_filter"; +static REMOVE_FILTER: &str = "/remove_filter"; static SET_GLOBAL_TEMPLATE: &str = "/set_global_template"; static GET_GLOBAL_TEMPLATE: &str = "/get_global_template"; static UNSUBSCRIBE: &str = "/unsubscribe"; @@ -24,13 +27,16 @@ static HELP: &str = "/help"; static START: &str = "/start"; static OWNER_TELEGRAM_ID: OnceCell> = OnceCell::new(); -static COMMANDS: [&str; 11] = [ +static COMMANDS: [&str; 14] = [ SUBSCRIBE, LIST_SUBSCRIPTIONS, SET_TIMEZONE, GET_TIMEZONE, SET_TEMPLATE, GET_TEMPLATE, + SET_FILTER, + GET_FILTER, + REMOVE_FILTER, SET_GLOBAL_TEMPLATE, GET_GLOBAL_TEMPLATE, UNSUBSCRIBE, @@ -114,8 +120,11 @@ fn commands_string() -> String { Example: /set_template https://www.badykov.com/feed.xml bot_datebot_spacebot_item_namebot_new_linebot_item_description\n\n\ {} url - get a template for the subscription\n\n\ {} template - set global template. This template will be used for all subscriptions. If the subscription has its own template, the subscription template will be used. See /set_template for available fields.\n\n\ - {} - get global template\n", - START, SUBSCRIBE, UNSUBSCRIBE, LIST_SUBSCRIPTIONS, HELP, SET_TIMEZONE, GET_TIMEZONE, SET_TEMPLATE, GET_TEMPLATE, SET_GLOBAL_TEMPLATE, GET_GLOBAL_TEMPLATE + {} - get global template\n\n\ + {} url - get a filter for the subscription\n\n\ + {} url template - set filter, for example, /set_filter https://www.badykov.com/feed.xml telegram,bots. You'll start receiving posts only containing words in the filter\n\n\ + {} url - remove filter\n\n", + START, SUBSCRIBE, UNSUBSCRIBE, LIST_SUBSCRIPTIONS, HELP, SET_TIMEZONE, GET_TIMEZONE, SET_TEMPLATE, GET_TEMPLATE, SET_GLOBAL_TEMPLATE, GET_GLOBAL_TEMPLATE, GET_FILTER, SET_FILTER, REMOVE_FILTER ) } @@ -250,6 +259,17 @@ async fn set_template(api: Api, message: MessageOrChannelPost, data: String) -> Ok(()) } +async fn set_filter(api: Api, message: MessageOrChannelPost, data: String) -> Result<(), Error> { + let chat_id = get_chat_id(&message); + let semaphored_connection = db::get_semaphored_connection().await; + let db_connection = semaphored_connection.connection; + + let response = logic::set_filter(&db_connection, chat_id, data); + + api.send(message.text_reply(response)).await?; + Ok(()) +} + async fn get_timezone(api: Api, message: MessageOrChannelPost) -> Result<(), Error> { let chat_id = get_chat_id(&message); let semaphored_connection = db::get_semaphored_connection().await; @@ -298,6 +318,28 @@ async fn get_template(api: Api, message: MessageOrChannelPost, data: String) -> Ok(()) } +async fn get_filter(api: Api, message: MessageOrChannelPost, data: String) -> Result<(), Error> { + let chat_id = get_chat_id(&message); + let semaphored_connection = db::get_semaphored_connection().await; + let db_connection = semaphored_connection.connection; + + let response = logic::get_filter(&db_connection, chat_id, data); + + api.send(message.text_reply(response)).await?; + Ok(()) +} + +async fn remove_filter(api: Api, message: MessageOrChannelPost, data: String) -> Result<(), Error> { + let chat_id = get_chat_id(&message); + let semaphored_connection = db::get_semaphored_connection().await; + let db_connection = semaphored_connection.connection; + + let response = logic::remove_filter(&db_connection, chat_id, data); + + api.send(message.text_reply(response)).await?; + Ok(()) +} + fn process_message(api: Api, orig_message: Message) { match orig_message.kind { MessageKind::Text { ref data, .. } => { @@ -377,6 +419,15 @@ async fn process_message_or_channel_post( } else if command.starts_with(GET_TEMPLATE) { let argument = parse_argument(command, GET_TEMPLATE); tokio::spawn(get_template(api, message, argument)); + } else if command.starts_with(GET_FILTER) { + let argument = parse_argument(command, GET_FILTER); + tokio::spawn(get_filter(api, message, argument)); + } else if command.starts_with(REMOVE_FILTER) { + let argument = parse_argument(command, REMOVE_FILTER); + tokio::spawn(remove_filter(api, message, argument)); + } else if command.starts_with(SET_FILTER) { + let argument = parse_argument(command, SET_FILTER); + tokio::spawn(set_filter(api, message, argument)); } else if command.starts_with(SET_TEMPLATE) { let argument = parse_argument(command, SET_TEMPLATE); tokio::spawn(set_template(api, message, argument)); diff --git a/src/bot/deliver_job.rs b/src/bot/deliver_job.rs index d9430b4f..b7ca7212 100644 --- a/src/bot/deliver_job.rs +++ b/src/bot/deliver_job.rs @@ -124,7 +124,7 @@ async fn deliver_subscription_updates( let chat = telegram::find_chat(&connection, chat_id).unwrap(); let delay = delay_period(&chat); - if feed_items.len() < undelivered_count as usize { + if subscription.filter_words.is_none() && feed_items.len() < undelivered_count as usize { let message = format!( "You have {} unread items, below {} last items for {}", undelivered_count, @@ -140,18 +140,7 @@ async fn deliver_subscription_updates( Err(error) => { let error_message = format!("{}", error); - log::error!("Failed to deliver updates: {} {}", chat_id, error_message); - - if bot_blocked(&error_message) { - match telegram::remove_chat(&connection, chat_id) { - Ok(_) => log::info!("Successfully removed chat {}", chat_id), - Err(error) => log::error!("Failed to remove a chat {}", error), - } - }; - - return Err(DeliverJobError { - msg: format!("Failed to send updates : {}", error), - }); + return Err(handle_error(error_message, &connection, chat_id)); } } } @@ -177,35 +166,66 @@ async fn deliver_subscription_updates( messages.reverse(); for (message, publication_date) in messages { - match api::send_message(chat_id, message).await { - Ok(_) => { - time::delay_for(delay).await; - update_last_deivered_at(&connection, &subscription, publication_date)?; - () - } - Err(error) => { - let error_message = format!("{}", error); - - log::error!("Failed to deliver updates: {}", error_message); + match subscription.filter_words.clone() { + None => match api::send_message(chat_id, message).await { + Ok(_) => { + time::delay_for(delay).await; + update_last_deivered_at(&connection, &subscription, publication_date)?; + () + } + Err(error) => { + let error_message = format!("{}", error); - if bot_blocked(&error_message) { - match telegram::remove_chat(&connection, chat_id) { - Ok(_) => log::info!("Successfully removed chat {}", chat_id), - Err(error) => log::error!("Failed to remove a chat {}", error), + return Err(handle_error(error_message, &connection, chat_id)); + } + }, + Some(words) => { + let mtch = words + .iter() + .any(|word| message.to_lowercase().contains(word)); + + if mtch { + match api::send_message(chat_id, message).await { + Ok(_) => { + time::delay_for(delay).await; + update_last_deivered_at( + &connection, + &subscription, + publication_date, + )?; + } + Err(error) => { + let error_message = format!("{}", error); + + return Err(handle_error(error_message, &connection, chat_id)); + } } - }; - - return Err(DeliverJobError { - msg: format!("Failed to send updates : {}", error), - }); + } else { + update_last_deivered_at(&connection, &subscription, publication_date)?; + } } - }; + } } } Ok(()) } +fn handle_error(error: String, connection: &PgConnection, chat_id: i64) -> DeliverJobError { + log::error!("Failed to deliver updates: {}", error); + + if bot_blocked(&error) { + match telegram::remove_chat(connection, chat_id) { + Ok(_) => log::info!("Successfully removed chat {}", chat_id), + Err(error) => log::error!("Failed to remove a chat {}", error), + } + }; + + DeliverJobError { + msg: format!("Failed to send updates : {}", error), + } +} + fn update_last_deivered_at( connection: &PgConnection, subscription: &TelegramSubscription, @@ -232,7 +252,7 @@ fn format_messages( let templ = match template { Some(t) => t, - None => "{{bot_feed_name}}\n\n{{bot_item_name}}\n\n{{bot_date}}\n\n{{bot_item_link}}\n\n" + None => "{{bot_feed_name}}\n\n{{bot_item_name}}\n\n{{bot_item_description}}\n\n{{bot_date}}\n\n{{bot_item_link}}\n\n" .to_string(), }; diff --git a/src/bot/logic.rs b/src/bot/logic.rs index bdc0151b..5fdc2d94 100644 --- a/src/bot/logic.rs +++ b/src/bot/logic.rs @@ -80,65 +80,31 @@ pub fn get_timezone(db_connection: &PgConnection, chat_id: i64) -> String { } pub fn get_template(db_connection: &PgConnection, chat_id: i64, feed_url: String) -> String { - let not_exists_error = "Subscription does not exist".to_string(); - let feed = match feeds::find_by_link(db_connection, feed_url) { - Some(feed) => feed, - None => return not_exists_error, - }; - - let chat = match telegram::find_chat(db_connection, chat_id) { - Some(chat) => chat, - None => return not_exists_error, - }; - - let telegram_subscription = NewTelegramSubscription { - chat_id: chat.id, - feed_id: feed.id, - }; - - let subscription = match telegram::find_subscription(db_connection, telegram_subscription) { - Some(subscription) => subscription, - None => return not_exists_error, - }; - - match subscription.template { - None => "You did not set a template for this subcription".to_string(), - Some(template) => template, + match find_subscription(db_connection, chat_id, feed_url) { + Err(message) => message, + Ok(subscription) => match subscription.template { + None => "You did not set a template for this subcription".to_string(), + Some(template) => template, + }, } } pub fn set_template(db_connection: &PgConnection, chat_id: i64, params: String) -> String { - let not_exists_error = "Subscription does not exist".to_string(); let vec: Vec<&str> = params.split(' ').collect(); if vec.len() != 2 { return "Wrong number of parameters".to_string(); } - let feed = match feeds::find_by_link(db_connection, vec[0].to_string()) { - Some(feed) => feed, - None => return not_exists_error, - }; - - let chat = match telegram::find_chat(db_connection, chat_id) { - Some(chat) => chat, - None => return not_exists_error, - }; - - let telegram_subscription = NewTelegramSubscription { - chat_id: chat.id, - feed_id: feed.id, - }; - - let subscription = match telegram::find_subscription(db_connection, telegram_subscription) { - Some(subscription) => subscription, - None => return not_exists_error, - }; - if vec[1] == "" { return "Template can not be empty".to_string(); } + let subscription = match find_subscription(db_connection, chat_id, vec[0].to_string()) { + Err(message) => return message, + Ok(subscription) => subscription, + }; + match parse_template_and_send_example(vec[1].to_string()) { Ok((template, example)) => { match telegram::set_template(db_connection, &subscription, template) { @@ -155,6 +121,55 @@ pub fn set_template(db_connection: &PgConnection, chat_id: i64, params: String) } } +pub fn get_filter(db_connection: &PgConnection, chat_id: i64, feed_url: String) -> String { + match find_subscription(db_connection, chat_id, feed_url) { + Err(message) => message, + Ok(subscription) => match subscription.filter_words { + None => "You did not set a filter for this subcription".to_string(), + Some(filter_words) => filter_words.join(", "), + }, + } +} + +pub fn remove_filter(db_connection: &PgConnection, chat_id: i64, feed_url: String) -> String { + let subscription = match find_subscription(db_connection, chat_id, feed_url) { + Err(message) => return message, + Ok(subscription) => subscription, + }; + + match telegram::set_filter(db_connection, &subscription, None) { + Ok(_) => "The filter was removed".to_string(), + Err(_) => "Failed to update the filter".to_string(), + } +} + +pub fn set_filter(db_connection: &PgConnection, chat_id: i64, params: String) -> String { + let vec: Vec<&str> = params.split(' ').collect(); + + if vec.len() != 2 { + return "Wrong number of parameters".to_string(); + } + + if vec[1] == "" { + return "Filter can not be empty".to_string(); + } + + let subscription = match find_subscription(db_connection, chat_id, vec[0].to_string()) { + Err(message) => return message, + Ok(subscription) => subscription, + }; + + let filter_words: Vec = vec[1] + .split(",") + .map(|s| s.trim().to_lowercase().to_string()) + .collect(); + + match telegram::set_filter(db_connection, &subscription, Some(filter_words.clone())) { + Ok(_) => format!("The filter was updated:\n\n{}", filter_words.join(", ")).to_string(), + Err(_) => "Failed to update the filter".to_string(), + } +} + pub fn set_global_template(db_connection: &PgConnection, chat_id: i64, template: String) -> String { if template == "".to_string() { return "Template can not be empty".to_string(); @@ -191,6 +206,33 @@ pub fn get_global_template(db_connection: &PgConnection, chat_id: i64) -> String } } +fn find_subscription( + db_connection: &PgConnection, + chat_id: i64, + feed_url: String, +) -> Result { + let not_exists_error = Err("Subscription does not exist".to_string()); + let feed = match feeds::find_by_link(db_connection, feed_url) { + Some(feed) => feed, + None => return not_exists_error, + }; + + let chat = match telegram::find_chat(db_connection, chat_id) { + Some(chat) => chat, + None => return not_exists_error, + }; + + let telegram_subscription = NewTelegramSubscription { + chat_id: chat.id, + feed_id: feed.id, + }; + + match telegram::find_subscription(db_connection, telegram_subscription) { + Some(subscription) => Ok(subscription), + None => return not_exists_error, + } +} + fn parse_template_and_send_example(raw_template: String) -> Result<(String, String), String> { let mut data = Map::new(); data.insert("bot_feed_name".to_string(), to_json("feed_name")); diff --git a/src/db/telegram.rs b/src/db/telegram.rs index a208845c..8c386295 100644 --- a/src/db/telegram.rs +++ b/src/db/telegram.rs @@ -86,6 +86,16 @@ pub fn set_template( .get_result::(conn) } +pub fn set_filter( + conn: &PgConnection, + chat: &TelegramSubscription, + filter_words: Option>, +) -> Result { + diesel::update(chat) + .set(telegram_subscriptions::filter_words.eq(filter_words)) + .get_result::(conn) +} + pub fn create_subscription( conn: &PgConnection, subscription: NewTelegramSubscription, @@ -816,6 +826,36 @@ mod tests { }); } + #[test] + fn set_filter_sets_filter() { + let connection = db::establish_connection(); + + connection.test_transaction::<(), Error, _>(|| { + let new_chat = build_new_chat(); + let chat = super::create_chat(&connection, new_chat).unwrap(); + let feed = feeds::create(&connection, "Link".to_string(), "rss".to_string()).unwrap(); + + let new_subscription = NewTelegramSubscription { + feed_id: feed.id, + chat_id: chat.id, + }; + + let subscription = + super::create_subscription(&connection, new_subscription.clone()).unwrap(); + + assert_eq!(subscription.filter_words, None); + + let filter = vec!["filter1".to_string(), "filter2".to_string()]; + + let updated_subscription = + super::set_filter(&connection, &subscription, Some(filter.clone())).unwrap(); + + assert_eq!(updated_subscription.filter_words, Some(filter)); + + Ok(()) + }); + } + #[test] fn find_chats_by_feed_id_find_chats() { let connection = db::establish_connection(); diff --git a/src/models/telegram_subscription.rs b/src/models/telegram_subscription.rs index af4a195e..fd50d0cd 100644 --- a/src/models/telegram_subscription.rs +++ b/src/models/telegram_subscription.rs @@ -13,4 +13,5 @@ pub struct TelegramSubscription { pub updated_at: DateTime, pub template: Option, + pub filter_words: Option>, } diff --git a/src/schema.rs b/src/schema.rs index a35e096b..a45cefe2 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -49,6 +49,7 @@ table! { created_at -> Timestamptz, updated_at -> Timestamptz, template -> Nullable, + filter_words -> Nullable>, } }