Skip to content

Commit f56414d

Browse files
authored
Merge pull request #17 from lladawn/dev
Add /mumbl join for one-step team room joins from Slack
2 parents 492e1c2 + 3d9f1ef commit f56414d

3 files changed

Lines changed: 68 additions & 11 deletions

File tree

app/api/slack/commands/route.js

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ export async function POST(request) {
3232
const triggerId = cleanString(form.get("trigger_id"), 200);
3333
const roomName = parseRoomCommand(text);
3434
const pinSlug = parsePinCommand(text);
35+
const joinSlug = parseJoinCommand(text);
3536

3637
if (!teamId || !slackUserId) return ok(ephemeralText("couldn't tell which Slack workspace this came from."));
3738
if (!text || text.toLowerCase() === "help") return ok(slackHelpPayload());
3839
if (pinSlug !== null && !pinSlug) return ok(ephemeralText("try `/mumbl pin` followed by the room invite link."));
40+
if (joinSlug !== null && !joinSlug) return ok(ephemeralText("try `/mumbl join` followed by the room invite link your teammate shared."));
3941
if (roomName !== null && !roomName) {
4042
after(async () => {
4143
try {
@@ -54,9 +56,11 @@ export async function POST(request) {
5456
? await startRoomFromSlack({ teamId, slackUserId, name: roomName })
5557
: pinSlug !== null
5658
? await pinSpaceFromSlack({ teamId, slackUserId, slug: pinSlug })
59+
: joinSlug !== null
60+
? await joinSpaceFromSlack({ teamId, slackUserId, slug: joinSlug })
5761
: await saveOrConnect({ teamId, slackUserId, content: text, sourceMeta: { trigger: "slash_command" } });
5862
await postSlackResponse(responseUrl, result);
59-
if (roomName !== null || pinSlug !== null) {
63+
if (roomName !== null || pinSlug !== null || joinSlug !== null) {
6064
try {
6165
await publishSlackAppHome({ teamId, slackUserId });
6266
} catch (error) {
@@ -91,6 +95,29 @@ function parsePinCommand(text) {
9195
return null;
9296
}
9397

98+
function parseJoinCommand(text) {
99+
const trimmed = cleanString(text, 4000);
100+
const lower = trimmed.toLowerCase();
101+
if (lower === "join") return "";
102+
if (lower.startsWith("join ")) return cleanString(trimmed.slice(5), 2000);
103+
return null;
104+
}
105+
106+
// Accept the `<room-name> <key>` two-token form (what we tell teams to share) and
107+
// normalize it into the slug-or-URL string findSpaceForSlackPin already understands.
108+
// A pasted full invite URL still works for backwards compatibility.
109+
function joinArgsToLookup(args) {
110+
const raw = cleanString(args, 2000) || "";
111+
if (/\/r\/|https?:/i.test(raw)) return raw;
112+
const parts = raw.split(/\s+/).filter(Boolean);
113+
const slug = parts[0] || "";
114+
const key = parts[1] || "";
115+
if (!slug) return "";
116+
// Slack may wrap a pasted slug in <...>; strip stray angle brackets.
117+
const cleanSlug = slug.replace(/[<>]/g, "");
118+
return key ? `/r/${cleanSlug}?key=${key}` : cleanSlug;
119+
}
120+
94121
async function startRoomFromSlack({ teamId, slackUserId, name }) {
95122
return createSlackStartedSpacePayload({ teamId, slackUserId, name });
96123
}
@@ -103,6 +130,15 @@ async function pinSpaceFromSlack({ teamId, slackUserId, slug }) {
103130
return ephemeralText(`${space.name} ${alreadyPinned ? "was already pinned" : "is pinned"} for team reads.${channelText}`);
104131
}
105132

133+
async function joinSpaceFromSlack({ teamId, slackUserId, slug }) {
134+
const lookup = joinArgsToLookup(slug);
135+
const { space, channelJoin } = await pinSlackSpaceBySlug({ teamId, slackUserId, slug: lookup });
136+
const channel = channelJoin?.channelName ? `#${channelJoin.channelName}` : "the team reads channel";
137+
return channelJoin?.joined
138+
? ephemeralText(`you're in 🎉 — ${space.name} reads will land in ${channel}, and the room is pinned in your mumbl App Home.`)
139+
: ephemeralText(`pinned ${space.name} for you. ask whoever set up the room to add you to ${channel} if you don't see it yet.`);
140+
}
141+
106142
async function saveOrConnect({ teamId, slackUserId, content, sourceMeta }) {
107143
const existingConnection = await findSlackConnection({ teamId, slackUserId });
108144
if (existingConnection) {

docs/slack-app-setup.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,14 @@ Create a slash command:
108108
- Command: `/mumbl`
109109
- Request URL: `https://mumbl.wtf/api/slack/commands`
110110
- Short description: `Save a private thought to Mumbl`
111-
- Usage hint: `[thought]`, `room [team name]`, or `pin [space-slug]`
111+
- Usage hint: `[thought]`, `room [team name]`, `join [room-name] [key]`, or `pin [invite-link]`
112112

113113
Expected behavior:
114114

115115
- `/mumbl something I want to keep` saves a private dump.
116116
- `/mumbl room platform team` creates a Mumbl room.
117117
- `/mumbl start platform team` is an alias.
118+
- `/mumbl join <room-name> <key>` is how teammates join in one step: it best-effort joins the room's private Slack reads channel and pins the room in that user's App Home. The two-token form (room slug + read key as separate words) is used instead of a pasted URL because Slack auto-linkifies URLs and crops the `key=`. Private rooms require the key because the join is access-gated by the room read token; it reuses the same lookup path as `pin` (a full invite URL is still accepted for backwards compatibility).
118119
- `/mumbl pin platform-team` pins an existing Mumbl room and best-effort joins the room's Slack reads channel if it exists.
119120
- Unpinning from App Home removes only that user's publish shortcut and best-effort removes them from the linked Slack reads channel if one exists.
120121

src/server/slack.js

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,24 @@ function slackRoomReadsUrl(space, accessToken = "") {
13321332
return `${appUrl}${roomInvitePath(slug, token)}`;
13331333
}
13341334

1335+
// Build the copy-pasteable `/mumbl join <slug> <key>` command from a room invite URL.
1336+
// Two plain tokens (not a URL) so Slack never linkifies and crops the room key.
1337+
export function slackJoinCommand(roomUrl) {
1338+
const raw = cleanString(roomUrl, 2000) || "";
1339+
const slug = raw.match(/\/r\/([^/?#]+)/)?.[1] || "";
1340+
let key = "";
1341+
const keyMatch = raw.match(/[?&]key=([^&]+)/);
1342+
if (keyMatch) {
1343+
try {
1344+
key = decodeURIComponent(keyMatch[1]);
1345+
} catch {
1346+
key = keyMatch[1];
1347+
}
1348+
}
1349+
if (!slug) return "/mumbl join";
1350+
return key ? `/mumbl join ${slug} ${key}` : `/mumbl join ${slug}`;
1351+
}
1352+
13351353
async function pinSlackSpace({ connection, spaceId }) {
13361354
const supabase = getSupabaseAdmin();
13371355
const { data: existingPins, error: existingError } = await supabase
@@ -1549,7 +1567,7 @@ export function slackHelpPayload() {
15491567
return blockResponse({
15501568
text: "use /mumbl to save a private thought or create a room.",
15511569
blocks: [
1552-
section("*mumbl in Slack*\n`/mumbl the thing I want to keep` saves a private dump.\n`/mumbl room platform team` creates a Mumbl room from Slack.\n`/mumbl pin <invite-link>` pins a room to your Slack publish list."),
1570+
section("*mumbl in Slack*\n`/mumbl the thing I want to keep` saves a private dump.\n`/mumbl room platform team` creates a Mumbl room from Slack.\n`/mumbl join <room-name> <key>` joins your team's room — reads land in Slack and the room pins in your App Home.\n`/mumbl pin <invite-link>` pins a room to your Slack publish list."),
15531571
context("Private dumps stay private. Team reads only post to Slack if you enable them."),
15541572
],
15551573
});
@@ -1579,9 +1597,9 @@ export function slackRoomCreatedPayload({ space, openUrl, roomUrl, teamReadsUrl,
15791597
{ text: "share with team", actionId: "share_room_invite", value: JSON.stringify({ roomUrl, spaceName: space.name }) },
15801598
]),
15811599
context(
1582-
pinned
1583-
? `Pinned for publishing. Invite link: <${roomUrl}|${roomUrl}>`
1584-
: `Pin this room in Mumbl App Home, or run \`/mumbl pin\` with the invite link: <${roomUrl}|${roomUrl}>`,
1600+
`Share this with your team so they can join in one step:\n\`${slackJoinCommand(roomUrl)}\`\n${
1601+
pinned ? "Pinned for publishing." : "Pin this room in Mumbl App Home, or run `/mumbl pin` with the invite link."
1602+
}`,
15851603
),
15861604
],
15871605
});
@@ -1604,16 +1622,18 @@ export function slackRoomCreatedModalView({ space, openUrl, roomUrl, teamReadsUr
16041622
{ text: "share with team", actionId: "share_room_invite", value: JSON.stringify({ roomUrl, spaceName: space.name }) },
16051623
]),
16061624
context(
1607-
pinned
1608-
? "Pinned for publishing. Slack reads channel is optional and only mirrors published team reads."
1609-
: "After connecting, Mumbl can pin this space for publishing from Slack.",
1625+
`Share with your team so they join in one step:\n\`${slackJoinCommand(roomUrl)}\`\n${
1626+
pinned
1627+
? "The Slack reads channel only mirrors published team reads."
1628+
: "After connecting, Mumbl can pin this space for publishing from Slack."
1629+
}`,
16101630
),
16111631
],
16121632
};
16131633
}
16141634

16151635
export function slackShareRoomInviteModalView({ roomUrl, spaceName }) {
1616-
const inviteMessage = `hey team — just set up a mumbl room for ${spaceName || "us"}.\nwrite private work thoughts, publish as team reads when ready.\n\njoin here: ${roomUrl}`;
1636+
const inviteMessage = `hey team — just set up a mumbl room for ${spaceName || "us"}.\nwrite private work thoughts, publish as team reads when ready.\n\nto join, run this in Slack:\n${slackJoinCommand(roomUrl)}`;
16171637
return {
16181638
type: "modal",
16191639
callback_id: "share_room_invite",
@@ -1998,7 +2018,7 @@ async function slackAppHomeBlocks({ teamId, slackUserId }) {
19982018
section(
19992019
pinnedSpaces.length
20002020
? `*pinned teamspaces*\n${pinnedList}`
2001-
: "*join your team's room*\nPaste a room invite link to pin it, or create a new team room.",
2021+
: "*join your team's room*\nGot a join command from a teammate? Run `/mumbl join <room-name> <key>` to join. You can also pin a room from its invite link, or create a new team room.",
20022022
),
20032023
actions(
20042024
pinnedSpaces.length

0 commit comments

Comments
 (0)