Merge pull request #550 from buster-so/dallin/bus-1431-auto-sharing-for-queries-from-slack-channels

Dallin/bus-1431-auto-sharing-for-queries-from-slack-channels
This commit is contained in:
dal 2025-07-18 11:58:08 -06:00 committed by GitHub
commit 47f03ac1d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 626 additions and 22 deletions

View File

@ -78,7 +78,7 @@ pub async fn list_chats_handler(
request: ListChatsRequest,
user: &AuthenticatedUser,
) -> Result<Vec<ChatListItem>> {
use database::schema::{asset_permissions, chats, messages, users};
use database::schema::{asset_permissions, chats, messages, users, user_favorites};
let mut conn = get_pg_pool().get().await?;
@ -152,7 +152,29 @@ pub async fn list_chats_handler(
// Get user's organization IDs
let user_org_ids: Vec<Uuid> = user.organizations.iter().map(|org| org.id).collect();
// Get user's favorited chat IDs
let favorited_chat_ids: Vec<Uuid> = if !request.admin_view {
asset_permissions::table
.filter(asset_permissions::identity_id.eq(user.id))
.filter(asset_permissions::asset_type.eq(AssetType::Chat))
.filter(asset_permissions::identity_type.eq(IdentityType::User))
.filter(asset_permissions::deleted_at.is_null())
.select(asset_permissions::asset_id)
.union(
user_favorites::table
.filter(user_favorites::user_id.eq(user.id))
.filter(user_favorites::asset_type.eq(AssetType::Chat))
.filter(user_favorites::deleted_at.is_null())
.select(user_favorites::asset_id)
)
.load::<Uuid>(&mut conn)
.await?
} else {
Vec::new()
};
// Second query: Get workspace-shared chats that the user doesn't have direct access to
// but has either contributed to or favorited
let workspace_shared_chats = if !request.admin_view && !user_org_ids.is_empty() {
chats::table
.inner_join(users::table.on(chats::created_by.eq(users::id)))
@ -173,6 +195,24 @@ pub async fn list_chats_handler(
))
)
)
// Only include if user has contributed (created or updated a message) or favorited
.filter(
// User has favorited the chat
chats::id.eq_any(&favorited_chat_ids)
.or(
// User has created a message in the chat
diesel::dsl::exists(
messages::table
.filter(messages::chat_id.eq(chats::id))
.filter(messages::created_by.eq(user.id))
.filter(messages::deleted_at.is_null())
)
)
.or(
// User has updated the chat
chats::updated_by.eq(user.id)
)
)
.filter(
diesel::dsl::exists(
messages::table

View File

@ -0,0 +1,309 @@
use anyhow::Result;
use database::{
enums::{AssetType, WorkspaceSharing},
models::{Chat, Message, UserFavorite},
pool::get_pg_pool,
schema::{chats, messages, user_favorites},
tests::common::db::{TestDb, TestSetup},
};
use diesel::prelude::*;
use diesel_async::RunQueryDsl;
use handlers::chats::{list_chats_handler, ListChatsRequest};
use middleware::UserOrganization;
use uuid::Uuid;
#[tokio::test]
async fn test_workspace_shared_chats_filtered_without_contribution() -> Result<()> {
let setup = TestSetup::new(None).await?;
let mut conn = setup.db.diesel_conn().await?;
// Create another user in the same organization
let other_user = setup.db.create_user("other@example.com", "Other User").await?;
// Create a workspace-shared chat by the other user
let shared_chat = Chat {
id: Uuid::new_v4(),
title: "Shared Chat".to_string(),
organization_id: setup.organization.id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
deleted_at: None,
created_by: other_user.id,
updated_by: other_user.id,
publicly_accessible: false,
publicly_enabled_by: None,
public_expiry_date: None,
most_recent_file_id: None,
most_recent_file_type: None,
most_recent_version_number: None,
workspace_sharing: WorkspaceSharing::CanView,
workspace_sharing_enabled_by: Some(other_user.id),
workspace_sharing_enabled_at: Some(chrono::Utc::now()),
};
diesel::insert_into(chats::table)
.values(&shared_chat)
.execute(&mut conn)
.await?;
// Create a message in the chat by the other user
let message = Message {
id: Uuid::new_v4(),
request_message: Some("Test message".to_string()),
response_messages: serde_json::json!({}),
reasoning: serde_json::json!({}),
title: "Test".to_string(),
raw_llm_messages: serde_json::json!({}),
final_reasoning_message: None,
chat_id: shared_chat.id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
deleted_at: None,
created_by: other_user.id,
feedback: None,
is_completed: true,
post_processing_message: None,
};
diesel::insert_into(messages::table)
.values(&message)
.execute(&mut conn)
.await?;
// List chats for the test user - should NOT see the shared chat
let request = ListChatsRequest {
page: Some(1),
page_size: 10,
admin_view: false,
};
let auth_user = middleware::AuthenticatedUser {
id: setup.user.id,
email: setup.user.email.clone(),
name: setup.user.name.clone(),
organizations: vec![UserOrganization {
id: setup.organization.id,
name: setup.organization.name.clone(),
}],
};
let result = list_chats_handler(request, &auth_user).await?;
// Should not see the shared chat since user hasn't contributed
assert_eq!(result.len(), 0);
// Clean up
setup.db.cleanup().await?;
Ok(())
}
#[tokio::test]
async fn test_workspace_shared_chats_visible_with_contribution() -> Result<()> {
let setup = TestSetup::new(None).await?;
let mut conn = setup.db.diesel_conn().await?;
// Create another user in the same organization
let other_user = setup.db.create_user("other@example.com", "Other User").await?;
// Create a workspace-shared chat by the other user
let shared_chat = Chat {
id: Uuid::new_v4(),
title: "Shared Chat With Contribution".to_string(),
organization_id: setup.organization.id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
deleted_at: None,
created_by: other_user.id,
updated_by: other_user.id,
publicly_accessible: false,
publicly_enabled_by: None,
public_expiry_date: None,
most_recent_file_id: None,
most_recent_file_type: None,
most_recent_version_number: None,
workspace_sharing: WorkspaceSharing::CanView,
workspace_sharing_enabled_by: Some(other_user.id),
workspace_sharing_enabled_at: Some(chrono::Utc::now()),
};
diesel::insert_into(chats::table)
.values(&shared_chat)
.execute(&mut conn)
.await?;
// Create a message by the other user
let message1 = Message {
id: Uuid::new_v4(),
request_message: Some("Other user message".to_string()),
response_messages: serde_json::json!({}),
reasoning: serde_json::json!({}),
title: "Test".to_string(),
raw_llm_messages: serde_json::json!({}),
final_reasoning_message: None,
chat_id: shared_chat.id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
deleted_at: None,
created_by: other_user.id,
feedback: None,
is_completed: true,
post_processing_message: None,
};
diesel::insert_into(messages::table)
.values(&message1)
.execute(&mut conn)
.await?;
// Create a message by the test user (contribution)
let message2 = Message {
id: Uuid::new_v4(),
request_message: Some("Test user message".to_string()),
response_messages: serde_json::json!({}),
reasoning: serde_json::json!({}),
title: "Test".to_string(),
raw_llm_messages: serde_json::json!({}),
final_reasoning_message: None,
chat_id: shared_chat.id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
deleted_at: None,
created_by: setup.user.id, // Test user contributes
feedback: None,
is_completed: true,
post_processing_message: None,
};
diesel::insert_into(messages::table)
.values(&message2)
.execute(&mut conn)
.await?;
// List chats for the test user - should see the shared chat
let request = ListChatsRequest {
page: Some(1),
page_size: 10,
admin_view: false,
};
let auth_user = middleware::AuthenticatedUser {
id: setup.user.id,
email: setup.user.email.clone(),
name: setup.user.name.clone(),
organizations: vec![UserOrganization {
id: setup.organization.id,
name: setup.organization.name.clone(),
}],
};
let result = list_chats_handler(request, &auth_user).await?;
// Should see the shared chat since user has contributed
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "Shared Chat With Contribution");
// Clean up
setup.db.cleanup().await?;
Ok(())
}
#[tokio::test]
async fn test_workspace_shared_chats_visible_when_favorited() -> Result<()> {
let setup = TestSetup::new(None).await?;
let mut conn = setup.db.diesel_conn().await?;
// Create another user in the same organization
let other_user = setup.db.create_user("other@example.com", "Other User").await?;
// Create a workspace-shared chat by the other user
let shared_chat = Chat {
id: Uuid::new_v4(),
title: "Favorited Shared Chat".to_string(),
organization_id: setup.organization.id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
deleted_at: None,
created_by: other_user.id,
updated_by: other_user.id,
publicly_accessible: false,
publicly_enabled_by: None,
public_expiry_date: None,
most_recent_file_id: None,
most_recent_file_type: None,
most_recent_version_number: None,
workspace_sharing: WorkspaceSharing::CanView,
workspace_sharing_enabled_by: Some(other_user.id),
workspace_sharing_enabled_at: Some(chrono::Utc::now()),
};
diesel::insert_into(chats::table)
.values(&shared_chat)
.execute(&mut conn)
.await?;
// Create a message by the other user
let message = Message {
id: Uuid::new_v4(),
request_message: Some("Other user message".to_string()),
response_messages: serde_json::json!({}),
reasoning: serde_json::json!({}),
title: "Test".to_string(),
raw_llm_messages: serde_json::json!({}),
final_reasoning_message: None,
chat_id: shared_chat.id,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
deleted_at: None,
created_by: other_user.id,
feedback: None,
is_completed: true,
post_processing_message: None,
};
diesel::insert_into(messages::table)
.values(&message)
.execute(&mut conn)
.await?;
// Add the chat to user's favorites
let favorite = UserFavorite {
user_id: setup.user.id,
asset_id: shared_chat.id,
asset_type: AssetType::Chat,
order_index: 0,
created_at: chrono::Utc::now(),
deleted_at: None,
};
diesel::insert_into(user_favorites::table)
.values(&favorite)
.execute(&mut conn)
.await?;
// List chats for the test user - should see the shared chat
let request = ListChatsRequest {
page: Some(1),
page_size: 10,
admin_view: false,
};
let auth_user = middleware::AuthenticatedUser {
id: setup.user.id,
email: setup.user.email.clone(),
name: setup.user.name.clone(),
organizations: vec![UserOrganization {
id: setup.organization.id,
name: setup.organization.name.clone(),
}],
};
let result = list_chats_handler(request, &auth_user).await?;
// Should see the shared chat since user has favorited it
assert_eq!(result.len(), 1);
assert_eq!(result[0].name, "Favorited Shared Chat");
// Clean up
setup.db.cleanup().await?;
Ok(())
}

View File

@ -33,3 +33,4 @@ pub mod sharing;
pub mod metrics;
pub mod dashboards;
pub mod collections;
pub mod chats_list_filter_test;

View File

@ -0,0 +1,224 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { db } from '@buster/database';
import { findOrCreateSlackChat } from './events';
vi.mock('@buster/database', () => ({
db: {
select: vi.fn(),
insert: vi.fn(),
},
chats: {},
slackIntegrations: {},
}));
describe('findOrCreateSlackChat', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should create a chat with workspace sharing when Slack integration has shareWithWorkspace', async () => {
// Mock implementations need to handle being called twice
let selectCallCount = 0;
vi.mocked(db.select).mockImplementation(() => {
selectCallCount++;
// First call is for existing chat, second is for slack integration
if (selectCallCount === 1) {
// Mock for existing chat query
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([]),
} as any;
} else {
// Mock for slack integration query
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([{ defaultSharingPermissions: 'shareWithWorkspace' }]),
} as any;
}
});
// Mock insert
const mockInsert = {
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockResolvedValue([{ id: 'new-chat-id' }]),
};
vi.mocked(db.insert).mockReturnValue(mockInsert as any);
const result = await findOrCreateSlackChat({
threadTs: 'thread-123',
channelId: 'channel-123',
organizationId: 'org-123',
userId: 'user-123',
slackChatAuthorization: 'authorized',
teamId: 'team-123',
});
expect(result).toBe('new-chat-id');
expect(mockInsert.values).toHaveBeenCalledWith({
title: '',
organizationId: 'org-123',
createdBy: 'user-123',
updatedBy: 'user-123',
slackChatAuthorization: 'authorized',
slackThreadTs: 'thread-123',
slackChannelId: 'channel-123',
workspaceSharing: 'can_view',
workspaceSharingEnabledBy: 'user-123',
workspaceSharingEnabledAt: expect.any(String),
});
});
it('should create a chat without workspace sharing when Slack integration has different setting', async () => {
// Mock implementations need to handle being called twice
let selectCallCount = 0;
vi.mocked(db.select).mockImplementation(() => {
selectCallCount++;
// First call is for existing chat, second is for slack integration
if (selectCallCount === 1) {
// Mock for existing chat query
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([]),
} as any;
} else {
// Mock for slack integration query
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([{ defaultSharingPermissions: 'shareWithChannel' }]),
} as any;
}
});
// Mock insert
const mockInsert = {
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockResolvedValue([{ id: 'new-chat-id' }]),
};
vi.mocked(db.insert).mockReturnValue(mockInsert as any);
const result = await findOrCreateSlackChat({
threadTs: 'thread-123',
channelId: 'channel-123',
organizationId: 'org-123',
userId: 'user-123',
slackChatAuthorization: 'authorized',
teamId: 'team-123',
});
expect(result).toBe('new-chat-id');
expect(mockInsert.values).toHaveBeenCalledWith({
title: '',
organizationId: 'org-123',
createdBy: 'user-123',
updatedBy: 'user-123',
slackChatAuthorization: 'authorized',
slackThreadTs: 'thread-123',
slackChannelId: 'channel-123',
workspaceSharing: 'none',
workspaceSharingEnabledBy: null,
workspaceSharingEnabledAt: null,
});
});
it('should return existing chat if found', async () => {
// Mock implementations need to handle being called twice
let selectCallCount = 0;
vi.mocked(db.select).mockImplementation(() => {
selectCallCount++;
// First call is for existing chat, second is for slack integration
if (selectCallCount === 1) {
// Mock for existing chat query - chat exists
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([{ id: 'existing-chat-id' }]),
} as any;
} else {
// Mock for slack integration query (won't be used due to early return)
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([]),
} as any;
}
});
const result = await findOrCreateSlackChat({
threadTs: 'thread-123',
channelId: 'channel-123',
organizationId: 'org-123',
userId: 'user-123',
slackChatAuthorization: 'authorized',
teamId: 'team-123',
});
expect(result).toBe('existing-chat-id');
expect(db.insert).not.toHaveBeenCalled();
});
it('should create chat without workspace sharing when no Slack integration found', async () => {
// Mock implementations need to handle being called twice
let selectCallCount = 0;
vi.mocked(db.select).mockImplementation(() => {
selectCallCount++;
// First call is for existing chat, second is for slack integration
if (selectCallCount === 1) {
// Mock for existing chat query
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([]),
} as any;
} else {
// Mock for slack integration query - no integration found
return {
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([]),
} as any;
}
});
// Mock insert
const mockInsert = {
values: vi.fn().mockReturnThis(),
returning: vi.fn().mockResolvedValue([{ id: 'new-chat-id' }]),
};
vi.mocked(db.insert).mockReturnValue(mockInsert as any);
const result = await findOrCreateSlackChat({
threadTs: 'thread-123',
channelId: 'channel-123',
organizationId: 'org-123',
userId: 'user-123',
slackChatAuthorization: 'authorized',
teamId: 'team-123',
});
expect(result).toBe('new-chat-id');
expect(mockInsert.values).toHaveBeenCalledWith({
title: '',
organizationId: 'org-123',
createdBy: 'user-123',
updatedBy: 'user-123',
slackChatAuthorization: 'authorized',
slackThreadTs: 'thread-123',
slackChannelId: 'channel-123',
workspaceSharing: 'none',
workspaceSharingEnabledBy: null,
workspaceSharingEnabledAt: null,
});
});
});

View File

@ -1,4 +1,4 @@
import { chats, db } from '@buster/database';
import { chats, db, slackIntegrations } from '@buster/database';
import type { SlackEventsResponse } from '@buster/server-shared/slack';
import { type SlackWebhookPayload, isEventCallback } from '@buster/slack';
import { tasks } from '@trigger.dev/sdk';
@ -37,26 +37,44 @@ export async function findOrCreateSlackChat({
organizationId,
userId,
slackChatAuthorization,
teamId,
}: {
threadTs: string;
channelId: string;
organizationId: string;
userId: string;
slackChatAuthorization: 'unauthorized' | 'authorized' | 'auto_added';
teamId: string;
}): Promise<string> {
// Find existing chat
const existingChat = await db
.select()
.from(chats)
.where(
and(
eq(chats.slackThreadTs, threadTs),
eq(chats.slackChannelId, channelId),
eq(chats.organizationId, organizationId),
eq(chats.createdBy, userId)
// Run both queries concurrently for better performance
const [existingChat, slackIntegration] = await Promise.all([
// Find existing chat
db
.select()
.from(chats)
.where(
and(
eq(chats.slackThreadTs, threadTs),
eq(chats.slackChannelId, channelId),
eq(chats.organizationId, organizationId)
)
)
)
.limit(1);
.limit(1),
// Fetch Slack integration settings if we have an organization
organizationId
? db
.select({ defaultSharingPermissions: slackIntegrations.defaultSharingPermissions })
.from(slackIntegrations)
.where(
and(
eq(slackIntegrations.organizationId, organizationId),
eq(slackIntegrations.teamId, teamId),
eq(slackIntegrations.status, 'active')
)
)
.limit(1)
: Promise.resolve([]),
]);
if (existingChat.length > 0) {
const chat = existingChat[0];
@ -66,6 +84,12 @@ export async function findOrCreateSlackChat({
return chat.id;
}
// Extract default sharing permissions
const defaultSharingPermissions =
slackIntegration.length > 0 && slackIntegration[0]
? slackIntegration[0].defaultSharingPermissions
: undefined;
// Create new chat
const newChat = await db
.insert(chats)
@ -77,6 +101,11 @@ export async function findOrCreateSlackChat({
slackChatAuthorization,
slackThreadTs: threadTs,
slackChannelId: channelId,
// Set workspace sharing based on Slack integration settings
workspaceSharing: defaultSharingPermissions === 'shareWithWorkspace' ? 'can_view' : 'none',
workspaceSharingEnabledBy: defaultSharingPermissions === 'shareWithWorkspace' ? userId : null,
workspaceSharingEnabledAt:
defaultSharingPermissions === 'shareWithWorkspace' ? new Date().toISOString() : null,
})
.returning();
@ -166,6 +195,7 @@ export async function eventsHandler(payload: SlackWebhookPayload): Promise<Slack
organizationId,
userId,
slackChatAuthorization: mapAuthResultToDbEnum(authResult.type),
teamId: payload.team_id,
});
// Queue the task

View File

@ -231,11 +231,11 @@ const SlackSharingPermissions = React.memo(() => {
value: 'shareWithWorkspace',
secondaryLabel: 'All workspace members will have access to any chat created from any channel.'
},
{
label: 'Channel',
value: 'shareWithChannel',
secondaryLabel: 'All channel members will have access to any chat created from that channel.'
},
// {
// label: 'Channel',
// value: 'shareWithChannel',
// secondaryLabel: 'All channel members will have access to any chat created from that channel.'
// },
{
label: 'None',
value: 'noSharing',

View File

@ -10,13 +10,13 @@ const mockCreate = vi.fn();
describe('Reranker - Unit Tests', () => {
beforeEach(() => {
vi.clearAllMocks();
process.env.RERANK_API_KEY = 'test-api-key';
process.env.RERANK_BASE_URL = 'https://test-api.com/rerank';
process.env.RERANK_MODEL = 'test-model';
vi.spyOn(console, 'error').mockImplementation(() => {});
const mockAxiosInstance = {
post: vi.fn(),
};