Slack Next-gen Platform – Button Interactions

This is translation of  Slack Next-gen Platform – Button Interactions – DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»written by Kaz (SDK Engineering & DevRel at Slack), I do not include any personal opinion here except guessing when meaning of original text is not clear for me)

이 글은 μŠ¬λž™μ˜ SDK Engineering / DevRel 인 Kaz 의 글을 λ²ˆμ—­ν•œκ²ƒμž…λ‹ˆλ‹€. μ €μ˜ 개인 μ˜κ²¬μ€ 듀어가지 μ•ŠμœΌλ©°, 일뢀 μ›λ¬Έμ˜ μ˜λ―Έκ°€ μ• λ§€ν•œ κ²½μš°μ—λ§Œ λΆ€μ—° μ„€λͺ…을 λ‹¬μ•˜μŠ΅λ‹ˆλ‹€.


이번 νŠœν† λ¦¬μ–Όμ—μ„œλŠ” λ²„νŠΌμ„ μ‚¬μš©ν•˜μ—¬ μΈν„°λ ‰μ…˜μ„ ν•˜λŠ” 방법을 λ°°μ›Œλ³΄κ² μŠ΅λ‹ˆλ‹€.

μ°¨μ„ΈλŒ€ ν”Œλž«νΌμ˜ μ•±μ—μ„œ λ²„νŠΌ μΈν„°λ ‰μ…˜μ„ μΆ”κ°€ν•˜λŠ” 방법은 2가지가 μžˆμŠ΅λ‹ˆλ‹€.

  • 빌트인 SendMessage νŽ‘μ…˜μ˜ interactive_blocks 을 μ‚¬μš©ν•˜κ³ , 거기에 μΆ”κ°€λ‘œ block_actions 이벀트λ₯Ό λ‹€λ£° 수 μžˆλŠ” μ»€μŠ€ν…€ νŽ‘μ…˜μ„ μΆ”κ°€ν•˜λŠ” 것
  • λ²„νŠΌμœΌλ‘œ 메세지λ₯Ό 보낼 수 μžˆλŠ” μ»€μŠ€ν…€ νŽ‘μ…˜μ„ λ§Œλ“€κ³ , block_actions 이벀트λ₯Ό μœ„ν•œ ν•Έλ“€λŸ¬λ₯Ό μΆ”κ°€ν•˜λŠ” 방법.

이번 κΈ€μ—μ„œλŠ” 두가지 λͺ¨λ‘ λ‹€ λ‹€λ£Ήλ‹ˆλ‹€.

Prerequisites

μ°¨μ„ΈλŒ€ ν”Œλž«νΌμ„ μ ‘ν•˜λŠ” 것이 μ²˜μŒμ΄μ‹œλΌλ©΄, μ΄μ „μ˜ νŠœν† λ¦¬μ–ΌμΈ β€œThe Simplest Hello World” λΆ€ν„° μ½μ–΄μ£Όμ„Έμš”. μš”μ•½ν•˜λ©΄ 유료 Slack μ›Œν¬μŠ€νŽ˜μ΄μŠ€μ™€ ν•΄λ‹Ή μ›Œν¬μŠ€νŽ˜μ΄μŠ€μ—μ„œ β€œbeta feature” λ₯Ό μ‚¬μš©ν•  수 μžˆλŠ” κΆŒν•œμ΄ ν•„μš”ν•©λ‹ˆλ‹€. 가지고 κ³„μ‹œλ‹€λ©΄ Slack CLI 와 μ›Œν¬μŠ€νŽ˜μ΄μŠ€λ₯Ό μ—°κ²°ν•˜κΈ°λ§Œ ν•˜λ©΄ λ©λ‹ˆλ‹€.

μ€€λΉ„λ˜μ—ˆλ‹€λ©΄ 앱을 ν•œλ²ˆ λ§Œλ“€μ–΄λ³΄λ„λ‘ ν•˜μ£ . μ‹œμž‘ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€.

Create a Blank Project

slack create λͺ…λ Ήμ–΄λ₯Ό μ‚¬μš©ν•˜μ—¬, μƒˆλ‘œμš΄ ν”„λ‘œμ νŠΈλ₯Ό μ‹œμž‘ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이 νŠœν† λ¦¬μ–Όμ—μ„œλŠ” 아무것도 μ—†λŠ” μƒνƒœμ—μ„œ 앱을 λ§Œλ“€μ–΄λ³΄κ² μŠ΅λ‹ˆλ‹€. β€œBlank Project” λ₯Ό μ„ νƒν•˜μ—¬ μ£Όμ‹­μ‹œμš”.

