1. Введение
Приложения Google Chat переносят ваши сервисы и ресурсы прямо в Google Chat, позволяя пользователям получать информацию и быстро выполнять действия, не выходя из беседы.
В этой лабораторной работе вы узнаете, как создать и развернуть приложение для опроса с использованием Node.js и Cloud Functions .
Чему вы научитесь
- Используйте Cloud Shell
- Развертывание в облачных функциях
- Получайте пользовательский ввод с помощью слэш-команд и диалогов
- Создавайте интерактивные карты
2. Настройка и требования
Создайте проект Google Cloud, затем включите API и службы, которые будет использовать приложение Chat.
Предпосылки
Для разработки приложения Google Chat требуется учётная запись Google Workspace с доступом к Google Chat. Если у вас ещё нет учётной записи Google Workspace, создайте её и войдите в систему, прежде чем приступать к выполнению этой практической работы.
Настройка среды для самостоятельного обучения
- Откройте Google Cloud Console и создайте проект .
Запомните идентификатор проекта — уникальное имя для всех проектов Google Cloud (имя, указанное выше, уже занято и не будет вам работать, извините!). Далее в этой практической работе он будет обозначаться какPROJECT_ID
.
- Далее, чтобы использовать ресурсы Google Cloud, включите биллинг в Cloud Console.
Выполнение этой лабораторной работы не должно обойтись дорого, если вообще обойтись. Обязательно следуйте инструкциям в разделе «Очистка» в конце лабораторной работы, где рассказывается, как отключить ресурсы, чтобы не платить за время, превышающее это руководство. Новые пользователи Google Cloud могут воспользоваться бесплатной пробной версией стоимостью 300 долларов США .
Google Cloud Shell
Хотя Google Cloud можно управлять удаленно с вашего ноутбука, в этой лабораторной работе мы будем использовать Google Cloud Shell — среду командной строки, работающую в Google Cloud.
Активировать Cloud Shell
- В консоли Cloud нажмите «Активировать Cloud Shell» .
.
При первом открытии Cloud Shell вы увидите подробное приветственное сообщение. Если вы его видите, нажмите «Продолжить» . Больше приветственное сообщение не появляется. Вот текст приветствия:
Подготовка и подключение к Cloud Shell займёт всего несколько минут. После подключения вы увидите терминал Cloud Shell:
Эта виртуальная машина оснащена всеми необходимыми инструментами разработки. Она предоставляет постоянный домашний каталог объёмом 5 ГБ и работает в Google Cloud, что значительно повышает производительность сети и аутентификацию. Всю работу в этой лабораторной работе можно выполнять в браузере или на Chromebook. После подключения к Cloud Shell вы увидите, что аутентификация выполнена, а проекту присвоен ваш идентификатор. - Выполните следующую команду в Cloud Shell, чтобы подтвердить, что вы прошли аутентификацию:
Если вам будет предложено авторизовать Cloud Shell для выполнения вызова API GCP, нажмите Авторизовать .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 Editor , нажав кнопку «Открыть редактор» в правой части панели инструментов Cloud Shell. В Cloud Shell также доступны популярные редакторы, такие как Vim и Emacs.
3. Включите API Cloud Functions, Cloud Build и Google Chat.
В Cloud Shell включите следующие API и сервисы:
gcloud services enable \ cloudfunctions \ cloudbuild.googleapis.com \ chat.googleapis.com
Выполнение этой операции может занять несколько минут.
После завершения появится сообщение об успешном завершении, похожее на это:
Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.
4. Создайте начальное приложение чата.
Инициализировать проект
Для начала вы создадите и развернёте простое приложение «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
, который мы создадим далее.
Создайте бэкэнд приложения чата
Пришло время начать создавать приложение. Создайте файл 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», вам нужно развернуть облачную функцию, настроить приложение чата в Google Cloud Console и отправить тестовое сообщение в приложение для проверки развертывания.
Развертывание облачной функции
Чтобы развернуть облачную функцию приложения «Hello world», введите следующую команду:
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'
Запишите URL-адрес развёрнутой функции в свойстве httpsTrigger.url
. Он понадобится вам на следующем шаге.
Настройте приложение
Чтобы настроить приложение, перейдите на страницу настройки чата в Cloud Console.
- В поле «Имя приложения » введите «PollCodelab».
- В поле URL аватара введите
https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png
. - В поле Описание введите «Приложение для опроса по кодлабу».
- В разделе «Функциональность» выберите «Принимать сообщения 1:1» и «Присоединяйтесь к пространствам и групповым беседам» .
- В разделе «Настройки подключения» выберите URL-адрес конечной точки HTTP и вставьте URL-адрес для облачной функции (свойство
httpsTrigger.url
из последнего раздела). - В разделе «Разрешения» выберите «Определенные люди и группы в вашем домене» и введите свой адрес электронной почты.
- Нажмите «Сохранить» .
Теперь приложение готово к отправке сообщений.
Протестируйте приложение
Прежде чем двигаться дальше, проверьте работу приложения, добавив его в чат Google.
- Перейдите в Google Чат .
- Рядом с Чатом нажмите + > Найти приложения .
- Введите «PollCodelab» в поиске.
- Нажмите «Чат» .
- Чтобы отправить сообщение приложению, введите «Привет» и нажмите 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 };
}
Помимо темы или вопроса и списка вариантов ответа, состояние включает идентификатор и имя автора, а также зарегистрированные голоса. Чтобы предотвратить многократное голосование, голоса хранятся в виде сопоставления идентификаторов пользователей с индексом выбранного ими варианта ответа.
Конечно, существует множество различных подходов, но этот вариант служит хорошей отправной точкой для проведения быстрых опросов в определенном пространстве.
Реализуйте команду конфигурации опроса
Чтобы пользователи могли запускать и настраивать опросы, создайте команду , открывающую диалоговое окно . Это многоэтапный процесс:
- Зарегистрируйте команду «слэш», которая запускает опрос.
- Создайте диалоговое окно для настройки опроса.
- Позвольте приложению распознать и обработать команду «слэш».
- Создавайте интерактивные карточки, облегчающие голосование в опросе.
- Реализуйте код, который позволит приложению проводить опросы.
- Повторно развернуть облачную функцию.
Зарегистрируйте команду «косая черта»
Чтобы зарегистрировать слэш-команду, вернитесь на страницу конфигурации чата в консоли ( API и службы > Панель управления > API чата Hangouts > Конфигурация ).
- В разделе «Команды косой черты» нажмите «Добавить новую команду косой черты» .
- В поле «Имя» введите «/poll».
- В поле «Идентификатор команды» введите «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;
Этот код генерирует диалоговую форму, позволяющую пользователю настроить опрос. Он также экспортирует константу для максимального количества вариантов ответа в вопросе. Рекомендуется изолировать разметку пользовательского интерфейса, разместив её в функциях без сохранения состояния, передавая любое состояние в качестве параметров. Это облегчает повторное использование, и впоследствии эта карточка будет отображаться в разных контекстах.
Эта реализация также разбивает карту на более мелкие блоки или компоненты. Хотя это не обязательно, этот метод рекомендуется, поскольку он, как правило, более удобен для чтения и поддержки при создании сложных интерфейсов.
Чтобы увидеть пример полного 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.
gcloud functions deploy app --trigger-http --security-level=secure-always
После развертывания облачной функции отправьте приложению команду /poll
, чтобы протестировать работу команды слэша и диалогового окна. Диалоговое окно отправляет событие CARD_CLICKED
с пользовательским действием start_poll
. Событие обрабатывается в обновлённой точке входа, где вызывается метод 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 Builder .
Реализовать действие голосования
Карточка для голосования включает кнопку для каждого варианта. К кнопке прикреплён индекс этого варианта и сериализованное состояние опроса. Приложение получает 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-запрос может быть только один, и он должен быть первым. Чтобы опубликовать карточку голосования, приложение должно использовать API чата для асинхронного создания нового сообщения.
Добавить клиентскую библиотеку
Выполните следующую команду, чтобы обновить зависимости приложения и включить клиент Google API для Node.js.
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. Поздравления
Поздравляем! Вы успешно создали и развернули приложение Google Chat с помощью Cloud Functions. Хотя в этой лабораторной работе были рассмотрены многие основные концепции создания приложения, многое ещё предстоит изучить. Ознакомьтесь с ресурсами ниже и не забудьте привести свой проект в порядок, чтобы избежать дополнительных расходов.
Дополнительные мероприятия
Если вы хотите более подробно изучить платформу чата и это приложение, вот несколько вещей, которые вы можете попробовать самостоятельно:
- Что происходит, когда вы упоминаете приложение через @? Попробуйте обновить приложение, чтобы улучшить поведение.
- Сериализация состояния опроса в карточке подходит для небольших пространств, но имеет ограничения. Попробуйте выбрать более подходящий вариант.
- Что делать, если автор захочет отредактировать опрос или прекратить приём новых голосов? Как реализовать эти функции?
- Конечная точка приложения пока не защищена. Попробуйте добавить проверку, чтобы убедиться, что запросы поступают из Google Chat.
Это лишь несколько способов улучшить приложение. Развлекайтесь и дайте волю фантазии!
Уборка
Чтобы избежать списания средств с вашего аккаунта Google Cloud Platform за ресурсы, используемые в этом руководстве:
- В консоли Cloud Console перейдите на страницу «Управление ресурсами» . В левом верхнем углу нажмите «Меню».
> IAM и администрирование > Управление ресурсами .
- В списке проектов выберите свой проект и нажмите «Удалить» .
- В диалоговом окне введите идентификатор проекта, а затем нажмите кнопку «Завершить» , чтобы удалить проект.
Узнать больше
Дополнительную информацию о разработке чат-приложений см. в разделах:
Дополнительную информацию о разработке в Google Cloud Console см. здесь:
1. Введение
Приложения Google Chat переносят ваши сервисы и ресурсы прямо в Google Chat, позволяя пользователям получать информацию и быстро выполнять действия, не выходя из беседы.
В этой лабораторной работе вы узнаете, как создать и развернуть приложение для опроса с использованием Node.js и Cloud Functions .
Чему вы научитесь
- Используйте Cloud Shell
- Развертывание в облачных функциях
- Получайте пользовательский ввод с помощью слэш-команд и диалогов
- Создавайте интерактивные карты
2. Настройка и требования
Создайте проект Google Cloud, затем включите API и службы, которые будет использовать приложение Chat.
Предпосылки
Для разработки приложения Google Chat требуется учётная запись Google Workspace с доступом к Google Chat. Если у вас ещё нет учётной записи Google Workspace, создайте её и войдите в систему, прежде чем приступать к выполнению этой практической работы.
Настройка среды для самостоятельного обучения
- Откройте Google Cloud Console и создайте проект .
Запомните идентификатор проекта — уникальное имя для всех проектов Google Cloud (имя, указанное выше, уже занято и не будет вам работать, извините!). Далее в этой практической работе он будет обозначаться какPROJECT_ID
.
- Далее, чтобы использовать ресурсы Google Cloud, включите биллинг в Cloud Console.
Выполнение этой лабораторной работы не должно обойтись дорого, если вообще обойтись. Обязательно следуйте инструкциям в разделе «Очистка» в конце лабораторной работы, где рассказывается, как отключить ресурсы, чтобы не платить за время, превышающее это руководство. Новые пользователи Google Cloud могут воспользоваться бесплатной пробной версией стоимостью 300 долларов США .
Google Cloud Shell
Хотя Google Cloud можно управлять удаленно с вашего ноутбука, в этой лабораторной работе мы будем использовать Google Cloud Shell — среду командной строки, работающую в Google Cloud.
Активировать Cloud Shell
- В консоли Cloud нажмите «Активировать Cloud Shell» .
.
При первом открытии Cloud Shell вы увидите подробное приветственное сообщение. Если вы его видите, нажмите «Продолжить» . Больше приветственное сообщение не появляется. Вот текст приветствия:
Подготовка и подключение к Cloud Shell займёт всего несколько минут. После подключения вы увидите терминал Cloud Shell:
Эта виртуальная машина оснащена всеми необходимыми инструментами разработки. Она предоставляет постоянный домашний каталог объёмом 5 ГБ и работает в Google Cloud, что значительно повышает производительность сети и аутентификацию. Всю работу в этой лабораторной работе можно выполнять в браузере или на Chromebook. После подключения к Cloud Shell вы увидите, что аутентификация выполнена, а проекту присвоен ваш идентификатор. - Выполните следующую команду в Cloud Shell, чтобы подтвердить, что вы прошли аутентификацию:
Если вам будет предложено авторизовать Cloud Shell для выполнения вызова API GCP, нажмите Авторизовать .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 Editor , нажав кнопку «Открыть редактор» в правой части панели инструментов Cloud Shell. В Cloud Shell также доступны популярные редакторы, такие как Vim и Emacs.
3. Включите API Cloud Functions, Cloud Build и Google Chat.
В Cloud Shell включите следующие API и сервисы:
gcloud services enable \ cloudfunctions \ cloudbuild.googleapis.com \ chat.googleapis.com
Выполнение этой операции может занять несколько минут.
После завершения появится сообщение об успешном завершении, похожее на это:
Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.
4. Создайте начальное приложение чата.
Инициализировать проект
Для начала вы создадите и развернёте простое приложение «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
, который мы создадим далее.
Создайте бэкэнд приложения чата
Пришло время начать создавать приложение. Создайте файл 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», вам нужно развернуть облачную функцию, настроить приложение чата в Google Cloud Console и отправить тестовое сообщение в приложение для проверки развертывания.
Развертывание облачной функции
Чтобы развернуть облачную функцию приложения «Hello world», введите следующую команду:
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'
Запишите URL-адрес развёрнутой функции в свойстве httpsTrigger.url
. Он понадобится вам на следующем шаге.
Настройте приложение
Чтобы настроить приложение, перейдите на страницу настройки чата в Cloud Console.
- В поле «Имя приложения» введите «PollCodelab».
- В поле URL аватара введите
https://raw.githubusercontent.com/google/material-design-icons/master/png/social/poll/materialicons/24dp/2x/baseline_poll_black_24dp.png
. - В поле Описание введите «Приложение для опроса по кодлабу».
- В разделе «Функциональность» выберите «Принимать сообщения 1:1» и «Присоединяйтесь к пространствам и групповым беседам» .
- В разделе «Настройки подключения» выберите URL-адрес конечной точки HTTP и вставьте URL-адрес для облачной функции (свойство
httpsTrigger.url
из последнего раздела). - В разделе «Разрешения» выберите «Определенные люди и группы в вашем домене» и введите свой адрес электронной почты.
- Нажмите «Сохранить» .
Теперь приложение готово к отправке сообщений.
Протестируйте приложение
Прежде чем двигаться дальше, проверьте работу приложения, добавив его в чат Google.
- Перейдите в Google Чат .
- Рядом с Чатом нажмите + > Найти приложения .
- Введите «PollCodelab» в поиске.
- Нажмите «Чат» .
- Чтобы отправить сообщение приложению, введите «Привет» и нажмите 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 };
}
Помимо темы или вопроса и списка вариантов ответа, состояние включает идентификатор и имя автора, а также зарегистрированные голоса. Чтобы предотвратить многократное голосование, голоса хранятся в виде сопоставления идентификаторов пользователей с индексом выбранного ими варианта ответа.
Конечно, существует множество различных подходов, но этот вариант служит хорошей отправной точкой для проведения быстрых опросов в определенном пространстве.
Реализуйте команду конфигурации опроса
Чтобы пользователи могли запускать и настраивать опросы, создайте команду , открывающую диалоговое окно . Это многоэтапный процесс:
- Зарегистрируйте команду «слэш», которая запускает опрос.
- Создайте диалоговое окно для настройки опроса.
- Позвольте приложению распознать и обработать команду «слэш».
- Создавайте интерактивные карточки, облегчающие голосование в опросе.
- Реализуйте код, который позволит приложению проводить опросы.
- Повторно развернуть облачную функцию.
Зарегистрируйте команду «косая черта»
Чтобы зарегистрировать слэш-команду, вернитесь на страницу конфигурации чата в консоли ( API и службы > Панель управления > API чата Hangouts > Конфигурация ).
- В разделе «Команды косой черты» нажмите «Добавить новую команду косой черты» .
- В поле «Имя» введите «/poll».
- В поле «Идентификатор команды» введите «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;
Этот код генерирует диалоговую форму, позволяющую пользователю настроить опрос. Он также экспортирует константу для максимального количества вариантов ответа в вопросе. Рекомендуется изолировать разметку пользовательского интерфейса, разместив её в функциях без сохранения состояния, передавая любое состояние в качестве параметров. Это облегчает повторное использование, и впоследствии эта карточка будет отображаться в разных контекстах.
Эта реализация также разбивает карту на более мелкие блоки или компоненты. Хотя это не обязательно, этот метод рекомендуется, поскольку он, как правило, более удобен для чтения и поддержки при создании сложных интерфейсов.
Чтобы увидеть пример полного 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.
gcloud functions deploy app --trigger-http --security-level=secure-always
После развертывания облачной функции отправьте приложению команду /poll
, чтобы протестировать работу команды слэша и диалогового окна. Диалоговое окно отправляет событие CARD_CLICKED
с пользовательским действием start_poll
. Событие обрабатывается в обновлённой точке входа, где вызывается метод 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 Builder .
Реализовать действие голосования
Карточка для голосования включает кнопку для каждого варианта. К кнопке прикреплён индекс этого варианта и сериализованное состояние опроса. Приложение получает 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-запрос может быть только один, и он должен быть первым. Чтобы опубликовать карточку голосования, приложение должно использовать API чата для асинхронного создания нового сообщения.
Добавить клиентскую библиотеку
Выполните следующую команду, чтобы обновить зависимости приложения и включить клиент Google API для Node.js.
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. Поздравления
Поздравляем! Вы успешно создали и развернули приложение Google Chat с помощью Cloud Functions. Хотя в этой лабораторной работе были рассмотрены многие основные концепции создания приложения, многое ещё предстоит изучить. Ознакомьтесь с ресурсами ниже и не забудьте привести свой проект в порядок, чтобы избежать дополнительных расходов.
Дополнительные мероприятия
Если вы хотите более подробно изучить платформу чата и это приложение, вот несколько вещей, которые вы можете попробовать самостоятельно:
- Что происходит, когда вы упоминаете приложение через @? Попробуйте обновить приложение, чтобы улучшить поведение.
- Сериализация состояния опроса в карточке подходит для небольших пространств, но имеет ограничения. Попробуйте выбрать более подходящий вариант.
- Что делать, если автор захочет отредактировать опрос или прекратить приём новых голосов? Как реализовать эти функции?
- Конечная точка приложения пока не защищена. Попробуйте добавить проверку, чтобы убедиться, что запросы поступают из Google Chat.
Это лишь несколько способов улучшить приложение. Развлекайтесь и дайте волю фантазии!
Уборка
Чтобы избежать списания средств с вашего аккаунта Google Cloud Platform за ресурсы, используемые в этом руководстве:
- В консоли Cloud Console перейдите на страницу «Управление ресурсами» . В левом верхнем углу нажмите «Меню».
> IAM и администрирование > Управление ресурсами .
- В списке проектов выберите свой проект и нажмите «Удалить» .
- В диалоговом окне введите идентификатор проекта, а затем нажмите кнопку «Завершить» , чтобы удалить проект.
Узнать больше
Дополнительную информацию о разработке чат-приложений см. в разделах:
Дополнительную информацию о разработке в Google Cloud Console см. здесь: