diff options
author | Nathan Ringo <nathan@remexre.com> | 2024-01-18 19:02:16 -0600 |
---|---|---|
committer | Nathan Ringo <nathan@remexre.com> | 2024-01-18 19:02:16 -0600 |
commit | 5588808852a2fd379be0e9c01cf67cfdcbcdd4c3 (patch) | |
tree | 0bd001973612cc858ac391ee009d943b15940393 /src/commands/discocaml.rs | |
parent | 81fb055292f49a76732c1966874b8d2ad2cb1807 (diff) |
Prepare to output non-single-expr messages.
Diffstat (limited to 'src/commands/discocaml.rs')
-rw-r--r-- | src/commands/discocaml.rs | 181 |
1 files changed, 144 insertions, 37 deletions
diff --git a/src/commands/discocaml.rs b/src/commands/discocaml.rs index 78c2d95..8565c6f 100644 --- a/src/commands/discocaml.rs +++ b/src/commands/discocaml.rs @@ -1,9 +1,12 @@ use anyhow::{anyhow, bail, Context as _, Error, Result}; +use futures::Future; use serde::{Deserialize, Serialize}; use serde_json::de::Deserializer; use serenity::{ all::{ - CommandDataOptionValue, CommandInteraction, CommandOptionType, CommandType, Member, RoleId, + CommandDataOptionValue, CommandInteraction, CommandOptionType, CommandType, + ComponentInteraction, ComponentInteractionDataKind, InteractionId, + InteractionResponseFlags, Member, RoleId, }, builder::{ CreateButton, CreateCommand, CreateCommandOption, CreateInteractionResponse, @@ -33,7 +36,12 @@ pub struct DiscocamlRequest { #[derive(Debug, PartialEq, Serialize)] pub enum DiscocamlCommand { - Roundtrip, + Parse, + DrawTree, + RunCBN, + RunCBV, + StepCBN, + StepCBV, } /// A response outputted by the discocaml subprocess as a JSON string. @@ -46,11 +54,13 @@ pub enum DiscocamlCommand { /// let example = r#" /// [ "Expr" /// , { "expr": "1 + 2" +/// , "has_redex": true /// } /// ] /// "#; /// let expected = DiscocamlResponse::Expr(DiscocamlResponseExpr { /// expr: "1 + 2".to_string(), +/// has_redex: true, /// }); /// /// let mut de = Deserializer::from_str(&example); @@ -70,6 +80,21 @@ pub enum DiscocamlResponse { #[serde(deny_unknown_fields)] pub struct DiscocamlResponseExpr { pub expr: String, + pub has_redex: bool, +} + +impl DiscocamlResponseExpr { + async fn save(&self, db: &SqlitePool, id: InteractionId) -> Result<()> { + let interaction_id = i64::from(id); + sqlx::query!( + "INSERT INTO discocaml_exprs (interaction_id, expr) VALUES (?, ?)", + interaction_id, + self.expr + ) + .execute(db) + .await?; + Ok(()) + } } pub fn command() -> CreateCommand { @@ -88,7 +113,7 @@ pub fn command() -> CreateCommand { ) } -async fn check_permissions(config: &DiscocamlConfig, member: &Option<Box<Member>>) -> Result<()> { +async fn check_permissions(config: &DiscocamlConfig, member: Option<&Member>) -> Result<()> { if let Some(member) = member { if !member.roles.contains(&config.role) { bail!("This command can only be used by <@&{}>.", config.role) @@ -99,12 +124,14 @@ async fn check_permissions(config: &DiscocamlConfig, member: &Option<Box<Member> } } -async fn respond_with_error(ctx: &Context, command: &CommandInteraction, err: &Error) { - let msg = CreateInteractionResponseMessage::new().content(format!(":no_entry_sign: {}", err)); - if let Err(err) = command - .create_response(ctx, CreateInteractionResponse::Message(msg)) - .await - { +async fn respond_with_error<E: std::error::Error, F: Future<Output = Result<(), E>>>( + err: &Error, + send: impl FnOnce(CreateInteractionResponse) -> F, +) { + let msg = CreateInteractionResponseMessage::new() + .content(format!(":no_entry_sign: {}", err)) + .flags(InteractionResponseFlags::EPHEMERAL); + if let Err(err) = send(CreateInteractionResponse::Message(msg)).await { log::error!( "failed to respond to command that failed permissions check: {:?}", err @@ -159,54 +186,40 @@ pub async fn run_discocaml( fn make_response_message(expr: &DiscocamlResponseExpr) -> CreateInteractionResponseMessage { // TODO: Real escaping - let mut out = CreateInteractionResponseMessage::new() + CreateInteractionResponseMessage::new() .content(format!("```ocaml\n{}\n```", expr.expr)) - .button( - CreateButton::new("draw-tree") - .label("Draw Tree") - .disabled(true), - ); - - out = out + .button(CreateButton::new("draw-tree").label("Draw Tree")) .button( CreateButton::new("step-cbv") .label("Step (CBV)") - .disabled(true), + .disabled(!expr.has_redex), ) .button( CreateButton::new("step-cbn") .label("Step (CBN)") - .disabled(true), + .disabled(!expr.has_redex), ) .button( CreateButton::new("run-cbv") .label("Run (CBV)") - .disabled(true), + .disabled(!expr.has_redex), ) .button( CreateButton::new("run-cbn") .label("Run (CBN)") - .disabled(true), - ); - - out = out.button( - CreateButton::new("start-proving") - .label("Prove it!") - .disabled(true), - ); - - out + .disabled(!expr.has_redex), + ) } pub async fn handle_command( ctx: &Context, config: &DiscocamlConfig, - _db: &SqlitePool, + db: &SqlitePool, command: &CommandInteraction, ) -> Result<()> { // Check that the required role was present. - if let Err(err) = check_permissions(config, &command.member).await { - respond_with_error(ctx, command, &err).await; + if let Err(err) = check_permissions(config, command.member.as_deref()).await { + respond_with_error(&err, |res| command.create_response(&ctx, res)).await; return Err(err.context("permissions check failed")); } @@ -217,7 +230,7 @@ pub async fn handle_command( ("expr", CommandDataOptionValue::String(value)) => expr = Some(value), _ => { let err = anyhow!("unknown option {:?}", option); - respond_with_error(ctx, command, &err).await; + respond_with_error(&err, |res| command.create_response(&ctx, res)).await; return Err(err); } } @@ -226,23 +239,33 @@ pub async fn handle_command( expr } else { let err = anyhow!("missing option {:?}", "expr"); - respond_with_error(ctx, command, &err).await; + respond_with_error(&err, |res| command.create_response(&ctx, res)).await; return Err(err); }; // Round-trip the expression through discocaml. let req = DiscocamlRequest { expr: expr.to_string(), - command: DiscocamlCommand::Roundtrip, + command: DiscocamlCommand::Parse, }; let res = match run_discocaml(config, &req).await { Ok(res) => res, Err(err) => { - respond_with_error(ctx, command, &err).await; + respond_with_error(&err, |res| command.create_response(&ctx, res)).await; return Err(err.context("failed to run discocaml")); } }; + // 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( @@ -252,3 +275,87 @@ pub async fn handle_command( .await .context("failed to respond") } + +pub async fn handle_button( + ctx: &Context, + config: &DiscocamlConfig, + db: &SqlitePool, + component: &ComponentInteraction, +) -> Result<()> { + // Check that the required role was present. + if let Err(err) = check_permissions(config, component.member.as_ref()).await { + respond_with_error(&err, |res| component.create_response(&ctx, res)).await; + return Err(err.context("permissions check failed")); + } + + // Find the interaction ID from the message it was in response to. + let interaction_id = if let Some(interaction) = &component.message.interaction { + i64::from(interaction.id) + } else { + let err = anyhow!( + "button was pressed in response to an unknown message {:?}", + component.message.id + ); + respond_with_error(&err, |res| component.create_response(&ctx, res)).await; + return Err(err); + }; + + // Fetch the expression it was in response to. + let result = sqlx::query!( + "SELECT expr FROM discocaml_exprs WHERE interaction_id = ?", + interaction_id, + ) + .fetch_one(db) + .await + .context("failed to load expression from database"); + let expr = match result { + Ok(expr) => expr.expr, + Err(err) => { + respond_with_error(&err, |res| component.create_response(&ctx, res)).await; + return Err(err); + } + }; + + // Come up with a command for discocaml based on which button it was. + let command = match (&component.data.kind, &component.data.custom_id as &str) { + (ComponentInteractionDataKind::Button, "draw-tree") => DiscocamlCommand::DrawTree, + (ComponentInteractionDataKind::Button, "step-cbv") => DiscocamlCommand::StepCBV, + (ComponentInteractionDataKind::Button, "step-cbn") => DiscocamlCommand::StepCBN, + (ComponentInteractionDataKind::Button, "run-cbv") => DiscocamlCommand::RunCBV, + (ComponentInteractionDataKind::Button, "run-cbn") => DiscocamlCommand::RunCBN, + _ => { + let err = anyhow!("unknown component {:?}", component.data); + respond_with_error(&err, |res| component.create_response(&ctx, res)).await; + return Err(err); + } + }; + + // Send discocaml the request. + let req = DiscocamlRequest { expr, command }; + let res = match run_discocaml(config, &req).await { + Ok(res) => res, + Err(err) => { + respond_with_error(&err, |res| component.create_response(&ctx, res)).await; + return Err(err.context("failed to run discocaml")); + } + }; + + // 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") +} |