コラボフローdeジャンケン


「[コラボフロー Advent Calendar 2023](https://qiita.com/advent-calendar/2023/collaboflow)」3日目の記事だよ～
コラボフローの詳細は [こちら](https://www.collabo-style.co.jp/about/)。

---

去年はJavaScriptカスタマイズしたので、今年はWebhookカスタマイズで遊んでみました。

## コラボフローでジャンケン！

対戦よろしくお願いします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/e57380be-42f9-e04d-856e-8e00c880dfec.png)

申請して挑みます。今回は「ぱー」でいきましょう！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/08d95342-54bb-3b89-a65a-021e0d6a19b5.png)

じゃんけん～

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/644bc67c-faa5-ca86-7c05-b240321b07fc.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/e1621ac5-0757-c748-3a56-96141d711f86.png)

どきどき・・・
　
 
　
 
　
 
　
 
　
 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/eefb182d-0f2b-de09-fdcd-07dd2e433445.png)

うわぁぁーー！　負けちゃいました。

戦績が見れるように [リストビュー](https://collaboflow.zendesk.com/hc/ja/articles/5923889187343) と [ビューフォルダ設定](https://collaboflow.zendesk.com/hc/ja/articles/360000383815) を組み合わせて一覧表示してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/db4f06c1-f916-a545-7157-25bf345bbeb6.png)

ちょき率が高そう。ふむふむ・・・。もう一回！

といきたいですが、そろそろ本題（実装）の紹介です。

## 用意するもの

### コラボフロー

[30日間無料お試し](https://www.collabo-style.co.jp/trial/) からクラウドの環境を申し込めます。

### Webhook 受信サーバー

Webhook は指定したURLに対して何かしらのイベント通知を行う仕組みで、コラボフローではPOST送信が使われています。イベント通知の受け手となる受信サーバー＆プログラムの用意が必要です。

今回は、お手軽に使えるみんな大好き [Google Action Script (GAS)](https://www.google.com/script/start/) を使います。

## 大まかな流れ

ざっくりとこんな流れでイベント・処理がすすみます。

1. コラボフロー で申請書が出されて判定段階に到達する
1. コラボフロー Webhook 通知が発火する
1. GAS `doPost(e)` メソッドが呼び出される
    - `e.postData.contents` に通知内容が入っている
1. GAS の自前処理
    - 判定段階チェック
    - ジャンケン処理、勝ち負け判定
    - コラボフローの判定REST APIを実行

## 実装

GAS で新しくプロジェクトを作成して「コード.gs」に Webhook を受信するための `doPost()` メソッドの実装と、フィードバックするためにコラボフローのREST API設定・認証情報を組み込みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/7c76ff53-bf6e-800b-0061-2bce8d73ba04.png)

### ソースコード

```javascript:コード.gs
/** APIエンドポイント (システム管理＞環境変数＞REST API) */
const API_ENDPOINT = "https://cloud.collaboflow.com/example/api/index.cfm";
/** 実行ユーザーID */
const API_USER = "tonakai";
/** APIキー */
const API_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

/**
 * POST で受け取る処理
 */
function doPost(e) {
  const out = ContentService.createTextOutput();
  out.setMimeType(ContentService.MimeType.JSON);

  const result = handlePost(e);

  out.setContent(JSON.stringify({
    ...result
  }));
  return out;
}

function handlePost(e) {
  // GASで受信時はJSON形式の文字列。オブジェクトに変換します
  const payload = e?.postData?.contents;
  if (!payload || typeof payload !== "string") {
    return {
      error: "エラー: 不正な payload",
    };
  }
  const webhook = JSON.parse(payload);

  // It's ゲームタイム！
  if (isGameTime(webhook, "ジャンケン相手")) {
    return gameJudge(webhook);
  }
}

/**
 * 指定した判定段階への到達チェック
 * @param phaseTitle {string} 判定アイテムで設定した判定名
 */
function isGameTime(webhook, phaseTitle) {
  // 判定中で受信時
  if (webhook.flow_status === "flow" && webhook.action_type === "receive") {
    if (!Array.isArray(webhook.determs)) {
      return false;
    }

    // 段階名チェック
    for (const d of webhook.determs) {
      if (d.phase_title === phaseTitle) {
        return true;
      }
    }
  }

  return false;
}

/**
 * ジャンケン！
 */
function gameJudge(webhook) {
  const appId = webhook.app_cd;
  const docId = webhook.document_id; // 文書ID
  const userName = webhook.request_user.name; // 申請者名
  const doc = webhook.contents; // 申請内容

  // トナカイの手札を決定する
  const hands = ["ぐー", "ちょき", "ぱー"];
  const cpuHandId = Math.round(Math.random() * 2);
  const cpuHand = hands[cpuHandId];

  // 挑戦者の手札を把握する
  const fighterHand = doc.fidFighterHand.value;

  // ジャッジメントですの！
  let judge = `「${fighterHand}」と「${cpuHand}」で、`;
  if (fighterHand === cpuHand) {
    judge += "あいこでした";
  } else {
    // あいこ以外
    // 「挑戦者の手札vsトナカイの手札」形式で組み合わせキーにする
    const matching = {
      "ぐーvsちょき": "勝ち！",
      "ぐーvsぱー": "負け",
      "ちょきvsぐー": "負け",
      "ちょきvsぱー": "勝ち！",
      "ぱーvsぐー": "勝ち！",
      "ぱーvsちょき": "負け",
    };
    const key = `${fighterHand}vs${cpuHand}`;
    judge += `${userName}さんの` + matching[key];
  }

  // コラボフローに判定フィードバック
  return api_acceptDocument(appId, docId, {
    fidCpuHand: cpuHand,
    fidJudge: judge,
  });
}

/**
 * 認証ヘッダーを生成
 */
function makeAuthHeader() {
  const credential = `${API_USER}/apikey:${API_KEY}`;
  return {
    "X-Collaboflow-Authorization": "basic " + Utilities.base64Encode(credential),
  }
}

/**
 * 申請書を承認判定する
 * @param appId {number} アプリケーションコード
 * @param docId {number} 対象の文書ID
 * @param document {object} 更新内容
 */
function api_acceptDocument(appId, docId, document) {
  // @see http://docs.collaboflow.com/api-docs/#/Document/putDocumentStatus
  const apiUrl = `${API_ENDPOINT}/v1/documents/${Number(docId)}`;
  const body = {
    app_cd: appId,
    action: "accept",
    document,
  };

  const response = UrlFetchApp.fetch(apiUrl, {
    method: "PUT",
    headers: makeAuthHeader(),
    contentType: "application/json",
    payload: JSON.stringify(body),
    muteHttpExceptions: true,
    followRedirects: false,
  });

  const responsePayload = response.getContentText("UTF-8");
  return {
    success: response.getResponseCode() === 200,
    result: responsePayload,
  };
}
```

### GAS 設定

コードを書いたらデプロイして、Webbhook で受け付けれるURLを入手します。
「デプロイ＞新しいデプロイ」を選択します。

![デプロイ＞新しいデプロイ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/dde06f14-2c05-7637-5fcd-920328cef315.png)

続いて、歯車アイコン＞ウェブアプリを選択します。

![歯車アイコン＞ウェブアプリ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/38455c25-c4e3-cc0c-9c81-c2a0190e2fad.png)

名称を適宜入れ、**アクセスできるユーザー**を「全員」にしてデプロイします。

![003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/ac55545e-ad17-2682-4375-db42d9375b57.png)

:::note info
初回のみ、各種アクセス権限を付与していいか確認があります。許可して続行します。
:::

デプロイが終わると、ウェブアプリの URL が表示されます。これをコピーしてメモしておきます。
※後でも「デプロイ＞デプロイを管理」から見れます。

![006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/48c9639d-a745-6758-8a0c-20c238377021.png)

:::note warn
アクセスできるユーザーが全員の、ウェブアプリURLは知られると誰でも送信できてしまいます。漏れないよう取扱いに注意します。
:::

### コラボフロー設定

#### フォーム

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/9648ee0e-c872-abb2-59bd-6fb0568d5525.png)

パーツの例。「パーツID」の自動OFFにして `fidFighterHand` と指定して作ります。
相手側は `fidCpuHand` と指定して作ります。これで Webhook 受信時もこれらの名称が使えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/5d9c3853-0c2d-bba3-e7b8-d012afe0e6a1.png)

