Node.js を使用して Google Chat 用のインタラクティブなアンケート アプリを作成する

1. はじめに

Google Chat アプリは、サービスやリソースを Google Chat で直接利用できるため、ユーザーは会話から離れることなく情報を入手し、迅速に操作できます。

この Codelab では、Node.js と Cloud Functions を使用してアンケート アプリをビルドしてデプロイする方法について学びます。

学習内容

  • Cloud Shell を使用する
  • Cloud Functions へのデプロイ
  • スラッシュ コマンドとダイアログでユーザー入力を取得する
  • インタラクティブなカードを作成する

2. 設定と要件

Google Cloud プロジェクトを作成し、Chat アプリで使用する API とサービスを有効にします

前提条件

Google Chat アプリを開発するには、Google Chat にアクセスできる Google Workspace アカウントが必要です。Google Workspace アカウントをまだお持ちでない場合は、この Codelab に進む前に Google Workspace アカウントを作成してログインしてください。

セルフペース型の環境設定

  1. Google Cloud Console を開き、プロジェクトを作成します。

    プロジェクトの選択メニュー[New Project] ボタンプロジェクト ID

    すべての Google Cloud プロジェクトに共通する、一意のプロジェクト ID を覚えておいてください(上記の名前はすでに使われており、正しく機能しません)。以降、この Codelab では PROJECT_ID と呼びます。
  1. 次に、Google Cloud リソースを使用するために、Cloud Console で課金を有効にします。

このコードラボを実行しても、費用はほとんどかからないはずです。このチュートリアル以降の課金が発生しないように、Codelab の最後にある「クリーンアップ」セクションの手順でリソースをシャットダウンする方法を確認してください。Google Cloud の新規ユーザーは $300 の無料トライアル プログラムをご利用いただけます。

Google Cloud Shell

Google Cloud はノートパソコンからリモートで操作できますが、この Codelab では Google Cloud で実行されるコマンドライン環境である Google Cloud Shell を使用します。

Cloud Shell をアクティブにする

  1. Cloud Console で、Cloud Shell をアクティブにする アイコン Cloud Shell アイコン をクリックします。

    メニューバーの Cloud Shell アイコン

    Cloud Shell を初めて開くと、わかりやすいウェルカム メッセージが表示されます。ウェルカム メッセージが表示されたら、[続行] をクリックします。ウェルカム メッセージは再度表示されません。ウェルカム メッセージが表示されます。

    Cloud Shell のウェルカム メッセージ

    プロビジョニングと Cloud Shell への接続にはそれほど時間はかかりません。接続後、Cloud Shell ターミナルが表示されます。

    Cloud Shell ターミナル

    この仮想マシンには、必要な開発ツールがすべて読み込まれています。5 GB の永続ホーム ディレクトリが用意されており、Google Cloud で稼働するため、ネットワーク パフォーマンスが充実しており認証もスムーズです。この Codelab では、すべての作業をブラウザまたは Chromebook で行うことができます。Cloud Shell に接続されると、すでに認証されており、プロジェクトがプロジェクト ID をすでに設定していることがわかります。
  2. Cloud Shell で次のコマンドを実行して、認証されたことを確認します。
    gcloud auth list
    
    GCP API 呼び出しの Cloud Shell の承認を求められたら、[承認] をクリックします。

    コマンドの出力
    Credentialed Accounts
    ACTIVE  ACCOUNT
    *       <my_account>@<my_domain.com>
    
    アカウントがデフォルトで選択されていない場合は、次のコマンドを実行します。
    $ gcloud config set account <ACCOUNT>
    
  1. 正しいプロジェクトが選択されていることを確認します。Cloud Shell で、次のコマンドを実行します。
    gcloud config list project
    
    コマンド出力
    [core]
    project = <PROJECT_ID>
    
    正しいプロジェクトが返されない場合は、次のコマンドでプロジェクトを設定できます。
    gcloud config set project <PROJECT_ID>
    
    コマンド出力
    Updated property [core/project].
    

この Codelab を完了したら、コマンドライン操作とファイルの編集を使用します。ファイルを編集する場合は、Cloud Shell ツールバーの右側にある [エディタを開く] をクリックすると、Cloud Shell に組み込まれているコードエディタである Cloud Shell エディタを使用できます。Cloud Shell では、Vim や Emacs などの人気のエディタも使用できます。

3. Cloud Functions、Cloud Build、Google Chat API を有効にする

Cloud Shell で、次の API とサービスを有効にします。

