slack次世代プラットフォームでじゃんけんアプリを作ってみた

## はじめに
2023年4月23日に正式リリースとなったslack次世代プラットフォームの練習として、じゃんけんゲームを作成してみました。
そのソースなどをメモしておきたいと思います

<img width="300" alt="slackapp_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206517/23b79f0e-7fe7-b9d4-f892-45da3cb606f9.png">

## 前提条件など
- slack次世代プラットフォーム開発環境が整っている
- slack次世代プラットフォームについてHelloWorld程度の知識がある

参考ページ
https://api.slack.com/automation/quickstart
https://zenn.dev/blendthink/articles/f175b26f94261f
https://qiita.com/seratch/items/8c91bb367bb942dc0ffe

#### 作成するゲームの仕様
- お題：　じゃんけんゲーム
- トリガー：　ショートカット
    - 起動チャンネルID取得
- フロー
    - アプリ起動で乱数生成
        - 生成した値に対応した手とユーザ3択ボタンをトリガー取得チャンネルに`postMessage`で投稿
            - `addBlockActionsHandler`でボタンに応じたテキストに`update`

## 開発環境
- macOS Ventura 13.4
- apple M2


## ソースコード例
- `slack create`でBlank projectを選択

```text:ディレクトリ構成
.
├─ assets
│  └─...
├─ functions
│  └─ game.ts
│  └─ hand_generate.ts
├─ triggers
│  └─ trigger.ts
├─ workflows
│  └─ workflow.ts
└─ manifest.ts
```

```ts:trigger.ts
import { Trigger } from "deno-slack-api/types.ts";
import { workflow } from "../workflows/workflow.ts";

const trigger: Trigger<typeof workflow.definition> = {
  type: "shortcut",
  name: "test",
  workflow: `#/workflows/${workflow.definition.callback_id}`,
  inputs: {
    user_id: { value: "{{data.user_id}}" },
    channel_id: { value: "{{data.channel_id}}" },
  },
};

export default trigger;
```
```ts:hand_generate.ts
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";

export const def = DefineFunction({
  callback_id: "hand_generate",
  title: "Hand Generate",
  source_file: "functions/hand_generate.ts",
  output_parameters: {
    properties: {
      hand: { type: Schema.types.number },
    },
    required: ["hand"],
  },
});

export default SlackFunction(def, () => {
  const hand = Math.floor(Math.random() * 3);
  return { outputs: { hand } };
});
```
```ts:game.ts
import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";
import { SlackAPI } from "deno-slack-api/mod.ts";

export const def = DefineFunction({
  callback_id: "game",
  title: "game",
  source_file: "functions/game.ts",
  input_parameters: {
    properties: {
      channel_id: { type: Schema.slack.types.channel_id },
      user_id: { type: Schema.slack.types.user_id },
      hand_number: { type: Schema.types.number },
    },
    required: ["channel_id", "user_id", "hand_number"],
  },
});

export default SlackFunction(def, async ({
  inputs,
  token,
}) => {
  // 0: グー, 1: チョキ, 2:パー
  const hand_number = inputs.hand_number;
  let hand = ":raised_hand_with_fingers_splayed:";
  if (hand_number === 0) {
    hand = ":fist:";
  } else if (hand_number === 1) {
    hand = ":v:";
  }

  //トリガーで取得したチャンネルに投稿
  const client = SlackAPI(token);
  await client.chat.postMessage({
    channel: inputs.channel_id,
    blocks: [
      {
        "type": "section",
        "text": {
          "type": "plain_text",
          "text": "じゃんけん",
        },
      },
      {
        "type": "section",
        "text": {
          "type": "plain_text",
          "text": hand,
          "emoji": true,
        },
      },
      {
        "type": "actions",
        "block_id": "buttons",
        "elements": [
          {
            "type": "button",
            "text": {
              "type": "plain_text",
              "emoji": true,
              "text": ":fist:",
            },
            "style": "primary",
            "action_id": "gu",
          },
          {
            "type": "button",
            "text": {
              "type": "plain_text",
              "emoji": true,
              "text": ":v:",
            },
            "style": "primary",
            "action_id": "chi",
          },
          {
            "type": "button",
            "text": {
              "type": "plain_text",
              "emoji": true,
              "text": ":raised_hand_with_fingers_splayed:",
            },
            "style": "primary",
            "action_id": "pa",
          },
        ],
      },
    ],
  });

  return { completed: false };
}).addBlockActionsHandler(
  ["gu", "chi", "pa"],
  ({ body, inputs, client, action }) => {

    //結果テキスト
    let resultText = "ざんねん";
    const hand = inputs.hand_number;
    switch (action.action_id) {
      case "gu":
        if (hand === 1) {
          resultText = "勝ち！";
        }
        break;
      case "chi":
        if (hand === 2) {
          resultText = "勝ち！";
        }
        break;
      case "pa":
        if (hand === 0) {
          resultText = "勝ち！";
        }
        break;
    }

    //投稿内容を更新
    client.chat.update({
      channel: inputs.channel_id,
      ts: body.container.message_ts,
      "blocks": [
        {
          "type": "section",
          "text": {
            "type": "plain_text",
            "text": `<@${body.user.name}>` + "さん\n結果: " + resultText,
          },
        },
      ],
    });

    //アプリ完了処理
    client.functions.completeSuccess({
      function_execution_id: body.function_data.execution_id,
      outputs: {},
    });
  },
);
```

```ts:workflow.ts
import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";
import { def as Game } from "../functions/game.ts";
import { def as HandGenerater } from "../functions/hand_generate.ts";

//トリガーから値を受け取る
export const workflow = DefineWorkflow({
  callback_id: "workflow",
  title: "workflow",
  input_parameters: {
    properties: {
      user_id: { type: Schema.slack.types.user_id },
      channel_id: { type: Schema.slack.types.channel_id },
    },
    required: ["user_id", "channel_id"],
  },
});

const generate = workflow.addStep(HandGenerater, {});
workflow.addStep(Game, {
  channel_id: workflow.inputs.channel_id,
  user_id: workflow.inputs.user_id,
  hand_number: generate.outputs.hand,
});
```
```ts:manifest.ts
import { Manifest } from "deno-slack-sdk/mod.ts";
import { workflow } from "./workflows/workflow.ts";
import { def as Game } from "./functions/game.ts";
import { def as HandGenerater } from "./functions/hand_generate.ts";

export default Manifest({
  name: "Rock-Paper-Scissors",
  description: "Rock-Paper-Scissors",
  icon: "assets/default_new_app_icon.png",
  functions: [HandGenerater, Game],
  workflows: [workflow],
  outgoingDomains: [],
  botScopes: [
    "commands",
    "chat:write",
    "chat:write.public",
  ],
});
```

簡単になりましたが以上です
疑問点、ご指摘等ありましたらお気軽にコメントください
よろしくお願いいたします