diff options
Diffstat (limited to 'src/commands')
-rw-r--r-- | src/commands/discocaml.rs | 138 |
1 files changed, 95 insertions, 43 deletions
diff --git a/src/commands/discocaml.rs b/src/commands/discocaml.rs index 8565c6f..fb6b05b 100644 --- a/src/commands/discocaml.rs +++ b/src/commands/discocaml.rs @@ -1,4 +1,5 @@ use anyhow::{anyhow, bail, Context as _, Error, Result}; +use bstr::BString; use futures::Future; use serde::{Deserialize, Serialize}; use serde_json::de::Deserializer; @@ -9,8 +10,8 @@ use serenity::{ InteractionResponseFlags, Member, RoleId, }, builder::{ - CreateButton, CreateCommand, CreateCommandOption, CreateInteractionResponse, - CreateInteractionResponseMessage, + CreateAttachment, CreateButton, CreateCommand, CreateCommandOption, CreateEmbed, + CreateInteractionResponse, CreateInteractionResponseMessage, }, client::Context, model::Permissions, @@ -74,6 +75,7 @@ pub enum DiscocamlCommand { pub enum DiscocamlResponse { Expr(DiscocamlResponseExpr), Error(String), + Graphviz(String), } #[derive(Debug, Deserialize, PartialEq)] @@ -139,10 +141,60 @@ async fn respond_with_error<E: std::error::Error, F: Future<Output = Result<(), } } +async fn respond_with<E, F>( + db: &SqlitePool, + interaction_id: InteractionId, + res: DiscocamlResponse, + send: impl FnOnce(CreateInteractionResponse) -> F, +) -> Result<()> +where + E: std::error::Error + 'static + Send + Sync, + F: Future<Output = Result<(), E>>, +{ + match res { + DiscocamlResponse::Expr(expr) => { + // Insert the output expression in the database. + if let Err(err) = expr + .save(db, interaction_id) + .await + .context("failed to save expression to database") + { + respond_with_error(&err, send).await; + return Err(err); + } + + // Respond with the expression and the buttons. + let res = expr_response_message(&expr); + send(CreateInteractionResponse::Message(res)).await?; + Ok(()) + } + DiscocamlResponse::Graphviz(dot) => { + let png = match run_dot(&dot).await { + Ok(png) => png, + Err(err) => { + respond_with_error(&err, send).await; + return Err(err); + } + }; + + let msg = CreateInteractionResponseMessage::new() + .add_file(CreateAttachment::bytes(png, "expr.png")) + .add_embed(CreateEmbed::new().attachment("expr.png")); + send(CreateInteractionResponse::Message(msg)).await?; + Ok(()) + } + DiscocamlResponse::Error(err) => { + let err = anyhow!("got an error from discocaml: `{}`", err); + respond_with_error(&err, send).await; + return Err(err); + } + } +} + pub async fn run_discocaml( config: &DiscocamlConfig, req: &DiscocamlRequest, -) -> Result<DiscocamlResponseExpr> { +) -> Result<DiscocamlResponse> { let mut child = Command::new(&config.command[0]) .args(&config.command) .stdin(Stdio::piped()) @@ -177,14 +229,40 @@ pub async fn run_discocaml( .context("failed to parse response from discocaml")?; de.end() .context("failed to parse response from discocaml")?; + Ok(out) +} + +async fn run_dot(dot: &str) -> Result<BString> { + let mut child = Command::new("dot") + .arg("-Tpng") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .kill_on_drop(true) + .spawn() + .context("failed to start dot")?; - match out { - DiscocamlResponse::Expr(expr) => Ok(expr), - DiscocamlResponse::Error(err) => bail!("got an error from discocaml: {:?}", err), + let mut stdin = child.stdin.take().unwrap(); + stdin + .write_all(dot.as_bytes()) + .await + .context("failed to write request to dot")?; + stdin + .shutdown() + .await + .context("failed to close pipe to dot")?; + drop(stdin); + + let output = child + .wait_with_output() + .await + .context("failed to wait for dot to complete")?; + if !output.status.success() { + bail!("dot exited with non-zero status {:?}", output.status) } + Ok(BString::new(output.stdout)) } -fn make_response_message(expr: &DiscocamlResponseExpr) -> CreateInteractionResponseMessage { +fn expr_response_message(expr: &DiscocamlResponseExpr) -> CreateInteractionResponseMessage { // TODO: Real escaping CreateInteractionResponseMessage::new() .content(format!("```ocaml\n{}\n```", expr.expr)) @@ -256,24 +334,11 @@ pub async fn handle_command( } }; - // Insert the output expression in the database. - if let Err(err) = res - .save(db, command.id) - .await - .context("failed to save expression to database") - { - respond_with_error(&err, |res| command.create_response(&ctx, res)).await; - return Err(err); - } - - // Respond with the expression and the buttons. - command - .create_response( - &ctx, - CreateInteractionResponse::Message(make_response_message(&res)), - ) - .await - .context("failed to respond") + // Respond with the resulting expression. + respond_with(db, command.id, res, |res| { + command.create_response(&ctx, res) + }) + .await } pub async fn handle_button( @@ -340,22 +405,9 @@ pub async fn handle_button( } }; - // Insert the output expression in the database. - if let Err(err) = res - .save(db, component.id) - .await - .context("failed to save expression to database") - { - respond_with_error(&err, |res| component.create_response(&ctx, res)).await; - return Err(err); - } - - // Respond with the expression and the buttons. - component - .create_response( - &ctx, - CreateInteractionResponse::Message(make_response_message(&res)), - ) - .await - .context("failed to respond") + // Respond. + respond_with(db, component.id, res, |res| { + component.create_response(&ctx, res) + }) + .await } |