$ slack create
? Select a template to build from:

  Hello World
  A simple workflow that sends a greeting

  Scaffolded project
  A solid foundational project that uses a Slack datastore

> Blank project
  A, well.. blank project

  To see all available samples, visit github.com/slack-samples.

ν”„λ‘œμ νŠΈκ°€ μƒμ„±λ˜λ©΄, slack run λͺ…λ Ήμ–΄κ°€ μ •μƒμ μœΌλ‘œ λ™μž‘ν•˜λŠ”μ§€ ν™•μΈν•΄λ³΄μ„Έμš”. 이 λͺ…λ Ήμ–΄λŠ” β€œdev” λ²„μ „μ˜ 앱을 μ›Œν¬μŠ€νŽ˜μ΄μŠ€μ— μ„€μΉ˜ν•©λ‹ˆλ‹€. 이 μ•±μ˜ 봇 μœ μ €κ°€ μƒμ„±λ˜κ³ , 이 봇은 API ν˜ΈμΆœμ„ μœ„ν•œ 봇 토큰 값을 가지고 μžˆμŠ΅λ‹ˆλ‹€.

$ cd affectionate-panther-654
$ slack run
? Choose a workspace  seratch  T03E94MJU
   App is not installed to this workspace

Updating dev app install for workspace "Acme Corp"

  Outgoing domains
   No allowed outgoing domains are configured
   If your function makes network requests, you will need to allow the outgoing domains
   Learn more about upcoming changes to outgoing domains: https://api.slack.com/future/changelog
  seratch of Acme Corp
Connected, awaiting events

Handle button clicks on SendMessage Function’s interactive_blocks

빌트인 Schema.slack.functions.SendMessage νŽ‘μ…˜μ€ μΈν„°λ ‰ν‹°λΈŒ Block Kit μ»΄ν¬λ„ŒνŠΈλ₯Ό κ°„μ†Œν™”μ‹œν‚¨ 버전을 μ œκ³΅ν•©λ‹ˆλ‹€. 메세지에 κ°„λ‹¨ν•œ 블둝을 μΆ”κ°€ν•  수 있고, κ·Έ λ‹€μŒμ˜ μ»€μŠ€ν…€ νŽ‘μ…˜μ€ 클릭 μ΄λ²€νŠΈμ— λŒ€ν•΄ λ°˜μ‘ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

interactive_blocks 핸듀링을 μœ„ν•œ 데λͺ¨ μ›Œν¬ν”Œλ‘œλ₯Ό λ§Œλ“€κ±΄λ°, λ‘κ°œμ˜ νŒŒμΌμ„ 생성 ν•˜κ² μŠ΅λ‹ˆλ‹€.

  • μ›Œν¬ν”Œλ‘œμ™€ link νŠΈλ¦¬κ±°κ°€ μ •μ˜λ˜μ–΄ μžˆλŠ” interactive_blocks_demo.ts
  • interactive_blocks λ‚΄μ˜ λ²„νŠΌ 클릭 이벀트λ₯Ό λ‹€λ£¨λŠ” μ»€μŠ€ν…€ νŽ‘μ…˜μ΄ μ •μ˜λ˜μ–΄ μžˆλŠ” handle_interactive_blocks.ts

λ‹€μŒ μ½”λ“œλ₯Ό interactive_blocks_demo.ts λΌλŠ” μ΄λ¦„μœΌλ‘œ μ €μž₯ν•©λ‹ˆλ‹€.

import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
export const workflow = DefineWorkflow({
  callback_id: "demo-workflow",
  title: "Demo Workflow",
  input_parameters: {
    properties: {
      channel_id: { type: Schema.slack.types.channel_id },
      user_id: { type: Schema.slack.types.user_id },
    },
    required: ["channel_id", "user_id"],
  },
});

// Send a message via SendMessage + interactive_blocks
const sendMessageStep = workflow.addStep(Schema.slack.functions.SendMessage, {
  channel_id: workflow.inputs.channel_id,
  message: `Do you approve <@${workflow.inputs.user_id}>'s time off request?`,
  // Simplified blocks for interactions
  interactive_blocks: [
    {
      "type": "actions",
      "block_id": "approve-deny-buttons",
      "elements": [
        {
          type: "button",
          action_id: "approve",
          text: { type: "plain_text", text: "Approve" },
          style: "primary",
        },
        {
          type: "button",
          action_id: "deny",
          text: { type: "plain_text", text: "Deny" },
          style: "danger",
        },
      ],
    },
  ],
});

