Skip to content

Commit f254d0b

Browse files
committed
Streamline Slack room creation and pinning
1 parent 08b412f commit f254d0b

4 files changed

Lines changed: 91 additions & 16 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ In Slack app settings:
103103

104104
Set `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, `SLACK_SIGNING_SECRET`, and `MUMBL_SLACK_TOKEN_ENCRYPTION_KEY` in the deployment environment. Install through `/api/slack/install`.
105105

106-
`/mumbl room platform team` creates a Mumbl room from Slack and returns a one-time creator handoff link for opening the room in a browser. `/mumbl start platform team` remains an alias. Slack-created rooms are auto-pinned for the creator when Mumbl can match their Slack email to a Mumbl login. `/mumbl pin platform-team` explicitly adds a Mumbl room to that Slack user's publish list without tracking room membership. App Home can draft, review, edit, publish private field notes to pinned Mumbl spaces, and manage personal pinned spaces. Optional team-read Slack posting is creator-enabled per room. If a creator switches it on, Mumbl starts an optional Slack permission upgrade that asks for `chat:write`, `groups:write`, and `groups:read` so it can create one private channel, post published team reads there, and auto-pin that Mumbl room when a connected user joins the Mumbl-created Slack channel. It still does not request Slack history scopes.
106+
`/mumbl room platform team` creates a Mumbl room from Slack and returns a one-time creator handoff link for opening the room in a browser. `/mumbl start platform team` remains an alias. Slack-created rooms are auto-pinned for the creator when Mumbl can match their Slack email to a Mumbl login. `/mumbl pin platform-team` explicitly adds a Mumbl room to that Slack user's publish list without tracking room membership, and best-effort invites them into the room's Mumbl-created Slack reads channel when one exists. App Home can draft, review, edit, publish private field notes to pinned Mumbl spaces, and manage personal pinned spaces. Optional team-read Slack posting is creator-enabled per room. If a creator switches it on, Mumbl starts an optional Slack permission upgrade that asks for `chat:write`, `groups:write`, and `groups:read` so it can create one private channel, post published team reads there, and auto-pin that Mumbl room when a connected user joins the Mumbl-created Slack channel. It still does not request Slack history scopes.
107107

108108
Slack reminders are intentionally not part of the beta because frequent scheduling does not fit the current free-tier posture.
109109

app/api/slack/commands/route.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
parseVerifiedSlackForm,
1313
pinSlackSpaceBySlug,
1414
postSlackResponse,
15+
publishSlackAppHome,
1516
saveSlackDump,
1617
slackConnectPayload,
1718
slackConnectUrl,
@@ -55,6 +56,13 @@ export async function POST(request) {
5556
? await pinSpaceFromSlack({ teamId, slackUserId, slug: pinSlug })
5657
: await saveOrConnect({ teamId, slackUserId, content: text, sourceMeta: { trigger: "slash_command" } });
5758
await postSlackResponse(responseUrl, result);
59+
if (roomName !== null || pinSlug !== null) {
60+
try {
61+
await publishSlackAppHome({ teamId, slackUserId });
62+
} catch (error) {
63+
console.error("Slack App Home refresh after command failed", { message: error.message, slackError: error.slack?.error });
64+
}
65+
}
5866
} catch (error) {
5967
await postSlackResponse(responseUrl, ephemeralText(error.message || "couldn't finish that Mumbl action yet."));
6068
}
@@ -88,8 +96,11 @@ async function startRoomFromSlack({ teamId, slackUserId, name }) {
8896
}
8997

9098
async function pinSpaceFromSlack({ teamId, slackUserId, slug }) {
91-
const { space } = await pinSlackSpaceBySlug({ teamId, slackUserId, slug });
92-
return ephemeralText(`${space.name} is pinned for team reads.`);
99+
const { space, channelJoin } = await pinSlackSpaceBySlug({ teamId, slackUserId, slug });
100+
const channelText = channelJoin?.joined
101+
? ` You're also in the ${channelJoin.channelName ? `#${channelJoin.channelName}` : "Slack reads"} channel.`
102+
: "";
103+
return ephemeralText(`${space.name} is pinned for team reads.${channelText}`);
93104
}
94105