判定メッセージも受け取れるようにしましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/ed71f20e-e46c-e450-44b5-875f03e5efc8.png)

### 経路・判定設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/c464047f-9499-0dcf-7195-8adfc0a7c07c.png)

申請書類の設定で、フォームと紐付け、ジャンケン挑戦者（申請者）がズルできないように相手の手をブロックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/8bbfea92-cb30-0086-3f0b-27d4671bc032.png)

判定アイテムの設定でポイントになるのは2つ。

- 判定アイテムの段階名を Webhook 内でチェックする段階名（`phase_title`）と一致させる。
- GAS の応答で書き込むパーツを編集可にする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/5737f192-eac7-1f89-b46c-42faa67c0add.png)

一通り経路設定ができたら保存します。

### 経路・Webhook 設定

いよいよ最後です。GAS へのつなぎ込みをします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/f98c87dd-08de-7a39-2f06-d52c79d9b584.png)

Webhook のタブから新規登録します。

- 通知先のWebhook URLに、メモしたウェブアプリのURLを入れます。
- 通知条件で、判定者・回覧者操作の「受信時」のみチェックを付けます。
  (`action_type` で受け取る "receive" に相当します。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/66062a92-f9ba-e3d7-1705-8e1ae34d7613.png)

## 完成！

これで GAS を利用してコラボフローの Webhook 通知を処理できるようになりました。

あとは、トナカイさんに挑みましょう。ねらい目はぐー✊ですね。

## リファレンス

### コラボフロー関連

- [Webhookの通知内容について
](https://collaboflow.zendesk.com/hc/ja/articles/360001538936)
- [判定者・回覧者操作時のWebhookの通知内容について](https://collaboflow.zendesk.com/hc/ja/articles/360001531075)
- [コラボフロー REST API - putDocumentStatus](http://docs.collaboflow.com/api-docs/#/Document/putDocumentStatus)

### Google Action Script 関連

- [UrlFetchApp.fetch](https://developers.google.com/apps-script/reference/url-fetch/url-fetch-app)
- [Class HTTPResponse](https://developers.google.com/apps-script/reference/url-fetch/http-response)
- [Class Utilities](https://developers.google.com/apps-script/reference/utilities/utilities)

## 謝辞

- [今から10分ではじめる Google Apps Script(GAS) で Web API公開](https://qiita.com/riversun/items/c924cfe70e16ee3fe3ba)
    - doPost 一連の処理について大変参考になりました