gcloud services enable \
  cloudfunctions \
  cloudbuild.googleapis.com \
  chat.googleapis.com

このオペレーションには数分かかることがあります。

完了すると、次のような成功メッセージが表示されます。

Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.

4.最初の Chat アプリを作成する

プロジェクトの初期化

まず、シンプルな「Hello World」アプリを作成してデプロイします。 Chat アプリは、https リクエストと JSON ペイロードで応答するウェブサービスです。このアプリでは、Node.js と Cloud Functions を使用します。

Cloud Shell で、poll-app という名前の新しいディレクトリを作成し、そのディレクトリに移動します。

mkdir ~/poll-app
cd ~/poll-app

Codelab の残りの作業と、これから作成するファイルはすべて、このディレクトリに配置されます。

Node.js プロジェクトを初期化します。

npm init

NPM は、プロジェクトの構成(名前やバージョンなど)についていくつか質問します。各質問で ENTER キーを押すと、デフォルト値をそのまま使用できます。デフォルトのエントリ ポイントは、次に作成する index.js という名前のファイルです。

Chat アプリのバックエンドを作成する

アプリの作成を開始しましょう。次の内容のファイルを index.js という名前で作成します。

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  if (event.type === 'MESSAGE') {
    reply = {
        text: `Hello ${event.user.displayName}`
    };
  }
  res.json(reply)
}

アプリはまだあまり動作しませんが、問題ありません。今後、さらに機能を追加していく予定です。

アプリをデプロイする

「Hello World」アプリをデプロイするには、Cloud Functions の関数をデプロイし、Google Cloud Console で Chat アプリを構成して、アプリにテスト メッセージを送信してデプロイを確認します。

Cloud Function のデプロイ

「Hello World」アプリの Cloud Functions の関数をデプロイするには、次のコマンドを入力します。

gcloud functions deploy app --trigger-http --security-level=secure-always --allow-unauthenticated --runtime nodejs14

完了すると、出力は次のようになります。

availableMemoryMb: 256
buildId: 993b2ca9-2719-40af-86e4-42c8e4563a4b
buildName: projects/595241540133/locations/us-central1/builds/993b2ca9-2719-40af-86e4-42c8e4563a4b
entryPoint: app
httpsTrigger:
  securityLevel: SECURE_ALWAYS
  url: https://us-central1-poll-app-codelab.cloudfunctions.net/app
ingressSettings: ALLOW_ALL
labels:
  deployment-tool: cli-gcloud
name: projects/poll-app-codelab/locations/us-central1/functions/app
runtime: nodejs14
serviceAccountEmail: poll-app-codelab@appspot.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/gcf-upload-us-central1-66a01777-67f0-46d7-a941-079c24414822/94057943-2b7c-4b4c-9a21-bb3acffc84c6.zip
status: ACTIVE
timeout: 60s
updateTime: '2021-09-17T19:30:33.694Z'
versionId: '1'

httpsTrigger.url プロパティで、デプロイされた関数の URL をメモします。次のステップでこれを使用します。

アプリの構成

アプリを構成するには、Cloud Console の [チャットの設定] ページ([API とサービス] > [ダッシュボード] > [Hangouts Chat API])に移動します。 3} > [設定])。

  1. [アプリ名] に「PollCodelab」と入力します。
  2. [アバターの URL] に「https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png」と入力します。
  3. [Description] に「Poll app for Codelab」と入力します。
  4. [機能] で [アプリは直接メッセージを送信できます] と [アプリは複数のユーザーが使用するスペースで動作します] を選択します。
  5. [接続設定] で [アプリの URL] を選択し、Cloud Functions の関数の URL(最後のセクションの httpsTrigger.url プロパティ)を貼り付けます。
  6. [権限] で [ドメイン内のユーザーとグループ] を選択し、メールアドレスを入力します。
  7. [保存] をクリックします。

これで、アプリでメッセージを送信できます。

アプリをテストする

次に進む前に、Google Chat のスペースにアプリを追加して、アプリが機能していることを確認してください。

  1. Google Chat に移動します。
  2. [Chat] の横にある [+] > [アプリを探す] をクリックします。
  3. 「PollCodelab」と入力します。
  4. [チャット] をクリックします。
  5. アプリにメッセージを送信するには、「Hello」と入力して Enter キーを押します。

アプリは短い「hello」メッセージを返します。

基本的なスケルトンを作成したら、さらに役立つものに仕上げましょう。

5. アンケート機能を作成する

アプリの仕組みの概要

