#![allow(unused)] mod climate; mod repo; mod utils; use anyhow::Result; use climate::SelfTemperature; use envconfig::Envconfig; use log::{debug, info, warn}; use teloxide::{ dispatching::{update_listeners::AsUpdateStream, UpdateFilterExt}, dptree::di::Injectable, filter_command, payloads::SendMessage, prelude::*, utils::command::BotCommands, }; use tokio_stream::StreamExt; use crate::repo::Storage; const VERSION: &str = env!("GIT_REVISION"); const BRANCH: &str = env!("GIT_BRANCH"); #[tokio::main] async fn main() { env_logger::init(); debug!("starting the application"); tokio::spawn(run()).await.unwrap(); } #[derive(Envconfig, Clone, Debug)] struct Settings { #[envconfig(from = "ALTEREGO_TELEGRAM_TOKEN")] pub telegram_token: String, #[envconfig( from = "ALTEREGO_CLIMATE_DSN", default = "http://127.0.0.1:18081/v1/home/temperature" )] pub climate_dsn: String, #[envconfig( from = "ALTEREGO_HOSTTEMP_CMD", default = "/opt/vc/bin/vcgencmd measure_temp" )] pub hosttemp_cmd: String, #[envconfig(from = "ALTEREGO_DATABASE_URL", default = "./db.sqlite")] pub db_source: String, } async fn run() -> anyhow::Result<()> { info!("starting"); let settings = Settings::init_from_env().expect("reading config values"); let bot = teloxide::Bot::new(&settings.telegram_token); let repo_config = repo::SqliteConfig { source: settings.db_source, ..Default::default() }; info!("repo config: {repo_config:?}"); let sqlite_storage = repo::SqliteRepo::from_config(repo_config).await?; let climate_client = climate::Client::new(&settings.climate_dsn); let self_temp_client = { let splitted: Vec<&str> = settings.hosttemp_cmd.split(' ').into_iter().collect(); let (cmd, arg) = (splitted[0], splitted[1]); climate::SelfTemperature::new(cmd, arg) }; let handler = Update::filter_message() .filter_command::() .chain(dptree::filter(|msg: Message| msg.chat.is_private())) .chain(dptree::filter_map_async( |msg: Message, storage: repo::SqliteRepo| async move { let chat_id = msg.chat.id.0; info!("checking if the user {chat_id} exists"); let user = storage.load_user_by_chat_id(chat_id).await.unwrap(); match user { Some(user) => Some(user), None => { let fname = msg.chat.first_name().unwrap_or_default(); let lname = msg.chat.last_name().unwrap_or_default(); info!("performing upsert for user {lname} {fname}"); let user_db = storage .create_user(chat_id, [fname, lname].join(" ")) .await .unwrap(); info!("upserted user {user_db:?}"); Some(user_db) } } }, )) .branch(dptree::case![Command::RoomTemperature].endpoint(handle_temperature_sensor)) .branch(dptree::case![Command::HostTemperature].endpoint(handle_host_temperature)) .branch(dptree::case![Command::VersionRequest].endpoint(handle_version)) .branch(dptree::case![Command::ChatID].endpoint(handle_chat_id)) .branch(dptree::case![Command::Help].endpoint(handle_help)); let mut dependencies = DependencyMap::new(); dependencies.insert(sqlite_storage); dependencies.insert(climate_client); dependencies.insert(self_temp_client); dependencies.insert(utils::Generators::new()); info!("running"); Dispatcher::builder(bot, handler) .dependencies(dependencies) .enable_ctrlc_handler() .default_handler(|upd| async move { warn!("unhandled update: {:?}", upd); }) .build() .dispatch() .await; Ok(()) } type HandlerResult = std::result::Result>; fn error_msg(reqid: &utils::RequestID) -> String { format!("There was an error handling command, sorry. Reffer to {reqid}") } async fn handle_temperature_sensor( bot: Bot, msg: Message, climate: climate::Client, next_req_id: utils::Generators, storage: repo::SqliteRepo, user: repo::UserDB, ) -> Result<()> { let chat_id = msg.chat.id; let reqid = next_req_id.next_request_id(); let name = user.name; bot.send_message(chat_id, format!("Just a second, {name}, asking...")) .await?; match climate.fetch().await { Ok(temp) => { let text = format!( "Humidity is {} and temperature is {}", temp.humidity, temp.temp, ); bot.send_message(chat_id, text).await?; } Err(err) => { warn!("[{reqid}] unable to fetch self_temp: {err}"); let msg = error_msg(&reqid); bot.send_message(chat_id, msg).await?; } }; Ok(()) } async fn handle_host_temperature( bot: Bot, msg: Message, temp: SelfTemperature, next_req_id: utils::Generators, ) -> Result<()> { let chat_id = msg.chat.id; let reqid = next_req_id.next_request_id(); bot.send_message(chat_id, "Just a second, asking...") .await?; match temp.fetch().await { Ok(temp) => { let text = format!("Host temperature is {} degrees celcius", temp,); bot.send_message(chat_id, text).await?; } Err(err) => { warn!("[{reqid}] unable to fetch self_temp: {err}"); let msg = error_msg(&reqid); bot.send_message(chat_id, msg).await?; } } Ok(()) } async fn handle_version(bot: Bot, msg: Message) -> Result<()> { let chat_id = msg.chat.id; let text = format!("Bot version is {} (branch: {})", VERSION, BRANCH,); bot.send_message(chat_id, text).await?; Ok(()) } async fn handle_chat_id(bot: Bot, msg: Message) -> Result<()> { let chat_id = msg.chat.id; let text = format!("Current chat id: {chat_id}"); bot.send_message(chat_id, text).await?; Ok(()) } async fn handle_help(bot: Bot, msg: Message) -> Result<()> { let chat_id = msg.chat.id; bot.send_message(chat_id, Command::descriptions().to_string()) .await?; Ok(()) } struct Handler { storage: S, climate: climate::Client, self_temp: climate::SelfTemperature, started: std::time::Instant, } fn log_error<'a, E: std::fmt::Display>(req_id: &'a str, msg: &'a str) -> impl FnOnce(E) -> E + 'a { move |err: E| -> E { warn!( "request_id={}, {} err={}", req_id.to_owned(), msg.to_owned(), err ); err } } fn log_message(req_id: &str, msg: Message) { info!( "message sent to chat_id={}, text={}", msg.chat.id, msg.text().unwrap_or_default(), ) } #[derive(serde::Deserialize, Debug)] struct Climate { humidity: f32, temp: f32, } #[derive(BotCommands, Debug, Clone, PartialEq, Eq)] #[command(description = "These commands are supported:")] enum Command { #[command(rename="help", description = "display this text.")] Help, #[command(rename="roomtemp", description = "temperature of your room.")] RoomTemperature, #[command(rename="hosttemp", description = "temperature of raspberry.")] HostTemperature, #[command(rename="version", description = "prints current version.")] VersionRequest, #[command(rename="chatid", description = "prints current chat id.")] ChatID, }