telegram.rs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. // This bot throws a dice on each incoming message.
  2. use crate::categories::{get_category_from_tg, CatStats};
  3. use crate::convert::{convert, non_cat_items};
  4. use crate::user::User;
  5. use derive_more::From;
  6. use qif_generator::{account::Account, account::AccountType};
  7. use std::sync::atomic::{AtomicBool, Ordering};
  8. use teloxide::types::*;
  9. use teloxide::{
  10. dispatching::dialogue::{InMemStorage, Storage},
  11. DownloadError, RequestError,
  12. };
  13. use teloxide::{net::Download, types::File as TgFile, Bot};
  14. use teloxide::{prelude::*, utils::command::BotCommand};
  15. use thiserror::Error;
  16. use tokio::fs::File;
  17. use tokio::io::AsyncWriteExt;
  18. #[cfg(feature = "telegram")]
  19. #[tokio::main]
  20. pub async fn bot() {
  21. run().await;
  22. }
  23. /// Possible error while receiving a file
  24. #[cfg(feature = "telegram")]
  25. #[derive(Debug, Error, From)]
  26. enum FileReceiveError {
  27. /// Telegram request error
  28. #[error("Web request error: {0}")]
  29. Request(#[source] RequestError),
  30. /// Io error while writing file
  31. #[error("An I/O error: {0}")]
  32. Io(#[source] std::io::Error),
  33. /// Download error while getting file from telegram
  34. #[error("File download error: {0}")]
  35. Download(#[source] DownloadError),
  36. }
  37. /// Possible error while receiving a file
  38. #[cfg(feature = "telegram")]
  39. #[derive(Debug, Error, From)]
  40. enum FileConvertError {
  41. /// Telegram request error
  42. #[error("JSON conversion error: {0}")]
  43. Request(String),
  44. /// Io error while writing file
  45. #[error("An I/O error: {0}")]
  46. Io(#[source] std::io::Error),
  47. }
  48. #[derive(BotCommand, Debug)]
  49. #[command(rename = "lowercase", description = "These commands are supported:")]
  50. enum Command {
  51. #[command(description = "display this text.")]
  52. Help,
  53. #[command(description = "Register new user in bot.")]
  54. Start,
  55. }
  56. #[cfg(feature = "telegram")]
  57. static IS_RUNNING: AtomicBool = AtomicBool::new(false);
  58. #[cfg(feature = "telegram")]
  59. async fn download_file(downloader: &Bot, file_id: &str) -> Result<String, FileReceiveError> {
  60. let TgFile {
  61. file_id, file_path, ..
  62. } = downloader.get_file(file_id).send().await?;
  63. let filepath = format!("/tmp/{}", file_id);
  64. let mut file = File::create(&filepath).await?;
  65. downloader.download_file(&file_path, &mut file).await?;
  66. Ok(filepath)
  67. }
  68. #[cfg(feature = "telegram")]
  69. async fn convert_file(
  70. jsonfile: &str,
  71. user: &mut User,
  72. ctx: &UpdateWithCx<AutoSend<Bot>, Message>,
  73. ) -> Result<String, FileConvertError> {
  74. let filepath = format!("{}.qif", jsonfile);
  75. log::info!("Converting file into {}", filepath);
  76. let mut file = File::create(&filepath).await?;
  77. log::info!("Got file");
  78. for i in non_cat_items(&jsonfile, &user) {
  79. log::info!("Message about {}", i);
  80. let newcat = input_category_from_tg(&i, &user.catmap, &user.accounts, &ctx).await;
  81. ctx.answer(format!("{} is set to {}", i, newcat))
  82. .await
  83. .unwrap();
  84. }
  85. let acc = Account::new()
  86. .name("Wallet")
  87. .account_type(AccountType::Cash)
  88. .build();
  89. let cat = &|item: &str, stats: &mut CatStats, accounts: &[String]| -> String {
  90. get_category_from_tg(&item, stats, accounts, &ctx)
  91. };
  92. let t = convert(jsonfile, "Test", user, &acc, cat)?;
  93. file.write(acc.to_string().as_bytes()).await?;
  94. file.write(t.to_string().as_bytes()).await?;
  95. Ok(filepath)
  96. }
  97. #[cfg(feature = "telegram")]
  98. pub fn bot_is_running() -> bool {
  99. IS_RUNNING.load(Ordering::SeqCst)
  100. }
  101. #[cfg(feature = "telegram")]
  102. pub async fn input_category_from_tg(
  103. item: &str,
  104. _cats: &CatStats,
  105. accounts: &[String],
  106. ctx: &UpdateWithCx<AutoSend<Bot>, Message>,
  107. ) -> String {
  108. log::info!("{:?}", accounts);
  109. let keyboard = InlineKeyboardMarkup::default().append_row(
  110. accounts
  111. .iter()
  112. .filter(|l| l.starts_with("Expenses:"))
  113. .map(|line| {
  114. InlineKeyboardButton::new(
  115. line.strip_prefix("Expenses:").unwrap(),
  116. InlineKeyboardButtonKind::CallbackData(line.into()),
  117. )
  118. }),
  119. );
  120. ctx.answer(format!("Input category for {}", item))
  121. .reply_markup(ReplyMarkup::InlineKeyboard(keyboard))
  122. .await
  123. .unwrap();
  124. String::new()
  125. }
  126. #[derive(Transition, From)]
  127. pub enum Dialogue {
  128. Start(StartState),
  129. HaveNumber(HaveNumberState),
  130. }
  131. impl Default for Dialogue {
  132. fn default() -> Self {
  133. Self::Start(StartState)
  134. }
  135. }
  136. pub struct StartState;
  137. pub struct HaveNumberState {
  138. pub number: i32,
  139. }
  140. #[teloxide(subtransition)]
  141. async fn start(
  142. state: StartState,
  143. cx: TransitionIn<AutoSend<Bot>>,
  144. ans: String,
  145. ) -> TransitionOut<Dialogue> {
  146. if let Ok(number) = ans.parse() {
  147. cx.answer(format!(
  148. "Remembered number {}. Now use /get or /reset",
  149. number
  150. ))
  151. .await?;
  152. next(HaveNumberState { number })
  153. } else {
  154. cx.answer("Please, send me a number").await?;
  155. next(state)
  156. }
  157. }
  158. #[teloxide(subtransition)]
  159. async fn have_number(
  160. state: HaveNumberState,
  161. cx: TransitionIn<AutoSend<Bot>>,
  162. ans: String,
  163. ) -> TransitionOut<Dialogue> {
  164. let num = state.number;
  165. if ans.starts_with("/get") {
  166. cx.answer(format!("Here is your number: {}", num)).await?;
  167. next(state)
  168. } else if ans.starts_with("/reset") {
  169. cx.answer("Resetted number").await?;
  170. next(StartState)
  171. } else {
  172. cx.answer("Please, send /get or /reset").await?;
  173. next(state)
  174. }
  175. }
  176. type StorageError = <InMemStorage<Dialogue> as Storage<Dialogue>>::Error;
  177. #[derive(Debug, Error)]
  178. enum Error {
  179. #[error("error from Telegram: {0}")]
  180. TelegramError(#[from] RequestError),
  181. }
  182. type In = DialogueWithCx<AutoSend<Bot>, Message, Dialogue, StorageError>;
  183. async fn handle_message(
  184. cx: UpdateWithCx<AutoSend<Bot>, Message>,
  185. dialogue: Dialogue,
  186. ) -> TransitionOut<Dialogue> {
  187. match cx.update.text().map(ToOwned::to_owned) {
  188. None => {
  189. let update = &cx.update;
  190. if let MessageKind::Common(msg) = &update.kind {
  191. if let MediaKind::Document(doc) = &msg.media_kind {
  192. if let Ok(newfile) =
  193. download_file(&cx.requester.inner(), &doc.document.file_id).await
  194. {
  195. cx.answer(format!("File received: {:} ", newfile)).await?;
  196. if let Some(tguser) = cx.update.from() {
  197. let mut user = User::new(tguser.id, &None);
  198. cx.answer(format!("Created user: {:} ", tguser.id)).await?;
  199. if let Ok(result) = convert_file(&newfile, &mut user, &cx).await {
  200. cx.answer(format!("File converted into: {:} ", result))
  201. .await?;
  202. }
  203. }
  204. }
  205. } else if let Some(line) = cx.update.text() {
  206. if let Ok(command) = Command::parse(line, "tgqif") {
  207. match command {
  208. Command::Help => {
  209. cx.answer(Command::descriptions()).send().await?;
  210. }
  211. Command::Start => {
  212. if let Some(user) = cx.update.from() {
  213. cx.answer(format!(
  214. "You registered as @{} with id {}.",
  215. user.first_name, user.id
  216. ))
  217. .await?;
  218. }
  219. }
  220. }
  221. }
  222. }
  223. }
  224. next(dialogue)
  225. }
  226. Some(ans) => dialogue.react(cx, ans).await,
  227. }
  228. }
  229. #[cfg(feature = "telegram")]
  230. async fn run() {
  231. teloxide::enable_logging!();
  232. log::info!("Starting telegram bot");
  233. IS_RUNNING.store(true, Ordering::SeqCst);
  234. let bot = Bot::from_env().auto_send();
  235. // TODO: Add Dispatcher to process UpdateKinds
  236. Dispatcher::new(bot)
  237. .messages_handler(DialogueDispatcher::with_storage(
  238. |DialogueWithCx { cx, dialogue }: In| async move {
  239. let dialogue = dialogue.expect("std::convert::Infallible");
  240. handle_message(cx, dialogue)
  241. .await
  242. .expect("Something wrong with the bot!")
  243. },
  244. InMemStorage::new(),
  245. ))
  246. .dispatch()
  247. .await;
  248. IS_RUNNING.store(false, Ordering::SeqCst);
  249. }