// Handle the button click events on interactive_blocks
import { def as handleInteractiveBlocks } from "./handle_interactive_blocks.ts";
workflow.addStep(handleInteractiveBlocks, {
  // The clicked action's details
  action: sendMessageStep.outputs.action,
  // For further interactions on a modal
  interactivity: sendMessageStep.outputs.interactivity,
  // The message's URL
  messageLink: sendMessageStep.outputs.message_link,
  // The message's unique ID in the channel
  messageTs: sendMessageStep.outputs.message_ts,
});

import { Trigger } from "deno-slack-api/types.ts";
const trigger: Trigger<typeof workflow.definition> = {
  type: "shortcut",
  name: "Interaction Demo Trigger",
  workflow: `#/workflows/${workflow.definition.callback_id}`,
  inputs: {
    channel_id: { value: "{{data.channel_id}}" },
    user_id: { value: "{{data.user_id}}" },
  },
};
export default trigger;

아직 handle_interactive_blocks.ts λ₯Ό μ•ˆλ§Œλ“€μ—ˆκΈ° λ•Œλ¬Έμ—, TS μ»΄νŒŒμΌμ€ μ‹€νŒ¨ν•©λ‹ˆλ‹€. handle_interactive_blocks.ts λ₯Ό λ§Œλ“€κ² μŠ΅λ‹ˆλ‹€. 이 μ»€μŠ€ν…€ νŽ‘μ…˜μ€ SendMessage νŽ‘μ…˜μœΌλ‘œλΆ€ν„° μ „λ‹¬λ˜λŠ” 클릭 이벀트λ₯Ό λ‹€λ£Ήλ‹ˆλ‹€.

import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";

export const def = DefineFunction({
  callback_id: "handle_interactive_blocks",
  title: "Handle button clicks in interactive_blocks",
  source_file: "handle_interactive_blocks.ts",
  input_parameters: {
    // The input values from the SendMessage function's interactive_blocks
    properties: {
      action: { type: Schema.types.object },
      interactivity: { type: Schema.slack.types.interactivity },
      messageLink: { type: Schema.types.string },
      messageTs: { type: Schema.types.string },
    },
    required: ["action", "interactivity"],
  },
  output_parameters: { properties: {}, required: [] },
});

export default SlackFunction(
  def,
  // When the worfklow is executed, this handler is called
  async ({ inputs, client }) => {
    if (inputs.action.action_id === "deny") {
      // Only when the click is on "Deny", this function opens a modal
      // to ask the reason of the denial
      const response = await client.views.open({
        interactivity_pointer: inputs.interactivity.interactivity_pointer,
        view: buildNewModalView(),
      });
      if (response.error) {
        const error = `Failed to open a modal due to ${response.error}`;
        return { error };
      }
      // Continue the interactions on the modal
      return { completed: false };
    }
    return { completed: true, outputs: {} };
  },
)
  // Handle the button click events on the modal
  .addBlockActionsHandler("clear-inputs", async ({ body, client }) => {
    const response = await client.views.update({
      interactivity_pointer: body.interactivity.interactivity_pointer,
      view_id: body.view.id,
      view: buildNewModalView(),
    });
    if (response.error) {
      const error = `Failed to update a modal due to ${response.error}`;
      return { error };
    }
    return { completed: false };
  })
  // Handle the data submission from the modal
  .addViewSubmissionHandler(
    ["deny-reason-submission"],
    ({ view }) => {
      const values = view.state.values;
      const reason = String(Object.values(values)[0]["deny-reason"].value);
      if (reason.length <= 5) {
        console.log(reason);
        const errors: Record<string, string> = {};
        const blockId = Object.keys(values)[0];
        errors[blockId] = "The reason must be 5 characters or longer";
        return { response_action: "errors", errors };
      }
      return {};
    },
  )
  // Handle the events when the end-user closes the modal
  .addViewClosedHandler(
    ["deny-reason-submission", "deny-reason-confirmation"],
    ({ view }) => {
      console.log(JSON.stringify(view, null, 2));
    },
  );

