1. 簡介
Google Chat 擴充應用程式可將服務和資源直接帶入 Google Chat,讓使用者不必離開對話,就能取得資訊並快速採取行動。
在本程式碼研究室中,您將瞭解如何使用 Node.js 和 Cloud Functions 建構及部署投票應用程式。
課程內容
- 使用 Cloud Shell
- 部署至 Cloud Functions
- 使用斜線指令和對話方塊取得使用者輸入內容
- 建立互動式資訊卡
2. 設定和需求
建立 Google Cloud 專案,然後啟用 Chat 應用程式會使用的 API 和服務
必要條件
如要開發 Google Chat 應用程式,您必須擁有可存取 Google Chat 的 Google Workspace 帳戶。如果您還沒有 Google Workspace 帳戶,請先建立並登入,再繼續進行本程式碼研究室。
自行設定環境
- 開啟 Google Cloud 控制台並建立專案。
請記下專案 ID,這是所有 Google Cloud 專案的專屬名稱 (上方的名稱已遭占用,因此不適用於您,抱歉!)。本程式碼研究室稍後會將其稱為PROJECT_ID
。
- 接著,如要使用 Google Cloud 資源,請在 Cloud Console 中啟用計費功能。
完成本程式碼研究室的練習不會產生任何費用,或只會產生少量費用。請務必按照程式碼研究室最後「清除」一節中的操作說明,關閉資源,以免產生本教學課程以外的費用。Google Cloud 新使用者可參加價值$300 美元的免費試用計畫。
Google Cloud Shell
雖然您可以透過筆電遠端操作 Google Cloud,但在本程式碼研究室中,我們將使用 Google Cloud Shell,這是 Google Cloud 中執行的指令列環境。
啟用 Cloud Shell
- 在 Cloud Shell 中,按一下「啟用 Cloud Shell」圖示
。
首次開啟 Cloud Shell 時,系統會顯示說明性歡迎訊息。如果看到歡迎訊息,請按一下「繼續」。系統不會再顯示歡迎訊息。歡迎訊息如下:
佈建並連線至 Cloud Shell 的作業很快就能完成。連線後,您會看到 Cloud Shell 終端機:
這部虛擬機器搭載您所需的所有開發工具。提供永久的 5 GB 主目錄,而且在 Google Cloud 中運作,可大幅提升網路效能和驗證功能。您可以使用瀏覽器或 Chromebook 完成本程式碼研究室的所有作業。連線至 Cloud Shell 後,您應會發現自己通過驗證,且專案已設為您的專案 ID。 - 在 Cloud Shell 中執行下列指令,確認您已通過驗證:
如果系統提示您授權 Cloud Shell 發出 GCP API 呼叫,請按一下「Authorize」(授權)。gcloud auth list
指令輸出內容 如果系統未預設選取您的帳戶,請執行下列指令:Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com>
$ gcloud config set account <ACCOUNT>
- 確認您已選取正確的專案。在 Cloud Shell 執行下列指令:
指令輸出gcloud config list project
如未傳回正確的專案,請輸入下列指令設定專案:[core] project = <PROJECT_ID>
指令輸出gcloud config set project <PROJECT_ID>
Updated property [core/project].
完成本程式碼研究室時,您會使用指令列作業並編輯檔案。如要編輯檔案,可以點選 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」應用程式。即時通訊應用程式是網路服務,會回應 https 要求,並以 JSON 酬載回覆。這個應用程式會使用 Node.js 和 Cloud Functions。
在 Cloud Shell 中,建立名為 poll-app
的新目錄,然後前往該目錄:
mkdir ~/poll-app cd ~/poll-app
您在本程式碼研究室中完成的所有其餘工作,以及建立的檔案,都會位於這個目錄中。
初始化 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 函式、在 Google Cloud 控制台中設定 Chat 應用程式,然後傳送測試訊息給應用程式,確認部署作業。
部署 Cloud 函式
如要部署「Hello world」應用程式的 Cloud 函式,請輸入下列指令:
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
屬性中已部署函式的網址。您將在下一個步驟中使用此項目。
設定應用程式
如要設定應用程式,請前往 Cloud 控制台的「Chat configuration」(即時通訊設定) 頁面
- 取消勾選「將這個 Chat 擴充應用程式建構為 Workspace 外掛程式」,然後按一下「停用」確認。
- 在「應用程式名稱」中輸入「PollCodelab」。
- 在「Avatar URL」中輸入
https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png
。 - 在「說明」中,輸入「Poll app for codelab」。
- 在「功能」下方,選取「接收一對一訊息」和「加入聊天室和群組對話」。
- 在「連線設定」下方,選取「HTTP 端點網址」,然後貼上 Cloud Function 的網址 (上一節中的
httpsTrigger.url
屬性)。 - 在「權限」下方,選取「僅限您網域中的特定使用者和群組」,然後輸入您的電子郵件地址。
- 按一下「儲存」。
應用程式現在可以傳送訊息。
測試應用程式
繼續操作前,請先將應用程式新增至 Google Chat 中的群組,確認應用程式是否正常運作。
- 前往 Google Chat。
- 依序按一下「即時通訊」旁邊的「+」>「尋找應用程式」。
- 在搜尋框中輸入「PollCodelab」。
- 按一下「Chat」。
- 如要傳送訊息給應用程式,請輸入「Hello」並按下 Enter 鍵。
應用程式應回覆簡短的問候訊息。
現在基本架構已就位,接下來要將其變成更有用的東西!
5. 建構意見調查功能
應用程式運作方式的快速總覽
這個應用程式主要包含兩個部分:
- 顯示對話方塊的斜線指令,用於設定投票。
- 可投票及查看結果的互動式資訊卡。
應用程式也需要一些狀態來儲存投票設定和結果。這項作業可透過 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 對應所選選項索引的對應表。
當然,方法有很多種,但這是不錯的起點,可讓您在聊天室中快速進行民調。
實作輪詢設定指令
如要讓使用者發起及設定投票活動,請設定可開啟對話方塊的斜線指令。這個過程分為多個步驟:
- 註冊啟動投票的斜線指令。
- 建立設定意見調查的對話方塊。
- 讓應用程式辨識及處理斜線指令。
- 建立互動式資訊卡,方便使用者在意見調查中投票。
- 實作可讓應用程式執行投票的程式碼。
- 重新部署 Cloud 函式。
註冊斜線指令
如要註冊斜線指令,請返回控制台的「Chat configuration」(Chat 設定) 頁面 (依序點選「APIs & Services」(API 和服務) >「Dashboard」(資訊主頁) >「Hangouts Chat API」 >「Configuration」(設定))。
- 在「斜線指令」下方,按一下「新增斜線指令」。
- 在「名稱」中輸入「/poll」
- 在「Command id」(指令 ID) 中輸入「1」
- 在「說明」中,輸入「發起投票」。
- 選取「開啟對話方塊」。
- 按一下 [完成]。
- 按一下 [儲存]。
應用程式現在會辨識 /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 Function,測試互動。
gcloud functions deploy app --trigger-http --security-level=secure-always
部署 Cloud 函式後,請使用 /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;
實作方式與對話方塊採用的方法類似,但互動式資訊卡的標記與對話方塊稍有不同。與先前相同,您可以在 資訊卡建構工具中查看產生的 JSON 範例。
實作投票動作
投票資訊卡會顯示每個選項的按鈕。該選項的索引和輪詢的序列化狀態會附加至按鈕。應用程式會收到 CARD_CLICKED
,其中包含動作 vote
,以及附加至按鈕的任何資料 (做為參數)。
使用下列項目更新 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
方法。
不過,請特別留意相關限制。
提交輪詢設定時,應用程式需要執行兩項動作:
- 關閉對話方塊。
- 在聊天室中張貼含有投票資訊卡的新訊息。
很抱歉,直接回覆 HTTP 要求時只能執行一項動作,且必須是第一個動作。如要發布投票資訊卡,應用程式必須使用 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 應用程式。雖然本程式碼研究室涵蓋了許多建構應用程式的核心概念,但還有許多內容值得探索。請參閱下列資源,並記得清除專案,以免產生額外費用。
其他活動
如要進一步瞭解 Chat 平台和這個應用程式,可以嘗試下列做法:
- 如果 @ 提及應用程式,會發生什麼事?請嘗試更新應用程式,改善這類行為。
- 在資訊卡中序列化投票狀態適用於小型空間,但有其限制。請改用其他選項。
- 如果作者想編輯投票或停止接受新票數,該怎麼做?你會如何實作這些功能?
- 應用程式端點尚未受到保護。請嘗試新增一些驗證,確保要求來自 Google Chat。
以上只是幾種改善應用程式的方法,歡迎盡情發揮想像力!
清除所用資源
如要避免系統向您的 Google Cloud Platform 帳戶收取您在本教學課程中所用資源的相關費用:
- 在專案清單中選取專案,然後按一下「刪除」。
- 在對話方塊中輸入專案 ID,然後按一下「Shut down」(關閉) 刪除專案。
瞭解詳情
如要進一步瞭解如何開發 Chat 應用程式,請參閱:
如要進一步瞭解如何在 Google Cloud 控制台中開發,請參閱: