Skip to content

Commit 7d66168

Browse files
authored
Merge pull request #19 from lladawn/dev
Add /mumbl draft, review, publish slash commands
2 parents 724298f + 61f190c commit 7d66168

3 files changed

Lines changed: 98 additions & 1 deletion

File tree

app/api/slack/commands/route.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import {
1919
slackHelpPayload,
2020
slackSavedDumpPayload,
2121
openSlackRoomModal,
22+
openSlackFieldNoteDraftModal,
23+
openSlackFieldNoteReviewModal,
24+
openSlackPublishPickModal,
2225
} from "../../../../src/server/slack";
2326
import { cleanString } from "../../../../src/server/validation";
2427

@@ -33,9 +36,26 @@ export async function POST(request) {
3336
const roomName = parseRoomCommand(text);
3437
const pinSlug = parsePinCommand(text);
3538
const joinSlug = parseJoinCommand(text);
39+
const modalCommand = parseModalCommand(text);
3640

3741
if (!teamId || !slackUserId) return ok(ephemeralText("couldn't tell which Slack workspace this came from."));
3842
if (!text || text.toLowerCase() === "help") return ok(slackHelpPayload());
43+
44+
// draft / review / publish each open a modal the same way the empty `room`
45+
// command does — fire the views.open in the background and ack immediately so
46+
// Slack doesn't time out the slash command.
47+
if (modalCommand) {
48+
after(async () => {
49+
try {
50+
if (modalCommand === "draft") await openSlackFieldNoteDraftModal({ teamId, slackUserId, triggerId });
51+
else if (modalCommand === "review") await openSlackFieldNoteReviewModal({ teamId, slackUserId, triggerId });
52+
else await openSlackPublishPickModal({ teamId, slackUserId, triggerId });
53+
} catch (error) {
54+
await postSlackResponse(responseUrl, ephemeralText(error.message || "couldn't open that Mumbl view."));
55+
}
56+
});
57+
return new Response(null, { status: 200 });
58+
}
3959
if (pinSlug !== null && !pinSlug) return ok(ephemeralText("try `/mumbl pin` followed by the room invite link."));
4060
if (joinSlug !== null && !joinSlug) return ok(ephemeralText("try `/mumbl join` followed by the room invite link your teammate shared."));
4161
if (roomName !== null && !roomName) {
@@ -95,6 +115,14 @@ function parsePinCommand(text) {
95115
return null;
96116
}
97117

118+
function parseModalCommand(text) {
119+
const lower = cleanString(text, 4000).toLowerCase();
120+
if (lower === "draft") return "draft";
121+
if (lower === "review") return "review";
122+
if (lower === "publish") return "publish";
123+
return null;
124+
}
125+
98126
function parseJoinCommand(text) {
99127
const trimmed = cleanString(text, 4000);
100128
const lower = trimmed.toLowerCase();

app/api/slack/interactions/route.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,28 @@ export async function POST(request) {
280280
}
281281
}
282282

283+
if (payload.type === "view_submission" && payload.view?.callback_id === "publish_field_note_pick") {
284+
const teamId = slackTeamId(payload);
285+
const slackUserId = cleanString(payload.user?.id, 80);
286+
const fieldNoteId = cleanString(payload.view?.state?.values?.field_note_id?.value?.selected_option?.value, 64);
287+
if (!fieldNoteId) {
288+
return ok({
289+
response_action: "errors",
290+
errors: { field_note_id: "choose a draft to publish." },
291+
});
292+
}
293+
294+
try {
295+
const view = await slackPublishOptionsView({ teamId, slackUserId, fieldNoteId });
296+
return ok({ response_action: "update", view });
297+
} catch (error) {
298+
return ok({
299+
response_action: "errors",
300+
errors: { field_note_id: error.message || "couldn't open publish options for that draft." },
301+
});
302+
}
303+
}
304+
283305
if (payload.type === "view_submission" && payload.view?.callback_id === "edit_field_note_draft") {
284306
const teamId = slackTeamId(payload);
285307
const slackUserId = cleanString(payload.user?.id, 80);

src/server/slack.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,29 @@ export async function openSlackFieldNoteReviewModal({ teamId, slackUserId, trigg
814814
});
815815
}
816816

817+
export async function openSlackPublishPickModal({ teamId, slackUserId, triggerId }) {
818+
const installation = await getSlackInstallation(teamId);
819+
const token = decryptSlackToken({
820+
ciphertext: installation.bot_access_token_ciphertext,
821+
iv: installation.bot_access_token_iv,
822+
tag: installation.bot_access_token_tag,
823+
});
824+
825+
let view;
826+
try {
827+
const connection = await findOrCreateSlackConnectionByEmail({ teamId, slackUserId });
828+
const fieldNotes = await recentSlackFieldNoteDrafts(connection);
829+
view = fieldNotes.length ? slackPublishPickModal({ fieldNotes }) : slackNoFieldNoteDraftsModal();
830+
} catch (error) {
831+
view = slackFieldNoteDraftUnavailableModal(error.message || "connect mumbl first.");
832+
}
833+
834+
return slackApi("views.open", token, {
835+
trigger_id: triggerId,
836+
view,
837+
});
838+
}
839+
817840
export async function updateSlackView({ teamId, viewId, view }) {
818841
const installation = await getSlackInstallation(teamId);
819842
const token = decryptSlackToken({
@@ -1673,7 +1696,7 @@ export function slackHelpPayload() {
16731696
return blockResponse({
16741697
text: "use /mumbl to save a private thought or create a room.",
16751698
blocks: [
1676-
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."),
1699+
section("*mumbl in Slack*\n`/mumbl the thing I want to keep` saves a private dump.\n`/mumbl draft` turns your recent dumps into an AI field-note draft.\n`/mumbl review` opens your private drafts to edit.\n`/mumbl publish` ships a draft to a room as a team read.\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."),
16771700
context("Private dumps stay private. Team reads only post to Slack if you enable them."),
16781701
],
16791702
});
@@ -2301,6 +2324,30 @@ function slackFieldNoteReviewModal({ fieldNotes }) {
23012324
};
23022325
}
23032326

2327+
function slackPublishPickModal({ fieldNotes }) {
2328+
return {
2329+
type: "modal",
2330+
callback_id: "publish_field_note_pick",
2331+
title: { type: "plain_text", text: "publish a draft" },
2332+
submit: { type: "plain_text", text: "next" },
2333+
close: { type: "plain_text", text: "cancel" },
2334+
blocks: [
2335+
{
2336+
type: "input",
2337+
block_id: "field_note_id",
2338+
label: { type: "plain_text", text: "draft" },
2339+
element: {
2340+
type: "static_select",
2341+
action_id: "value",
2342+
placeholder: { type: "plain_text", text: "choose a draft to publish" },
2343+
options: fieldNotes.map((fieldNote) => fieldNoteOption(fieldNote)),
2344+
},
2345+
},
2346+
context("Next you'll pick the room and how you want to show up."),
2347+
],
2348+
};
2349+
}
2350+
23042351
function slackNoFieldNoteDraftsModal() {
23052352
const { appUrl } = getServerEnv();
23062353
return {

0 commit comments

Comments
 (0)