Convert command execution to use results.
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details

This commit is contained in:
projectmoon 2021-01-30 14:17:34 +00:00
parent 1b0003ff1b
commit bf1e2a248f
9 changed files with 147 additions and 132 deletions

View File

@ -1,4 +1,5 @@
use chronicle_dicebot::commands; use chronicle_dicebot::commands;
use chronicle_dicebot::commands::ResponseExtractor;
use chronicle_dicebot::context::{Context, RoomContext}; use chronicle_dicebot::context::{Context, RoomContext};
use chronicle_dicebot::db::Database; use chronicle_dicebot::db::Database;
use chronicle_dicebot::error::BotError; use chronicle_dicebot::error::BotError;
@ -24,6 +25,9 @@ async fn main() -> Result<(), BotError> {
message_body: &input, message_body: &input,
}; };
println!("{}", command.execute(&context).await.plain()); println!(
"{}",
command.execute(&context).await.message_plain("fakeuser")
);
Ok(()) Ok(())
} }

View File

@ -139,13 +139,15 @@ impl DiceBot {
results.push(cmd_result); results.push(cmd_result);
} }
use crate::commands::ResponseExtractor;
if results.len() >= 1 { if results.len() >= 1 {
if results.len() == 1 { if results.len() == 1 {
let cmd_result = &results[0]; let cmd_result = &results[0];
let response = AnyMessageEventContent::RoomMessage(MessageEventContent::Notice( let response = AnyMessageEventContent::RoomMessage(MessageEventContent::Notice(
NoticeMessageEventContent::html( NoticeMessageEventContent::html(
cmd_result.plain.clone(), cmd_result.message_plain(&sender_username),
cmd_result.html.clone(), cmd_result.message_html(&sender_username),
), ),
)); ));

View File

@ -1,4 +1,5 @@
use crate::context::Context; use crate::context::Context;
use crate::error::BotError;
use async_trait::async_trait; use async_trait::async_trait;
use thiserror::Error; use thiserror::Error;
@ -10,6 +11,8 @@ pub mod misc;
pub mod parser; pub mod parser;
pub mod variables; pub mod variables;
/// A custom error type specifically related to parsing command text.
/// Does not wrap an execution failure.
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum CommandError { pub enum CommandError {
#[error("invalid command: {0}")] #[error("invalid command: {0}")]
@ -19,59 +22,105 @@ pub enum CommandError {
IgnoredCommand, IgnoredCommand,
} }
pub struct Execution { /// A successfully executed command returns a message to be sent back
/// to the user in both plain text and HTML, one of which will be
/// displayed in the user's client depending on its capabilities.
#[derive(Debug)]
pub struct Response {
plain: String, plain: String,
html: String, html: String,
} }
impl Execution { impl Response {
pub fn plain(&self) -> &str { pub fn success(plain: String, html: String) -> CommandResult {
&self.plain Ok(Response { plain, html })
} }
pub fn html(&self) -> &str { /// Response message in plain text.
&self.html pub fn plain(&self) -> String {
self.plain.clone()
}
/// Response message in HTML.
pub fn html(&self) -> String {
self.html.clone()
} }
} }
/// Wraps a command execution failure. Provides plain-text and HTML
/// formatting for any error message from the BotError type, similar
/// to how Response provides formatting for successfully executed
/// commands.
#[derive(Error, Debug)]
#[error("{0}")]
pub struct ExecutionError(#[from] BotError);
impl From<crate::db::errors::DataError> for ExecutionError {
fn from(error: crate::db::errors::DataError) -> Self {
Self(BotError::DataError(error))
}
}
impl ExecutionError {
/// Error message in plain text.
pub fn plain(&self) -> String {
format!("{}", self.0)
}
/// Error message in bolded HTML.
pub fn html(&self) -> String {
format!("<p><strong>{}</strong></p>", self.0)
}
}
/// Wraps either a successful command execution response, or an error
/// that occurred.
pub type CommandResult = Result<Response, ExecutionError>;
/// Extract response messages out of a type, whether it is success or
/// failure.
pub trait ResponseExtractor {
/// Plain-text representation of the message, directly mentioning
/// the username.
fn message_plain(&self, username: &str) -> String;
/// HTML representation of the message, directly mentioning the
/// username.
fn message_html(&self, username: &str) -> String;
}
impl ResponseExtractor for CommandResult {
/// Error message in plain text.
fn message_plain(&self, username: &str) -> String {
match self {
Ok(resp) => format!("{}\n{}", username, resp.plain()),
Err(e) => format!("{}\n{}", username, e.plain()),
}
}
/// Error message in bolded HTML.
fn message_html(&self, username: &str) -> String {
match self {
Ok(resp) => format!("<p>{}</p>\n{}", username, resp.html),
Err(e) => format!("<p>{}</p>\n{}", username, e.html()),
}
}
}
/// The trait that any command that can be executed must implement.
#[async_trait] #[async_trait]
pub trait Command: Send + Sync { pub trait Command: Send + Sync {
async fn execute(&self, ctx: &Context<'_>) -> Execution; async fn execute(&self, ctx: &Context<'_>) -> CommandResult;
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
} }
#[derive(Debug)]
pub struct CommandResult {
pub plain: String,
pub html: String,
}
/// Attempt to execute a command, and return the content that should /// Attempt to execute a command, and return the content that should
/// go back to Matrix, if the command was executed (successfully or /// go back to Matrix, if the command was executed (successfully or
/// not). If a command is determined to be ignored, this function will /// not). If a command is determined to be ignored, this function will
/// return None, signifying that we should not send a response. /// return None, signifying that we should not send a response.
pub async fn execute_command(ctx: &Context<'_>) -> CommandResult { pub async fn execute_command(ctx: &Context<'_>) -> CommandResult {
let res = parser::parse_command(&ctx.message_body); let cmd = parser::parse_command(&ctx.message_body)?;
cmd.execute(ctx).await
let (plain, html) = match res {
Ok(cmd) => {
let execution = cmd.execute(ctx).await;
(execution.plain().into(), execution.html().into())
}
Err(e) => {
let message = format!("Error parsing command: {}", e);
let html_message = format!("<p><strong>{}</strong></p>", message);
(message, html_message)
}
};
let plain = format!("{}\n{}", ctx.username, plain);
let html = format!("<p>{}</p>\n{}", ctx.username, html);
CommandResult {
plain: plain,
html: html,
}
} }
#[cfg(test)] #[cfg(test)]
@ -98,6 +147,6 @@ mod tests {
message_body: "!notacommand", message_body: "!notacommand",
}; };
let result = execute_command(&ctx).await; let result = execute_command(&ctx).await;
assert!(result.plain.contains("Error")); assert!(result.is_err());
} }
} }

