Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 50 additions & 10 deletions crates/ai_tools/src/tool_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ use documents::{domain::ports::TaskPropertiesPort, inbound::toolset::DocumentToo
use email::{
domain::service::EmailServiceImpl, inbound::toolset::EmailToolContext, outbound::EmailPgRepo,
};
use entity_access::domain::models::EditAccessLevel;
use entity_access::domain::models::{BotAccessScope, EditAccessLevel};
use entity_access::domain::ports::EntityAccessService as _;
use foreign_entity::{
domain::service::ForeignEntityServiceImpl,
Expand Down Expand Up @@ -377,6 +377,40 @@ pub fn build_team_tool_context(pool: sqlx::PgPool) -> ToolTeamToolContext {
}

/// No-op task properties service for tests and contexts that do not create tasks.
async fn task_property_edit_receipt<A: entity_access::domain::ports::EntityAccessService>(
entity_access: &A,
user_id: &MacroUserIdStr<'_>,
attribution: &activity::Attribution,
entity_id: &str,
) -> anyhow::Result<entity_access::domain::models::EntityAccessReceipt<EditAccessLevel>> {
if let activity::Attribution::Delegated { actor, subject } = attribution
&& let Some(bot) = actor.as_bot()
{
return entity_access
.generate_bot_entity_access_receipt::<EditAccessLevel>(
bot.bot_id(),
BotAccessScope::User {
user_id: subject.clone(),
user_org_id: None,
},
entity_id,
model_entity::EntityType::Document,
)
.await
.map_err(Into::into);
}

entity_access
.generate_entity_access_receipt::<EditAccessLevel>(
user_id,
None,
entity_id,
model_entity::EntityType::Document,
)
.await
.map_err(Into::into)
}

#[derive(Clone)]
pub struct NoOpTaskProperties;

Expand All @@ -393,6 +427,7 @@ impl TaskPropertiesPort for NoOpTaskProperties {
_entity_id: &str,
_property_definition_id: uuid::Uuid,
_value: Option<models_properties::api::requests::SetPropertyValue>,
_attribution: &activity::Attribution,
) -> anyhow::Result<()> {
Ok(())
}
Expand Down Expand Up @@ -440,19 +475,18 @@ impl TaskPropertiesPort for TaskPropertiesAdapter {
entity_id: &str,
property_definition_id: uuid::Uuid,
value: Option<models_properties::api::requests::SetPropertyValue>,
attribution: &activity::Attribution,
) -> anyhow::Result<()> {
use properties::PropertiesService as _;

let user_id = macro_user_id::user_id::MacroUserIdStr::parse_from_str(user_id)?;
let entity_access_receipt = self
.entity_access_service
.generate_entity_access_receipt::<EditAccessLevel>(
&user_id,
None,
entity_id,
model_entity::EntityType::Document,
)
.await?;
let entity_access_receipt = task_property_edit_receipt(
self.entity_access_service.as_ref(),
&user_id,
attribution,
entity_id,
)
.await?;
self.properties
.set_entity_property(&entity_access_receipt, property_definition_id, value)
.await
Expand Down Expand Up @@ -1019,6 +1053,9 @@ impl ToolEntityCreator {
use models_properties::api::requests::SetPropertyValue;
use system_properties::SystemPropertyKey;

let attribution =
activity::Attribution::direct(activity::Actor::new_from_user(user.clone()));

if let Some(status) = properties.status.as_deref()
&& let Err(e) = self
.task_properties
Expand Down Expand Up @@ -1047,6 +1084,7 @@ impl ToolEntityCreator {
Some(SetPropertyValue::SelectOption {
option_id: option.uuid(),
}),
&attribution,
)
.await
{
Expand All @@ -1068,6 +1106,7 @@ impl ToolEntityCreator {
task_id,
SystemPropertyKey::DueDate.uuid(),
Some(SetPropertyValue::Date { value }),
&attribution,
)
.await
{
Expand Down Expand Up @@ -1102,6 +1141,7 @@ impl ToolEntityCreator {
Some(SetPropertyValue::MultiEntityReference {
references: vec![reference],
}),
&attribution,
)
.await
{
Expand Down
1 change: 1 addition & 0 deletions crates/channels/src/domain/activity/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,6 @@ fn created_maps_to_created_by_the_actor() {
panic!("expected activities");
};
assert_eq!(activities[0].action, Action::Created);
assert_eq!(activities[0].actor.as_ref(), "macro|owner@example.com");
assert_eq!(activities[0].subject_id, "macro|owner@example.com");
}
14 changes: 14 additions & 0 deletions crates/channels/src/domain/ports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,20 @@ pub trait ChannelService: Send + Sync + 'static {
}
}

/// Create a channel owned by `owner` and attributed to the system principal.
fn create_system_channel(
&self,
_owner: MacroUserIdStr<'static>,
_actor_org_id: Option<i64>,
_req: CreateChannelRequest,
) -> impl Future<Output = Result<CreateChannelResponse, ChannelMutationErr>> + Send {
async move {
Err(ChannelMutationErr::NotFound(
"channel mutations are not configured".to_string(),
))
}
}

/// Add or reactivate a user in every auto-join channel for a team.
fn auto_join_by_team_id(
&self,
Expand Down
41 changes: 37 additions & 4 deletions crates/channels/src/domain/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,31 @@ where
_actor_org_id: Option<i64>,
req: crate::domain::models::CreateChannelRequest,
) -> Result<crate::domain::models::CreateChannelResponse, ChannelMutationErr> {
let actor = require_user_actor(&actor)?;
let owner = require_user_actor(&actor)?;
self.create_owned_channel(owner.clone(), Sender::new_from_user(owner), req)
.await
}

#[tracing::instrument(err, skip(self, req))]
async fn create_system_channel(
&self,
owner: MacroUserIdStr<'static>,
req: crate::domain::models::CreateChannelRequest,
) -> Result<crate::domain::models::CreateChannelResponse, ChannelMutationErr> {
self.create_owned_channel(
owner,
Sender::new_from_bot(bot_id::MACRO_SYSTEM_BOT_ID),
req,
)
.await
}

async fn create_owned_channel(
&self,
owner: MacroUserIdStr<'static>,
activity_actor: Sender,
req: crate::domain::models::CreateChannelRequest,
) -> Result<crate::domain::models::CreateChannelResponse, ChannelMutationErr> {
if req.auto_join_team && req.channel_type != ChannelType::Team {
return Err(ChannelMutationErr::BadRequest(
"auto-join is only available for team channels".to_string(),
Expand All @@ -343,7 +367,7 @@ where
})?;
let has_team = self
.repo
.user_has_team(actor.as_ref().to_string(), team_id)
.user_has_team(owner.as_ref().to_string(), team_id)
.await
.map_err(|e| ChannelMutationErr::Repo(e.into()))?;
if !has_team {
Expand All @@ -370,12 +394,12 @@ where
let channel_name = req.name.clone();

let created_channel = self
.create_channel_record(actor.copied(), org_id, req)
.create_channel_record(owner.copied(), org_id, req)
.await?;

self.events.dispatch(ChannelEvent::ChannelCreated {
channel_id: created_channel.id,
actor: Sender::new_from_user(actor),
actor: activity_actor,
channel_type,
channel_name,
participant_user_ids: created_channel.participant_user_ids,
Expand Down Expand Up @@ -1788,6 +1812,15 @@ where
ChannelServiceImpl::create_channel(self, actor, None, req).await
}

async fn create_system_channel(
&self,
owner: MacroUserIdStr<'static>,
_actor_org_id: Option<i64>,
req: crate::domain::models::CreateChannelRequest,
) -> Result<crate::domain::models::CreateChannelResponse, ChannelMutationErr> {
ChannelServiceImpl::create_system_channel(self, owner, req).await
}

#[tracing::instrument(err, skip(self, user_id))]
async fn auto_join_by_team_id(
&self,
Expand Down
61 changes: 61 additions & 0 deletions crates/channels/src/domain/service/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2233,6 +2233,67 @@ async fn remove_participants_allows_removing_non_owner() {
assert!(state.touched_channel_ids.is_empty());
}

#[tokio::test]
async fn create_system_channel_event_uses_system_actor() {
let channel_id = Uuid::new_v4();
let repo = FakeMutationRepo::new(channel_id, "macro|owner@test.com");
let events = FakeEvents::default();
let svc = mutation_service(repo, events.clone(), FakeReferenceSharing::default());

svc.create_system_channel(
macro_id("macro|owner@test.com"),
crate::domain::models::CreateChannelRequest {
name: Some("Macro Support x owner".to_string()),
channel_type: ChannelType::Private,
team_id: None,
auto_join_team: false,
participants: HashSet::from([macro_id("macro|teo@macro.com")]),
},
)
.await
.unwrap();

let events = events.events.lock().unwrap();
assert!(matches!(
events.as_slice(),
[ChannelEvent::ChannelCreated { actor, channel_name: Some(name), .. }]
if actor == &Sender::new_from_bot(bot_id::MACRO_SYSTEM_BOT_ID)
&& name == "Macro Support x owner"
));
}

/// Signup on main called `create_channel(Sender::new_from_user(owner))`.
/// That is the path that made "Created # Macro Support x …" render as You.
#[tokio::test]
async fn signup_support_channel_via_user_create_channel_attributes_created_to_owner() {
let channel_id = Uuid::new_v4();
let repo = FakeMutationRepo::new(channel_id, "macro|owner@test.com");
let events = FakeEvents::default();
let svc = mutation_service(repo, events.clone(), FakeReferenceSharing::default());

svc.create_channel(
sender("macro|owner@test.com"),
None,
crate::domain::models::CreateChannelRequest {
name: Some("Macro Support x owner".to_string()),
channel_type: ChannelType::Private,
team_id: None,
auto_join_team: false,
participants: HashSet::from([macro_id("macro|teo@macro.com")]),
},
)
.await
.unwrap();

let events = events.events.lock().unwrap();
assert!(matches!(
events.as_slice(),
[ChannelEvent::ChannelCreated { actor, channel_name: Some(name), .. }]
if actor == &sender("macro|owner@test.com")
&& name == "Macro Support x owner"
));
}

#[tokio::test]
async fn create_channel_event_carries_channel_name() {
let channel_id = Uuid::new_v4();
Expand Down
Loading
Loading