diff --git a/src/cmd_authentication.rs b/src/cmd_authentication.rs new file mode 100644 index 0000000..e675e7c --- /dev/null +++ b/src/cmd_authentication.rs @@ -0,0 +1,175 @@ +use std::sync::Arc; + +use sqlx::SqlitePool; +use teloxide::{requests::Requester, types::Message, Bot}; + +use crate::{config::config, HandlerResult}; + + +pub async fn authenticate( + bot: Bot, + msg: Message, + (token, name): (String, String), + db: Arc<SqlitePool>, +) -> HandlerResult { + if token == config().admin_token { + let id = msg.chat.id.to_string(); + sqlx::query!( + r#"INSERT INTO admins(telegram_id, "name") VALUES($1, $2)"#, + id, + name + ) + .execute(db.as_ref()) + .await?; + bot.send_message(msg.chat.id, "Authentification réussie !") + .await?; + } else { + bot.send_message(msg.chat.id, "Le token est incorrect") + .await?; + } + + Ok(()) +} + +pub async fn admin_list(bot: Bot, msg: Message, db: Arc<SqlitePool>) -> HandlerResult { + let admins = sqlx::query!(r#"SELECT "name" FROM admins"#) + .fetch_all(db.as_ref()) + .await?; + + bot.send_message( + msg.chat.id, + format!( + "Admin(s) actuel(s):\n{}", + admins + .into_iter() + .map(|r| format!(" - {}", r.name)) + .collect::<Vec<_>>() + .join("\n"), + ), + ) + .await?; + + Ok(()) +} + +pub async fn admin_remove(bot: Bot, msg: Message, name: String, db: Arc<SqlitePool>) -> HandlerResult { + let mut tx = db.begin().await?; + + if sqlx::query!("SELECT COUNT(*) AS count FROM admins WHERE name = $1", name) + .fetch_one(tx.as_mut()) + .await? + .count + == 0 + { + bot.send_message(msg.chat.id, format!("{} n'est pas admin", name)) + .await?; + return Ok(()); + } + + sqlx::query!("DELETE FROM admins WHERE name = $1", name) + .execute(tx.as_mut()) + .await?; + tx.commit().await?; + + bot.send_message(msg.chat.id, format!("{} a été retiré(e) des admins", name)) + .await?; + + Ok(()) +} + +pub async fn authorize(bot: Bot, msg: Message, command: String, db: Arc<SqlitePool>) -> HandlerResult { + let mut tx = db.begin().await?; + + let chat_id_str = msg.chat.id.to_string(); + let already_authorized = sqlx::query!( + r#"SELECT COUNT(*) AS count FROM authorizations WHERE chat_id = $1 AND command = $2"#, + chat_id_str, + command + ) + .fetch_one(tx.as_mut()) + .await?; + + if already_authorized.count == 0 { + sqlx::query!( + r#"INSERT INTO authorizations(command, chat_id) VALUES($1, $2)"#, + command, + chat_id_str + ) + .execute(tx.as_mut()) + .await?; + } + + tx.commit().await?; + + bot.send_message( + msg.chat.id, + format!("Ce groupe peut désormais utiliser la commande /{}", command), + ) + .await?; + Ok(()) +} + +pub async fn unauthorize( + bot: Bot, + msg: Message, + command: String, + db: Arc<SqlitePool>, +) -> HandlerResult { + let mut tx = db.begin().await?; + + let chat_id_str = msg.chat.id.to_string(); + let already_authorized = sqlx::query!( + r#"SELECT COUNT(*) AS count FROM authorizations WHERE chat_id = $1 AND command = $2"#, + chat_id_str, + command + ) + .fetch_one(tx.as_mut()) + .await?; + + if already_authorized.count > 0 { + sqlx::query!( + r#"DELETE FROM authorizations WHERE command = $1 AND chat_id = $2"#, + command, + chat_id_str + ) + .execute(tx.as_mut()) + .await?; + } + + tx.commit().await?; + + bot.send_message( + msg.chat.id, + format!( + "Ce groupe ne peut désormais plus utiliser la commande /{}", + command + ), + ) + .await?; + Ok(()) +} + +pub async fn authorizations(bot: Bot, msg: Message, db: Arc<SqlitePool>) -> HandlerResult { + let chat_id_str = msg.chat.id.to_string(); + let authorizations = sqlx::query!( + r#"SELECT command FROM authorizations WHERE chat_id = $1"#, + chat_id_str + ) + .fetch_all(db.as_ref()) + .await?; + + bot.send_message( + msg.chat.id, + format!( + "Ce groupe peut utiliser les commandes suivantes:\n{}", + authorizations + .into_iter() + .map(|s| format!(" - {}", s.command)) + .collect::<Vec<_>>() + .join("\n") + ), + ) + .await?; + + Ok(()) +} diff --git a/src/cmd_bureau.rs b/src/cmd_bureau.rs new file mode 100644 index 0000000..ba2e7e4 --- /dev/null +++ b/src/cmd_bureau.rs @@ -0,0 +1,21 @@ +use teloxide::{payloads::SendPollSetters, requests::Requester, types::Message, Bot}; + +use crate::HandlerResult; + +pub async fn bureau(bot: Bot, msg: Message) -> HandlerResult { + bot.send_poll( + msg.chat.id, + "Qui est au bureau ?", + [ + "Je suis actuellement au bureau".to_owned(), + "Je suis à proximité du bureau".to_owned(), + "Je compte m'y rendre bientôt".to_owned(), + "J'y suis pas".to_owned(), + "Je suis à Satellite".to_owned(), + "Je suis pas en Suisse".to_owned(), + ], + ) + .is_anonymous(false) + .await?; + Ok(()) +} \ No newline at end of file diff --git a/src/cmd_poll.rs b/src/cmd_poll.rs new file mode 100644 index 0000000..4e4c238 --- /dev/null +++ b/src/cmd_poll.rs @@ -0,0 +1,210 @@ +const POLL_MAX_OPTIONS_COUNT: u8 = 10; // max poll options + +use crate::directus::{get_committee, update_committee, Committee}; +use log::error; +use rand::{seq::SliceRandom, thread_rng, Rng}; +use teloxide::{ + dispatching::dialogue::{GetChatId, InMemStorage}, + payloads::{SendMessageSetters, SendPollSetters}, + prelude::Dialogue, + requests::Requester, + types::{ + CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, MessageId, + ReplyMarkup, + }, + Bot, +}; + +use crate::HandlerResult; + +#[derive(Default, Clone, Debug)] +pub enum PollState { + #[default] + Start, + ChooseTarget { + /// ID of the message querying the target of the /poll. + /// Used to delete the message after the selection. + message_id: MessageId, + }, + SetQuote { + /// ID of the message querying the quote. + /// Used to delete the message after the selection. + message_id: MessageId, + target: String, + }, +} +pub type PollDialogue = Dialogue<PollState, InMemStorage<PollState>>; + +/// Starts the /poll dialogue by sending a message with an inline keyboard to select the target of the /poll. +pub async fn start_poll_dialogue( + bot: Bot, + msg: Message, + dialogue: PollDialogue, +) -> HandlerResult { + log::info!("Starting /poll dialogue"); + + log::debug!("Removing /poll message"); + bot.delete_message(msg.chat.id, msg.id).await?; + + let committee = match get_committee().await { + Ok(v) => v, + Err(e) => { + error!("Could not fetch committee: {e:#?}"); + return Ok(()); + } + }; + + log::debug!("Sending message with inline keyboard for callback"); + let msg = bot + .send_message(msg.chat.id, "Qui l'a dit ?") + .reply_markup(ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup::new( + committee + .into_iter() + .map(|s| { + InlineKeyboardButton::new( + s.name.clone(), + teloxide::types::InlineKeyboardButtonKind::CallbackData(s.name), + ) + }) + .fold(vec![], |mut vec: Vec<Vec<InlineKeyboardButton>>, value| { + if let Some(v) = vec.last_mut() { + if v.len() < 3 { + v.push(value); + return vec; + } + } + vec.push(vec![value]); + vec + }), + ))) + .await?; + + log::debug!("Updating dialogue to ChooseTarget"); + dialogue + .update(PollState::ChooseTarget { message_id: msg.id }) + .await?; + + Ok(()) +} + +/// Handles the callback from the inline keyboard, and sends a message to query the quote. +/// The CallbackQuery data contains the name of the target. +pub async fn choose_target( + bot: Bot, + callback_query: CallbackQuery, + dialogue: PollDialogue, + message_id: MessageId, +) -> HandlerResult { + if let Some(id) = callback_query.chat_id() { + log::debug!("Removing target query message"); + bot.delete_message(dialogue.chat_id(), message_id).await?; + + log::debug!("Sending quote query message"); + let msg = bot.send_message(id, "Qu'a-t'il/elle dit ?").await?; + + log::debug!("Updating dialogue to SetQuote"); + dialogue + .update(PollState::SetQuote { + message_id: msg.id, + target: callback_query.data.unwrap_or_default(), + }) + .await?; + } + + Ok(()) +} + +/// Receives the quote and creates the poll. Since a poll can have at most 10 options, +/// it is split in two polls, each containing half of the comittee. +pub async fn set_quote( + bot: Bot, + msg: Message, + dialogue: PollDialogue, + (message_id, target): (MessageId, String), +) -> HandlerResult { + if let Some(text) = msg.text() { + log::debug!("Removing quote query message"); + bot.delete_message(dialogue.chat_id(), message_id).await?; + log::debug!("Removing quote message"); + bot.delete_message(dialogue.chat_id(), msg.id).await?; + + let committee = match get_committee().await { + Ok(v) => v, + Err(e) => { + error!("Could not fetch committee: {e:#?}"); + return Ok(()); + } + }; + + let mut poll = committee.iter().map(|c| c.name.clone()).collect::<Vec<_>>(); + + // Splits the committee to have only 10 answers possible. + poll.retain(|s| -> bool { *s != target }); // filter the target from options + poll.shuffle(&mut thread_rng()); // shuffle the options + let index = thread_rng().gen_range(0..(POLL_MAX_OPTIONS_COUNT - 1)); // generate a valid index to insert target back + poll.insert(index as usize, target.clone()); // insert target back in options + + if poll.len() > POLL_MAX_OPTIONS_COUNT as usize { + // split options to have only 10 options + poll = poll.split_at(POLL_MAX_OPTIONS_COUNT as usize).0.to_vec(); + } + + log::debug!("Sending poll"); + bot.send_poll( + dialogue.chat_id(), + format!(r#"Qui a dit: "{}" ?"#, text), + poll, + ) + .type_(teloxide::types::PollType::Quiz) + .is_anonymous(false) + .correct_option_id(index) + .await?; + + update_committee( + committee + .into_iter() + .map(|c| { + if c.name == target { + Committee { + poll_count: c.poll_count + 1, + ..c + } + } else { + c + } + }) + .collect(), + ) + .await; + + log::debug!("Resetting dialogue status"); + dialogue.update(PollState::Start).await?; + } + + Ok(()) +} + +pub async fn stats(bot: Bot, msg: Message) -> HandlerResult { + let mut committee = match get_committee().await { + Ok(v) => v, + Err(e) => { + error!("Could not fetch committee: {e:#?}"); + return Ok(()); + } + }; + + committee.sort_by_key(|r| r.poll_count); + + bot.send_message( + msg.chat.id, + committee + .into_iter() + .rev() + .map(|c| format!("- {} (polls: {})", c.name, c.poll_count)) + .collect::<Vec<_>>() + .join("\n"), + ) + .await?; + + Ok(()) +} \ No newline at end of file diff --git a/src/commands.rs b/src/commands.rs index 6577221..bba635b 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use log::error; use sqlx::SqlitePool; use teloxide::{ dispatching::DpHandlerDescription, @@ -10,11 +9,19 @@ use teloxide::{ Bot, }; -use crate::{config::config, directus::get_committee, HandlerResult}; - -pub use self::poll::PollState; - -const POLL_MAX_OPTIONS_COUNT: u8 = 10; // max poll options +use crate::{ + cmd_authentication::{ + admin_list, admin_remove, authenticate, authorizations, authorize, unauthorize + }, + cmd_bureau::bureau, + cmd_poll::{ + choose_target, + set_quote, + start_poll_dialogue, + stats, PollState + }, + HandlerResult +}; pub fn command_message_handler( ) -> Endpoint<'static, DependencyMap, HandlerResult, DpHandlerDescription> { @@ -27,7 +34,7 @@ pub fn command_message_handler( .branch( require_authorization() .branch(dptree::case![Command::Bureau].endpoint(bureau)) - .branch(dptree::case![Command::Poll].endpoint(poll::start_poll_dialogue)) + .branch(dptree::case![Command::Poll].endpoint(start_poll_dialogue)) .branch(dptree::case![Command::Stats].endpoint(stats)), ) .branch( @@ -47,12 +54,12 @@ pub fn command_message_handler( ), ), ) - .branch(dptree::case![PollState::SetQuote { message_id, target }].endpoint(poll::set_quote)) + .branch(dptree::case![PollState::SetQuote { message_id, target }].endpoint(set_quote)) } pub fn command_callback_query_handler( ) -> Endpoint<'static, DependencyMap, HandlerResult, DpHandlerDescription> { - dptree::case![PollState::ChooseTarget { message_id }].endpoint(poll::choose_target) + dptree::case![PollState::ChooseTarget { message_id }].endpoint(choose_target) } // ----------------------------- ACCESS CONTROL ------------------------------- @@ -167,403 +174,3 @@ async fn help(bot: Bot, msg: Message) -> HandlerResult { .await?; Ok(()) } - -async fn bureau(bot: Bot, msg: Message) -> HandlerResult { - bot.send_poll( - msg.chat.id, - "Qui est au bureau ?", - [ - "Je suis actuellement au bureau".to_owned(), - "Je suis à proximité du bureau".to_owned(), - "Je compte m'y rendre bientôt".to_owned(), - "J'y suis pas".to_owned(), - "Je suis à Satellite".to_owned(), - "Je suis pas en Suisse".to_owned(), - ], - ) - .is_anonymous(false) - .await?; - Ok(()) -} - -async fn authenticate( - bot: Bot, - msg: Message, - (token, name): (String, String), - db: Arc<SqlitePool>, -) -> HandlerResult { - if token == config().admin_token { - let id = msg.chat.id.to_string(); - sqlx::query!( - r#"INSERT INTO admins(telegram_id, "name") VALUES($1, $2)"#, - id, - name - ) - .execute(db.as_ref()) - .await?; - bot.send_message(msg.chat.id, "Authentification réussie !") - .await?; - } else { - bot.send_message(msg.chat.id, "Le token est incorrect") - .await?; - } - - Ok(()) -} - -async fn admin_list(bot: Bot, msg: Message, db: Arc<SqlitePool>) -> HandlerResult { - let admins = sqlx::query!(r#"SELECT "name" FROM admins"#) - .fetch_all(db.as_ref()) - .await?; - - bot.send_message( - msg.chat.id, - format!( - "Admin(s) actuel(s):\n{}", - admins - .into_iter() - .map(|r| format!(" - {}", r.name)) - .collect::<Vec<_>>() - .join("\n"), - ), - ) - .await?; - - Ok(()) -} - -async fn admin_remove(bot: Bot, msg: Message, name: String, db: Arc<SqlitePool>) -> HandlerResult { - let mut tx = db.begin().await?; - - if sqlx::query!("SELECT COUNT(*) AS count FROM admins WHERE name = $1", name) - .fetch_one(tx.as_mut()) - .await? - .count - == 0 - { - bot.send_message(msg.chat.id, format!("{} n'est pas admin", name)) - .await?; - return Ok(()); - } - - sqlx::query!("DELETE FROM admins WHERE name = $1", name) - .execute(tx.as_mut()) - .await?; - tx.commit().await?; - - bot.send_message(msg.chat.id, format!("{} a été retiré(e) des admins", name)) - .await?; - - Ok(()) -} - -async fn authorize(bot: Bot, msg: Message, command: String, db: Arc<SqlitePool>) -> HandlerResult { - let mut tx = db.begin().await?; - - let chat_id_str = msg.chat.id.to_string(); - let already_authorized = sqlx::query!( - r#"SELECT COUNT(*) AS count FROM authorizations WHERE chat_id = $1 AND command = $2"#, - chat_id_str, - command - ) - .fetch_one(tx.as_mut()) - .await?; - - if already_authorized.count == 0 { - sqlx::query!( - r#"INSERT INTO authorizations(command, chat_id) VALUES($1, $2)"#, - command, - chat_id_str - ) - .execute(tx.as_mut()) - .await?; - } - - tx.commit().await?; - - bot.send_message( - msg.chat.id, - format!("Ce groupe peut désormais utiliser la commande /{}", command), - ) - .await?; - Ok(()) -} - -async fn unauthorize( - bot: Bot, - msg: Message, - command: String, - db: Arc<SqlitePool>, -) -> HandlerResult { - let mut tx = db.begin().await?; - - let chat_id_str = msg.chat.id.to_string(); - let already_authorized = sqlx::query!( - r#"SELECT COUNT(*) AS count FROM authorizations WHERE chat_id = $1 AND command = $2"#, - chat_id_str, - command - ) - .fetch_one(tx.as_mut()) - .await?; - - if already_authorized.count > 0 { - sqlx::query!( - r#"DELETE FROM authorizations WHERE command = $1 AND chat_id = $2"#, - command, - chat_id_str - ) - .execute(tx.as_mut()) - .await?; - } - - tx.commit().await?; - - bot.send_message( - msg.chat.id, - format!( - "Ce groupe ne peut désormais plus utiliser la commande /{}", - command - ), - ) - .await?; - Ok(()) -} - -async fn authorizations(bot: Bot, msg: Message, db: Arc<SqlitePool>) -> HandlerResult { - let chat_id_str = msg.chat.id.to_string(); - let authorizations = sqlx::query!( - r#"SELECT command FROM authorizations WHERE chat_id = $1"#, - chat_id_str - ) - .fetch_all(db.as_ref()) - .await?; - - bot.send_message( - msg.chat.id, - format!( - "Ce groupe peut utiliser les commandes suivantes:\n{}", - authorizations - .into_iter() - .map(|s| format!(" - {}", s.command)) - .collect::<Vec<_>>() - .join("\n") - ), - ) - .await?; - - Ok(()) -} - -async fn stats(bot: Bot, msg: Message) -> HandlerResult { - let mut committee = match get_committee().await { - Ok(v) => v, - Err(e) => { - error!("Could not fetch committee: {e:#?}"); - return Ok(()); - } - }; - - committee.sort_by_key(|r| r.poll_count); - - bot.send_message( - msg.chat.id, - committee - .into_iter() - .rev() - .map(|c| format!("- {} (polls: {})", c.name, c.poll_count)) - .collect::<Vec<_>>() - .join("\n"), - ) - .await?; - - Ok(()) -} - -mod poll { - use crate::{ - commands::POLL_MAX_OPTIONS_COUNT, - directus::{get_committee, update_committee, Committee}, - }; - use log::error; - use rand::{seq::SliceRandom, thread_rng, Rng}; - use teloxide::{ - dispatching::dialogue::{GetChatId, InMemStorage}, - payloads::{SendMessageSetters, SendPollSetters}, - prelude::Dialogue, - requests::Requester, - types::{ - CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, Message, MessageId, - ReplyMarkup, - }, - Bot, - }; - - use crate::HandlerResult; - - #[derive(Default, Clone, Debug)] - pub enum PollState { - #[default] - Start, - ChooseTarget { - /// ID of the message querying the target of the /poll. - /// Used to delete the message after the selection. - message_id: MessageId, - }, - SetQuote { - /// ID of the message querying the quote. - /// Used to delete the message after the selection. - message_id: MessageId, - target: String, - }, - } - pub type PollDialogue = Dialogue<PollState, InMemStorage<PollState>>; - - /// Starts the /poll dialogue by sending a message with an inline keyboard to select the target of the /poll. - pub async fn start_poll_dialogue( - bot: Bot, - msg: Message, - dialogue: PollDialogue, - ) -> HandlerResult { - log::info!("Starting /poll dialogue"); - - log::debug!("Removing /poll message"); - bot.delete_message(msg.chat.id, msg.id).await?; - - let committee = match get_committee().await { - Ok(v) => v, - Err(e) => { - error!("Could not fetch committee: {e:#?}"); - return Ok(()); - } - }; - - log::debug!("Sending message with inline keyboard for callback"); - let msg = bot - .send_message(msg.chat.id, "Qui l'a dit ?") - .reply_markup(ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup::new( - committee - .into_iter() - .map(|s| { - InlineKeyboardButton::new( - s.name.clone(), - teloxide::types::InlineKeyboardButtonKind::CallbackData(s.name), - ) - }) - .fold(vec![], |mut vec: Vec<Vec<InlineKeyboardButton>>, value| { - if let Some(v) = vec.last_mut() { - if v.len() < 3 { - v.push(value); - return vec; - } - } - vec.push(vec![value]); - vec - }), - ))) - .await?; - - log::debug!("Updating dialogue to ChooseTarget"); - dialogue - .update(PollState::ChooseTarget { message_id: msg.id }) - .await?; - - Ok(()) - } - - /// Handles the callback from the inline keyboard, and sends a message to query the quote. - /// The CallbackQuery data contains the name of the target. - pub async fn choose_target( - bot: Bot, - callback_query: CallbackQuery, - dialogue: PollDialogue, - message_id: MessageId, - ) -> HandlerResult { - if let Some(id) = callback_query.chat_id() { - log::debug!("Removing target query message"); - bot.delete_message(dialogue.chat_id(), message_id).await?; - - log::debug!("Sending quote query message"); - let msg = bot.send_message(id, "Qu'a-t'il/elle dit ?").await?; - - log::debug!("Updating dialogue to SetQuote"); - dialogue - .update(PollState::SetQuote { - message_id: msg.id, - target: callback_query.data.unwrap_or_default(), - }) - .await?; - } - - Ok(()) - } - - /// Receives the quote and creates the poll. Since a poll can have at most 10 options, - /// it is split in two polls, each containing half of the comittee. - pub async fn set_quote( - bot: Bot, - msg: Message, - dialogue: PollDialogue, - (message_id, target): (MessageId, String), - ) -> HandlerResult { - if let Some(text) = msg.text() { - log::debug!("Removing quote query message"); - bot.delete_message(dialogue.chat_id(), message_id).await?; - log::debug!("Removing quote message"); - bot.delete_message(dialogue.chat_id(), msg.id).await?; - - let committee = match get_committee().await { - Ok(v) => v, - Err(e) => { - error!("Could not fetch committee: {e:#?}"); - return Ok(()); - } - }; - - let mut poll = committee.iter().map(|c| c.name.clone()).collect::<Vec<_>>(); - - // Splits the committee to have only 10 answers possible. - poll.retain(|s| -> bool { *s != target }); // filter the target from options - poll.shuffle(&mut thread_rng()); // shuffle the options - let index = thread_rng().gen_range(0..(POLL_MAX_OPTIONS_COUNT - 1)); // generate a valid index to insert target back - poll.insert(index as usize, target.clone()); // insert target back in options - - if poll.len() > POLL_MAX_OPTIONS_COUNT as usize { - // split options to have only 10 options - poll = poll.split_at(POLL_MAX_OPTIONS_COUNT as usize).0.to_vec(); - } - - log::debug!("Sending poll"); - bot.send_poll( - dialogue.chat_id(), - format!(r#"Qui a dit: "{}" ?"#, text), - poll, - ) - .type_(teloxide::types::PollType::Quiz) - .is_anonymous(false) - .correct_option_id(index) - .await?; - - update_committee( - committee - .into_iter() - .map(|c| { - if c.name == target { - Committee { - poll_count: c.poll_count + 1, - ..c - } - } else { - c - } - }) - .collect(), - ) - .await; - - log::debug!("Resetting dialogue status"); - dialogue.update(PollState::Start).await?; - } - - Ok(()) - } -} diff --git a/src/main.rs b/src/main.rs index fe42fae..b63db1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,13 +9,17 @@ use teloxide::{ }; use crate::{ - commands::{command_callback_query_handler, command_message_handler, Command, PollState}, + commands::{command_callback_query_handler, command_message_handler, Command}, directus::{update_committee, Committee}, + cmd_poll::PollState }; mod commands; mod config; mod directus; +mod cmd_poll; +mod cmd_bureau; +mod cmd_authentication; pub type HandlerResult = Result<(), Box<dyn std::error::Error + Send + Sync>>; @@ -58,7 +62,7 @@ async fn main() { let mut bot_dispatcher = Dispatcher::builder( bot, - dialogue::enter::<Update, InMemStorage<commands::PollState>, commands::PollState, _>() + dialogue::enter::<Update, InMemStorage<PollState>, PollState, _>() .branch(message_handler) .branch(callback_handler), )