View File

@ -1,4 +1,4 @@
use super::{Command, Execution}; use super::{Command, CommandResult, Response};
use crate::basic::dice::ElementExpression; use crate::basic::dice::ElementExpression;
use crate::basic::roll::Roll; use crate::basic::roll::Roll;
use crate::context::Context; use crate::context::Context;
@ -12,13 +12,14 @@ impl Command for RollCommand {
"roll regular dice" "roll regular dice"
} }
async fn execute(&self, _ctx: &Context<'_>) -> Execution { async fn execute(&self, _ctx: &Context<'_>) -> CommandResult {
let roll = self.0.roll(); let roll = self.0.roll();
let plain = format!("Dice: {}\nResult: {}", self.0, roll); let plain = format!("Dice: {}\nResult: {}", self.0, roll);
let html = format!( let html = format!(
"<p><strong>Dice:</strong> {}</p><p><strong>Result</strong>: {}</p>", "<p><strong>Dice:</strong> {}</p><p><strong>Result</strong>: {}</p>",
self.0, roll self.0, roll
); );
Execution { plain, html }
Response::success(plain, html)
} }
} }

View File

@ -1,4 +1,4 @@
use super::{Command, Execution}; use super::{Command, CommandResult, Response};
use crate::cofd::dice::{roll_pool, DicePool, DicePoolWithContext}; use crate::cofd::dice::{roll_pool, DicePool, DicePoolWithContext};
use crate::context::Context; use crate::context::Context;
use async_trait::async_trait; use async_trait::async_trait;
@ -11,26 +11,16 @@ impl Command for PoolRollCommand {
"roll dice pool" "roll dice pool"
} }
async fn execute(&self, ctx: &Context<'_>) -> Execution { async fn execute(&self, ctx: &Context<'_>) -> CommandResult {
let pool_with_ctx = DicePoolWithContext(&self.0, ctx); let pool_with_ctx = DicePoolWithContext(&self.0, ctx);
let roll_result = roll_pool(&pool_with_ctx).await; let rolled_pool = roll_pool(&pool_with_ctx).await?;
let (plain, html) = match roll_result {
Ok(rolled_pool) => {
let plain = format!("Pool: {}\nResult: {}", rolled_pool, rolled_pool.roll); let plain = format!("Pool: {}\nResult: {}", rolled_pool, rolled_pool.roll);
let html = format!( let html = format!(
"<p><strong>Pool:</strong> {}</p><p><strong>Result</strong>: {}</p>", "<p><strong>Pool:</strong> {}</p><p><strong>Result</strong>: {}</p>",
rolled_pool, rolled_pool.roll rolled_pool, rolled_pool.roll
); );
(plain, html)
}
Err(e) => {
let plain = format!("Error: {}", e);
let html = format!("<p><strong>Error:</strong> {}</p>", e);
(plain, html)
}
};
Execution { plain, html } Response::success(plain, html)
} }
} }

