Від простого до складного, перед вами друга частина з серії статей про розробку ботів до Google чату.
Сьогодні я буду вас вчити як вести діалог з ботом.
Тож, в цій частині, я розповім вам, як створити діалог, та що треба робити, щоб можна було відправити до спейсу картку з картинками та текстом, наприклад, якщо вам треба відправити поздоровлення до дня народження. Буде більше коду, ніж в попередній частині та я буду намагатися пояснювати та додавати посилання на офіційну документацію.
Діалоги
Почнемо знов з додавання slash-команди до нашого боту, нагадую, що для цього нам треба перейти до налаштувань Google Chat API.
На відміну від попередніх команд які ми додавали тут вам треба поставити позначку ✅ Opens a dialog
щоб далі працювати вже з діалогом.
Зміни до Code.gs
теж не складні, нам треба лише піймати нову команду з ID 20:
/** * Responds to a MESSAGE event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onMessage(event) { if (event.message.slashCommand) { // Checks for the presence of event.message.slashCommand // The ID for your slash command switch (event.message.slashCommand.commandId) { case 1: return slashHelp(event) case 10: return slashBender(event) case 11: return slashWhisky(event) case 20: return slashCard(event) } } else { // If the Chat app doesn't detect a slash command // ... } }
Створими файл slashCard.gs
з відповідною функцією:
/** * Opens a dialog in Google Chat. * * @param {Object} event the event object from Chat API. * * @return {object} open a Dialog in Google Chat. */ function slashCard (event) { return { 'action_response': { 'type': 'DIALOG', 'dialog_action': { 'dialog': { 'body': { 'sections': [ { 'header': 'Nice Card Builder', 'collapsible': true, 'uncollapsibleWidgetsCount': 2, 'widgets': [ { 'textInput': { 'name': 'description', 'type': 'MULTIPLE_LINE', 'label': '📝 Description' } }, { 'textInput': { 'name': 'image_1', 'label': '1️⃣ Image URL', 'placeholderText': 'https://source.unsplash.com/featured/320x320' } }, { 'textInput': { 'name': 'image_2', 'label': '2️⃣' } }, { 'textInput': { 'name': 'image_3', 'label': '3️⃣' } } ] } ], 'fixedFooter': { 'primaryButton': { 'icon': { 'materialIcon': { 'name': 'send' } }, 'text': 'Send', 'color': { 'red': 0, 'green': 0.5, 'blue': 1, 'alpha': 1 }, 'onClick': { 'action': { 'function': 'receiveCard' } } } } } } } } } }
Тут окремо слід зазначити 'type': 'DIALOG'
, це обов’язкове, а ось все що в нас у dialog_action
– то ця конструкція нам вже знайома, цей JSON можна створити за допомоги Card Builder. Можете спробувати погратися та створити свій власний діалог.
Спробуємо команду /card
, отримаємо наступний діалог:
Якщо спробуємо її відправити, то нічого не відбудеться! Що в цьому випадку робити? Дебажити?
Давайте спочатку подивимось чи не з’явилось в нас помилок у Apps Script, для цього перейдіть до розділу Executions
, там вас буде чекати інформація про хід виконання скриптів:
Так, і чому воно в мене питає про функцію, про яку я нічого не знаю, та ніде не використовую, що це за onCardClick
?
Це така службова функція яка потрібна нам щоб обробляти якраз події типу CARD_CLICKED, тож давайте створимо відповідну функцію обробник у файлі Code.gs
:
/** * Responds to a CARD_CLICKED event in Google Chat. * * @param {Object} event the event object from Google Chat */ function onCardClick (event) { switch (event.common.invokedFunction) { case 'receiveCard': return receiveCard(event) } }
Як бачите, я тут чекаю на ім’я функції яку треба визвати, та цю назву функції receiveCard
я прописав у onClick
екшені попередньої форми вище, у строчці 64, тож коли відкриється діалог, та ми клікнемо по кнопці Send то ми визвемо функцію receiveCard(event)
яку нам ще треба написати. Давайте поступово, почнемо з пустої функції:
/** * @param {Object} event the event object from Google Chat */ function receiveCard (event) { console.log(event) }
Коли ми використовуємо console.log
, то результат ми зможемо знайти на сторінці Executions
, давайте подивимось що зараз ми отримаємо коли спробуємо знову відправити форму (якщо у вас нічого не з’явилось відразу, попробуйте потицкати кнопку Refresh, поки не отримаєте результат):
{ type: 'CARD_CLICKED', common: { formInputs: { image_3: [Object], image_2: [Object], image_5: [Object], image_1: [Object], image_4: [Object], description: [Object] }, timeZone: { id: 'Europe/Kyiv', offset: 3600000 }, userLocale: 'en-US', hostApp: 'CHAT', invokedFunction: 'receiveCard' }, isDialogEvent: true, message: { annotations: [ [Object] ], createTime: { seconds: 1707324122, nanos: 333183000 }, retentionSettings: { state: 'PERMANENT' }, thread: { retentionSettings: [Object], name: 'spaces/.../threads/...' }, name: 'spaces/.../messages/.......', messageHistoryState: 'HISTORY_ON', text: '/card', space: { singleUserBotDm: true, spaceThreadingState: 'UNTHREADED_MESSAGES', name: 'spaces/...', spaceType: 'DIRECT_MESSAGE', type: 'DM', spaceHistoryState: 'HISTORY_ON' }, formattedText: '/card', sender: { displayName: 'Anton Shevchuk', avatarUrl: '...', type: 'HUMAN', domainId: '...', email: '...', name: 'users/...' }, slashCommand: { commandId: 20 } }, eventTime: { nanos: 716454000, seconds: 1707324126 }, configCompleteRedirectUrl: '...', space: { type: 'DM', spaceThreadingState: 'UNTHREADED_MESSAGES', spaceType: 'DIRECT_MESSAGE', name: 'spaces/...', singleUserBotDm: true, spaceHistoryState: 'HISTORY_ON' }, dialogEventType: 'SUBMIT_DIALOG', user: { email: '...', domainId: '...', type: 'HUMAN', name: 'users/...', displayName: 'Anton Shevchuk', avatarUrl: '...' }, action: { actionMethodName: 'receiveCard' } }
Ми отримуємо досить багато інформації, але нас цікавить саме інформація з полів, які ми дали заповнити користувачу, а за це відповідає event.common.formInputs
, загляніть самостійно як виглядає формат цих даних, як на мене він трохи дивний, але най буде.
Щоб зручніше працювати з даними з форми я створив ось такий помічник, цу лістинг файлу FormInputHandler.gs
:
class FormInputHandler { constructor(event) { // Check if formInputs exist in the event structure if (event.common && event.common.formInputs) { this.formInputs = event.common.formInputs; this.isValid = true; // Mark as valid if formInputs are found } else { // Handle the invalid case this.isValid = false; console.log('Invalid event: formInputs not found.'); // Depending on your application, you might throw an error or handle this case differently } } // Method to check if the handler instance is valid isValidHandler() { return this.isValid; } // Retrieve a text input value by field name, adjusted for the specific structure getTextValue(fieldName) { if (this.isValid && this.formInputs[fieldName] && this.formInputs[fieldName][''] && this.formInputs[fieldName][''].stringInputs && this.formInputs[fieldName][''].stringInputs.value) { return this.formInputs[fieldName][''].stringInputs.value[0]; } return null; } // Additional methods to handle other types of inputs can be added here // ... }
Ось приклад як ним можна користуватися:
/** * @param {Object} event the event object from Google Chat */ function receiveCard (event) { const formHandler = new FormInputHandler(event) if (!formHandler.isValidHandler()) { console.log('Invalid event: formInputs not found.') return } console.log( formHandler.getTextValue('description') ) }
Давайте спробуємо обробити запит з картки та створити щось цікавіше, ніж функціонал slashBender()
(це те що ми реалізовували у попередньому уроці).
Валідація
Перевіремо, що ми отримали текст з поля Description, та якщо ні, то повернемо текст помилки:
let description = formHandler.getTextValue('description') if (!description || description.trim() === '') { return { 'actionResponse': { 'type': 'DIALOG', 'dialogAction': { 'actionStatus': { 'statusCode': 'INVALID_ARGUMENT', 'userFacingMessage': 'You should write the description' } } } } }
На мій погляд виглядає досить крінжово, тому я знов таки вирішив трохи погратися в передчасну оптимізацію кода, перед вами файл Response.gs
:
/** * The canonical error codes for gRPC APIs. * * @link https://developers.google.com/chat/api/reference/rest/v1/spaces.messages#code */ // Not an error; returned on success. // HTTP Mapping: 200 OK const CODE_OK = 'OK' // The client specified an invalid argument. // INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the system (e.g., a malformed file name). // HTTP Mapping: 400 Bad Request const CODE_INVALID_ARGUMENT = 'INVALID_ARGUMENT' /** * The type of Chat app response * * @link https://developers.google.com/chat/api/reference/rest/v1/spaces.messages#responsetype */ // Post as a new message in the topic const RESPONSE_TYPE_NEW_MESSAGE = 'NEW_MESSAGE' // Update the message const RESPONSE_TYPE_UPDATE_MESSAGE = 'UPDATE_MESSAGE' // Update the cards on message of user const RESPONSE_TYPE_UPDATE_USER_MESSAGE_CARDS = 'UPDATE_USER_MESSAGE_CARDS' // Privately ask the user for additional authentication or configuration. const RESPONSE_TYPE_REQUEST_CONFIG = 'REQUEST_CONFIG' // Presents a dialog const RESPONSE_TYPE_DIALOG = 'DIALOG' // Widget text autocomplete options query const RESPONSE_TYPE_UPDATE_WIDGET = 'UPDATE_WIDGET' function actionResponse(type = RESPONSE_TYPE_DIALOG, statusCode = CODE_OK, message = '') { return { 'actionResponse': { 'type': type, 'dialogAction': { 'actionStatus': { 'statusCode': statusCode, 'userFacingMessage': message } } } } } /** * Card Helper * - When all OK, status code */ function OK () { return actionResponse(RESPONSE_TYPE_DIALOG, CODE_OK, '👌') } /** * Card Helper * - When argument is invalid */ function INVALID_ARGUMENT (message) { return actionResponse(RESPONSE_TYPE_DIALOG, CODE_INVALID_ARGUMENT, message) }
Насправді ви ж розумієте, що це вже версія бота 2.0, тож всі граблі я вже зібрав у попередній версії 😅
Тож попередній приклад трохи змінемо, ось так він буде виглядати:
/** * @param {Object} event the event object from Google Chat */ function receiveCard (event) { const formHandler = new FormInputHandler(event) if (!formHandler.isValidHandler()) { console.log('Invalid event: formInputs not found.') return } let description = formHandler.getTextValue('description') if (!description || description.trim() === '') { return INVALID_ARGUMENT('You should write the description') } // ... }
Якщо на цьому етапі спробувати відправити пусту форму, то отримаємо ось таке повідомлення:
Якщо дані заповнити, то отримаємо ось таке повідомлення, бо ще немає валідної відповіді від нашої функції:
Над цим і будемо працювати далі.
Ви нам форму, ми вам картку
Тож, якщо повернутися до нашого діалогу, то ви там побачите 4 поля, перше – це текстове поле на кілька рядків, та 3 поля в які, за моїм задумом, треба вставляти посилання на світлини. Давайте всю цю інформацію обробимо та сформуємо картку у відповідь:
/** * @param {Object} event the event object from Google Chat */ function receiveCard(event) { const formHandler = new FormInputHandler(event) if (!formHandler.isValidHandler()) { console.log('Invalid event: formInputs not found.') return } let description = formHandler.getTextValue('description') if (!description || description.trim() === '') { return INVALID_ARGUMENT('You should write the description') } let images = [ formHandler.getTextValue('image_1'), formHandler.getTextValue('image_2'), formHandler.getTextValue('image_3') ] images = images.filter(n => n) let widgets = [] while (images.length) { widgets.push({ 'image': { 'imageUrl': images.shift(), 'altText': '' } }) } widgets.push( { 'textParagraph': { 'text': description } } ) return { 'actionResponse': { 'type': RESPONSE_TYPE_NEW_MESSAGE, }, 'cardsV2': [ { 'cardId': 'niceCard', 'card': { 'sections': [ { 'collapsible': false, 'widgets': widgets } ] } } ] } }
У цьому вигляді ваш бот зможе вже створювати картку з однією світлиною:
Мені тут не дуже подобається оце створення віджетів у такий спосіб, мені краще програмний спосіб, але на жаль я не знайшов такої можливості тому створив файл Widgets.gs
та почав наповнювати його функціоналом:
/** * @link https://developers.google.com/chat/api/reference/rest/v1/cards#TextParagraph_1 */ function widgetTextParagraph(text) { return { 'textParagraph': { 'text': text } } } /** * @link https://developers.google.com/chat/api/reference/rest/v1/cards#image */ function widgetImage(url, alt = '') { return { 'image': { 'imageUrl': url, 'altText': alt } } }
Тепер виглядає краще:
let widgets = [] while (images.length) { widgets.push( widgetImage(images.shift()) ) } widgets.push( widgetTextParagraph(description) )
А потім я ще трохи розширив функціонал, додав підтримку до 5-ти картинок, та формую grid, щоб красиво це виводити. Щоб у коді не було безладу, то я додав ще один клас у файлі
Grid.gs
, але то буде вже домашнє завдання подивитися його код та зрозуміти що там відбувається.
Source Code
Код бота доступний на GitHub, реліз 2.0.0 відповідає коду з цієї статті.
One thought on “Google Chat Bot. Створюємо діалог”