diff --git a/Cargo.lock b/Cargo.lock index 91030f00..f0fb1355 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,9 +300,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90d59d9acd2a682b4e40605a242f6670eaa58c5957471cbf85e8aa6a0b97a5e8" +checksum = "86d3488e7665a7a483b57e25bdd90d0aeb2bc7608c8d0346acf2ad3f1caf1d62" dependencies = [ "cc", "cxxbridge-flags", @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebfa40bda659dd5c864e65f4c9a2b0aff19bea56b017b9b77c73d3766a453a38" +checksum = "48fcaf066a053a41a81dfb14d57d99738b767febb8b735c3016e469fac5da690" dependencies = [ "cc", "codespan-reporting", @@ -327,15 +327,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "457ce6757c5c70dc6ecdbda6925b958aae7f959bda7d8fb9bde889e34a09dc03" +checksum = "a2ef98b8b717a829ca5603af80e1f9e2e48013ab227b68ef37872ef84ee479bf" [[package]] name = "cxxbridge-macro" -version = "1.0.90" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebf883b7aacd7b2aeb2a7b338648ee19f57c140d4ee8e52c68979c6b2f7f2263" +checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" dependencies = [ "proc-macro2", "quote", @@ -857,9 +857,9 @@ checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", @@ -1136,9 +1136,9 @@ dependencies = [ [[package]] name = "mockito" -version = "0.32.2" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05f934fadad9733e80709888e3f9e80db85965509596f7ef1e93b1d0789d4e7" +checksum = "09908a0c0e8c956c1360268b54c0f7eeefc7ef98020cfb81077fa71761a18d29" dependencies = [ "assert-json-diff", "async-trait", diff --git a/migrations/2023-02-18-045257_add_thread_id_to_subscriptions/down.sql b/migrations/2023-02-18-045257_add_thread_id_to_subscriptions/down.sql new file mode 100644 index 00000000..efbf089c --- /dev/null +++ b/migrations/2023-02-18-045257_add_thread_id_to_subscriptions/down.sql @@ -0,0 +1 @@ +ALTER TABLE telegram_subscriptions DROP COLUMN thread_id; diff --git a/migrations/2023-02-18-045257_add_thread_id_to_subscriptions/up.sql b/migrations/2023-02-18-045257_add_thread_id_to_subscriptions/up.sql new file mode 100644 index 00000000..cadd9d96 --- /dev/null +++ b/migrations/2023-02-18-045257_add_thread_id_to_subscriptions/up.sql @@ -0,0 +1 @@ +ALTER TABLE telegram_subscriptions ADD COLUMN thread_id integer; diff --git a/src/bot/commands.rs b/src/bot/commands.rs index 56569e0a..28da912b 100644 --- a/src/bot/commands.rs +++ b/src/bot/commands.rs @@ -324,6 +324,7 @@ pub trait Command { let message_params = SimpleMessageParams::builder() .message(text) .chat_id(message.chat.id) + .message_thread_id(message.message_thread_id) .build(); if let Err(error) = self.api().reply_with_text_message(&message_params) { @@ -359,7 +360,7 @@ pub trait Command { self.api().remove_message(message) } - fn simple_keyboard(&self, message: String, back_command: String, chat_id: i64) -> Response { + fn simple_keyboard(&self, text: String, back_command: String, message: &Message) -> Response { let mut buttons: Vec> = Vec::new(); let mut row: Vec = Vec::new(); @@ -376,13 +377,15 @@ pub trait Command { .inline_keyboard(buttons) .build(); - let params = SendMessageParams::builder() - .chat_id(chat_id) + let mut params = SendMessageParams::builder() + .chat_id(message.chat.id) .disable_web_page_preview(true) - .text(message) + .text(text) .reply_markup(ReplyMarkup::InlineKeyboardMarkup(keyboard)) .build(); + params.message_thread_id = message.message_thread_id; + Response::Params(params) } @@ -441,7 +444,10 @@ pub trait Command { chat_id: i64, feed_id: i64, ) -> Option { - let telegram_subscription = NewTelegramSubscription { chat_id, feed_id }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat_id) + .feed_id(feed_id) + .build(); telegram::find_subscription(db_connection, telegram_subscription) } diff --git a/src/bot/commands/commands_keyboard.rs b/src/bot/commands/commands_keyboard.rs index a331b4ec..8c1edb6e 100644 --- a/src/bot/commands/commands_keyboard.rs +++ b/src/bot/commands/commands_keyboard.rs @@ -91,11 +91,15 @@ impl CommandsKeyboard { .inline_keyboard(buttons) .build(); - SendMessageParams::builder() + let mut params = SendMessageParams::builder() .chat_id(self.message.chat.id) .text("Select a command") .reply_markup(ReplyMarkup::InlineKeyboardMarkup(keyboard)) - .build() + .build(); + + params.message_thread_id = self.message.message_thread_id; + + params } } diff --git a/src/bot/commands/get_filter.rs b/src/bot/commands/get_filter.rs index 6cbbed91..9b398b40 100644 --- a/src/bot/commands/get_filter.rs +++ b/src/bot/commands/get_filter.rs @@ -40,7 +40,7 @@ impl GetFilter { ShowFeedKeyboard::command(), subscription.external_id ), - self.message.chat.id, + &self.message, ) } else { Response::Simple(response) diff --git a/src/bot/commands/get_template.rs b/src/bot/commands/get_template.rs index 2d709cea..ed9132f6 100644 --- a/src/bot/commands/get_template.rs +++ b/src/bot/commands/get_template.rs @@ -46,7 +46,7 @@ impl Command for GetTemplate { self.simple_keyboard( response, format!("{} {}", ShowFeedKeyboard::command(), self.args), - self.message.chat.id, + &self.message, ) } else { Response::Simple(response) diff --git a/src/bot/commands/help.rs b/src/bot/commands/help.rs index 0655b633..73a12bce 100644 --- a/src/bot/commands/help.rs +++ b/src/bot/commands/help.rs @@ -202,12 +202,16 @@ impl Help { .inline_keyboard(buttons) .build(); - SendMessageParams::builder() + let mut params = SendMessageParams::builder() .chat_id(self.message.chat.id) .text("In private chats use keyboards to interact with the bot. Send /commands to display the keyboard. \n\nIn channels and groups you will have to type commands directly.\n\nJoin https://t.me/el_monitorro with your feedback, suggestions, found bugs, etc.\n\nSelect a command:") .reply_markup(ReplyMarkup::InlineKeyboardMarkup(keyboard)) .disable_web_page_preview(true) - .build() + .build(); + + params.message_thread_id = self.message.message_thread_id; + + params } pub fn button_row() -> Vec { diff --git a/src/bot/commands/help_command_info.rs b/src/bot/commands/help_command_info.rs index 8c83c1a3..0dc43083 100644 --- a/src/bot/commands/help_command_info.rs +++ b/src/bot/commands/help_command_info.rs @@ -104,11 +104,7 @@ impl Command for HelpCommandInfo { fn response(&self) -> Response { let help_for_command = self.command_info(); - self.simple_keyboard( - help_for_command, - Help::command().to_string(), - self.message.chat.id, - ) + self.simple_keyboard(help_for_command, Help::command().to_string(), &self.message) } fn send_message(&self, send_message_params: SendMessageParams) { diff --git a/src/bot/commands/list_subscriptions_keyboard.rs b/src/bot/commands/list_subscriptions_keyboard.rs index db9fa5fd..24a46ae3 100644 --- a/src/bot/commands/list_subscriptions_keyboard.rs +++ b/src/bot/commands/list_subscriptions_keyboard.rs @@ -66,11 +66,15 @@ impl ListSubscriptionsKeyboard { .inline_keyboard(buttons) .build(); - SendMessageParams::builder() + let mut params = SendMessageParams::builder() .chat_id(self.message.chat.id) .text(message) .reply_markup(ReplyMarkup::InlineKeyboardMarkup(keyboard)) - .build() + .build(); + + params.message_thread_id = self.message.message_thread_id; + + params } pub fn command() -> &'static str { diff --git a/src/bot/commands/remove_filter.rs b/src/bot/commands/remove_filter.rs index 929424f4..76484c08 100644 --- a/src/bot/commands/remove_filter.rs +++ b/src/bot/commands/remove_filter.rs @@ -50,7 +50,7 @@ impl Command for RemoveFilter { self.simple_keyboard( response, format!("{} {}", ShowFeedKeyboard::command(), self.args), - self.message.chat.id, + &self.message, ) } else { Response::Simple(response) diff --git a/src/bot/commands/remove_template.rs b/src/bot/commands/remove_template.rs index 05b3b6a5..c1197539 100644 --- a/src/bot/commands/remove_template.rs +++ b/src/bot/commands/remove_template.rs @@ -50,7 +50,7 @@ impl Command for RemoveTemplate { self.simple_keyboard( response, format!("{} {}", ShowFeedKeyboard::command(), self.args), - self.message.chat.id, + &self.message, ) } else { Response::Simple(response) diff --git a/src/bot/commands/show_feed_keyboard.rs b/src/bot/commands/show_feed_keyboard.rs index 5e30cecf..0265a55f 100644 --- a/src/bot/commands/show_feed_keyboard.rs +++ b/src/bot/commands/show_feed_keyboard.rs @@ -117,11 +117,15 @@ impl ShowFeedKeyboard { .inline_keyboard(buttons) .build(); - SendMessageParams::builder() + let mut params = SendMessageParams::builder() .chat_id(self.message.chat.id) .text(feed.link) .reply_markup(ReplyMarkup::InlineKeyboardMarkup(keyboard)) - .build() + .build(); + + params.message_thread_id = self.message.message_thread_id; + + params } } diff --git a/src/bot/commands/subscribe.rs b/src/bot/commands/subscribe.rs index 03d01c30..9ee622e2 100644 --- a/src/bot/commands/subscribe.rs +++ b/src/bot/commands/subscribe.rs @@ -72,10 +72,11 @@ impl Subscribe { telegram::create_chat(db_connection, (*self.message.chat.clone()).into()).unwrap(); let feed = feeds::create(db_connection, &self.args, feed_type).unwrap(); - let new_telegram_subscription = NewTelegramSubscription { - chat_id: chat.id, - feed_id: feed.id, - }; + let new_telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .thread_id(self.message.message_thread_id) + .build(); self.check_if_subscription_exists(db_connection, new_telegram_subscription)?; self.check_number_of_subscriptions(db_connection, chat.id)?; diff --git a/src/bot/commands/unknown_command.rs b/src/bot/commands/unknown_command.rs index d9968cff..e504f6f5 100644 --- a/src/bot/commands/unknown_command.rs +++ b/src/bot/commands/unknown_command.rs @@ -59,13 +59,15 @@ impl Command for UnknownCommand { .inline_keyboard(buttons) .build(); - let params = SendMessageParams::builder() + let mut params = SendMessageParams::builder() .chat_id(self.message.chat.id) .text(text) .reply_markup(ReplyMarkup::InlineKeyboardMarkup(keyboard)) .reply_to_message_id(message.message_id) .build(); + params.message_thread_id = message.message_thread_id; + self.send_message(params) } } diff --git a/src/bot/commands/unsubscribe.rs b/src/bot/commands/unsubscribe.rs index 9f269be8..aa7cf6cc 100644 --- a/src/bot/commands/unsubscribe.rs +++ b/src/bot/commands/unsubscribe.rs @@ -51,10 +51,10 @@ impl Unsubscribe { let feed = feeds::find(db_connection, subscription.feed_id).unwrap(); - let telegram_subscription = NewTelegramSubscription { - chat_id: self.message.chat.id, - feed_id: feed.id, - }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(self.message.chat.id) + .feed_id(feed.id) + .build(); match telegram::remove_subscription(db_connection, telegram_subscription) { Ok(_) => Ok(feed.link), @@ -78,7 +78,7 @@ impl Command for Unsubscribe { self.simple_keyboard( response, ListSubscriptionsKeyboard::command().to_string(), - self.message.chat.id, + &self.message, ) } else { Response::Simple(response) @@ -120,10 +120,10 @@ mod unsubscribe_tests { let chat = telegram::create_chat(connection, new_chat).unwrap(); let feed = feeds::create(connection, &link, "rss".to_string()).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }; + let new_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(); telegram::create_subscription(connection, new_subscription).unwrap(); diff --git a/src/bot/telegram_client.rs b/src/bot/telegram_client.rs index 9e741ee2..a6a187a4 100644 --- a/src/bot/telegram_client.rs +++ b/src/bot/telegram_client.rs @@ -62,6 +62,8 @@ pub struct SimpleMessageParams { reply_message_id: Option, #[builder(default = false)] preview_enabled: bool, + #[builder(default, setter(into))] + message_thread_id: Option, } impl Api { @@ -116,19 +118,17 @@ impl Api { &self, simple_params: &SimpleMessageParams, ) -> Result<(), Error> { - let message_params = SendMessageParams::builder() + let mut message_params = SendMessageParams::builder() .chat_id(simple_params.chat_id) .text(simple_params.message.clone()) .disable_web_page_preview(!simple_params.preview_enabled) - .parse_mode(ParseMode::Html); - - let send_message_params = match simple_params.reply_message_id { - None => message_params.build(), + .parse_mode(ParseMode::Html) + .build(); - Some(message_id_value) => message_params.reply_to_message_id(message_id_value).build(), - }; + message_params.reply_to_message_id = simple_params.reply_message_id; + message_params.message_thread_id = simple_params.message_thread_id; - self.send_message_with_params(&send_message_params) + self.send_message_with_params(&message_params) } pub fn send_message_with_params( diff --git a/src/db/feeds.rs b/src/db/feeds.rs index e264bf1a..5a72fd7f 100644 --- a/src/db/feeds.rs +++ b/src/db/feeds.rs @@ -792,10 +792,10 @@ mod tests { }; let chat = telegram::create_chat(connection, new_chat).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }; + let new_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(); telegram::create_subscription(connection, new_subscription).unwrap() } diff --git a/src/db/telegram.rs b/src/db/telegram.rs index 63dccb3f..5fd3c1de 100644 --- a/src/db/telegram.rs +++ b/src/db/telegram.rs @@ -12,6 +12,7 @@ use diesel::prelude::*; use diesel::result::Error; use diesel::sql_types::BigInt; use diesel::{ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl}; +use typed_builder::TypedBuilder; use uuid::Uuid; #[derive(Insertable, Clone, Debug)] @@ -25,11 +26,14 @@ pub struct NewTelegramChat { pub last_name: Option, } -#[derive(Insertable, Clone, Copy, Debug)] +#[derive(Insertable, Clone, Copy, Debug, TypedBuilder)] #[diesel(table_name = telegram_subscriptions)] pub struct NewTelegramSubscription { pub chat_id: i64, pub feed_id: i64, + + #[builder(default)] + pub thread_id: Option, } pub fn create_chat( @@ -450,13 +454,13 @@ mod tests { let feed = feeds::create(connection, "Link", "rss".to_string()).unwrap(); let chat = super::create_chat(connection, new_chat).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(); let new_subscription = - super::create_subscription(connection, new_subscription).unwrap(); + super::create_subscription(connection, telegram_subscription).unwrap(); assert_eq!(new_subscription.feed_id, feed.id); assert_eq!(new_subscription.chat_id, chat.id); @@ -475,23 +479,23 @@ mod tests { let feed = feeds::create(connection, "Link", "atom".to_string()).unwrap(); let chat = super::create_chat(connection, new_chat).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(); let new_subscription = - super::create_subscription(connection, new_subscription).unwrap(); + super::create_subscription(connection, telegram_subscription).unwrap(); assert_eq!(new_subscription.feed_id, feed.id); assert_eq!(new_subscription.chat_id, chat.id); let result = super::create_subscription( connection, - NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }, + NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(), ); match result.err().unwrap() { @@ -515,10 +519,10 @@ mod tests { let result = super::create_subscription( connection, - NewTelegramSubscription { - feed_id: feed.id, - chat_id: 42, - }, + NewTelegramSubscription::builder() + .chat_id(42) + .feed_id(feed.id) + .build(), ); match result.err().unwrap() { @@ -543,23 +547,23 @@ mod tests { let feed = feeds::create(connection, "Link11111", "rss".to_string()).unwrap(); let chat = super::create_chat(connection, new_chat).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(); let new_subscription = - super::create_subscription(connection, new_subscription).unwrap(); + super::create_subscription(connection, telegram_subscription).unwrap(); assert_eq!(new_subscription.feed_id, feed.id); assert_eq!(new_subscription.chat_id, chat.id); let result = super::find_subscription( connection, - NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }, + NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(), ) .unwrap(); @@ -577,10 +581,10 @@ mod tests { connection.test_transaction::<(), Error, _>(|connection| { let result = super::find_subscription( connection, - NewTelegramSubscription { - feed_id: 42, - chat_id: 42, - }, + NewTelegramSubscription::builder() + .chat_id(42) + .feed_id(42) + .build(), ); assert!(result.is_none()); @@ -598,12 +602,12 @@ mod tests { let chat = super::create_chat(connection, new_chat).unwrap(); let feed = feeds::create(connection, "Link99", "rss".to_string()).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(); - super::create_subscription(connection, new_subscription).unwrap(); + super::create_subscription(connection, telegram_subscription).unwrap(); let result = super::fetch_chats_with_subscriptions(connection, 1, 1).unwrap(); @@ -638,19 +642,19 @@ mod tests { let feed = feeds::create(connection, "Link98", "atom".to_string()).unwrap(); let chat1 = super::create_chat(connection, build_new_chat_with_id(10)).unwrap(); - let new_subscription1 = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat1.id, - }; + let new_subscription1 = NewTelegramSubscription::builder() + .chat_id(chat1.id) + .feed_id(feed.id) + .build(); super::create_subscription(connection, new_subscription1).unwrap(); let chat2 = super::create_chat(connection, build_new_chat_with_id(20)).unwrap(); - let new_subscription2 = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat2.id, - }; + let new_subscription2 = NewTelegramSubscription::builder() + .chat_id(chat2.id) + .feed_id(feed.id) + .build(); super::create_subscription(connection, new_subscription2).unwrap(); @@ -675,17 +679,17 @@ mod tests { let feed2 = feeds::create(connection, "Link96", "atom".to_string()).unwrap(); let chat = super::create_chat(connection, build_new_chat()).unwrap(); - let new_subscription1 = NewTelegramSubscription { - feed_id: feed1.id, - chat_id: chat.id, - }; + let new_subscription1 = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed1.id) + .build(); super::create_subscription(connection, new_subscription1).unwrap(); - let new_subscription2 = NewTelegramSubscription { - feed_id: feed2.id, - chat_id: chat.id, - }; + let new_subscription2 = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed2.id) + .build(); super::create_subscription(connection, new_subscription2).unwrap(); @@ -711,12 +715,12 @@ mod tests { let feed = feeds::create(connection, "Link", "atom".to_string()).unwrap(); let chat = super::create_chat(connection, new_chat).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(); - super::create_subscription(connection, new_subscription).unwrap(); + super::create_subscription(connection, telegram_subscription).unwrap(); let result = super::count_subscriptions_for_chat(connection, chat.id); @@ -738,19 +742,19 @@ mod tests { let feed2 = feeds::create(connection, "Link79", "atom".to_string()).unwrap(); let chat = super::create_chat(connection, new_chat).unwrap(); - let new_subscription1 = NewTelegramSubscription { - feed_id: feed1.id, - chat_id: chat.id, - }; + let new_telegram_subscription1 = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed1.id) + .build(); - super::create_subscription(connection, new_subscription1).unwrap(); + super::create_subscription(connection, new_telegram_subscription1).unwrap(); - let new_subscription2 = NewTelegramSubscription { - feed_id: feed2.id, - chat_id: chat.id, - }; + let new_telegram_subscription2 = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed2.id) + .build(); - super::create_subscription(connection, new_subscription2).unwrap(); + super::create_subscription(connection, new_telegram_subscription2).unwrap(); let result = super::find_unread_subscriptions_for_chat(connection, chat.id).unwrap(); @@ -772,12 +776,12 @@ mod tests { let chat2 = super::create_chat(connection, build_new_chat_with_id(89)).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat1.id, - }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat1.id) + .feed_id(feed.id) + .build(); - super::create_subscription(connection, new_subscription).unwrap(); + super::create_subscription(connection, telegram_subscription).unwrap(); let result = super::find_unread_subscriptions_for_chat(connection, chat2.id).unwrap(); @@ -797,12 +801,13 @@ mod tests { let feed = feeds::create(connection, "Link", "rss".to_string()).unwrap(); let chat = super::create_chat(connection, new_chat).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(); - let subscription = super::create_subscription(connection, new_subscription).unwrap(); + let subscription = + super::create_subscription(connection, telegram_subscription).unwrap(); assert!(subscription.last_delivered_at.is_none()); @@ -829,14 +834,14 @@ mod tests { let feed = feeds::create(connection, "Link", "rss".to_string()).unwrap(); let chat = super::create_chat(connection, new_chat).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(); - super::create_subscription(connection, new_subscription).unwrap(); + super::create_subscription(connection, telegram_subscription).unwrap(); - let result = super::remove_subscription(connection, new_subscription).unwrap(); + let result = super::remove_subscription(connection, telegram_subscription).unwrap(); assert_eq!(result, 1); @@ -930,12 +935,13 @@ mod tests { let chat = super::create_chat(connection, new_chat).unwrap(); let feed = feeds::create(connection, "Link two", "rss".to_string()).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(); - let subscription = super::create_subscription(connection, new_subscription).unwrap(); + let subscription = + super::create_subscription(connection, telegram_subscription).unwrap(); assert_eq!(subscription.template, None); @@ -961,12 +967,13 @@ mod tests { let chat = super::create_chat(connection, new_chat).unwrap(); let feed = feeds::create(connection, "Link one", "rss".to_string()).unwrap(); - let new_subscription = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat.id, - }; + let telegram_subscription = NewTelegramSubscription::builder() + .chat_id(chat.id) + .feed_id(feed.id) + .build(); - let subscription = super::create_subscription(connection, new_subscription).unwrap(); + let subscription = + super::create_subscription(connection, telegram_subscription).unwrap(); assert_eq!(subscription.filter_words, None); @@ -993,17 +1000,17 @@ mod tests { let chat1 = super::create_chat(connection, new_chat1).unwrap(); let chat2 = super::create_chat(connection, new_chat2).unwrap(); - let new_subscription1 = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat1.id, - }; + let new_subscription1 = NewTelegramSubscription::builder() + .chat_id(chat1.id) + .feed_id(feed.id) + .build(); super::create_subscription(connection, new_subscription1).unwrap(); - let new_subscription2 = NewTelegramSubscription { - feed_id: feed.id, - chat_id: chat2.id, - }; + let new_subscription2 = NewTelegramSubscription::builder() + .chat_id(chat2.id) + .feed_id(feed.id) + .build(); super::create_subscription(connection, new_subscription2).unwrap(); @@ -1029,26 +1036,26 @@ mod tests { let chat1 = super::create_chat(connection, new_chat1).unwrap(); let chat2 = super::create_chat(connection, new_chat2).unwrap(); - let new_subscription1 = NewTelegramSubscription { - feed_id: feed1.id, - chat_id: chat1.id, - }; + let new_subscription1 = NewTelegramSubscription::builder() + .chat_id(chat1.id) + .feed_id(feed1.id) + .build(); let subscription1 = super::create_subscription(connection, new_subscription1).unwrap(); super::mark_subscription_delivered(connection, &subscription1).unwrap(); - let new_subscription2 = NewTelegramSubscription { - feed_id: feed1.id, - chat_id: chat2.id, - }; + let new_subscription2 = NewTelegramSubscription::builder() + .chat_id(chat2.id) + .feed_id(feed1.id) + .build(); let subscription2 = super::create_subscription(connection, new_subscription2).unwrap(); super::mark_subscription_delivered(connection, &subscription2).unwrap(); - let new_subscription3 = NewTelegramSubscription { - feed_id: feed2.id, - chat_id: chat2.id, - }; + let new_subscription3 = NewTelegramSubscription::builder() + .chat_id(chat2.id) + .feed_id(feed2.id) + .build(); let subscription3 = super::create_subscription(connection, new_subscription3).unwrap(); super::mark_subscription_delivered(connection, &subscription3).unwrap(); diff --git a/src/deliver/deliver_chat_updates_job.rs b/src/deliver/deliver_chat_updates_job.rs index c6df959d..b5cbf63f 100644 --- a/src/deliver/deliver_chat_updates_job.rs +++ b/src/deliver/deliver_chat_updates_job.rs @@ -18,6 +18,7 @@ use fang::Queueable; use fang::Runnable; use serde::{Deserialize, Serialize}; use std::time::Duration; +use typed_builder::TypedBuilder; const TELEGRAM_ERRORS: [&str; 15] = [ "Bad Request: CHAT_WRITE_FORBIDDEN", @@ -37,7 +38,7 @@ const TELEGRAM_ERRORS: [&str; 15] = [ "Forbidden: user is deactivated", ]; -const MESSAGES_LIMIT: i64 = 10; +const MESSAGES_LIMIT: usize = 10; const JOB_TYPE: &str = "deliver"; #[derive(Debug)] @@ -53,111 +54,173 @@ impl From for DeliverJobError { } } -#[derive(Serialize, Deserialize)] -pub struct DeliverChatUpdatesJob { - pub chat_id: i64, +#[derive(TypedBuilder)] +pub struct DeliverChatUpdates<'a> { + chat: TelegramChat, + feed: Feed, + subscription: TelegramSubscription, + db_connection: &'a mut PgConnection, + api: &'a Api, } -impl DeliverChatUpdatesJob { - pub fn new(chat_id: i64) -> Self { - Self { chat_id } +impl<'a> DeliverChatUpdates<'a> { + pub fn deliver(&mut self) -> Result<(), DeliverJobError> { + let feed_items = telegram::find_undelivered_feed_items( + self.db_connection, + &self.subscription, + MESSAGES_LIMIT as i64, + )?; + + if feed_items.is_empty() { + return Ok(()); + } + + let filter_words = self.filter_words(); + + if filter_words.is_none() { + self.maybe_send_unread_messages_count(feed_items.len())?; + } + + let formatted_messages = self.format_messages(feed_items); + + match self.filter_words() { + None => self.send_messages_without_filter(formatted_messages), + + Some(words) => self.send_messages_with_filter(words, formatted_messages), + } } - pub fn deliver(&self, db_connection: &mut PgConnection) -> Result<(), FangError> { - let subscriptions = - telegram::find_unread_subscriptions_for_chat(db_connection, self.chat_id)?; - let api = telegram_client::api(); + fn filter_words(&self) -> Option> { + if self.chat.filter_words.is_some() { + return self.chat.filter_words.clone(); + } - for subscription in subscriptions { - match self.deliver_subscription_updates(&subscription, db_connection, api) { - Ok(()) => { - telegram::mark_subscription_delivered(db_connection, &subscription)?; - } + self.subscription.filter_words.clone() + } - Err(error) => { - log::error!( - "Failed to deliver updates for subscription: {subscription:?} {error:?}", - ); - break; - } - } + fn maybe_send_unread_messages_count( + &mut self, + feed_items_count: usize, + ) -> Result<(), DeliverJobError> { + let undelivered_count = + telegram::count_undelivered_feed_items(self.db_connection, &self.subscription); + + if self.chat.kind == "channel" { + return Ok(()); + } + + if feed_items_count == MESSAGES_LIMIT && undelivered_count > MESSAGES_LIMIT as i64 { + let message = format!("You have {undelivered_count} unread items, below {feed_items_count} last items for {}", self.feed.link); + + self.send_text_message(message)?; } + Ok(()) } - fn deliver_subscription_updates( - &self, - subscription: &TelegramSubscription, - connection: &mut PgConnection, - api: &Api, - ) -> Result<(), DeliverJobError> { - let feed_items = - telegram::find_undelivered_feed_items(connection, subscription, MESSAGES_LIMIT)?; + fn send_text_message(&mut self, message: String) -> Result<(), DeliverJobError> { + let delay = self.delay_period(); + + let message_params = SimpleMessageParams::builder() + .message(message) + .chat_id(self.chat.id) + .preview_enabled(self.chat.preview_enabled) + .message_thread_id(self.subscription.thread_id) + .build(); - let chat_id = subscription.chat_id; - let feed = feeds::find(connection, subscription.feed_id).ok_or(DeliverJobError { - msg: "Sub not found :(".to_string(), - })?; + match self.api.reply_with_text_message(&message_params) { + Ok(_) => { + std::thread::sleep(delay); + Ok(()) + } - let chat = telegram::find_chat(connection, chat_id).ok_or(DeliverJobError { - msg: "Chat not found :(".to_string(), - })?; - let filter_words = fetch_filter_words(&chat, subscription); + Err(error) => { + let error_message = format!("{error:?}"); - if filter_words.is_none() { - self.maybe_send_unread_messages_count( - subscription, - connection, - feed_items.len() as i64, - feed.link.clone(), - api, - &chat, - )?; + Err(self.handle_error(error_message)) + } } + } - if !feed_items.is_empty() { - let template = match subscription.template.clone() { - Some(template) => Some(template), - None => chat.template.clone(), - }; - - let messages = format_messages(template, chat.utc_offset_minutes, feed_items, feed); - - match filter_words { - None => { - for (message, publication_date) in messages { - self.send_text_message_and_updated_subscription( - subscription, - message, - connection, - &chat, - api, - publication_date, - )? - } - } - Some(words) => self.send_messages_with_filter( - words, - messages, - connection, - subscription, - api, - &chat, - )?, + fn delay_period(&self) -> Duration { + match self.chat.kind.as_str() { + "group" | "supergroup" => Duration::from_millis(2200), + _ => Duration::from_millis(35), + } + } + + fn handle_error(&mut self, error: String) -> DeliverJobError { + log::error!("Failed to deliver updates: {}", error); + + if self.bot_blocked(&error) { + match telegram::remove_chat(self.db_connection, self.chat.id) { + Ok(_) => log::info!("Successfully removed chat {}", self.chat.id), + Err(error) => log::error!("Failed to remove a chat {error}"), } + }; + + DeliverJobError { + msg: format!("Failed to send updates : {error}"), + } + } + + fn bot_blocked(&self, error_message: &str) -> bool { + TELEGRAM_ERRORS + .iter() + .any(|&message| error_message.contains(message)) + } + + fn format_messages(&self, feed_items: Vec) -> Vec<(String, DateTime)> { + let template = match &self.subscription.template { + Some(template) => Some(template.clone()), + None => self.chat.template.clone(), + }; + + let message_renderer_builder = MessageRenderer::builder() + .offset(self.chat.utc_offset_minutes) + .template(template) + .bot_feed_name(self.feed.title.clone()) + .bot_feed_link(Some(self.feed.link.clone())); + + let mut formatted_messages = feed_items + .iter() + .map(|item| { + let message_renderer = message_renderer_builder + .clone() + .bot_date(item.publication_date) + .bot_item_name(item.title.clone()) + .bot_item_link(item.link.clone()) + .bot_item_description(item.description.clone()) + .bot_item_author(item.author.clone()) + .build(); + + match message_renderer.render() { + Ok(message) => (message, item.created_at), + Err(error_message) => (error_message, item.created_at), + } + }) + .collect::)>>(); + + formatted_messages.reverse(); + + formatted_messages + } + + fn send_messages_without_filter( + &mut self, + messages: Vec<(String, DateTime)>, + ) -> Result<(), DeliverJobError> { + for (message, publication_date) in messages { + self.send_text_message_and_updated_subscription(message, publication_date)? } Ok(()) } fn send_messages_with_filter( - &self, + &mut self, words: Vec, messages: Vec<(String, DateTime)>, - connection: &mut PgConnection, - subscription: &TelegramSubscription, - api: &Api, - chat: &TelegramChat, ) -> Result<(), DeliverJobError> { let (negated_words, regular_words): (Vec, Vec) = words.into_iter().partition(|word| word.starts_with('!')); @@ -172,112 +235,42 @@ impl DeliverChatUpdatesJob { let lowercase_message = message.to_lowercase(); if !regular_words.is_empty() { - mtch = check_filter_words(&lowercase_message, ®ular_words); + mtch = self.check_filter_words(&lowercase_message, ®ular_words); } if !negated_words.is_empty() { - let negated_mtch = check_filter_words(&lowercase_message, &negated_words); + let negated_mtch = self.check_filter_words(&lowercase_message, &negated_words); mtch = mtch && !negated_mtch; } if mtch { - self.send_text_message_and_updated_subscription( - subscription, - message, - connection, - chat, - api, - publication_date, - )?; + self.send_text_message_and_updated_subscription(message, publication_date)?; } else { - self.update_last_deivered_at(connection, subscription, publication_date)?; + self.update_last_deivered_at(publication_date)?; } } Ok(()) } - fn maybe_send_unread_messages_count( - &self, - subscription: &TelegramSubscription, - connection: &mut PgConnection, - feed_items_count: i64, - feed_link: String, - api: &Api, - chat: &TelegramChat, - ) -> Result<(), DeliverJobError> { - let undelivered_count = telegram::count_undelivered_feed_items(connection, subscription); - - if chat.kind == "channel" { - return Ok(()); - } - - if subscription.filter_words.is_some() { - return Ok(()); - } - - if feed_items_count == MESSAGES_LIMIT && undelivered_count > MESSAGES_LIMIT { - let message = format!("You have {undelivered_count} unread items, below {feed_items_count} last items for {feed_link}"); - - self.send_text_message(chat, message, connection, api)?; - } - - Ok(()) - } - - fn send_text_message( - &self, - chat: &TelegramChat, - message: String, - connection: &mut PgConnection, - api: &Api, - ) -> Result<(), DeliverJobError> { - let delay = delay_period(chat); - - let message_params = SimpleMessageParams::builder() - .message(message) - .chat_id(chat.id) - .preview_enabled(chat.preview_enabled) - .build(); - - match api.reply_with_text_message(&message_params) { - Ok(_) => { - std::thread::sleep(delay); - Ok(()) - } - - Err(error) => { - let error_message = format!("{error:?}"); - - Err(handle_error(error_message, connection, chat.id)) - } - } - } - fn send_text_message_and_updated_subscription( - &self, - subscription: &TelegramSubscription, + &mut self, message: String, - connection: &mut PgConnection, - chat: &TelegramChat, - api: &Api, publication_date: DateTime, ) -> Result<(), DeliverJobError> { - self.send_text_message(chat, message, connection, api)?; + self.send_text_message(message)?; - self.update_last_deivered_at(connection, subscription, publication_date) + self.update_last_deivered_at(publication_date) } fn update_last_deivered_at( - &self, - connection: &mut PgConnection, - subscription: &TelegramSubscription, + &mut self, publication_date: DateTime, ) -> Result<(), DeliverJobError> { match telegram::set_subscription_last_delivered_at( - connection, - subscription, + self.db_connection, + &self.subscription, publication_date, ) { Ok(_) => Ok(()), @@ -289,248 +282,85 @@ impl DeliverChatUpdatesJob { } } } -} -#[typetag::serde] -impl Runnable for DeliverChatUpdatesJob { - fn run(&self, _queue: &dyn Queueable) -> Result<(), FangError> { - let mut db_connection = crate::db::pool().get()?; - - self.deliver(&mut db_connection) - } + fn check_filter_words(&self, text: &str, words: &Vec) -> bool { + let ac = AhoCorasickBuilder::new().build(words); - fn uniq(&self) -> bool { - true + ac.find(text).is_some() } +} - fn max_retries(&self) -> i32 { - 0 - } +#[derive(Serialize, Deserialize)] +pub struct DeliverChatUpdatesJob { + pub chat_id: i64, +} - fn task_type(&self) -> String { - JOB_TYPE.to_string() +impl DeliverChatUpdatesJob { + pub fn new(chat_id: i64) -> Self { + Self { chat_id } } -} -fn handle_error(error: String, connection: &mut PgConnection, chat_id: i64) -> DeliverJobError { - log::error!("Failed to deliver updates: {}", error); + pub fn deliver(&self, db_connection: &mut PgConnection) -> Result<(), FangError> { + let chat = telegram::find_chat(db_connection, self.chat_id); - 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}"), + if chat.is_none() { + return Ok(()); } - }; - - DeliverJobError { - msg: format!("Failed to send updates : {error}"), - } -} -fn fetch_filter_words( - chat: &TelegramChat, - subscription: &TelegramSubscription, -) -> Option> { - if chat.filter_words.is_some() { - return chat.filter_words.clone(); - } + let subscriptions = + telegram::find_unread_subscriptions_for_chat(db_connection, self.chat_id)?; - subscription.filter_words.clone() -} + let api = telegram_client::api(); -fn format_messages( - template: Option, - offset: Option, - feed_items: Vec, - feed: Feed, -) -> Vec<(String, DateTime)> { - let message_renderer_builder = MessageRenderer::builder() - .offset(offset) - .template(template) - .bot_feed_name(feed.title) - .bot_feed_link(Some(feed.link)); - - let mut formatted_messages = feed_items - .iter() - .map(|item| { - let message_renderer = message_renderer_builder - .clone() - .bot_date(item.publication_date) - .bot_item_name(item.title.clone()) - .bot_item_link(item.link.clone()) - .bot_item_description(item.description.clone()) - .bot_item_author(item.author.clone()) - .build(); + for subscription in subscriptions { + let feed = feeds::find(db_connection, subscription.feed_id); - match message_renderer.render() { - Ok(message) => (message, item.created_at), - Err(error_message) => (error_message, item.created_at), + if feed.is_none() { + continue; } - }) - .collect::)>>(); - formatted_messages.reverse(); - - formatted_messages -} + let mut deliver_chat_updates = DeliverChatUpdates::builder() + .chat(chat.clone().unwrap()) + .feed(feed.unwrap()) + .subscription(subscription.clone()) + .db_connection(db_connection) + .api(api) + .build(); -fn bot_blocked(error_message: &str) -> bool { - TELEGRAM_ERRORS - .iter() - .any(|&message| error_message.contains(message)) -} + match deliver_chat_updates.deliver() { + Ok(()) => { + telegram::mark_subscription_delivered(db_connection, &subscription)?; + } -fn delay_period(chat: &TelegramChat) -> Duration { - match chat.kind.as_str() { - "group" | "supergroup" => Duration::from_millis(2200), - _ => Duration::from_millis(35), + Err(error) => { + log::error!( + "Failed to deliver updates for subscription: {subscription:?} {error:?}", + ); + break; + } + } + } + Ok(()) } } -fn check_filter_words(text: &str, words: &Vec) -> bool { - let ac = AhoCorasickBuilder::new().build(words); - - ac.find(text).is_some() -} - -#[cfg(test)] -mod tests { - use crate::db; - use crate::models::Feed; - use crate::models::FeedItem; - use chrono::{DateTime, Utc}; - - #[test] - fn format_messages_uses_default_template_if_custom_template_is_missing() { - let publication_date: DateTime = - DateTime::parse_from_rfc2822("Wed, 13 May 2020 15:54:02 EDT") - .unwrap() - .into(); - let feed_items = vec![FeedItem { - publication_date, - feed_id: 1, - title: "Title".to_string(), - description: Some("Description".to_string()), - link: "dsd".to_string(), - author: None, - guid: None, - content_hash: "".to_string(), - created_at: publication_date, - updated_at: db::current_time(), - }]; - let feed = Feed { - id: 1, - title: Some("FeedTitle".to_string()), - link: "link".to_string(), - error: None, - description: None, - synced_at: None, - created_at: db::current_time(), - updated_at: db::current_time(), - feed_type: "rss".to_string(), - sync_retries: 0, - sync_skips: 0, - content_fields: None, - }; - - let result = super::format_messages(None, Some(5), feed_items, feed); - - assert_eq!( - result[0].0, - "FeedTitle\n\nTitle\n\nDescription\n\n2020-05-13 19:59:02 +00:05\n\ndsd".to_string() - ); +#[typetag::serde] +impl Runnable for DeliverChatUpdatesJob { + fn run(&self, _queue: &dyn Queueable) -> Result<(), FangError> { + let mut db_connection = crate::db::pool().get()?; - assert_eq!(result[0].1, publication_date); + self.deliver(&mut db_connection) } - #[test] - fn format_messages_uses_custom_template() { - let publication_date: DateTime = - DateTime::parse_from_rfc2822("Wed, 13 May 2020 15:54:02 EDT") - .unwrap() - .into(); - let current_time = db::current_time(); - let feed_items = vec![FeedItem { - publication_date, - feed_id: 1, - title: "Title".to_string(), - description: Some("Description".to_string()), - link: "dsd".to_string(), - author: None, - guid: None, - content_hash: "".to_string(), - created_at: current_time, - updated_at: current_time, - }]; - - let feed = Feed { - id: 1, - title: Some("FeedTitle".to_string()), - link: "link".to_string(), - error: None, - description: None, - synced_at: None, - created_at: db::current_time(), - updated_at: db::current_time(), - feed_type: "rss".to_string(), - sync_retries: 0, - sync_skips: 0, - content_fields: None, - }; - - let result = super::format_messages(Some("{{bot_feed_name}} {{bot_feed_link}} {{bot_date}} {{bot_item_link}} {{bot_item_description}} {{bot_item_name}} {{bot_item_name}}".to_string()), Some(600), feed_items, feed); - - assert_eq!( - result[0].0, - "FeedTitle link 2020-05-14 05:54:02 +10:00 dsd Description Title Title".to_string() - ); - - assert_eq!(result[0].1, current_time); + fn uniq(&self) -> bool { + true } - #[test] - fn removes_empty_unicode_characters() { - let publication_date: DateTime = - DateTime::parse_from_rfc2822("Wed, 13 May 2020 15:54:02 EDT") - .unwrap() - .into(); - let current_time = db::current_time(); - - let feed = Feed { - id: 1, - title: Some("".to_string()), - link: "".to_string(), - error: None, - description: None, - synced_at: None, - created_at: db::current_time(), - updated_at: db::current_time(), - feed_type: "".to_string(), - sync_retries: 0, - sync_skips: 0, - content_fields: None, - }; + fn max_retries(&self) -> i32 { + 0 + } - let feed_items = vec![FeedItem { - publication_date, - feed_id: 1, - title: "".to_string(), - description: Some("\u{200b}".to_string()), - link: "".to_string(), - author: None, - guid: None, - content_hash: "".to_string(), - created_at: current_time, - updated_at: current_time, - }]; - - let result = super::format_messages(Some("{{bot_feed_name}} {{bot_feed_link}} {{bot_item_link}} {{bot_item_description}} {{bot_item_name}} {{bot_item_name}}".to_string()), Some(60), feed_items, feed); - - assert_eq!( - result[0].0, - "According to your template the message is empty. Telegram doesn't support empty messages. That's why we're sending this placeholder message.".to_string() - ); - - assert_eq!(result[0].1, current_time); + fn task_type(&self) -> String { + JOB_TYPE.to_string() } } diff --git a/src/models/telegram_chat.rs b/src/models/telegram_chat.rs index 26c8b786..b1e93400 100644 --- a/src/models/telegram_chat.rs +++ b/src/models/telegram_chat.rs @@ -1,7 +1,7 @@ use crate::schema::telegram_chats; use chrono::{DateTime, Utc}; -#[derive(Queryable, Identifiable, Debug)] +#[derive(Queryable, Identifiable, Debug, Clone)] #[diesel(table_name = telegram_chats)] #[diesel(primary_key(id))] pub struct TelegramChat { diff --git a/src/models/telegram_subscription.rs b/src/models/telegram_subscription.rs index 5d9cb38f..2604a16e 100644 --- a/src/models/telegram_subscription.rs +++ b/src/models/telegram_subscription.rs @@ -2,7 +2,7 @@ use crate::schema::telegram_subscriptions; use chrono::{DateTime, Utc}; use uuid::Uuid; -#[derive(Queryable, Identifiable, Debug)] +#[derive(Queryable, Identifiable, Debug, Clone)] #[diesel(table_name = telegram_subscriptions)] #[diesel(primary_key(chat_id, feed_id))] pub struct TelegramSubscription { @@ -17,4 +17,5 @@ pub struct TelegramSubscription { pub filter_words: Option>, pub has_updates: bool, pub external_id: Uuid, + pub thread_id: Option, } diff --git a/src/schema.rs b/src/schema.rs index 32a89b48..962a845a 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -59,6 +59,7 @@ table! { filter_words -> Nullable>, has_updates -> Bool, external_id -> Uuid, + thread_id -> Nullable, } }