mirror of
https://github.com/System-End/plura.git
synced 2026-04-19 22:05:08 +00:00
feat(interactions::message): Add the ability to reproxy message
This also cleans up some related APIs.
This commit is contained in:
parent
4005e8167c
commit
17a9622739
8 changed files with 367 additions and 16 deletions
|
|
@ -159,7 +159,7 @@ impl Member {
|
|||
|
||||
fields!(user_id = %user_id.clone());
|
||||
|
||||
let Some(system) = models::System::fetch_by_user_id(&user_state.db, &user_id)
|
||||
let Some(system) = models::System::fetch_by_user_id(&user_id, &user_state.db)
|
||||
.await
|
||||
.change_context(CommandError::Sqlx)?
|
||||
else {
|
||||
|
|
@ -178,7 +178,7 @@ impl Member {
|
|||
fields!(system_id = %system.id);
|
||||
|
||||
let members = system
|
||||
.get_members(&user_state.db)
|
||||
.members(&user_state.db)
|
||||
.await
|
||||
.change_context(CommandError::Sqlx)?;
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ impl System {
|
|||
fields!(user_id = %&user_id);
|
||||
trace!("Mapped user ID");
|
||||
|
||||
let system = models::System::fetch_by_user_id(&user_state.db, &user_id)
|
||||
let system = models::System::fetch_by_user_id(&user_id, &user_state.db)
|
||||
.await
|
||||
.change_context(CommandError::Sqlx)?;
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ impl System {
|
|||
let user_state = states.get_user_state::<user::State>().unwrap();
|
||||
|
||||
let Some(system_id) =
|
||||
models::System::fetch_by_user_id(&user_state.db, &event.user_id.into())
|
||||
models::System::fetch_by_user_id(&event.user_id.into(), &user_state.db)
|
||||
.await
|
||||
.change_context(CommandError::Sqlx)?
|
||||
.map(|s| s.id)
|
||||
|
|
@ -175,7 +175,7 @@ impl System {
|
|||
let user_state = states.get_user_state::<user::State>().unwrap();
|
||||
let user_id = user::Id::new(event.user_id);
|
||||
|
||||
if let Some(system) = models::System::fetch_by_user_id(&user_state.db, &user_id)
|
||||
if let Some(system) = models::System::fetch_by_user_id(&user_id, &user_state.db)
|
||||
.await
|
||||
.change_context(CommandError::Sqlx)?
|
||||
{
|
||||
|
|
@ -228,8 +228,8 @@ impl System {
|
|||
macro_rules! fetch_system {
|
||||
($event:expr, $user_state:expr => $system_var_name:ident) => {
|
||||
let Some($system_var_name) = $crate::models::System::fetch_by_user_id(
|
||||
&$user_state.db,
|
||||
&$crate::models::user::Id::new($event.user_id),
|
||||
&$user_state.db,
|
||||
)
|
||||
.await
|
||||
.change_context(CommandError::Sqlx)?
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ impl Trigger {
|
|||
let user_state = states.get_user_state::<user::State>().unwrap();
|
||||
|
||||
let Some(system_id) =
|
||||
models::System::fetch_by_user_id(&user_state.db, &user::Id::new(event.user_id))
|
||||
models::System::fetch_by_user_id(&user::Id::new(event.user_id), &user_state.db)
|
||||
.await
|
||||
.change_context(CommandError::Sqlx)?
|
||||
.map(|system| system.id)
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ async fn handle_message(
|
|||
|
||||
fields!(user_id = ?&user_id);
|
||||
|
||||
let Some(mut system) = models::System::fetch_by_user_id(&user_state.db, &user_id)
|
||||
let Some(mut system) = models::System::fetch_by_user_id(&user_id, &user_state.db)
|
||||
.await
|
||||
.change_context(PushEventError::SystemFetch)?
|
||||
else {
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ pub async fn create_member(
|
|||
trace!("Creating member");
|
||||
let data = member::View::try_from(view_state).change_context(Error::ParsingView)?;
|
||||
|
||||
let Some(system_id) = System::fetch_by_user_id(&user_state.db, &user_id)
|
||||
let Some(system_id) = System::fetch_by_user_id(&user_id, &user_state.db)
|
||||
.await
|
||||
.attach_printable("Error checking if system exists")
|
||||
.change_context(Error::Sqlx)?
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ use slack_morphism::prelude::*;
|
|||
|
||||
use crate::{
|
||||
BOT_TOKEN, fields,
|
||||
models::{message, user::State},
|
||||
models::{
|
||||
Member, MessageLog, System, Trusted, member,
|
||||
user::{self, State},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, displaydoc::Display, thiserror::Error)]
|
||||
|
|
@ -30,7 +33,7 @@ pub async fn start_edit(
|
|||
.message
|
||||
.expect("Expected message to edit to, well, have a message");
|
||||
|
||||
let Some(log) = message::MessageLog::fetch_by_message_id(&message.origin.ts, &user_state.db)
|
||||
let Some(log) = MessageLog::fetch_by_message_id(&message.origin.ts, &user_state.db)
|
||||
.await
|
||||
.change_context(Error::Sqlx)?
|
||||
else {
|
||||
|
|
@ -108,14 +111,13 @@ pub async fn edit(
|
|||
) -> Result<(), Error> {
|
||||
let session = client.open_session(&BOT_TOKEN);
|
||||
|
||||
let Some(log) = message::MessageLog::fetch_by_message_id(&message_id, &user_state.db)
|
||||
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(());
|
||||
};
|
||||
|
||||
|
|
@ -131,7 +133,6 @@ pub async fn edit(
|
|||
|
||||
if system.owner_id != user_id {
|
||||
warn!("User is not the owner of the system. This shouldn't happen. Bailing");
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
|
@ -211,3 +212,323 @@ impl TryFrom<SlackViewState> for EditMessageView {
|
|||
Ok(view)
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, fields(trigger_id = ?event.trigger_id))]
|
||||
pub async fn start_reproxy(
|
||||
event: SlackInteractionMessageActionEvent,
|
||||
client: Arc<SlackHyperClient>,
|
||||
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<SlackHyperClient>,
|
||||
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<Trusted> = 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<SlackHyperClient>,
|
||||
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<i64>,
|
||||
}
|
||||
|
||||
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<SlackBlock> {
|
||||
let options = members
|
||||
.iter()
|
||||
.map(|member| {
|
||||
SlackBlockChoiceItem::<SlackBlockPlainTextOnly>::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::<SlackBlockPlainTextOnly>::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<SlackViewState> for ReproxyView {
|
||||
type Error = MissingFieldError;
|
||||
|
||||
fn try_from(value: SlackViewState) -> std::result::Result<Self, Self::Error> {
|
||||
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::<i64>().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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@ async fn interaction_event(
|
|||
)
|
||||
.await?;
|
||||
}
|
||||
"reproxy_message" => {
|
||||
message::start_reproxy(
|
||||
message_event,
|
||||
client,
|
||||
states.read().await.get_user_state().unwrap(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
id => warn!(id, "Unknown message action callback ID"),
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -135,6 +143,28 @@ async fn handle_modal_view(
|
|||
handle_user_error(e, user_id.into(), client).await;
|
||||
}
|
||||
}
|
||||
Some(id) if id.starts_with("reproxy_message_") => {
|
||||
debug!("Received reproxy message modal view");
|
||||
|
||||
let stripped = id.strip_prefix("reproxy_message_").unwrap();
|
||||
|
||||
let (message_id, channel_id) = stripped.split_once('_').unwrap();
|
||||
let message_id = SlackTs::new(message_id.to_owned());
|
||||
let channel_id = SlackChannelId::new(channel_id.to_owned());
|
||||
|
||||
if let Err(e) = message::reproxy(
|
||||
view_state,
|
||||
&client,
|
||||
user_state,
|
||||
user_id.clone().into(),
|
||||
message_id,
|
||||
channel_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
handle_user_error(e, user_id.into(), client).await;
|
||||
}
|
||||
}
|
||||
Some(id) if id.starts_with("edit_member_") => {
|
||||
debug!("Received edit member modal view");
|
||||
|
||||
|
|
|
|||
|
|
@ -136,8 +136,8 @@ pub struct System {
|
|||
impl System {
|
||||
#[tracing::instrument(skip(db))]
|
||||
pub async fn fetch_by_user_id<T>(
|
||||
db: &SqlitePool,
|
||||
user_id: &user::Id<T>,
|
||||
db: &SqlitePool,
|
||||
) -> Result<Option<Self>, sqlx::Error>
|
||||
where
|
||||
T: Trustability,
|
||||
|
|
@ -188,7 +188,7 @@ impl System {
|
|||
Ok(new_active_member)
|
||||
}
|
||||
|
||||
pub async fn get_members(&self, db: &SqlitePool) -> Result<Vec<Member>, sqlx::Error> {
|
||||
pub async fn members(&self, db: &SqlitePool) -> Result<Vec<Member>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Member,
|
||||
r#"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue