Skip to content

Commit 901a390

Browse files
committed
Fix Slack file visibility in history
1 parent c221be0 commit 901a390

3 files changed

Lines changed: 55 additions & 5 deletions

File tree

packages/@emulators/slack/src/__tests__/slack.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3150,7 +3150,7 @@ describe("Slack plugin - files", () => {
31503150
headers: authHeaders(),
31513151
body: JSON.stringify({
31523152
files: [{ id: upload.file_id, title: "Mixed" }],
3153-
channels: `${publicChannel},${privateChannel.channel_id}`,
3153+
channels: `${privateChannel.channel_id},${publicChannel}`,
31543154
}),
31553155
});
31563156
expect(((await completeRes.json()) as any).ok).toBe(true);
@@ -3188,6 +3188,18 @@ describe("Slack plugin - files", () => {
31883188
const publicList = (await publicListRes.json()) as any;
31893189
expect(publicList.ok).toBe(true);
31903190
expect(publicList.files.map((file: any) => file.id)).toContain(upload.file_id);
3191+
3192+
const historyRes = await app.request(`${base}/api/conversations.history`, {
3193+
method: "POST",
3194+
headers: outsiderHeaders,
3195+
body: JSON.stringify({ channel: publicChannel }),
3196+
});
3197+
const history = (await historyRes.json()) as any;
3198+
expect(history.ok).toBe(true);
3199+
const fileShare = history.messages.find((message: any) => message.subtype === "file_share");
3200+
expect(fileShare.files[0].channels).toEqual([publicChannel]);
3201+
expect(fileShare.files[0].groups).toEqual([]);
3202+
expect(fileShare.files[0].shares.private).toBeUndefined();
31913203
});
31923204

31933205
it("does not partially complete multi-file uploads when one file is invalid", async () => {

packages/@emulators/slack/src/routes/conversations.ts

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { RouteContext } from "@emulators/core";
2-
import type { SlackChannel, SlackMessage, SlackUser } from "../entities.js";
2+
import type { SlackChannel, SlackFile, SlackFileShare, SlackMessage, SlackUser } from "../entities.js";
33
import { getSlackStore } from "../store.js";
44
import {
55
formatSlackMessage,
@@ -33,6 +33,35 @@ export function conversationsRoutes(ctx: RouteContext): void {
3333
getChannelMemberKey(channel, user, userId) !== undefined;
3434
const canReadConversation = (channel: SlackChannel, user: SlackUser | undefined, userId: string) =>
3535
!channel.is_private || isChannelMember(channel, user, userId);
36+
const visibleFileChannelIds = (file: SlackFile, authUser: { login: string }) => {
37+
const authSlackUser = getAuthSlackUser(authUser);
38+
const authUserId = authSlackUser?.user_id ?? authUser.login;
39+
return fileChannels(file).filter((channelId) => {
40+
const channel = ss().channels.findOneBy("channel_id", channelId);
41+
return channel ? canReadConversation(channel, authSlackUser, authUserId) : false;
42+
});
43+
};
44+
const visibleFileForAuth = (file: SlackFile, authUser: { login: string }): SlackFile => {
45+
const visibleIds = new Set(visibleFileChannelIds(file, authUser));
46+
const publicShares = filterVisibleShares(file.shares.public, visibleIds);
47+
const privateShares = filterVisibleShares(file.shares.private, visibleIds);
48+
const shares: SlackFile["shares"] = {};
49+
if (publicShares) shares.public = publicShares;
50+
if (privateShares) shares.private = privateShares;
51+
52+
return {
53+
...file,
54+
channels: file.channels.filter((channelId) => visibleIds.has(channelId)),
55+
groups: file.groups.filter((channelId) => visibleIds.has(channelId)),
56+
ims: file.ims.filter((channelId) => visibleIds.has(channelId)),
57+
shares,
58+
};
59+
};
60+
const formatSlackMessageForAuth = (msg: SlackMessage, authUser: { login: string }) =>
61+
formatSlackMessage({
62+
...msg,
63+
...(msg.files ? { files: msg.files.map((file) => visibleFileForAuth(file, authUser)) } : {}),
64+
});
3665
const dispatchConversationEvent = async (type: string, event: Record<string, unknown>) => {
3766
await webhooks.dispatch(
3867
type,
@@ -428,7 +457,7 @@ export function conversationsRoutes(ctx: RouteContext): void {
428457
const nextCursor = hasMore ? allMessages[startIndex + limit].ts : "";
429458

430459
return slackOk(c, {
431-
messages: page.map(formatSlackMessage),
460+
messages: page.map((message) => formatSlackMessageForAuth(message, authUser)),
432461
has_more: hasMore,
433462
response_metadata: { next_cursor: nextCursor },
434463
});
@@ -458,7 +487,7 @@ export function conversationsRoutes(ctx: RouteContext): void {
458487
.sort((a, b) => (a.ts > b.ts ? 1 : -1));
459488

460489
return slackOk(c, {
461-
messages: allMessages.map(formatSlackMessage),
490+
messages: allMessages.map((message) => formatSlackMessageForAuth(message, authUser)),
462491
has_more: false,
463492
});
464493
});
@@ -920,6 +949,15 @@ function findNamedChannel(channels: SlackChannel[], name: string): SlackChannel
920949
return channels.find((ch) => !ch.is_im && !ch.is_mpim && ch.name === name);
921950
}
922951

952+
function fileChannels(file: SlackFile): string[] {
953+
return [...file.channels, ...file.groups, ...file.ims];
954+
}
955+
956+
function filterVisibleShares(shares: Record<string, SlackFileShare[]> | undefined, visibleIds: Set<string>) {
957+
const entries = Object.entries(shares ?? {}).filter(([channelId]) => visibleIds.has(channelId));
958+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
959+
}
960+
923961
function channelTypeLetter(ch: SlackChannel): "C" | "D" | "G" {
924962
if (ch.is_im) return "D";
925963
if (ch.is_private || ch.is_mpim) return "G";

packages/emulate/src/registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export const SERVICE_REGISTRY: Record<ServiceName, ServiceEntry> = {
215215

216216
slack: {
217217
label: "Slack API emulator",
218-
endpoints: "auth, chat, conversations, users, reactions, team, OAuth, incoming webhooks",
218+
endpoints: "auth, chat, conversations, users, files, reactions, team, OAuth, incoming webhooks",
219219
async load() {
220220
const mod = await import("@emulators/slack");
221221
return { plugin: mod.slackPlugin, seedFromConfig: mod.seedFromConfig };

0 commit comments

Comments
 (0)