From e2e9b0584577b28419cfeddbe48cdfbf07947ea5 Mon Sep 17 00:00:00 2001 From: Suya1671 Date: Sat, 21 Jun 2025 13:22:42 +0200 Subject: [PATCH] feat(triggers): change triggers to edit entirely through commands --- src/commands/mod.rs | 2 +- src/commands/trigger.rs | 110 ++++++---------- src/events/mod.rs | 3 +- src/interactions/mod.rs | 81 ------------ src/interactions/trigger.rs | 109 ---------------- src/models/trigger.rs | 250 +++++++----------------------------- 6 files changed, 89 insertions(+), 466 deletions(-) delete mode 100644 src/interactions/trigger.rs diff --git a/src/commands/mod.rs b/src/commands/mod.rs index ac53ee8..031626c 100755 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -49,7 +49,7 @@ impl Command { .await .change_context(CommandError::System), Self::Triggers(triggers) => triggers - .run(event, client, state) + .run(event, state) .await .change_context(CommandError::Triggers), Self::Aliases(aliases) => aliases diff --git a/src/commands/trigger.rs b/src/commands/trigger.rs index 1b4b8b2..27ea36b 100755 --- a/src/commands/trigger.rs +++ b/src/commands/trigger.rs @@ -1,11 +1,9 @@ -use std::sync::Arc; - use error_stack::{Result, ResultExt}; use slack_morphism::prelude::*; use tracing::debug; use crate::{ - BOT_TOKEN, fetch_member, fetch_system, fields, + fetch_member, fetch_system, fields, models::{self, Untrusted, member::MemberRef, trigger, user}, }; @@ -15,6 +13,11 @@ pub enum Trigger { Add { /// The member to add the trigger for. member: MemberRef, + /// The type of trigger + #[clap(name = "type")] + typ: trigger::Type, + /// The trigger content + content: String, }, /// Deletes a trigger Delete { @@ -30,13 +33,17 @@ pub enum Trigger { Edit { /// The trigger to edit. Use the trigger id from /trigger list id: trigger::Id, + /// The type of trigger + #[clap(name = "type", long = "type", short)] + typ: Option, + /// The trigger content + #[clap(long, short)] + content: Option, }, } #[derive(thiserror::Error, displaydoc::Display, Debug)] pub enum CommandError { - /// Error while calling the Slack API - Slack, /// Error while calling the database Sqlx, } @@ -46,64 +53,43 @@ impl Trigger { pub async fn run( self, event: SlackCommandEvent, - client: Arc, state: SlackClientEventsUserState, ) -> Result { match self { - Self::Add { member } => { - let token = &BOT_TOKEN; - let session = client.open_session(token); - Self::create_trigger(event, &state, session, member).await - } + Self::Add { + member, + typ, + content, + } => Self::create_trigger(event, &state, member, typ, content).await, Self::Delete { id } => Self::delete_trigger(event, &state, id).await, Self::List { member } => Self::list_triggers(event, &state, member).await, - Self::Edit { id } => { - let token = &BOT_TOKEN; - let session = client.open_session(token); - Self::edit_trigger(event, &state, session, id).await + Self::Edit { id, typ, content } => { + Self::edit_trigger(event, &state, id, typ, content).await } } } - #[tracing::instrument(skip(event, state, session), fields(system_id, member_id))] + #[tracing::instrument(skip(event, state), fields(system_id, member_id))] async fn create_trigger( event: SlackCommandEvent, state: &SlackClientEventsUserState, - session: SlackClientSession<'_, SlackClientHyperHttpsConnector>, member_id: MemberRef, + typ: trigger::Type, + content: String, ) -> Result { let states = state.read().await; let user_state = states.get_user_state::().unwrap(); fetch_system!(event, user_state => system_id); + fetch_member!(member_id, user_state, system_id => member_id); - let Some(member_id) = member_id - .validate_by_system(system_id, &user_state.db) + models::Trigger::insert(member_id, system_id, typ, content, &user_state.db) .await - .change_context(CommandError::Sqlx)? - else { - debug!("Member not found"); - return Ok(SlackCommandEventResponse::new( - SlackMessageContent::new() - .with_text("Member not found. Make sure you used the correct ID".into()), - )); - }; + .change_context(CommandError::Sqlx)?; - fields!(member_id = %member_id); - - let view = trigger::View::default().create_add_view(member_id); - let view = session - .views_open(&SlackApiViewsOpenRequest::new( - event.trigger_id.clone(), - view, - )) - .await - .attach_printable("Error opening view") - .change_context(CommandError::Slack)?; - - debug!(?view, "Opened view"); - - Ok(SlackCommandEventResponse::new(SlackMessageContent::new())) + Ok(SlackCommandEventResponse::new( + SlackMessageContent::new().with_text("Trigger created!".into()), + )) } #[tracing::instrument(skip(event, state), fields(system_id))] @@ -142,14 +128,13 @@ impl Trigger { )); }; - // Fetch the trigger to delete it trigger_id .delete(&user_state.db) .await .change_context(CommandError::Sqlx)?; Ok(SlackCommandEventResponse::new( - SlackMessageContent::new().with_text("Successfully deleted trigger!".into()), + SlackMessageContent::new().with_text("Deleted trigger!".into()), )) } @@ -191,13 +176,12 @@ impl Trigger { .into_iter() .map(|trigger| { let fields = vec![ - md!("Trigger ID: {}", trigger.id), md!("Member ID: {}", trigger.member_id), md!("{}: {}", trigger.typ, trigger.text), ]; SlackSectionBlock::new() - .with_text(md!("**Trigger {}**", trigger.id)) + .with_text(md!("*Trigger {}*", trigger.id)) .with_fields(fields) }) .map(Into::into) @@ -208,12 +192,13 @@ impl Trigger { )) } - #[tracing::instrument(skip(event, state, session), fields(system_id))] + #[tracing::instrument(skip(event, state), fields(system_id))] pub async fn edit_trigger( event: SlackCommandEvent, state: &SlackClientEventsUserState, - session: SlackClientSession<'_, SlackClientHyperHttpsConnector>, trigger_id: trigger::Id, + typ: Option, + text: Option, ) -> Result { let states = state.read().await; let user_state = states.get_user_state::().unwrap(); @@ -233,32 +218,13 @@ impl Trigger { fields!(trigger_id = %trigger_id); - // Fetch the trigger to edit - let trigger = models::Trigger::fetch_by_id(trigger_id, &user_state.db) + trigger_id + .update(typ, text, &user_state.db) .await .change_context(CommandError::Sqlx)?; - let Some(trigger) = trigger else { - debug!("Trigger not found"); - return Ok(SlackCommandEventResponse::new( - SlackMessageContent::new() - .with_text("Trigger not found. Make sure you used the correct ID".into()), - )); - }; - - let view = trigger::View::from(trigger).create_edit_view(trigger_id); - - let view = session - .views_open(&SlackApiViewsOpenRequest::new( - event.trigger_id.clone(), - view, - )) - .await - .attach_printable("Error opening view") - .change_context(CommandError::Slack)?; - - debug!(?view, "Opened view"); - - Ok(SlackCommandEventResponse::new(SlackMessageContent::new())) + Ok(SlackCommandEventResponse::new( + SlackMessageContent::new().with_text("Updated trigger!".into()), + )) } } diff --git a/src/events/mod.rs b/src/events/mod.rs index c0b9a24..5a9028f 100755 --- a/src/events/mod.rs +++ b/src/events/mod.rs @@ -109,7 +109,7 @@ async fn push_event_callback( } } -#[tracing::instrument(skip_all)] +#[tracing::instrument(skip(client, state))] async fn handle_message( message_event: SlackMessageEvent, client: &SlackHyperClient, @@ -156,6 +156,7 @@ async fn handle_message( .await .change_context(PushEventError::MemberFetch)? else { + debug!("Member not triggered"); return Ok(()); }; diff --git a/src/interactions/mod.rs b/src/interactions/mod.rs index f488ad8..b8e3cdc 100755 --- a/src/interactions/mod.rs +++ b/src/interactions/mod.rs @@ -1,5 +1,4 @@ mod member; -mod trigger; use std::error::Error; use std::sync::Arc; @@ -8,10 +7,8 @@ use error_stack::Report; use member::{create_member, edit_member}; use slack_morphism::prelude::*; use tracing::{debug, error}; -use trigger::{create_trigger, edit_trigger}; use crate::BOT_TOKEN; -use crate::models::system::System; use crate::models::{self, Trusted, user}; #[tracing::instrument(skip(event, environment))] @@ -141,84 +138,6 @@ async fn handle_modal_view( handle_user_error(error, user_id.into(), client).await; } } - Some(id) if id.starts_with("create_trigger_") => { - debug!("Creating trigger"); - - let member_id = id - .strip_prefix("create_trigger_") - .expect("Failed to parse member id from external id") - .parse::() - .map(models::member::Id::new) - .expect("Failed to parse member id from external id"); - - // TO-DO: Better handling of Err case - let Ok(Some(trusted_member_id)) = - member_id.validate_by_user(&user_id, &user_state.db).await - else { - error!( - id, - "Failed to validate member id from external id. Bailing in case this was a malicious call", - ); - return; - }; - - if let Err(error) = create_trigger( - view_state, - &client, - user_state, - user_id.clone(), - trusted_member_id, - ) - .await - { - handle_user_error(error, user_id.into(), client).await; - }; - } - Some(id) if id.starts_with("edit_trigger_") => { - debug!("Editing trigger"); - - let trigger_id = id - .strip_prefix("edit_trigger_") - .expect("Failed to parse member id from external id") - .parse::() - .map(models::trigger::Id::new) - .expect("Failed to parse member id from external id"); - - let Some(system) = System::fetch_by_user_id(&user_state.db, &user_id) - .await - .ok() - .flatten() - else { - error!( - %user_id, - "Failed to fetch system id for user id. Bailing in case this was a malicious call" - ); - return; - }; - - let Ok(trusted_trigger_id) = trigger_id - .validate_by_system(system.id, &user_state.db) - .await - else { - error!( - "Failed to validate member id from external id {}. Bailing in case this was a malicious call", - id - ); - return; - }; - - if let Err(error) = edit_trigger( - view_state, - &client, - user_state, - user_id.clone(), - trusted_trigger_id, - ) - .await - { - handle_user_error(error, user_id.into(), client.clone()).await; - } - } Some(id) => { error!("receieved unknown external id: {id}"); } diff --git a/src/interactions/trigger.rs b/src/interactions/trigger.rs deleted file mode 100644 index b06ecd1..0000000 --- a/src/interactions/trigger.rs +++ /dev/null @@ -1,109 +0,0 @@ -use error_stack::{Result, ResultExt, bail}; -use slack_morphism::prelude::*; -use tracing::debug; - -use crate::{ - BOT_TOKEN, fields, - models::{ - Trusted, member, - system::System, - trigger, - user::{self, State}, - }, -}; - -#[derive(thiserror::Error, displaydoc::Display, Debug)] -pub enum Error { - /// Error while calling the database - Sqlx, - /// Error while calling the Slack API - Slack, - /// No system found for the user - NoSystem, -} - -#[tracing::instrument(skip(view_state, client, user_state), fields(system_id))] -pub async fn create_trigger( - view_state: SlackViewState, - client: &SlackHyperClient, - user_state: &State, - user_id: user::Id, - member_id: member::Id, -) -> Result<(), Error> { - let data = trigger::View::from(view_state); - - let Some(system_id) = System::fetch_by_user_id(&user_state.db, &user_id) - .await - .attach_printable("Error checking if system exists") - .change_context(Error::Sqlx)? - .map(|system| system.id) - else { - bail!(Error::NoSystem); - }; - - fields!(system_id = %system_id); - - let id = data - .add(system_id, member_id, &user_state.db) - .await - .change_context(Error::Sqlx)?; - - fields!(id = %id); - debug!("Trigger created"); - - let session = client.open_session(&BOT_TOKEN); - let user: SlackUserId = user_id.into(); - - let conversation = session - .conversations_open(&SlackApiConversationsOpenRequest::new().with_users(vec![user.clone()])) - .await - .change_context(Error::Slack)? - .channel; - - session - .chat_post_ephemeral(&SlackApiChatPostEphemeralRequest::new( - conversation.id, - user, - SlackMessageContent::new().with_text("Successfully added trigger!".into()), - )) - .await - .change_context(Error::Slack)?; - - Ok(()) -} - -#[tracing::instrument(skip(view_state, client, user_state))] -pub async fn edit_trigger( - view_state: SlackViewState, - client: &SlackHyperClient, - user_state: &State, - user_id: user::Id, - trigger_id: trigger::Id, -) -> Result<(), Error> { - let trigger_view = trigger::View::from(view_state); - - trigger_view - .update(trigger_id, &user_state.db) - .await - .change_context(Error::Sqlx)?; - - let session = client.open_session(&BOT_TOKEN); - let user: SlackUserId = user_id.into(); - - let conversation = session - .conversations_open(&SlackApiConversationsOpenRequest::new().with_users(vec![user.clone()])) - .await - .change_context(Error::Slack)? - .channel; - - session - .chat_post_ephemeral(&SlackApiChatPostEphemeralRequest::new( - conversation.id, - user, - SlackMessageContent::new().with_text("Successfully edited trigger!".into()), - )) - .await - .change_context(Error::Slack)?; - - Ok(()) -} diff --git a/src/models/trigger.rs b/src/models/trigger.rs index 9b48171..f549bd8 100755 --- a/src/models/trigger.rs +++ b/src/models/trigger.rs @@ -2,11 +2,9 @@ use std::str::FromStr; use crate::id; -use super::{Trustability, Trusted, Untrusted, member, system}; +use super::{Trusted, Untrusted, member, system}; use error_stack::{Result, ResultExt}; -use slack_morphism::prelude::*; use sqlx::{SqlitePool, prelude::*, sqlite::SqliteQueryResult}; -use tracing::{debug, warn}; id!( /// For an ID to be trusted, it must @@ -17,13 +15,6 @@ id!( ); impl Id { - pub const fn new(id: i64) -> Self { - Self { - id, - trusted: std::marker::PhantomData, - } - } - #[tracing::instrument(skip(db))] pub async fn validate_by_system( self, @@ -59,15 +50,36 @@ impl Id { .await .attach_printable("Error deleting trigger") } + + #[tracing::instrument(skip(db))] + pub async fn update( + self, + typ: Option, + content: Option, + db: &SqlitePool, + ) -> error_stack::Result { + sqlx::query!( + r#" + UPDATE triggers + SET + typ = coalesce($2, typ), + text = coalesce($3, text) + WHERE id = $1 + RETURNING + id as "id: Id" + "#, + self, + typ, + content + ) + .fetch_one(db) + .await + .attach_printable("Failed to update trigger") + .map(|record| record.id) + } } -#[derive(thiserror::Error, displaydoc::Display, Debug)] -pub enum Error { - /// Error while calling the database - Sqlx, -} - -#[derive(Debug, sqlx::Type, displaydoc::Display, PartialEq, Eq)] +#[derive(Debug, sqlx::Type, displaydoc::Display, PartialEq, Eq, clap::ValueEnum, Clone, Copy)] #[repr(i64)] pub enum Type { /// Suffix @@ -115,30 +127,6 @@ pub struct Trigger { } impl Trigger { - pub async fn fetch_by_id(id: Id, db: &SqlitePool) -> Result, sqlx::Error> - where - T: Trustability, - { - sqlx::query_as!( - Trigger, - r#" - SELECT - id as "id: Id", - member_id as "member_id: member::Id", - system_id as "system_id: system::Id", - text, - typ - FROM - triggers - WHERE id = $1 - "#, - id.id, - ) - .fetch_optional(db) - .await - .attach_printable("Error fetching trigger") - } - #[tracing::instrument(skip(db))] pub async fn fetch_by_system_id( system_id: system::Id, @@ -189,176 +177,34 @@ impl Trigger { .await .attach_printable("Error fetching triggers") } -} -#[derive(Debug)] -pub struct View { - pub text: String, - pub typ: Type, -} - -impl Default for View { - fn default() -> Self { - Self { - text: String::new(), - typ: Type::Prefix, - } - } -} - -impl View { - pub fn create_blocks(self) -> Vec { - let prefix_choice = SlackBlockChoiceItem::new( - SlackBlockText::Plain(Type::Prefix.to_string().into()), - Type::Prefix.to_string(), - ); - let suffix_choice = SlackBlockChoiceItem::new( - SlackBlockText::Plain(Type::Suffix.to_string().into()), - Type::Suffix.to_string(), - ); - - slack_blocks!( - some_into( - SlackHeaderBlock::new("Trigger settings".into()) - .with_block_id("trigger_settings".into()) - ), - some_into( - SlackInputBlock::new( - "Trigger Text".into(), - SlackBlockPlainTextInputElement::new("trigger_text".into()) - .with_initial_value(self.text) - .into(), - ) - .with_optional(false) - ), - some_into( - SlackInputBlock::new( - "Trigger Type".into(), - SlackBlockRadioButtonsElement::new( - "type".into(), - vec![prefix_choice, suffix_choice] - ) - .with_initial_option(SlackBlockChoiceItem::new( - SlackBlockText::Plain(Type::Prefix.to_string().into()), - Type::Prefix.to_string(), - )) - .into(), - ) - .with_optional(false) - ) - ) - } - - /// Add a trigger to the database - /// - /// Returns the id of the new trigger #[tracing::instrument(skip(db))] - pub async fn add( - &self, - system_id: system::Id, + pub async fn insert( member_id: member::Id, + system_id: system::Id, + typ: Type, + content: String, db: &SqlitePool, - ) -> error_stack::Result, Error> { - debug!( - "Adding trigger for {} (Member ID {}) to database", - system_id, member_id - ); - - sqlx::query!( + ) -> error_stack::Result { + sqlx::query_as!( + Self, r#" - INSERT INTO triggers (system_id, member_id, text, typ) + INSERT INTO triggers (member_id, system_id, typ, text) VALUES ($1, $2, $3, $4) - RETURNING id + RETURNING + id as "id: Id", + member_id as "member_id: member::Id", + system_id as "system_id: system::Id", + typ, + text "#, - system_id.id, - member_id.id, - self.text, - self.typ + member_id, + system_id, + typ, + content ) .fetch_one(db) .await - .attach_printable("Error adding trigger to database") - .change_context(Error::Sqlx) - .map(|row| Id { - id: row.id, - trusted: std::marker::PhantomData, - }) - } - - /// Update a trigger in the database to match this view - #[tracing::instrument(skip(db))] - pub async fn update( - &self, - trigger_id: Id, - db: &SqlitePool, - ) -> Result { - sqlx::query!( - r#" - UPDATE triggers - SET text = $1, typ = $2 - WHERE id = $3 - "#, - self.text, - self.typ, - trigger_id.id, - ) - .execute(db) - .await - .attach_printable("Error updating trigger in database") - } - - pub fn create_add_view(self, member_id: member::Id) -> SlackView { - SlackView::Modal( - SlackModalView::new("Add a new trigger".into(), self.create_blocks()) - .with_submit("Add".into()) - .with_external_id(format!("create_trigger_{}", member_id.id)), - ) - } - - pub fn create_edit_view(self, trigger_id: Id) -> SlackView { - SlackView::Modal( - SlackModalView::new("Edit trigger".into(), self.create_blocks()) - .with_submit("Save".into()) - .with_external_id(format!("edit_trigger_{}", trigger_id.id)), - ) - } -} - -impl From for View { - fn from(value: SlackViewState) -> Self { - let mut view = Self::default(); - for (_id, values) in value.values { - for (id, content) in values { - match &*id.0 { - "trigger_text" => { - if let Some(text) = content.value { - view.text = text; - } - } - "typ" => { - if let Some(option) = content.selected_option { - match option.value.parse::() { - Ok(typ) => view.typ = typ, - Err(error) => warn!(?error, "Error parsing trigger type"), - } - } - } - other => { - warn!("Unknown field in view when parsing a trigger::View: {other}"); - } - } - } - } - - view - } -} - -impl From for View { - fn from(trigger: Trigger) -> Self { - Self { - text: trigger.text, - typ: trigger.typ, - } + .attach_printable("Failed to insert trigger into database") } }