Kaynağa Gözat

[Telegram] Add dialogue support

Signed-off-by: Slava Barinov <rayslava@gmail.com>
Slava Barinov 4 yıl önce
ebeveyn
işleme
52bd836b28
3 değiştirilmiş dosya ile 147 ekleme ve 53 silme
  1. 8 6
      Cargo.lock
  2. 3 2
      Cargo.toml
  3. 136 45
      src/telegram.rs

+ 8 - 6
Cargo.lock

@@ -81,9 +81,9 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.6.1"
+version = "3.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe"
+checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631"
 
 [[package]]
 name = "byteorder"
@@ -1020,6 +1020,7 @@ dependencies = [
  "const_format",
  "csv",
  "derive_more",
+ "futures",
  "libc",
  "log",
  "pickledb",
@@ -1327,6 +1328,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d336ef631944ed9d696be4e06be884b7e9c60947639f224b862fef2f1d0c2f2e"
 dependencies = [
  "async-trait",
+ "bincode",
  "bytes",
  "derive_more",
  "futures",
@@ -1460,9 +1462,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
 
 [[package]]
 name = "tokio"
-version = "1.6.0"
+version = "1.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37"
+checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975"
 dependencies = [
  "autocfg",
  "bytes",
@@ -1572,9 +1574,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-normalization"
-version = "0.1.17"
+version = "0.1.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
+checksum = "33717dca7ac877f497014e10d73f3acf948c342bee31b5ca7892faf94ccc6b49"
 dependencies = [
  "tinyvec",
 ]

+ 3 - 2
Cargo.toml

@@ -16,7 +16,8 @@ shellexpand = "2.1"
 radix_trie = { version = "0.2", features = ["serde"] }
 libc = { version = "0.2" }
 const_format = "0.2"
-teloxide = { version = "0.4.0", features = ["auto-send", "macros"], optional = true }
+futures = { version = "0.3.0", optional = true }
+teloxide = { version = "0.4.0", features = ["auto-send", "macros", "bincode-serializer"], optional = true }
 log = { version = "0.4.8", optional = true }
 pretty_env_logger = { version = "0.4.0", optional = true }
 tokio = { version =  "1.3", features = ["rt-multi-thread", "macros"], optional = true }
@@ -30,4 +31,4 @@ pkg-config = { version = "0.3", optional = true }
 [features]
 default = [ "telegram" ]
 tv = [ "cc", "pkg-config" ]
-telegram = [ "teloxide", "log", "pretty_env_logger", "tokio", "derive_more", "thiserror" ]
+telegram = [ "teloxide", "log", "pretty_env_logger", "tokio", "derive_more", "thiserror", "futures" ]

+ 136 - 45
src/telegram.rs

@@ -3,13 +3,17 @@
 use crate::categories::{get_category_from_tg, CatStats};
 use crate::convert::{convert, non_cat_items};
 use crate::user::User;
+
 use derive_more::From;
 use qif_generator::{account::Account, account::AccountType};
 use std::sync::atomic::{AtomicBool, Ordering};
 use teloxide::types::*;
+use teloxide::{
+    dispatching::dialogue::{serializer::Bincode, InMemStorage, Storage},
+    DownloadError, RequestError,
+};
 use teloxide::{net::Download, types::File as TgFile, Bot};
 use teloxide::{prelude::*, utils::command::BotCommand};
-use teloxide::{DownloadError, RequestError};
 use thiserror::Error;
 use tokio::fs::File;
 use tokio::io::AsyncWriteExt;
@@ -24,15 +28,15 @@ pub async fn bot() {
 #[cfg(feature = "telegram")]
 #[derive(Debug, Error, From)]
 enum FileReceiveError {
-    /// Download process error
-    #[error("File download error: {0}")]
-    Download(#[source] DownloadError),
     /// Telegram request error
     #[error("Web request error: {0}")]
     Request(#[source] RequestError),
     /// Io error while writing file
     #[error("An I/O error: {0}")]
     Io(#[source] std::io::Error),
+    /// Download error while getting file from telegram
+    #[error("File download error: {0}")]
+    Download(#[source] DownloadError),
 }
 
 /// Possible error while receiving a file
@@ -133,61 +137,148 @@ pub async fn input_category_from_tg(
     String::new()
 }
 
-#[cfg(feature = "telegram")]
-async fn run() {
-    teloxide::enable_logging!();
-    log::info!("Starting telegram bot");
-    IS_RUNNING.store(true, Ordering::SeqCst);
+use serde::{Deserialize, Serialize};
 
-    let bot = Bot::from_env().auto_send();
+#[derive(Transition, From, Serialize, Deserialize)]
+pub enum Dialogue {
+    Start(StartState),
+    HaveNumber(HaveNumberState),
+}
 
-    // TODO: Add Dispatcher to process UpdateKinds
-    teloxide::repl(bot, |message| async move {
-        let update = &message.update;
-        if let MessageKind::Common(msg) = &update.kind {
-            if let MediaKind::Document(doc) = &msg.media_kind {
-                if let Ok(newfile) =
-                    download_file(&message.requester.inner(), &doc.document.file_id).await
-                {
-                    message
-                        .answer(format!("File received: {:} ", newfile))
-                        .await?;
-                    if let Some(tguser) = message.update.from() {
-                        let mut user = User::new(tguser.id, &None);
-                        message
-                            .answer(format!("Created user: {:} ", tguser.id))
-                            .await?;
-                        if let Ok(result) = convert_file(&newfile, &mut user, &message).await {
-                            message
-                                .answer(format!("File converted into: {:} ", result))
-                                .await?;
+impl Default for Dialogue {
+    fn default() -> Self {
+        Self::Start(StartState)
+    }
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct StartState;
+
+#[derive(Serialize, Deserialize)]
+pub struct HaveNumberState {
+    pub number: i32,
+}
+
+#[teloxide(subtransition)]
+async fn start(
+    state: StartState,
+    cx: TransitionIn<AutoSend<Bot>>,
+    ans: String,
+) -> TransitionOut<Dialogue> {
+    if let Ok(number) = ans.parse() {
+        cx.answer(format!(
+            "Remembered number {}. Now use /get or /reset",
+            number
+        ))
+        .await?;
+        next(HaveNumberState { number })
+    } else {
+        cx.answer("Please, send me a number").await?;
+        next(state)
+    }
+}
+
+#[teloxide(subtransition)]
+async fn have_number(
+    state: HaveNumberState,
+    cx: TransitionIn<AutoSend<Bot>>,
+    ans: String,
+) -> TransitionOut<Dialogue> {
+    let num = state.number;
+
+    if ans.starts_with("/get") {
+        cx.answer(format!("Here is your number: {}", num)).await?;
+        next(state)
+    } else if ans.starts_with("/reset") {
+        cx.answer("Resetted number").await?;
+        next(StartState)
+    } else {
+        cx.answer("Please, send /get or /reset").await?;
+        next(state)
+    }
+}
+
+type StorageError = <InMemStorage<Bincode> as Storage<Dialogue>>::Error;
+
+#[derive(Debug, Error)]
+enum Error {
+    #[error("error from Telegram: {0}")]
+    TelegramError(#[from] RequestError),
+}
+
+type In = DialogueWithCx<AutoSend<Bot>, Message, Dialogue, StorageError>;
+
+async fn handle_message(
+    cx: UpdateWithCx<AutoSend<Bot>, Message>,
+    dialogue: Dialogue,
+) -> TransitionOut<Dialogue> {
+    match cx.update.text().map(ToOwned::to_owned) {
+        None => {
+            let update = &cx.update;
+            if let MessageKind::Common(msg) = &update.kind {
+                if let MediaKind::Document(doc) = &msg.media_kind {
+                    if let Ok(newfile) =
+                        download_file(&cx.requester.inner(), &doc.document.file_id).await
+                    {
+                        cx.answer(format!("File received: {:} ", newfile)).await?;
+                        if let Some(tguser) = cx.update.from() {
+                            let mut user = User::new(tguser.id, &None);
+                            cx.answer(format!("Created user: {:} ", tguser.id)).await?;
+                            if let Ok(result) = convert_file(&newfile, &mut user, &cx).await {
+                                cx.answer(format!("File converted into: {:} ", result))
+                                    .await?;
+                            }
                         }
                     }
-                }
 
-                message.answer_dice().await?;
-            } else if let Some(line) = message.update.text() {
-                if let Ok(command) = Command::parse(line, "tgqif") {
-                    match command {
-                        Command::Help => {
-                            message.answer(Command::descriptions()).send().await?;
-                        }
-                        Command::Start => {
-                            if let Some(user) = message.update.from() {
-                                message
-                                    .answer(format!(
+                    cx.answer_dice().await?;
+                } else if let Some(line) = cx.update.text() {
+                    if let Ok(command) = Command::parse(line, "tgqif") {
+                        match command {
+                            Command::Help => {
+                                cx.answer(Command::descriptions()).send().await?;
+                            }
+                            Command::Start => {
+                                if let Some(user) = cx.update.from() {
+                                    cx.answer(format!(
                                         "You registered as @{} with id {}.",
                                         user.first_name, user.id
                                     ))
                                     .await?;
+                                }
                             }
                         }
                     }
                 }
             }
+            respond(());
+            //            cx.answer("Send me a text message.").await?;
+            next(dialogue)
         }
-        respond(())
-    })
-    .await;
+        Some(ans) => dialogue.react(cx, ans).await,
+    }
+}
+
+#[cfg(feature = "telegram")]
+async fn run() {
+    teloxide::enable_logging!();
+    log::info!("Starting telegram bot");
+    IS_RUNNING.store(true, Ordering::SeqCst);
+
+    let bot = Bot::from_env().auto_send();
+
+    // TODO: Add Dispatcher to process UpdateKinds
+    Dispatcher::new(bot)
+        .messages_handler(DialogueDispatcher::with_storage(
+            |DialogueWithCx { cx, dialogue }: In| async move {
+                let dialogue = dialogue.expect("std::convert::Infallible");
+                handle_message(cx, dialogue)
+                    .await
+                    .expect("Something wrong with the bot!")
+            },
+            InMemStorage::new(),
+        ))
+        .dispatch()
+        .await;
     IS_RUNNING.store(false, Ordering::SeqCst);
 }