/**
 * Returns the initial state of the modal view
 * @returns the initial modal view
 */
function buildNewModalView() {
  return {
    "type": "modal",
    "callback_id": "deny-reason-submission",
    "title": { "type": "plain_text", "text": "Reason for the denial" },
    "notify_on_close": true,
    "submit": { "type": "plain_text", "text": "Confirm" },
    "blocks": [
      {
        "type": "input",
        // If you reuse block_id when refreshing an existing modal view,
        // the old block may remain. To avoid this, always set a random value.
        "block_id": crypto.randomUUID(),
        "label": { "type": "plain_text", "text": "Reason" },
        "element": {
          "type": "plain_text_input",
          "action_id": "deny-reason",
          "multiline": true,
          "placeholder": {
            "type": "plain_text",
            "text": "Share the reason why you denied the request in detail",
          },
        },
      },
      {
        "type": "actions",
        "block_id": "clear",
        "elements": [
          {
            type: "button",
            action_id: "clear-inputs",
            text: { type: "plain_text", text: "Clear all the inputs" },
            style: "danger",
          },
        ],
      },
    ],
  };
}

manifest.ts 에 μΆ”κ°€ν•΄μ£Όμ‹œκ΅¬μš”

import { Manifest } from "deno-slack-sdk/mod.ts";
// Add this
import { workflow as InteractiveBlocksDemo } from "./interactive_blocks_demo.ts";

export default Manifest({
  name: "stoic-wolf-344",
  description: "Demo workflow",
  icon: "assets/default_new_app_icon.png",
  workflows: [InteractiveBlocksDemo], // Add this
  outgoingDomains: [],
  botScopes: ["commands", "chat:write", "chat:write.public"],
});

이제 μ€€λΉ„κ°€ λμŠ΅λ‹ˆλ‹€. slack run μ»€λ§¨λ“œλ₯Ό μ‹€ν–‰ν•˜μ—¬ μ—λŸ¬ λ°œμƒ μ—¬λΆ€λ₯Ό ν™•μΈν•˜μ„Έμš”.

$ slack run
? Choose a workspace  seratch  T03E94MJU
   stoic-wolf-344 A04G9S43G2K

Updating dev app install for workspace "Acme Corp"

⚠️  Outgoing domains
   No allowed outgoing domains are configured
   If your function makes network requests, you will need to allow the outgoing domains
   Learn more about upcoming changes to outgoing domains: https://api.slack.com/future/changelog
✨  seratch of Acme Corp
Connected, awaiting events

그리고 λ˜λ‹€λ₯Έ 터미널을 μ—΄μ–΄μ„œ slack triggers create –trigger-def interactive_blocks_demo.ts λ₯Ό μ‹€ν–‰ν•˜μ—¬ link 트리거λ₯Ό μƒμ„±ν•΄μ€λ‹ˆλ‹€.

$ slack triggers create --trigger-def interactive_blocks_demo.ts
? Choose an app  seratch (dev)  T03E94MJU
   stoic-wolf-344 (dev) A04G9S43G2K

⚑ Trigger created
   Trigger ID:   Ft04HCF4SSBB
   Trigger Type: shortcut
   Trigger Name: Interaction Demo Trigger
   URL: https://slack.com/shortcuts/***/***
$

μƒμ„ λœ link 트리거λ₯Ό 채널에 남기고 λˆŒλŸ¬λ΄…μ‹œλ‹€. λ‘κ°œμ˜ λ²„νŠΌμ΄ μžˆλŠ” 메세지λ₯Ό 보싀 수 μžˆμ„ κ²λ‹ˆλ‹€.

“Approve” λ²„νŠΌμ„ λˆ„λ₯΄λ©΄, handle_interactive.ts νŽ‘μ…˜μ΄ 이벀트 μš”μ²­μ„ μˆ˜λ½ν•˜κ³  아무일도 μ•ˆν•©λ‹ˆλ‹€. 이 경우 interactive_blocks 뢀뢄이 ν”Œλž«νΌμ— μ˜ν•΄ λŒ€μ²΄λ˜λŠ” 것 λ§κ³ λŠ” 아무일도 λ°œμƒν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

λ°˜λŒ€λ‘œ “Deny” λ²„νŠΌμ„ λˆ„λ₯΄λ©΄, μ»€μŠ€ν…€ νŽ‘μ…˜μ΄ Deny μ‚¬μœ λ₯Ό λ¬Όμ–΄λ³΄λŠ” μƒˆλ‘œμš΄ λͺ¨λ‹¬ 화면을 λ„μ›λ‹ˆλ‹€.