View File

@ -1,4 +1,4 @@
use super::{Command, Execution}; use super::{Command, CommandResult, Response};
use crate::context::Context; use crate::context::Context;
use crate::cthulhu::dice::{regular_roll, AdvancementRoll, DiceRoll, DiceRollWithContext}; use crate::cthulhu::dice::{regular_roll, AdvancementRoll, DiceRoll, DiceRollWithContext};
use async_trait::async_trait; use async_trait::async_trait;
@ -11,27 +11,17 @@ impl Command for CthRoll {
"roll percentile pool" "roll percentile pool"
} }
async fn execute(&self, ctx: &Context<'_>) -> Execution { async fn execute(&self, ctx: &Context<'_>) -> CommandResult {
let roll_with_ctx = DiceRollWithContext(&self.0, ctx); let roll_with_ctx = DiceRollWithContext(&self.0, ctx);
let roll = regular_roll(&roll_with_ctx).await; let executed_roll = regular_roll(&roll_with_ctx).await?;
let (plain, html) = match roll {
Ok(executed_roll) => {
let plain = format!("Roll: {}\nResult: {}", executed_roll, executed_roll.roll); let plain = format!("Roll: {}\nResult: {}", executed_roll, executed_roll.roll);
let html = format!( let html = format!(
"<p><strong>Roll:</strong> {}</p><p><strong>Result</strong>: {}</p>", "<p><strong>Roll:</strong> {}</p><p><strong>Result</strong>: {}</p>",
executed_roll, executed_roll.roll executed_roll, executed_roll.roll
); );
(plain, html)
}
Err(e) => {
let plain = format!("Error: {}", e);
let html = format!("<p><strong>Error:</strong> {}</p>", e);
(plain, html)
}
};
Execution { plain, html } Response::success(plain, html)
} }
} }
@ -43,7 +33,7 @@ impl Command for CthAdvanceRoll {
"roll percentile pool" "roll percentile pool"
} }
async fn execute(&self, _ctx: &Context<'_>) -> Execution { async fn execute(&self, _ctx: &Context<'_>) -> CommandResult {
//TODO this will be converted to a result when supporting variables. //TODO this will be converted to a result when supporting variables.
let roll = self.0.roll(); let roll = self.0.roll();
let plain = format!("Roll: {}\nResult: {}", self.0, roll); let plain = format!("Roll: {}\nResult: {}", self.0, roll);
@ -52,6 +42,6 @@ impl Command for CthAdvanceRoll {
self.0, roll self.0, roll
); );
Execution { plain, html } Response::success(plain, html)
} }
} }

View File