アプリは主に次の 2 つの部分で構成されます。

  1. アンケートを設定するためのダイアログを表示するスラッシュ コマンド。
  2. 投票と結果の表示を行うためのインタラクティブ カード。

アプリには、アンケートの設定と結果を保存する状態も必要です。これは、Firestore や他のデータベースを使用して行えます。また、状態をアプリ メッセージ自体に保存することもできます。このアプリは、チームに関する非公式なアンケートをすばやく行えるようにすることが目的なので、このユースケースにはアプリ メッセージに状態を保存することをおすすめします。

アプリのデータモデル(Typescript で表現)は次のとおりです。

interface Poll {
  /* Question/topic of poll */
  topic: string;
  /** User that submitted the poll */
  author: {
    /** Unique resource name of user */
    name: string;
    /** Display name */
    displayName: string;
  };
  /** Available choices to present to users */
  choices: string[];
  /** Map of user ids to the index of their selected choice */
  votes: { [key: string]: number };
}

トピックまたは質問、選択肢のリストのほかに、投稿者の ID と名前、記録された投票が含まれます。ユーザーが複数回投票するのを防ぐために、投票は、ユーザー ID と選択した選択肢のインデックスのマップとして保存されます。

もちろん、さまざまなアプローチがありますが、スペースで簡単なアンケートを行うための出発点として使用できます。

ポーリング構成コマンドを実装する

ユーザーがアンケートを開始して設定できるようにするには、ダイアログを開くスラッシュ コマンドを設定します。これは複数のステップで構成されるプロセスです。

  1. アンケートを開始するスラッシュ コマンドを登録します。
  2. アンケートを設定するダイアログを作成します。
  3. アプリがスラッシュ コマンドを認識して処理できるようにします。
  4. アンケートでの投票に役立つインタラクティブなカードを作成します。
  5. アプリがアンケートを実行するためのコードを実装します。
  6. Cloud Functions の関数を再デプロイする。

スラッシュ コマンドを登録する

スラッシュ コマンドを登録するには、チャットの設定コンソール内のAPI とサービス>ダッシュボード>Hangouts Chat API >構成)。

  1. [スラッシュ コマンド] で、[Add slash command] をクリックします。
  2. [名前] に「/poll」と入力します。
  3. [Command id] に「1」と入力します。
  4. [説明] に「アンケートを開始」と入力します。
  5. [ダイアログを開く] を選択します。
  6. [完了] をクリックします。
  7. [保存] をクリックします。

アプリが /poll コマンドを認識し、ダイアログが開きます。次に、ダイアログを構成しましょう。

設定フォームをダイアログとして作成する

スラッシュ コマンドは、ダイアログを開いてアンケートのトピックや選択肢を指定します。次の内容のファイルを、config-form.js という名前で新たに作成します。

/** Upper bounds on number of choices to present. */
const MAX_NUM_OF_OPTIONS = 5;

/**
 * Build widget with instructions on how to use form.
 *
 * @returns {object} card widget
 */
function helpText() {
  return {
    textParagraph: {
      text: 'Enter the poll topic and up to 5 choices in the poll. Blank options will be omitted.',
    },
  };
}

/**
 * Build the text input for a choice.
 *
 * @param {number} index - Index to identify the choice
 * @param {string|undefined} value - Initial value to render (optional)
 * @returns {object} card widget
 */
function optionInput(index, value) {
  return {
    textInput: {
      label: `Option ${index + 1}`,
      type: 'SINGLE_LINE',
      name: `option${index}`,
      value: value || '',
    },
  };
}

/**
 * Build the text input for the poll topic.
 *
 * @param {string|undefined} topic - Initial value to render (optional)
 * @returns {object} card widget
 */
function topicInput(topic) {
  return {
    textInput: {
      label: 'Topic',
      type: 'MULTIPLE_LINE',
      name: 'topic',
      value: topic || '',
    },
  };
}

/**
 * Build the buttons/actions for the form.
 *
 * @returns {object} card widget
 */
function buttons() {
  return {
    buttonList: {
      buttons: [
        {
          text: 'Submit',
          onClick: {
            action: {
              function: 'start_poll',
            },
          },
        },
      ],
    },
  };
}

/**
 * Build the configuration form.
 *
 * @param {object} options - Initial state to render with form
 * @param {string|undefined} options.topic - Topic of poll (optional)
 * @param {string[]|undefined} options.choices - Text of choices to display to users (optional)
 * @returns {object} card
 */
