diff --git a/src/audio.rs b/src/audio.rs index 5215a56..e2173b0 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -37,11 +37,15 @@ async fn play_audio_file( Ok(()) } +#[derive(Debug)] pub enum AudioEvent { + CancelAll, Bell, FireAlarm, + PlayFile(String), } +#[derive(Debug)] pub struct Audio { bell_path: String, fire_alarm_path: String, @@ -68,6 +72,9 @@ impl Audio { }; maybe_cancellation_token = Option::Some(CancellationToken::new()); match event { + AudioEvent::CancelAll => { + // This cancels the audio since any AudioEvent cancels the previous + } AudioEvent::Bell => { nextcloud_sender .send(NextcloudEvent::Chat( @@ -110,6 +117,13 @@ impl Audio { } }); } + AudioEvent::PlayFile(path) => { + spawn(play_audio_file( + path, + "", + maybe_cancellation_token.clone().unwrap(), + )); + } } } Err(ModuleError::new(String::from( diff --git a/src/buttons.rs b/src/buttons.rs index 73b14c3..5ab20a6 100644 --- a/src/buttons.rs +++ b/src/buttons.rs @@ -71,6 +71,7 @@ pub enum StateChange { LightsOff, } +#[derive(Debug)] pub enum CommandToButtons { OpenDoor, RingBell(u32, u32), // maybe implement it with interval diff --git a/src/nextcloud/command_list.txt b/src/nextcloud/command_list.txt new file mode 100644 index 0000000..c90ec50 --- /dev/null +++ b/src/nextcloud/command_list.txt @@ -0,0 +1,22 @@ +Info commands: + '?' show this message. + 'open?' (or 'offen?') get door states. + 'lights?' (or 'licht?') get lights states. + 'battery?' (or 'batterie?') see battery status. + 'weather?' (or 'wetter?') see weather data. + 'status?' (or 'status?') see Opensesame state. + 'sensors?' (or 'sensoren?') get sensor data. + 'indoor climate?' (or 'innenklima?') see indoor environment. + 'time?' (or 'uhrzeit?') see current time. + 'code?' (or 'pin?') and gets a list of names and codes. +Action commands: + 'open!' (or 'öffnen!') open doors. + 'lights [in|out]!' (or 'licht [innen|aussen]!') toggle lights. + 'play !' (or 'abspielen !') play audio file from path. + 'bell!' (or 'glocke!') play bell sound. + 'alarm!' play alarm sound. + 'all clear!' (or 'entwarnen!') cancle all playing audio. + 'quit!' (or 'beenden!') quit Opensesame (to restart it). + 'code add !' (or pin hinzufügen ') add new code. + 'code del !' (or 'pin löschen ') remove code. + 'code set !' (or 'pin ändern !') set code. \ No newline at end of file diff --git a/src/nextcloud/commands.rs b/src/nextcloud/commands.rs new file mode 100644 index 0000000..a5698ae --- /dev/null +++ b/src/nextcloud/commands.rs @@ -0,0 +1,138 @@ +use std::process::exit; + +use crate::{ + audio::{self, AudioEvent}, + buttons::CommandToButtons, +}; +use chrono::Local; +use tokio::sync::mpsc::Sender; + +use super::nextcloud::NextcloudEvent; + +#[derive(Debug)] +pub enum CommandType { + Action, + Info, +} + +// Commands have this structure: +// [@user](command)(!|?) +#[derive(Debug)] +pub struct Command { + user_prefix: Option, // @user + words: Vec, // the actual command + command_type: CommandType, // ! = Action ? = Info +} + +impl Command { + pub fn new(user_input: &str) -> Result { + eprintln!("user_input: {}", user_input); + let input_trimmed = user_input.trim(); + + let command_type = match input_trimmed.chars().last() { + Some('!') => CommandType::Action, + Some('?') => CommandType::Info, + _ => return Err(String::from("Command must end with either '!' or '?'.")), + }; + + let mut words = input_trimmed[..input_trimmed.len() - 1] + .split_whitespace() + .map(|word| word.to_lowercase()) + .collect::>(); + + let user_prefix = match words.get(0) { + None => None, + Some(first_word) => { + if first_word.starts_with("@") { + Some(first_word.clone()) + } else { + None + } + } + }; + + if user_prefix.is_some() { + words.drain(0..1); + } + + Ok(Command { + user_prefix, + words, + command_type, + }) + } +} + +pub async fn run_command( + command: Command, + nextcloud_sender: Sender, + command_sender: Sender, + audio_sender: Sender, + user: &str, +) -> String { + if let Some(user_prefix) = command.user_prefix { + if user_prefix != user { + return "".to_string(); + } + } + + let words = command + .words + .iter() + .map(|word| word.as_str()) + .collect::>(); + + match command.command_type { + CommandType::Info => match words[..] { + [] => include_str!("command_list.txt").to_owned(), + ["open"] | ["offen"] => todo!(), // show_door_status(), + ["lights"] | ["licht"] => todo!(), // switch_lights(), + ["battery"] | ["batterie"] => todo!(), // show_battery_status(), + ["weather"] | ["wetter"] => todo!(), // show_weather(), + ["indoor", "climate"] | ["innenklima"] => todo!(), // report_indoor_climate(), + ["status"] => todo!(), // report_nextcloud_status(), + ["sensors"] | ["sensoren"] => todo!(), // report_sensor_data(), + ["time"] | ["uhrzeit"] => Local::now().to_string(), + ["code"] | ["pin"] => todo!(), // list_codes(), + _ => String::from("Unknown command!"), + }, + CommandType::Action => match words[..] { + ["open"] | ["öffnen"] => { + command_sender + .send(CommandToButtons::OpenDoor) + .await + .unwrap(); + "Sending open door command...".to_owned() + } // open_door(), + ["lights", "in"] | ["licht", "innen"] => todo!(), // lights, + ["lights", "out"] | ["licht", "aussen"] => todo!(), // lights, + ["play", audio_file_path] | ["abspielen", audio_file_path] => { + audio_sender + .send(AudioEvent::PlayFile(audio_file_path.to_owned())) + .await + .unwrap(); + format!("Started playing audio file {}", audio_file_path).to_owned() + } + ["bell"] | ["glocke"] => { + audio_sender.send(AudioEvent::Bell).await.unwrap(); + "Started bell audio...".to_owned() + } + ["alarm"] => { + audio_sender.send(AudioEvent::FireAlarm).await.unwrap(); + "Started fire alarm sound...".to_owned() + } + ["all", "clear"] | ["entwarnen"] => { + audio_sender.send(AudioEvent::CancelAll).await.unwrap(); + "Canceled audio events...".to_owned() + } + + ["quit"] | ["beenden"] => exit(0), + ["code", "add", name, pin] | ["pin", "hinzufügen", name, pin] => { + todo!() //add_code(name, pin) + } + ["code", "del"] | ["pin", "löschen"] => todo!(), //delete_code(), + ["code", "set"] | ["pin", "ändern"] => todo!(), //set_code(), + _ => String::from("Unknown command!"), + }, + } +} diff --git a/src/nextcloud/mod.rs b/src/nextcloud/mod.rs new file mode 100644 index 0000000..92bc8a9 --- /dev/null +++ b/src/nextcloud/mod.rs @@ -0,0 +1,4 @@ +pub mod commands; + +pub mod nextcloud; +pub use nextcloud::*; // avoid nextcloud::nextcloud input diff --git a/src/nextcloud.rs b/src/nextcloud/nextcloud.rs similarity index 81% rename from src/nextcloud.rs rename to src/nextcloud/nextcloud.rs index fcb2a01..562a0ab 100644 --- a/src/nextcloud.rs +++ b/src/nextcloud/nextcloud.rs @@ -1,28 +1,37 @@ -use crate::{audio::AudioEvent, buttons::CommandToButtons, config::Config, types::ModuleError}; -use futures::{never::Never, try_join}; +use std::collections::HashMap; + +use futures::never::Never; use gettextrs::gettext; use reqwest::{ header::{HeaderMap, ACCEPT, CONTENT_TYPE}, Client, }; -use std::collections::HashMap; use tokio::{ sync::mpsc::{Receiver, Sender}, time::{self, interval}, + try_join, }; +use crate::{audio::AudioEvent, buttons::CommandToButtons, config::Config, types::ModuleError}; + +use super::commands::{run_command, Command}; + +#[derive(Debug)] pub enum NextcloudChat { Default, Ping, Licht, + Command, } +#[derive(Debug)] pub enum NextcloudStatus { Online, Env, Door, } +#[derive(Debug)] pub enum NextcloudEvent { Chat(NextcloudChat, String), SendStatus, @@ -115,6 +124,20 @@ impl Nextcloud { }; } + async fn comand(&self, message: String) { + let result = self.send_message_once(&message, &self.chat_commands).await; + + match result { + Ok(..) => (), + Err(error) => { + eprintln!( + "Couldn't post {} to commands chat because {}", + message, error + ); + } + }; + } + // logs and sends message, retries once, if it fails twice it logs the error async fn send_message(&self, message: String) { let result = self.send_message_once(&message, &self.chat).await; @@ -228,7 +251,7 @@ impl Nextcloud { self.startup_time = startup_time; try_join!( self.clone().message_sender_loop(nextcloud_receiver), - self.command_loop(nextcloud_sender, command_sender, audio_sender) + self.command_loop(nextcloud_sender, command_sender, audio_sender, &self.user) )?; Err(ModuleError::new(String::from( "Exit get_background_task loop!", @@ -251,6 +274,7 @@ impl Nextcloud { NextcloudChat::Default => self.send_message(message).await, NextcloudChat::Ping => self.ping(message).await, NextcloudChat::Licht => self.licht(message).await, + NextcloudChat::Command => self.comand(message).await, }, NextcloudEvent::SendStatus => self.set_status_in_chat().await, NextcloudEvent::Status(status, message) => match status { @@ -270,6 +294,7 @@ impl Nextcloud { nextcloud_sender: Sender, command_sender: Sender, audio_sender: Sender, + user: &str, ) -> Result { let a = self .send_message_once("Started listening to commands here", &self.chat_commands) @@ -285,73 +310,44 @@ impl Nextcloud { if status == 200 { let json = response.json::().await.unwrap(); + eprintln!("json: {}", json); + let messages = json["ocs"]["data"].as_array(); + for message in messages .unwrap() .iter() + .filter(|m| !m["actorId"].to_string().contains("opensesame")) // ignore messages from opensesame users .map(|m| m["message"].as_str().unwrap()) { - if message.starts_with('\\') { - let command_and_args = message - .strip_prefix('\\') - .unwrap() - .split_whitespace() - .collect::>(); - let command = command_and_args[0]; - let args = &command_and_args[1..]; - match command { - "status" => { - nextcloud_sender.send(NextcloudEvent::SendStatus).await? - } - "setpin" => { - // TODO: How do we access config? - } - "switchlights" => { - if args.len() != 2 { - nextcloud_sender - .send(NextcloudEvent::Chat( - NextcloudChat::Default, - String::from( - "Usage: switchlights ", - ), - )) - .await?; - } - - let inner_light = args[0].eq_ignore_ascii_case("true"); - let outer_light = args[1].eq_ignore_ascii_case("true"); - - command_sender - .send(CommandToButtons::SwitchLights( - inner_light, - outer_light, - String::from("Switch lights {} {}"), - )) - .await?; - } - "opensesame" => { - nextcloud_sender - .send(NextcloudEvent::Chat( - NextcloudChat::Default, - String::from("Opening door"), - )) - .await?; - command_sender.send(CommandToButtons::OpenDoor).await?; - } - "ring_bell" => audio_sender.send(AudioEvent::Bell).await?, - "fire_alarm" => { - audio_sender.send(AudioEvent::FireAlarm).await? - } - _ => { - nextcloud_sender - .send(NextcloudEvent::Chat( - NextcloudChat::Default, - format!("Unknown command {}!", command), - )) - .await?; - } + let maybe_command = Command::new(message); + + match maybe_command { + Ok(command) => { + nextcloud_sender + .send(NextcloudEvent::Chat( + NextcloudChat::Command, + run_command( + command, + nextcloud_sender.clone(), + command_sender.clone(), + audio_sender.clone(), + user, + ) + .await, + )) + .await + } + Err(error_message) => { + nextcloud_sender + .send(NextcloudEvent::Chat( + NextcloudChat::Command, + error_message, + )) + .await } } + .unwrap(); } if let Some(last_message) = messages.unwrap().last() { last_known_message_id = last_message["id"].to_string();