λ˜ν•œ ν™”λ©΄μ˜ 데이터 μ„œλΈŒλ―Έμ…˜μ„ μœ„ν•œ 좔가적인 ν•Έλ“€λŸ¬κ°€, μž…λ ₯된 데이터가 μœ νš¨ν•œμ§€ (길이) μ²΄ν¬ν•˜κ³ , λ™μ‹œμ— μž…λ ₯값을 μ§€μšΈ 수 μžˆλŠ” λ²„νŠΌκΉŒμ§€ μ œκ³΅ν•©λ‹ˆλ‹€.

λ³΄μ‹œλŠ” 바와 같이, μ΄λŸ¬ν•œ κ°„λ‹¨ν•œ 승인 절차λ₯Ό λ§Œλ“€ λ•Œ, 빌트인 interactive_blocks λ₯Ό μ‚¬μš©ν•˜λ©΄ κ°„λ‹¨ν•˜κ²Œ κ΅¬ν˜„ν•  수 μžˆμŠ΅λ‹ˆλ‹€. κ·Έλ ‡μ§€λ§Œ, μ–΄λ–€ 뢀뢄은 μ»€μŠ€ν„°λ§ˆμ΄μ§• ν•  수 μ—†λŠ”λ°μš”, 예λ₯Ό λ“€λ©΄, λ²„νŠΌμ΄ 클릭 λ˜μ—ˆμ„ λ•Œ interactive_blocks 파트 뢀뢄을 μ—…λ°μ΄νŠΈ ν•œλ‹€κ±°λ‚˜ ν•˜λŠ” 뢀뢄이 μ•ˆλ©λ‹ˆλ‹€. λ§Œμ•½ μΈν„°λ ‰μ…˜μ— λŒ€ν•΄μ„œ λͺ¨λ“  μ œμ–΄λ₯Ό ν•˜κ³  μ‹Άλ‹€λ©΄, Block Kit 을 μ‚¬μš©ν•΄μ„œ μΈν„°λ ‰ν‹°λΈŒν•œ λ©”μ‹œμ§€ 블둝을 λ§Œλ“€ 수 μžˆμŠ΅λ‹ˆλ‹€. λ‹€μŒ μ„Ήμ…˜μ—μ„œ λ°°μ›Œλ³Ό κ²ƒμž…λ‹ˆλ‹€.

Write Custom Function With Full Interactivity Features

send_interactive_message.ts λΌλŠ” 이름을 가진 νŒŒμΌμ„ λ§Œλ“­λ‹ˆλ‹€. 이 μ†ŒμŠ€νŒŒμΌμ—λŠ” Block Kits 의 블둝을 μ‚¬μš©ν•΄μ„œ μ±„λ„λ‘œ 메세지λ₯Ό 보내고, 메세지 λΈ”λ‘μ˜ λͺ¨λ“  μΈν„°λ ‰ν‹°λΈŒ 이벀트λ₯Ό λ‹€λ£¨λŠ” μ»€μŠ€ν…€ νŽ‘μ…˜μ΄ μ •μ˜λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€.

import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";

export const def = DefineFunction({
  callback_id: "send_interactive_message",
  title: "Send a message with interactive blocks",
  source_file: "send_interactive_message.ts",
  input_parameters: {
    properties: {
      user_id: { type: Schema.slack.types.user_id },
      channel_id: { type: Schema.slack.types.channel_id },
    },
    required: ["user_id", "channel_id"],
  },
  output_parameters: { properties: {}, required: [] },
});