function buildConfigurationForm(options) {
  const widgets = [];
  widgets.push(helpText());
  widgets.push(topicInput(options.topic));
  for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
    const choice = options?.choices?.[i];
    widgets.push(optionInput(i, choice));
  }
  widgets.push(buttons());

  // Assemble the card
  return {
    sections: [
      {
        widgets,
      },
    ],
  };
}

exports.MAX_NUM_OF_OPTIONS = MAX_NUM_OF_OPTIONS;
exports.buildConfigurationForm = buildConfigurationForm;

このコードは、ユーザーがアンケートを設定するためのダイアログ フォームを生成します。また、質問の選択肢の最大数を示す定数もエクスポートされます。UI マークアップの構築をステートレス関数に分離し、任意の状態をパラメータとして渡すことをおすすめします。流用が容易になり、後でさまざまなコンテキストでこのカードにレンダリングされます。

この実装では、カードをより小さなユニットまたはコンポーネントに分解します。この手法は必須ではありませんが、複雑なインターフェースを構築する際に読みやすく保守しやすい傾向があるため、ベスト プラクティスとなっています。

構築済みの完全な JSON のサンプルは、Card Builder ツールでご確認ください。

スラッシュ コマンドの処理

スラッシュ コマンドは、アプリへの送信時に MESSAGE イベントとして表示されます。 index.js を更新して、MESSAGE イベントを介してスラッシュ コマンドの有無を確認し、ダイアログで応答するようにします。index.js を次のように置き換えます。

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function startPoll(event) {
  // Not fully implemented yet -- just close the dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  }
}

これで、/poll コマンドが呼び出されると、アプリにダイアログが表示されます。Cloud Shell で Cloud Functions の関数を赤色にして、インタラクションをテストします。

gcloud functions deploy app --trigger-http --security-level=secure-always

Cloud Functions の関数がデプロイされたら、/poll コマンドを使用してアプリにメッセージを送信し、スラッシュ コマンドとダイアログをテストします。ダイアログからカスタム アクション start_poll とともに CARD_CLICKED イベントが送信されます。このイベントは更新されたエントリ ポイントで処理され、startPoll メソッドを呼び出します。ここでは、ダイアログを閉じるために startPoll メソッドをスタブします。次のセクションでは、投票機能を実装し、すべての要素をつなぎ合わせます。

投票カードを実装する

アプリの投票機能を実装するには、まず、投票を行うためのインターフェースを提供するインタラクティブ カードを定義します。

投票インターフェースを実装する

次の内容で vote-card.js という名前のファイルを作成します。

/**
 * Creates a small progress bar to show percent of votes for an option. Since
 * width is limited, the percentage is scaled to 20 steps (5% increments).
 *
 * @param {number} voteCount - Number of votes for this option
 * @param {number} totalVotes - Total votes cast in the poll
 * @returns {string} Text snippet with bar and vote totals
 */
function progressBarText(voteCount, totalVotes) {
  if (voteCount === 0 || totalVotes === 0) {
    return '';
  }

  // For progress bar, calculate share of votes and scale it
  const percentage = (voteCount * 100) / totalVotes;
  const progress = Math.round((percentage / 100) * 20);
  return '▀'.repeat(progress);
}

/**
 * Builds a line in the card for a single choice, including
 * the current totals and voting action.
 *
 * @param {number} index - Index to identify the choice
 * @param {string|undefined} value - Text of the choice
 * @param {number} voteCount - Current number of votes cast for this item
 * @param {number} totalVotes - Total votes cast in poll
 * @param {string} state - Serialized state to send in events
 * @returns {object} card widget
 */
function choice(index, text, voteCount, totalVotes, state) {
  const progressBar = progressBarText(voteCount, totalVotes);
  return {
    keyValue: {
      bottomLabel: `${progressBar} ${voteCount}`,
      content: text,
      button: {
        textButton: {
          text: 'vote',
          onClick: {
            action: {
              actionMethodName: 'vote',
              parameters: [
                {
                  key: 'state',
                  value: state,
                },
                {
                  key: 'index',
                  value: index.toString(10),
                },
              ],
            },
          },
        },
      },
    },
  };
}

/**
 * Builds the card header including the question and author details.
 *
 * @param {string} topic - Topic of the poll
 * @param {string} author - Display name of user that created the poll
 * @returns {object} card widget
 */
function header(topic, author) {
  return {
    title: topic,
    subtitle: `Posted by ${author}`,
    imageUrl:
      'https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png',
    imageStyle: 'AVATAR',
  };
}