@ -1,46 +1,33 @@
use super::{Command, Execution}; use super::{Command, CommandResult, Response};
use crate::context::Context; use crate::context::Context;
use crate::db::errors::DataError;
use crate::logic::record_room_information; use crate::logic::record_room_information;
use async_trait::async_trait; use async_trait::async_trait;
use matrix_sdk::identifiers::UserId; use matrix_sdk::identifiers::UserId;
pub struct ResyncCommand; pub struct ResyncCommand;
type ResyncResult = Result<(), DataError>;
#[async_trait] #[async_trait]
impl Command for ResyncCommand { impl Command for ResyncCommand {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"resync room information" "resync room information"
} }
async fn execute(&self, ctx: &Context<'_>) -> Execution { async fn execute(&self, ctx: &Context<'_>) -> CommandResult {
let our_username: Option<UserId> = ctx.matrix_client.user_id().await; let our_username: Option<UserId> = ctx.matrix_client.user_id().await;
let our_username: &str = our_username.as_ref().map_or("", UserId::as_str); let our_username: &str = our_username.as_ref().map_or("", UserId::as_str);
let result: ResyncResult = record_room_information( record_room_information(
ctx.matrix_client, ctx.matrix_client,
&ctx.db, &ctx.db,
ctx.room.id, ctx.room.id,
&ctx.room.display_name, &ctx.room.display_name,
our_username, our_username,
) )
.await; .await?;
let (plain, html) = match result {
Ok(()) => {
let plain = "Room information resynced".to_string(); let plain = "Room information resynced".to_string();
let html = "<p>Room information resynced.</p>".to_string(); let html = "<p>Room information resynced.</p>".to_string();
(plain, html)
}
Err(e) => {
let plain = format!("Error: {}", e);
let html = format!("<p><strong>Error:</strong> {}</p>", e);
(plain, html)
}
};
Execution { plain, html } Response::success(plain, html)
} }
} }

View File

@ -1,4 +1,4 @@
use super::{Command, Execution}; use super::{Command, CommandResult, Response};
use crate::context::Context; use crate::context::Context;
use crate::help::HelpTopic; use crate::help::HelpTopic;
use async_trait::async_trait; use async_trait::async_trait;
@ -11,7 +11,7 @@ impl Command for HelpCommand {
"help information" "help information"
} }
async fn execute(&self, _ctx: &Context<'_>) -> Execution { async fn execute(&self, _ctx: &Context<'_>) -> CommandResult {
let help = match &self.0 { let help = match &self.0 {
Some(topic) => topic.message(), Some(topic) => topic.message(),
_ => "There is no help for this topic", _ => "There is no help for this topic",
@ -19,6 +19,6 @@ impl Command for HelpCommand {
let plain = format!("Help: {}", help); let plain = format!("Help: {}", help);
let html = format!("<p><strong>Help:</strong> {}", help.replace("\n", "<br/>")); let html = format!("<p><strong>Help:</strong> {}", help.replace("\n", "<br/>"));
Execution { plain, html } Response::success(plain, html)
} }
} }

View File