95106
async function saveOrConnect({ teamId, slackUserId, content, sourceMeta }) {

app/api/slack/interactions/route.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,13 @@ export async function POST(request) {
495495

496496
try {
497497
const view = await createSlackStartedSpaceModalView({ teamId, slackUserId, name: roomName });
498+
after(async () => {
499+
try {
500+
await publishSlackAppHome({ teamId, slackUserId });
501+
} catch (error) {
502+
console.error("Slack App Home refresh after room creation failed", { message: error.message, slackError: error.slack?.error });
503+
}
504+
});
498505
return ok({ response_action: "update", view });
499506
} catch (error) {
500507
return ok({

src/server/slack.js

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,8 @@ export async function createSlackStartedSpace({ teamId, slackUserId, name }) {
269269
return {
270270
space: insertedSpace,
271271
creatorToken,
272+
creatorLinked: Boolean(connection?.mumbl_user_id),
273+
pinned: Boolean(connection),
272274
openUrl: slackSpaceHandoffUrl(handoff),
273275
roomUrl: `${getServerEnv().appUrl}/r/${insertedSpace.slug}`,
274276
teamReadsUrl: slackTeamReadsInstallUrl(teamReadsSetup),
@@ -786,7 +788,8 @@ export async function pinSlackSpaceBySlug({ teamId, slackUserId, slug }) {
786788
const connection = await findOrCreateSlackConnectionByEmail({ teamId, slackUserId });
787789
const space = await findSpaceForSlackPin(slug);
788790
await pinSlackSpace({ connection, spaceId: space.id });
789-
return { space };
791+
const channelJoin = await inviteSlackUserToSpaceChannel({ teamId, slackUserId, spaceId: space.id });
792+
return { space, channelJoin };
790793
}
791794

792795
export async function pinSlackSpaceForMumblUser({ mumblUserId, spaceId }) {
@@ -802,6 +805,11 @@ export async function pinSlackSpaceForMumblUser({ mumblUserId, spaceId }) {
802805
if (error) throw error;
803806
if (!connection) throw new Error("connect Slack once before pinning this room.");
804807
await pinSlackSpace({ connection, spaceId });
808+
await inviteSlackUserToSpaceChannel({
809+
teamId: connection.slack_team_id,
810+
slackUserId: connection.slack_user_id,
811+
spaceId,
812+
});
805813
return connection;
806814
}
807815

@@ -1012,6 +1020,42 @@ async function getPinnedSpace({ connection, spaceId }) {
10121020
return data;
10131021
}
10141022

1023+
async function inviteSlackUserToSpaceChannel({ teamId, slackUserId, spaceId }) {
1024+
const supabase = getSupabaseAdmin();
1025+
const { data: channel, error } = await supabase
1026+
.from("slack_space_channels")
1027+
.select("slack_channel_id, slack_channel_name, posting_enabled")
1028+
.eq("slack_team_id", cleanString(teamId, 80))
1029+
.eq("space_id", cleanString(spaceId, 64))
1030+
.maybeSingle();
1031+
if (error) throw error;
1032+
if (!channel?.slack_channel_id) return { joined: false, reason: "no_slack_channel" };
1033+
1034+
try {
1035+
const installation = await getSlackInstallation(teamId);
1036+
const token = decryptSlackToken({
1037+
ciphertext: installation.bot_access_token_ciphertext,
1038+
iv: installation.bot_access_token_iv,
1039+
tag: installation.bot_access_token_tag,
1040+
});
1041+
await slackApi("conversations.invite", token, {
1042+
channel: channel.slack_channel_id,
1043+
users: cleanString(slackUserId, 80),
1044+
});
1045+
return { joined: true, channelName: channel.slack_channel_name };
1046+
} catch (error) {
1047+
if (error.slack?.error === "already_in_channel") {
1048+
return { joined: true, alreadyInChannel: true, channelName: channel.slack_channel_name };
1049+
}
1050+
console.error("Slack channel invite after pin failed", {
1051+
message: error.message,
1052+
slackError: error.slack?.error,
1053+
spaceId,
1054+
});
1055+
return { joined: false, reason: error.slack?.error || "invite_failed", channelName: channel.slack_channel_name };
1056+
}
1057+
}
1058+
10151059
export async function createSlackFieldNoteDraft({ teamId, slackUserId, dumpIds }) {
10161060
const cleanedDumpIds = Array.isArray(dumpIds) ? dumpIds.map((id) => cleanString(id, 64)).filter(Boolean) : [];
10171061
if (!cleanedDumpIds.length) throw new Error("choose at least one dump.");
@@ -1118,34 +1162,47 @@ export function slackRoomNeedsNamePayload() {
11181162
});
11191163
}
11201164

1121-
export function slackRoomCreatedPayload({ space, openUrl, roomUrl, teamReadsUrl }) {
1165+
export function slackRoomCreatedPayload({ space, openUrl, roomUrl, teamReadsUrl, creatorLinked, pinned }) {
1166+
const status = creatorLinked
1167+
? "linked to your Mumbl login and pinned in Slack."
1168+
: "created from Slack. Connect once to claim creator access in Mumbl.";
11221169
return blockResponse({
11231170
text: `created a mumbl room for ${space.name}.`,
11241171
blocks: [
1125-
section(`*created a mumbl room for ${escapeSlackText(space.name)}.*\nprivate dumps stay private. team reads only post to Slack if you enable them.`),
1172+
section(`*${escapeSlackText(space.name)} is ready.*\n${status}`),
11261173
actions([
1127-
{ text: "open room", url: openUrl },
1128-
{ text: "open invite link", url: roomUrl },
1129-
{ text: "enable Slack team reads", url: teamReadsUrl },
1174+
{ text: "create Slack reads channel", url: teamReadsUrl, style: "primary" },
1175+
{ text: creatorLinked ? "open room" : "claim room", url: openUrl },
11301176
]),
1131-
context(`invite link: <${roomUrl}|${roomUrl}>`),
1177+
context(
1178+
pinned
1179+
? `Pinned for publishing. Invite link: <${roomUrl}|${roomUrl}>`
1180+
: `Use /mumbl pin ${space.slug} after connecting Mumbl. Invite link: <${roomUrl}|${roomUrl}>`,
1181+
),
11321182
],
11331183
});
11341184
}
11351185

1136-
export function slackRoomCreatedModalView({ space, openUrl, roomUrl, teamReadsUrl }) {
1186+
export function slackRoomCreatedModalView({ space, openUrl, roomUrl, teamReadsUrl, creatorLinked, pinned }) {
11371187
return {
11381188
type: "modal",
11391189
title: { type: "plain_text", text: "room created" },
11401190
close: { type: "plain_text", text: "done" },
11411191
blocks: [
1142-
section(`*created a mumbl room for ${escapeSlackText(space.name)}.*\nIt is ready for team reads. Open it once to save creator access in this browser.`),
1192+
section(
1193+
creatorLinked
1194+
? `*${escapeSlackText(space.name)} is ready.*\nIt is linked to your Mumbl login and pinned in Slack.`
1195+
: `*${escapeSlackText(space.name)} is ready.*\nConnect once to claim creator access in Mumbl.`,
1196+
),
11431197
actions([
1144-
{ text: "open room", url: openUrl },
1145-
{ text: "open invite link", url: roomUrl },
1146-
{ text: "enable Slack team reads", url: teamReadsUrl },
1198+
{ text: "create Slack reads channel", url: teamReadsUrl, style: "primary" },
1199+
{ text: creatorLinked ? "open room" : "claim room", url: openUrl },
11471200
]),
1148-
context("Publishing to Mumbl team reads works now. Posting those reads into a Slack channel stays optional."),
1201+
context(
1202+
pinned
1203+
? "Pinned for publishing. Slack reads channel is optional and only mirrors published team reads."
1204+
: "After connecting, Mumbl can pin this space for publishing from Slack.",
1205+
),
11491206
],
11501207
};
11511208
}

0 commit comments

Comments
 (0)