/**
 * Builds the configuration form.
 *
 * @param {object} poll - Current state of poll
 * @param {object} poll.author - User that submitted the poll
 * @param {string} poll.topic - Topic of poll
 * @param {string[]} poll.choices - Text of choices to display to users
 * @param {object} poll.votes - Map of cast votes keyed by user ids
 * @returns {object} card
 */
function buildVoteCard(poll) {
  const widgets = [];
  const state = JSON.stringify(poll);
  const totalVotes = Object.keys(poll.votes).length;

  for (let i = 0; i < poll.choices.length; ++i) {
    // Count votes for this choice
    const votes = Object.values(poll.votes).reduce((sum, vote) => {
      if (vote === i) {
        return sum + 1;
      }
      return sum;
    }, 0);
    widgets.push(choice(i, poll.choices[i], votes, totalVotes, state));
  }

  return {
    header: header(poll.topic, poll.author.displayName),
    sections: [
      {
        widgets,
      },
    ],
  };
}

exports.buildVoteCard = buildVoteCard;

この実装はダイアログのアプローチと似ていますが、インタラクティブ カードのマークアップはダイアログとは若干異なります。以前と同様に、生成されたカードのサンプルを Card Builder ツールで確認できます。

投票アクションを実装する

投票カードには、各選択肢のボタンが含まれています。選択したインデックスが、アンケートのシリアル化された状態とともにボタンに追加されます。アプリは、アクション vote を含む CARD_CLICKED を、パラメータとしてボタンに添付されたデータとともに受け取ります。