@ -1,4 +1,4 @@
use super::{Command, Execution}; use super::{Command, CommandResult, Response};
use crate::context::Context; use crate::context::Context;
use crate::db::errors::DataError; use crate::db::errors::DataError;
use crate::db::variables::UserAndRoom; use crate::db::variables::UserAndRoom;
@ -12,29 +12,24 @@ impl Command for GetAllVariablesCommand {
"get all variables" "get all variables"
} }
async fn execute(&self, ctx: &Context<'_>) -> Execution { async fn execute(&self, ctx: &Context<'_>) -> CommandResult {
let key = UserAndRoom(&ctx.username, &ctx.room.id.as_str()); let key = UserAndRoom(&ctx.username, &ctx.room.id.as_str());
let result = ctx.db.variables.get_user_variables(&key); let variables = ctx.db.variables.get_user_variables(&key)?;
let value = match result {
Ok(variables) => {
let mut variable_list = variables let mut variable_list = variables
.into_iter() .into_iter()
.map(|(name, value)| format!(" - {} = {}", name, value)) .map(|(name, value)| format!(" - {} = {}", name, value))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
variable_list.sort(); variable_list.sort();
variable_list.join("\n")
}
Err(e) => format!("error getting variables: {}", e),
};
let value = variable_list.join("\n");
let plain = format!("Variables:\n{}", value); let plain = format!("Variables:\n{}", value);
let html = format!( let html = format!(
"<p><strong>Variables:</strong><br/>{}", "<p><strong>Variables:</strong><br/>{}",
value.replace("\n", "<br/>") value.replace("\n", "<br/>")
); );
Execution { plain, html } Response::success(plain, html)
} }
} }
@ -46,7 +41,7 @@ impl Command for GetVariableCommand {
"retrieve variable value" "retrieve variable value"
} }
async fn execute(&self, ctx: &Context<'_>) -> Execution { async fn execute(&self, ctx: &Context<'_>) -> CommandResult {
let name = &self.0; let name = &self.0;
let key = UserAndRoom(&ctx.username, &ctx.room.id.as_str()); let key = UserAndRoom(&ctx.username, &ctx.room.id.as_str());
let result = ctx.db.variables.get_user_variable(&key, name); let result = ctx.db.variables.get_user_variable(&key, name);
@ -54,12 +49,12 @@ impl Command for GetVariableCommand {
let value = match result { let value = match result {
Ok(num) => format!("{} = {}", name, num), Ok(num) => format!("{} = {}", name, num),
Err(DataError::KeyDoesNotExist(_)) => format!("{} is not set", name), Err(DataError::KeyDoesNotExist(_)) => format!("{} is not set", name),
Err(e) => format!("error getting {}: {}", name, e), Err(e) => return Err(e.into()),
}; };
let plain = format!("Variable: {}", value); let plain = format!("Variable: {}", value);
let html = format!("<p><strong>Variable:</strong> {}", value); let html = format!("<p><strong>Variable:</strong> {}", value);
Execution { plain, html } Response::success(plain, html)
} }
} }
@ -71,20 +66,17 @@ impl Command for SetVariableCommand {
"set variable value" "set variable value"
} }
async fn execute(&self, ctx: &Context<'_>) -> Execution { async fn execute(&self, ctx: &Context<'_>) -> CommandResult {
let name = &self.0; let name = &self.0;
let value = self.1; let value = self.1;
let key = UserAndRoom(&ctx.username, ctx.room.id.as_str()); let key = UserAndRoom(&ctx.username, ctx.room.id.as_str());
let result = ctx.db.variables.set_user_variable(&key, name, value);
let content = match result { ctx.db.variables.set_user_variable(&key, name, value)?;
Ok(_) => format!("{} = {}", name, value),
Err(e) => format!("error setting {}: {}", name, e),
};
let content = format!("{} = {}", name, value);
let plain = format!("Set Variable: {}", content); let plain = format!("Set Variable: {}", content);
let html = format!("<p><strong>Set Variable:</strong> {}", content); let html = format!("<p><strong>Set Variable:</strong> {}", content);
Execution { plain, html } Response::success(plain, html)
} }
} }
@ -96,7 +88,7 @@ impl Command for DeleteVariableCommand {
"delete variable" "delete variable"
} }
async fn execute(&self, ctx: &Context<'_>) -> Execution { async fn execute(&self, ctx: &Context<'_>) -> CommandResult {
let name = &self.0; let name = &self.0;
let key = UserAndRoom(&ctx.username, ctx.room.id.as_str()); let key = UserAndRoom(&ctx.username, ctx.room.id.as_str());
let result = ctx.db.variables.delete_user_variable(&key, name); let result = ctx.db.variables.delete_user_variable(&key, name);
@ -104,11 +96,11 @@ impl Command for DeleteVariableCommand {
let value = match result { let value = match result {
Ok(()) => format!("{} now unset", name), Ok(()) => format!("{} now unset", name),
Err(DataError::KeyDoesNotExist(_)) => format!("{} is not currently set", name), Err(DataError::KeyDoesNotExist(_)) => format!("{} is not currently set", name),
Err(e) => format!("error deleting {}: {}", name, e), Err(e) => return Err(e.into()),
}; };
let plain = format!("Remove Variable: {}", value); let plain = format!("Remove Variable: {}", value);
let html = format!("<p><strong>Remove Variable:</strong> {}", value); let html = format!("<p><strong>Remove Variable:</strong> {}", value);
Execution { plain, html } Response::success(plain, html)
} }
} }