export default SlackFunction(
  def,
  // When the worfklow is executed, this handler is called
  async ({ inputs, client }) => {
    const text = `Do you approve <@${inputs.user_id}>'s time off request?`;
    // Block Kit elements (https://api.slack.com/block-kit)
    const blocks = [
      {
        type: "section",
        text: { type: "mrkdwn", text },
      },
      { type: "divider" },
      {
        type: "actions",
        block_id: "approve-deny-buttons",
        elements: [
          {
            type: "button",
            action_id: "approve",
            text: { type: "plain_text", text: "Approve" },
            style: "primary",
          },
          {
            type: "button",
            action_id: "deny",
            text: { type: "plain_text", text: "Deny" },
            style: "danger",
          },
        ],
      },
    ];
    const response = await client.chat.postMessage({
      channel: inputs.channel_id,
      text,
      blocks,
    });
    if (response.error) {
      console.log(JSON.stringify(response, null, 2));
      const error = `Failed to post a message due to ${response.error}`;
      return { error };
    }
    // To continue with this interaction, return false for the completion
    return { completed: false };
  },
)
  // Handle the "Approve" button clicks
  .addBlockActionsHandler("approve", async ({ body, client, inputs }) => {
    const text = "Thank you for approving the request!";
    const response = await client.chat.update({
      channel: inputs.channel_id,
      ts: body.container.message_ts,
      text,
      blocks: [{ type: "section", text: { type: "mrkdwn", text } }],
    });
    if (response.error) {
      const error = `Failed to update the message due to ${response.error}`;
      return { error };
    }
    return { completed: true, outputs: {} };
  })
  // Handle the "Deny" button clicks
  .addBlockActionsHandler("deny", async ({ body, client, inputs }) => {
    const text =
      "OK, we need more information... Could you share the reason for denial?";
    const messageResponse = await client.chat.update({
      channel: inputs.channel_id,
      ts: body.container.message_ts,
      text,
      blocks: [{ type: "section", text: { type: "mrkdwn", text } }],
    });
    if (messageResponse.error) {
      const error =
        `Failed to update the message due to ${messageResponse.error}`;
      return { error };
    }
    const modalResponse = await client.views.open({
      interactivity_pointer: body.interactivity.interactivity_pointer,
      view: buildNewModalView(),
    });
    if (modalResponse.error) {
      const error = `Failed to open a modal due to ${modalResponse.error}`;
      return { error };
    }
    return { completed: false };
  })
  // Handle the button click events on the modal
  .addBlockActionsHandler("clear-inputs", async ({ body, client }) => {
    const response = await client.views.update({
      interactivity_pointer: body.interactivity.interactivity_pointer,
      view_id: body.view.id,
      view: buildNewModalView(),
    });
    if (response.error) {
      const error = `Failed to update a modal due to ${response.error}`;
      return { error };
    }
    return { completed: false };
  })
  // Handle the data submission from the modal
  .addViewSubmissionHandler(
    ["deny-reason-submission"],
    ({ view }) => {
      const values = view.state.values;
      const reason = String(Object.values(values)[0]["deny-reason"].value);
      if (reason.length <= 5) {
        console.log(reason);
        const errors: Record<string, string> = {};
        const blockId = Object.keys(values)[0];
        errors[blockId] = "The reason must be 5 characters or longer";
        return { response_action: "errors", errors };
      }
      return {};
    },
  )
  // Handle the events when the end-user closes the modal
  .addViewClosedHandler(
    ["deny-reason-submission", "deny-reason-confirmation"],
    ({ view }) => {
      console.log(JSON.stringify(view, null, 2));
    },
  );

/**
 * Returns the initial state of the modal view
 * @returns the initial modal view
 */
function buildNewModalView() {
  return {
    "type": "modal",
    "callback_id": "deny-reason-submission",
    "title": { "type": "plain_text", "text": "Reason for the denial" },
    "notify_on_close": true,
    "submit": { "type": "plain_text", "text": "Confirm" },
    "blocks": [
      {
        "type": "input",
        // If you reuse block_id when refreshing an existing modal view,
        // the old block may remain. To avoid this, always set a random value.
        "block_id": crypto.randomUUID(),
        "label": { "type": "plain_text", "text": "Reason" },
        "element": {
          "type": "plain_text_input",
          "action_id": "deny-reason",
          "multiline": true,
          "placeholder": {
            "type": "plain_text",
            "text": "Share the reason why you denied the request in detail",
          },
        },
      },
      {
        "type": "actions",
        "block_id": "clear",
        "elements": [
          {
            type: "button",
            action_id: "clear-inputs",
            text: { type: "plain_text", text: "Clear all the inputs" },
            style: "danger",
          },
        ],
      },
    ],
  };
}

λ‹€μŒμœΌλ‘œ, μœ„μ—μ„œ λ§Œλ“  νŽ‘μ…˜μ„ μ‚¬μš©ν•˜λŠ” μ›Œν¬ν”Œλ‘œλ₯Ό λ§Œλ“€κ³ , interactive_message_demo.ts λΌλŠ” μ΄λ¦„μœΌλ‘œ μ €μž₯ν•©λ‹ˆλ‹€.

