use error_stack::{Result, ResultExt}; use std::sync::Arc; use tracing::{debug, warn}; use slack_morphism::prelude::*; use crate::{ BOT_TOKEN, fields, models::{ Member, MessageLog, System, member, trust::Trusted, user::{self, State}, }, }; #[derive(Debug, displaydoc::Display, thiserror::Error)] pub enum Error { /// Error while calling the Slack API Slack, /// Error while calling the database Sqlx, /// Unable to parse view ParsingView, } #[tracing::instrument(skip_all, fields(trigger_id = ?event.trigger_id))] pub async fn start_edit( event: SlackInteractionMessageActionEvent, client: Arc, user_state: &State, ) -> Result<(), Error> { let session = client.open_session(&BOT_TOKEN); let message = event .message .expect("Expected message to edit to, well, have a message"); let Some(log) = MessageLog::fetch_by_message_id(&message.origin.ts, &user_state.db) .await .change_context(Error::Sqlx)? else { debug!( "Message not found in database. User is trying to edit a message that isn't sent by us. Send an error back" ); session .chat_post_ephemeral(&SlackApiChatPostEphemeralRequest::new( event.channel.unwrap().id, event.user.id, SlackMessageContent::new().with_text( "This message was not sent by a member! Did you maybe want to reproxy instead?" .into(), ), )) .await .change_context(Error::Slack)?; return Ok(()); }; let system = log .member_id .fetch(&user_state.db) .await .change_context(Error::Sqlx)? .system_id .fetch(&user_state.db) .await .change_context(Error::Sqlx)?; if system.owner_id != event.user.id { debug!("User is not the owner of the system"); session .chat_post_ephemeral(&SlackApiChatPostEphemeralRequest::new( event.channel.unwrap().id, event.user.id, SlackMessageContent::new().with_text("This message was not sent by you!".into()), )) .await .change_context(Error::Slack)?; return Ok(()); } let message_content = message.content.text.unwrap_or_default(); let view = EditMessageView { message: message_content, } .create_view(&message.origin.ts, &event.channel.unwrap().id); fields!(view = ?&view); session .views_open(&SlackApiViewsOpenRequest::new(event.trigger_id, view)) .await .change_context(Error::Slack)?; debug!("Opened view"); Ok(()) } #[tracing::instrument(skip(client, user_state))] pub async fn edit( view_state: SlackViewState, client: &SlackHyperClient, user_state: &State, user_id: SlackUserId, message_id: SlackTs, channel_id: SlackChannelId, ) -> Result<(), Error> { let session = client.open_session(&BOT_TOKEN); let Some(log) = MessageLog::fetch_by_message_id(&message_id, &user_state.db) .await .change_context(Error::Sqlx)? else { warn!( "Message not found in database. User is trying to edit a message that isn't sent by us. Bailing since this shouldn't happen" ); return Ok(()); }; let system = log .member_id .fetch(&user_state.db) .await .change_context(Error::Sqlx)? .system_id .fetch(&user_state.db) .await .change_context(Error::Sqlx)?; if system.owner_id != user_id { warn!("User is not the owner of the system. This shouldn't happen. Bailing"); return Ok(()); } let view = EditMessageView::try_from(view_state).change_context(Error::ParsingView)?; fields!(view = ?&view); session .chat_update(&SlackApiChatUpdateRequest::new( channel_id, SlackMessageContent::new().with_text(view.message), message_id, )) .await .change_context(Error::Slack)?; debug!("Edited message"); Ok(()) } #[derive(Debug, Default, Clone)] pub struct EditMessageView { pub message: String, } impl EditMessageView { /// Due to the way the slack blocks are created, all fields are moved. /// Clone the whole struct if you need to keep the original. pub fn create_blocks(self) -> Vec { slack_blocks![some_into(SlackInputBlock::new( // https://github.com/abdolence/slack-morphism-rust/issues/327 "Message (No rich text support. Sorry!)".into(), SlackBlockPlainTextInputElement::new("message".into()) .with_initial_value(self.message) .into(), ))] } pub fn create_view(self, message_id: &SlackTs, channel_id: &SlackChannelId) -> SlackView { SlackView::Modal( SlackModalView::new("Edit message".into(), self.create_blocks()) .with_submit("Edit".into()) .with_external_id(format!("edit_message_{}_{}", message_id.0, channel_id.0)), ) } } #[derive(thiserror::Error, displaydoc::Display, Debug)] /// A field was missing from the view pub struct MissingFieldError(String); impl TryFrom for EditMessageView { type Error = MissingFieldError; fn try_from(value: SlackViewState) -> std::result::Result { let mut view = Self::default(); for (_id, values) in value.values { for (id, content) in values { match &*id.0 { "message" => { view.message = content .value .ok_or_else(|| MissingFieldError("message".to_string()))?; } other => { warn!("Unknown field in view when parsing a member::View: {other}"); } } } } if view.message.is_empty() { return Err(MissingFieldError("message".to_string())); } Ok(view) } } #[tracing::instrument(skip_all, fields(trigger_id = ?event.trigger_id))] pub async fn start_reproxy( event: SlackInteractionMessageActionEvent, client: Arc, user_state: &State, ) -> Result<(), Error> { let message = event .message .as_ref() .expect("Expected message to reproxy to, well, have a message"); match MessageLog::fetch_by_message_id(&message.origin.ts, &user_state.db) .await .change_context(Error::Sqlx)? { Some(log) => start_reproxy_log(log, event, client, user_state).await?, None => start_reproxy_user(event, client, user_state).await?, } Ok(()) } #[tracing::instrument(skip(client, user_state))] async fn start_reproxy_user( event: SlackInteractionMessageActionEvent, client: Arc, user_state: &State, ) -> Result<(), Error> { let session = client.open_session(&BOT_TOKEN); let message = event .message .expect("Expected message to reproxy to, well, have a message"); let Some(user_id) = message.sender.user.filter(|user| *user == event.user.id) else { debug!("User is not the owner of the system"); session .chat_post_ephemeral(&SlackApiChatPostEphemeralRequest::new( event.channel.unwrap().id, event.user.id, SlackMessageContent::new().with_text("This message was not sent by you!".into()), )) .await .change_context(Error::Slack)?; return Ok(()); }; let user_id: user::Id = user_id.into(); let Some(system) = System::fetch_by_user_id(&user_id, &user_state.db) .await .change_context(Error::Sqlx)? else { debug!("System not found for user"); session .chat_post_ephemeral(&SlackApiChatPostEphemeralRequest::new( event.channel.unwrap().id, event.user.id, SlackMessageContent::new().with_text("System not found! Make sure you have a system set up. You can use /system create to create one.".into()), )) .await .change_context(Error::Slack)?; return Ok(()); }; let members = system .members(&user_state.db) .await .change_context(Error::Sqlx)?; let view = ReproxyView { member: None }.create_view( &members, &message.origin.ts, &event.channel.unwrap().id, ); fields!(view = ?&view); session .views_open(&SlackApiViewsOpenRequest::new(event.trigger_id, view)) .await .change_context(Error::Slack)?; debug!("Opened view"); Ok(()) } #[tracing::instrument(skip(event, client, user_state))] async fn start_reproxy_log( log: MessageLog, event: SlackInteractionMessageActionEvent, client: Arc, user_state: &State, ) -> Result<(), Error> { let session = client.open_session(&BOT_TOKEN); let system = log .member_id .fetch(&user_state.db) .await .change_context(Error::Sqlx)? .system_id .fetch(&user_state.db) .await .change_context(Error::Sqlx)?; if system.owner_id != event.user.id { debug!("User is not the owner of the system"); session .chat_post_ephemeral(&SlackApiChatPostEphemeralRequest::new( event.channel.unwrap().id, event.user.id, SlackMessageContent::new().with_text("This message was not sent by you!".into()), )) .await .change_context(Error::Slack)?; return Ok(()); } let members = system .members(&user_state.db) .await .change_context(Error::Sqlx)?; let view = ReproxyView { member: Some(log.member_id.id), } .create_view(&members, &log.message_id, &event.channel.unwrap().id); fields!(view = ?&view); session .views_open(&SlackApiViewsOpenRequest::new(event.trigger_id, view)) .await .change_context(Error::Slack)?; debug!("Opened view"); Ok(()) } pub async fn reproxy( view_state: SlackViewState, client: &SlackHyperClient, user_state: &State, user_id: SlackUserId, message_id: SlackTs, channel_id: SlackChannelId, ) -> Result<(), Error> { let session = client.open_session(&BOT_TOKEN); let view = ReproxyView::try_from(view_state).change_context(Error::ParsingView)?; fields!(view = ?&view); let Some(id) = view.member.map(member::Id::new) else { warn!("Missing member on view. This should not happen. bailing"); return Ok(()); }; let Some(system) = System::fetch_by_user_id(&user_id.into(), &user_state.db) .await .change_context(Error::Sqlx)? else { warn!("System not found for user. This should not happen. bailing"); return Ok(()); }; let Some(id) = id .validate_by_system(system.id, &user_state.db) .await .change_context(Error::Sqlx)? else { warn!("Member not found in database. This should not happen. bailing"); return Ok(()); }; let member = id.fetch(&user_state.db).await.change_context(Error::Sqlx)?; let Ok(messages) = session .conversations_history( &SlackApiConversationsHistoryRequest::new() .with_channel(channel_id.clone()) .with_latest(message_id.clone()) .with_limit(1) .with_inclusive(true), ) .await else { warn!("Failed to fetch message history"); return Ok(()); }; let Some(message) = messages.messages.first() else { warn!(?messages, "Message not found in history"); return Ok(()); }; let message_request = SlackApiChatPostMessageRequest::new(channel_id.clone(), message.content.clone()) .with_username(member.display_name.clone()) .opt_icon_url(member.profile_picture_url.clone()); session .chat_post_message(&message_request) .await .change_context(Error::Slack)?; let token = SlackApiToken::new(system.slack_oauth_token.expose().into()) .with_token_type(SlackApiTokenType::User); let user_session = client.open_session(&token); user_session .chat_delete(&SlackApiChatDeleteRequest::new(channel_id, message_id)) .await .change_context(Error::Slack)?; debug!("Reproxied message"); Ok(()) } #[derive(Debug, Default, Clone)] pub struct ReproxyView { pub member: Option, } impl ReproxyView { /// Due to the way the slack blocks are created, all fields are moved. /// Clone the whole struct if you need to keep the original. pub fn create_blocks(self, members: &[Member]) -> Vec { let options = members .iter() .map(|member| { SlackBlockChoiceItem::::new( format!( "{} ({}, ID: {})", member.display_name, member.full_name, member.id ) .into(), member.id.to_string(), ) }) .collect(); let value = self.member.and_then(|member_id| { members .iter() .find(|member| member.id.id == member_id) .map(|member| { SlackBlockChoiceItem::::new( format!( "{} ({}, ID: {})", member.display_name, member.full_name, member.id ) .into(), member.id.to_string(), ) }) }); slack_blocks![some_into( SlackSectionBlock::new() .with_text(SlackBlockText::Plain("Member".into())) .with_accessory( SlackBlockStaticSelectElement::new("member".into()) .with_options(options) .opt_initial_option(value) .into() ) )] } pub fn create_view( self, members: &[Member], message_id: &SlackTs, channel_id: &SlackChannelId, ) -> SlackView { SlackView::Modal( SlackModalView::new("Reproxy message".into(), self.create_blocks(members)) .with_submit("Reproxy".into()) .with_external_id(format!("reproxy_message_{}_{}", message_id.0, channel_id.0)), ) } } impl TryFrom for ReproxyView { type Error = MissingFieldError; fn try_from(value: SlackViewState) -> std::result::Result { let mut view = Self::default(); for (_id, values) in value.values { for (id, content) in values { match &*id.0 { "member" => { view.member = content .selected_option .and_then(|option| option.value.parse::().ok()); } other => { warn!("Unknown field in view when parsing a member::View: {other}"); } } } } if view.member.is_none() { return Err(MissingFieldError("member".to_string())); } Ok(view) } } #[tracing::instrument(skip(client, user_state))] pub async fn delete( event: SlackInteractionMessageActionEvent, client: Arc, user_state: &State, ) -> Result<(), Error> { let session = client.open_session(&BOT_TOKEN); let message = event .message .expect("Expected message to edit to, well, have a message"); let Some(log) = MessageLog::fetch_by_message_id(&message.origin.ts, &user_state.db) .await .change_context(Error::Sqlx)? else { debug!( "Message not found in database. User is trying to delete a message that isn't sent by us." ); session .chat_post_ephemeral(&SlackApiChatPostEphemeralRequest::new( event.channel.unwrap().id, event.user.id, SlackMessageContent::new().with_text("A member didn't send this message.".into()), )) .await .change_context(Error::Slack)?; return Ok(()); }; let system = log .member_id .fetch(&user_state.db) .await .change_context(Error::Sqlx)? .system_id .fetch(&user_state.db) .await .change_context(Error::Sqlx)?; if system.owner_id != event.user.id { session .chat_post_ephemeral(&SlackApiChatPostEphemeralRequest::new( event.channel.unwrap().id, event.user.id, SlackMessageContent::new() .with_text("Your system didn't send this message.".into()), )) .await .change_context(Error::Slack)?; return Ok(()); } session .chat_delete(&SlackApiChatDeleteRequest::new( event.channel.unwrap().id, message.origin.ts, )) .await .change_context(Error::Slack)?; debug!("Deleted message"); Ok(()) } #[tracing::instrument(skip(client, user_state))] pub async fn info( event: SlackInteractionMessageActionEvent, client: Arc, user_state: &State, ) -> Result<(), Error> { let session = client.open_session(&BOT_TOKEN); let message = event .message .expect("Expected message to edit to, well, have a message"); let Some(log) = MessageLog::fetch_by_message_id(&message.origin.ts, &user_state.db) .await .change_context(Error::Sqlx)? else { debug!( "Message not found in database. User is trying to get information about a message that isn't sent by us." ); session .chat_post_ephemeral(&SlackApiChatPostEphemeralRequest::new( event.channel.unwrap().id, event.user.id, SlackMessageContent::new().with_text("A member didn't send this message.".into()), )) .await .change_context(Error::Slack)?; return Ok(()); }; let member = log .member_id .fetch(&user_state.db) .await .change_context(Error::Sqlx)?; let system = member .system_id .fetch(&user_state.db) .await .change_context(Error::Sqlx)?; let blocks = slack_blocks![ some_into(SlackHeaderBlock::new(member.full_name.into())), some_into(SlackDividerBlock::new()), some_into( SlackSectionBlock::new() .with_text(md!( "*{}*\n{}{}\n*System*: {}", member.display_name, member.pronouns.unwrap_or_default(), member .name_pronunciation .map(|pronunciation| format!(" - {pronunciation}")) .unwrap_or_default(), system.owner_id.to_slack_format() )) .opt_accessory(member.profile_picture_url.and_then(|url| Some( SlackSectionBlockElement::Image(SlackBlockImageElement::new( url.parse().ok()?, "Profile picture".into() )) ))) ), optionally_into(system.currently_fronting_member_id.is_some_and(|id| id == member.id) => SlackSectionBlock::new().with_text(md!("*Fronting*"))) // TO-DO: fields ]; session .chat_post_ephemeral(&SlackApiChatPostEphemeralRequest::new( event.channel.unwrap().id, event.user.id, SlackMessageContent::new().with_blocks(blocks), )) .await .change_context(Error::Slack)?; debug!("Deleted message"); Ok(()) }