次のコマンドで index.js を更新します。

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');
const { buildVoteCard } = require('./vote-card');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    } else if (event.action?.actionMethodName === 'vote') {
        reply = recordVote(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function startPoll(event) {
  // Not fully implemented yet -- just close the dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  }
}

/**
 * Handle the custom vote action. Updates the state to record
 * the user's vote then rerenders the card.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function recordVote(event) {
  const parameters = event.common?.parameters;

  const choice = parseInt(parameters['index']);
  const userId = event.user.name;
  const state = JSON.parse(parameters['state']);

  // Add or update the user's selected option
  state.votes[userId] = choice;

  const card = buildVoteCard(state);
  return {
    thread: event.message.thread,
    actionResponse: {
      type: 'UPDATE_MESSAGE',
    },
    cards: [card],
  }
}

recordVote メソッドは、保存された状態を解析し、ユーザーの投票によってステータスを更新してから、カードを再レンダリングします。アンケート結果はカードが更新されるたびにシリアル化され、カードに保存されます。

ピースを接続する

アプリの準備はあと少しで完了します。投票と一緒にスラッシュ コマンドを実装すると、最後に startPoll メソッドを終了します。

しかし、落とし穴があります。

アンケートの設定を送信すると、アプリは次の 2 つのアクションを実行する必要があります。

  1. ダイアログを閉じます。
  2. 投票カードを使用してスペースに新しいメッセージを投稿する。

残念ながら、HTTP リクエストへの直接返信は 1 つのみであり、最初のリクエストである必要があります。投票カードを送信するには、Chat API を使用して新しいメッセージを非同期に作成する必要があります。

クライアント ライブラリの追加

次のコマンドを実行して、Node.js 用 Google API クライアントが含まれるようにアプリの依存関係を更新します。

npm install --save googleapis

アンケートを開始する

index.js を以下の最終バージョンに更新します。

const { buildConfigurationForm, MAX_NUM_OF_OPTIONS } = require('./config-form');
const { buildVoteCard } = require('./vote-card');
const {google} = require('googleapis');

/**
 * App entry point.
 */
exports.app = async (req, res) => {
  if (!(req.method === 'POST' && req.body)) {
      res.status(400).send('')
  }
  const event = req.body;
  let reply = {};
  // Dispatch slash and action events
  if (event.type === 'MESSAGE') {
    const message = event.message;
    if (message.slashCommand?.commandId === '1') {
      reply = showConfigurationForm(event);
    }
  } else if (event.type === 'CARD_CLICKED') {
    if (event.action?.actionMethodName === 'start_poll') {
      reply = await startPoll(event);
    } else if (event.action?.actionMethodName === 'vote') {
        reply = recordVote(event);
    }
  }
  res.json(reply);
}

/**
 * Handles the slash command to display the config form.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function showConfigurationForm(event) {
  // Seed the topic with any text after the slash command
  const topic = event.message?.argumentText?.trim();
  const dialog = buildConfigurationForm({
    topic,
    choices: [],
  });
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        dialog: {
          body: dialog,
        },
      },
    },
  };
}

/**
 * Handle the custom start_poll action.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
async function startPoll(event) {
  // Get the form values
  const formValues = event.common?.formInputs;
  const topic = formValues?.['topic']?.stringInputs.value[0]?.trim();
  const choices = [];
  for (let i = 0; i < MAX_NUM_OF_OPTIONS; ++i) {
    const choice = formValues?.[`option${i}`]?.stringInputs.value[0]?.trim();
    if (choice) {
      choices.push(choice);
    }
  }

  if (!topic || choices.length === 0) {
    // Incomplete form submitted, rerender
    const dialog = buildConfigurationForm({
      topic,
      choices,
    });
    return {
      actionResponse: {
        type: 'DIALOG',
        dialogAction: {
          dialog: {
            body: dialog,
          },
        },
      },
    };
  }

  // Valid configuration, build the voting card to display
  // in the space
  const pollCard = buildVoteCard({
    topic: topic,
    author: event.user,
    choices: choices,
    votes: {},
  });
  const message = {
    cards: [pollCard],
  };
  const request = {
    parent: event.space.name,
    requestBody: message,
  };
  // Use default credentials (service account)
  const credentials = new google.auth.GoogleAuth({
    scopes: ['https://www.googleapis.com/auth/chat.bot'],
  });
  const chatApi = google.chat({
    version: 'v1',
    auth: credentials,
  });
  await chatApi.spaces.messages.create(request);

  // Close dialog
  return {
    actionResponse: {
      type: 'DIALOG',
      dialogAction: {
        actionStatus: {
          statusCode: 'OK',
          userFacingMessage: 'Poll started.',
        },
      },
    },
  };
}

/**
 * Handle the custom vote action. Updates the state to record
 * the user's vote then rerenders the card.
 *
 * @param {object} event - chat event
 * @returns {object} Response to send back to Chat
 */
function recordVote(event) {
  const parameters = event.common?.parameters;

  const choice = parseInt(parameters['index']);
  const userId = event.user.name;
  const state = JSON.parse(parameters['state']);

  // Add or update the user's selected option
  state.votes[userId] = choice;

  const card = buildVoteCard(state);
  return {
    thread: event.message.thread,
    actionResponse: {
      type: 'UPDATE_MESSAGE',
    },
    cards: [card],
  }
}

関数を再デプロイします。

gcloud functions deploy app --trigger-http --security-level=secure-always

これでアプリを完全に操作できます。 /poll コマンドを呼び出して、いくつかの質問を指定します。送信すると、アンケート カードが表示されます。

投票して、結果を確認しましょう。

もちろん、アンケートはそれほど役立ちません。友だちや同僚を招待して、お試しください。

6. 完了

これで、Cloud Functions を使用して Google Chat アプリを作成し、デプロイしました。Codelab はアプリ作成の中心となるコンセプトの多くをカバーしてきましたが、他にも多くの発見があります。以下のリソースを確認し、追加料金が発生しないようにプロジェクトを必ずクリーンアップしてください。

その他のアクティビティ

Chat プラットフォームとこのアプリについて詳しく知りたい場合は、以下の方法をお試しください。

  • アプリに @ メンションするとどうなりますか?動作を改善するためにアプリを更新してみてください。
  • カード内のアンケートの状態のシリアル化は小規模なスペースでは問題ありませんが、制限があります。よりよいオプションに切り替えてみてください。
  • 作成者がアンケートの編集、または新しい投票の停止を希望する場合はどうすればよいですか?これらの機能をどのように実装しますか。
  • アプリ エンドポイントはまだ保護されていません。Google Chat からのリクエストであることを確認するために、確認機能を追加してみてください。

上記は、アプリを改善する方法のほんの一例です。想像力を発揮して楽しみましょう!

クリーンアップ

このチュートリアルで使用したリソースについて、Google Cloud Platform アカウントに課金されないようにする手順は次のとおりです。

  • Cloud Console で [リソースの管理] ページに移動します。左上の メニュー アイコン メニュー アイコン > [ IAM と管理] > [リソースの管理] の順にクリックします。
  1. プロジェクト リストでプロジェクトを選択し、[削除] をクリックします。
  2. ダイアログでプロジェクト ID を入力し、[シャットダウン] をクリックしてプロジェクトを削除します。

詳細

Chat アプリの開発について詳しくは、以下をご覧ください。

Google Cloud Console での開発の詳細については、以下をご覧ください。