import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
export const workflow = DefineWorkflow({
  callback_id: "demo-workflow",
  title: "Demo Workflow",
  input_parameters: {
    properties: {
      channel_id: { type: Schema.slack.types.channel_id },
      user_id: { type: Schema.slack.types.user_id },
    },
    required: ["channel_id", "user_id"],
  },
});

import { def as sendInteractiveMessage } from "./send_interactive_message.ts";
workflow.addStep(sendInteractiveMessage, {
  user_id: workflow.inputs.user_id,
  channel_id: workflow.inputs.channel_id,
});

import { Trigger } from "deno-slack-api/types.ts";
const trigger: Trigger<typeof workflow.definition> = {
  type: "shortcut",
  name: "Interaction Demo Trigger",
  workflow: `#/workflows/${workflow.definition.callback_id}`,
  inputs: {
    channel_id: { value: "{{data.channel_id}}" },
    user_id: { value: "{{data.user_id}}" },
  },
};
export default trigger;

manifest.ts 에 μΆ”κ°€ν•˜κ³ μš”

import { Manifest } from "deno-slack-sdk/mod.ts";
import { workflow as InteractiveBlocksDemo } from "./interactive_blocks_demo.ts";
// Add this
import { workflow as InteractiveMessageDemo } from "./interactive_message_demo.ts";

export default Manifest({
  name: "stoic-wolf-344",
  description: "Demo workflow",
  icon: "assets/default_new_app_icon.png",
  workflows: [InteractiveBlocksDemo, InteractiveMessageDemo], // Add this
  outgoingDomains: [],
  botScopes: ["commands", "chat:write", "chat:write.public"],
});

λ§ˆμ§€λ§‰μœΌλ‘œ, μœ„μ—μ„œ ν–ˆλ˜ κ²ƒμ²˜λŸΌ λ˜‘κ°™μ€ λ°©λ²•μœΌλ‘œ link 트리거λ₯Ό λ§Œλ“­λ‹ˆλ‹€. μ›Œν¬ν”Œλ‘œλ₯Ό μ‹œμž‘ν•˜κ²Œ 되면, λ²„νŠΌμ΄ λ‹¬λ €μžˆλŠ” 메세지λ₯Ό 보싀 κ²λ‹ˆλ‹€. 그리고 λ²„νŠΌμ„ λˆ„λ₯΄λ©΄, μ΄μ „μ˜ SendMessage 의 interactive_blocks κ³ΌλŠ” λ‹€λ₯Έ 점을 보싀 수 μžˆμ„ κ²λ‹ˆλ‹€. (μ΄μ „μ—λŠ” λ‹¨μˆœνžˆ Approve λ²„νŠΌμ„ 클릭 ν–ˆλ‹€ λΌλŠ” 메세지가 λ‚˜μ™”λ‹€λ©΄, μ΄λ²ˆμ—λŠ” Approve μ—μ„œ κ°μ‚¬ν•˜λ‹€ λΌλŠ” 메세지가 λ‚˜μ˜΅λ‹ˆλ‹€.)

좜λ ₯λ˜λŠ” 메세지λ₯Ό λ³€κ²½ν•œ μ½”λ“œλŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.

  .addBlockActionsHandler("approve", async ({ body, client, inputs }) => {
    const text = "Thank you for approving the request!";
    await client.chat.update({
      channel: inputs.channel_id,
      ts: body.container.message_ts,
      text,
      blocks: [{ type: "section", text: { type: "mrkdwn", text } }],
    });
    return { completed: true, outputs: {} };
  })

Deny λ²„νŠΌμ— λŒ€ν•΄μ„œλŠ” 첫번째 μ˜ˆμ œμ™€ 같이 λ˜‘κ°™μ΄ λͺ¨λ‹¬ 화면을 λ„μ›λ‹ˆλ‹€. κ·Έλ ‡μ§€λ§Œ 그와 λ™μ‹œμ— μ˜λ―ΈμžˆλŠ” 메세지도 같이 λ‚¨κΉλ‹ˆλ‹€.

Deny λ²„νŠΌμ„ ν΄λ¦­ν–ˆμ„λ•Œμ˜ 핸듀링을 μœ„ν•œ μ½”λ“œ μž…λ‹ˆλ‹€. μ—¬κΈ°μ„œ μ•Œκ³  κ°€μ…”μ•Ό ν•˜λŠ” 뢀뢄은 interactivity λ₯Ό inputs μ—μ„œ λ°›μ•„μ˜€λŠ”κ²Œ μ•„λ‹ˆλΌ body λ°μ΄ν„°μ—μ„œ 가지고 μ˜¨λ‹€λŠ” κ²ƒμž…λ‹ˆλ‹€.

  .addBlockActionsHandler("deny", async ({ body, client, inputs }) => {
    const text =
      "OK, we need more information... Could you share the reason for denial?";
    await client.chat.update({
      channel: inputs.channel_id,
      ts: body.container.message_ts,
      text,
      blocks: [{ type: "section", text: { type: "mrkdwn", text } }],
    });
    await client.views.open({
      interactivity_pointer: body.interactivity.interactivity_pointer,
      view: buildNewModalView(),
    });
    // To continue interactions, return completed: false
    return { completed: false };
  })

Block Kit μ΄λ‚˜ λͺ¨λ‹¬μ— μ΅μˆ™ν•˜μ§€ μ•Šλ‹€λ©΄, 잘 이해가 μ•ˆλ˜λŠ” μ½”λ“œκ°€ μžˆμ„ 수 μžˆμŠ΅λ‹ˆλ‹€. μƒλŒ€μ μœΌλ‘œ λ‹¨μˆœν•œ μ½”λ“œλΆ€ν„° μ‹œμž‘ν•˜μ—¬ μ½”λ“œλ₯Ό λ°”κΏ”λ³΄λ©΄μ„œ 더 λ°°μ›Œλ³΄μ‹€ 수 μžˆμœΌμ‹€κ²λ‹ˆλ‹€.

블둝을 νŽΈμ§‘ν• λ•ŒλŠ”, Block Kit Builder λ₯Ό μ‚¬μš©ν•΄ λ³΄μ‹œλ©΄ μ’‹μŠ΅λ‹ˆλ‹€. 아직 μ‚¬μš©ν•΄λ³΄μ§€ μ•Šμ•˜λ‹€λ©΄, ν•΄λ‹Ή μ‚¬μ΄νŠΈλ₯Ό λ°©λ¬Έν•˜μ—¬ 쒌츑의 μ‚¬μš© κ°€λŠ₯ν•œ 블둝듀을 λˆŒλŸ¬λ³΄μ„Έμš”.

Wrapping Up

이번 νŠœν† λ¦¬μ–Όμ—μ„œλŠ” λ‹€μŒκ³Ό 같은 λ‚΄μš©μ„ λ°°μ› μŠ΅λ‹ˆλ‹€.

  • SendMessage 의 interactive_blocks 을 μ‚¬μš©ν•˜λŠ” 방법과, μ»€μŠ€ν…€ νŽ‘μ…˜μ—μ„œ ν•΄λ‹Ή 이벀트λ₯Ό λ‹€λ£¨λŠ” 방법
  • μΈν„°λ ‰ν‹°λΈŒν•œ 메세지λ₯Ό 보내고,ν•΄λ‹Ή λ©”μ„Έμ§€μ˜ μΈν„°λ ‰ν‹°λΈŒ 이벀트λ₯Ό λ‹€λ£¨λŠ” μ»€μŠ€ν…€ νŽ‘μ…˜μ„ λ§Œλ“œλŠ” 방법

이 ν”„λ‘œμ νŠΈλŠ” λ‹€μŒ μ£Όμ†Œμ—μ„œλ„ ν™•μΈν•˜μ‹€ 수 μžˆμŠ΅λ‹ˆλ‹€.  https://github.com/seratch/slack-next-generation-platform-tutorials/tree/main/12_Button_Interactions

이번 νŠœν† λ¦¬μ–Όλ„ μ¦κ±°μš°μ…¨μœΌλ©΄ μ’‹κ² λ„€μš”. λ§ˆμ°¬κ°€μ§€λ‘œ ν”Όλ“œλ°±μ΄λ‚˜ μ½”λ©˜νŠΈκ°€ μžˆμœΌμ‹œλ‹€λ©΄ νŠΈμœ„ν„° (@seratch) 둜 μ—°λ½μ£Όμ‹œκ±°λ‚˜, μ—¬κΈ° λ‚¨κ²¨μ£Όμ„Έμš”.

Happy hacking with Slack’s next-generation platform πŸš€