Google Chat Bot. Взаємодія та оновлення карток

Настав час продовжити розробку бота для Google Chat. Цього разу, ми навчимо його створювати голосування, це той функціонал якого дуже не вистачає при постійній комунікації у робочому спейсі. Для реалізації нам треба буде розібратися як взаємодіяти та оновлювати картку у чаті.

Діалог

Почнемо знов з додавання slash-команди до нашого боту, нагадую, що для цього нам треба перейти до налаштувань Google Chat API.

Ми вже створювали діалоги, тож не забудьте поставити позначку ✅ Opens a dialog.

Зміни до Code.gs теж не складні, нам треба лише піймати нову команду з ID 22:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
 * 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)
      case 21:
        return slashNotes(event)
      case 22:
        return slashPoll(event)
    }
  } else {
    // If the Chat app doesn't detect a slash command
    // ...
  }
}

Створимо функцію slashPoll() у відповідному файлі:

slashPoll.gs
01
02
03
04
05
06
07
08
09
10
11
/**
 * Opens a dialog in Google Chat.
 *
 * @param {Object} event the event object from Chat API.
 *
 * @return {object} open a Dialog in Google Chat.
 */
function slashPoll(event) {
  // nothing for now
  return dialogPoll(event)
}

Тут в нас буде лише виклик dialogPoll(), так зроблено для подальшого розширення функціоналу, наразі у dialogPoll.gs буде лише створення діалогу:

Нагадую, що для створення діалогів та карток зручно використовувати сервіс Card Builder

Після цього ви вже можете користуватися командою /poll, та навіть зможете отримати наступний діалог:

Тепер слід додати обробку onclick-функції з попереднього лістингу кода, для цього внесіть зміни до onCardClick(event):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
/**
 * 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) {
  
    //
    // ... the code was cropped
    //
 
    // - /poll
    case 'actionNewPoll':
      return actionNewPoll(event)
  }
}

Знов створимо нову функцію actionNewPoll() у новому файлі actionNewPoll.gs:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
/**
 * @param {Object} event the event object from Google Chat
 */
function actionNewPoll(event) {
 
  const formHandler = new FormInputHandler(event)
 
  formHandler.getTextValue('question')
  formHandler.getBooleanValue('multi')
  formHandler.getTextValue('option1')
  // ...
  formHandler.getTextValue('option10')
 
  //
  // ... the code was cropped
  //
 
}

Тут трохи треба призупинитися, та нагадати, що клас FormInputHandler ми створювали раніше, та він відповідає за отримання даних з форми, а зараз ми лише додали новий метод getBooleanValue(), та я гадаю з ним не виникне непорозумінь.

Але я не просто хочу отримати дані, я хочу зробити валідацію, щоб можна було внести зміни у форму у випадку, якщо виникла помилка.
Для цього я додам декілька перевірок, і у випадку невдачі буду повертати попередню форму з текстом помилки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * @param {Object} event the event object from Google Chat
 */
function actionNewPoll(event) {
 
  const formHandler = new FormInputHandler(event)
 
  let data = {
    question: formHandler.getTextValue('question'),
    multi: formHandler.getBooleanValue('multi'),
    options: []
  }
 
  for (let i = 1; i <= 10; i++) {
    let option = formHandler.getTextValue(`option${i}`)
    if (option.length) {
      data.options.push(option)
    }
  }
 
  if (!data.question.length) {
    return dialogPoll(event, data)
  }
 
  if (data.options.length < 2) {
    return dialogPoll(event, data)
  }
 
  //
  // ... the code was cropped
  //
 
}

У цьомі коді я формую об’єкт data, щоб було зручніше працювати з даними з форми, та роблю усього дві перевірки — що в нас є питання і що варіантів відповідей не менше двох. Коли виникає помилка я повертаю знов діалог, який ми додали до функції dialogPoll(). Нам лише треба внести зміни, щоб на формі зберігались попередньо внесені дані та відображався текст помилки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/**
 * Opens a dialog in Google Chat.
 *
 * @param {Object} event the event object from Chat API.
 * @param {Object} data the data from a form.
 *
 * @return {object} open a Dialog in Google Chat.
 */
function dialogPoll(event, data = null) {
 
  let card = {
    'action_response': {
      'type': 'DIALOG',
      'dialog_action': {
        'dialog': {
          'body': {
            'sections': [
              {
                'header': 'Create New Poll',
                'collapsible': true,
                'uncollapsibleWidgetsCount': 4,
                'widgets': [
                  {
                    "textParagraph": {
                      "text": "Enter the poll topic and up to 10 choices in the poll. Blank options will be omitted."
                    }
                  },
                  {
                    'textInput': {
                      'name': 'question',
                      'label': 'Ask a question*',
                      'value': data && data.question ? data.question : ''
                    }
                  },
                  {
                    'textInput': {
                      'name': 'option1',
                      'label': '1️⃣ Option*',
                      'value': data && data.options && data.options[0] ? data.options[0] : ''
                    }
                  }
                  /* Options 2, 3, 4, 5 .. 10 */
                ]
              },
              {
                "header": "Options",
                "collapsible": false,
                "widgets": [
                  {
                    "decoratedText": {
                      "text": "Multiple Answers",
                      "bottomLabel": "If this checked the voters can choose more than option",
                      "switchControl": {
                        "name": "multi",
                        "selected": (data && data.multi) ? data.multi : true,
                        "controlType": "SWITCH"
                      }
                    }
                  }
                ]
              }
            ],
            'fixedFooter': { /* ... */ }
          }
        }
      }
    }
  }
 
  if (data) {
    let section = {
      'widgets': [
        {
          "textParagraph": {
            "text": "<b><font color='#ff0000'>Please fill in all required data, including the question and two or more options.</font></b>"
          }
        }
      ]
    }
 
    card.action_response.dialog_action.dialog.body.sections.unshift(section)
  }
 
  return card;
}

У такий спосіб ми повернемо користувачу форму редагування голосування, та не загубимо його дані (зверніть увагу на рядки 9,32,39 та 55). Для більшої інформативності додаємо текст помилки, щоб користувач не розгубився, що наразі відбувається (рядок 81).

Картка для голосування

Перевірки всі зроблені, настав час створити картку для голосування та відправити її до чату. Відразу продемонструю дизайн картки, і потім приведу відповідний код до неї:

Повернемось на попередній крок, та додамо до функції actionNewPoll() виклик іншої функції — cardPoll():

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
/**
 * @param {Object} event the event object from Google Chat
 */
function actionNewPoll(event) {
  const formHandler = new FormInputHandler(event)
 
  let data = {
    question: formHandler.getTextValue('question'),
    multi: formHandler.getBooleanValue('multi'),
    options: []
  }
 
  //
  // ... the code was cropped
  //
 
  return cardPoll(event, data)
}

Звісно реалізація cardPoll() буде у відповідному файлі:

Тут з основного — додано виклик функції actionVotePoll() у action кнопки, і про неї розповім далі.

Оновлення картки

Для початку звісно знов повернемося до onCardClick(), та додамо підтримку та виклик функції actionVotePoll():

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
/**
 * 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) {
  
    //
    // ... the code was cropped
    //
 
    // - /poll
    case 'actionNewPoll':
      return actionNewPoll(event)
    case 'actionVotePoll':
      return actionVotePoll(event)
  }
}

Далі будемо створювати вже саму функцію у файлі actionVotePoll.gs:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
/**
 * @param {Object} event the event object from Google Chat
 */
function actionVotePoll(event) {
 
  let parameters = event.common.parameters
 
  let card = event.message.cardsV2[0]
 
  //
  // ... the code was cropped
  //
 
 
}

Давайте поступово — окрім параметрів функції (рядок 6) нас загалом цікавить уся картка яка в нас є у чаті (рядок 8).

А тепер ключовий момент — ми можемо оновити картку у чаті після того як користувач буде взаємодіяти з нею:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
/**
 * @param {Object} event the event object from Google Chat
 */
function actionVotePoll(event) {
 
  let parameters = event.common.parameters
 
  let card = event.message.cardsV2[0]
 
  //
  // ... the code was cropped
  //
 
  return {
    "actionResponse": {
      "type": "UPDATE_MESSAGE",
    },
    "cardsV2": [ card ]
  }
}

Використовуючи цю можливість можна зберігати голоси користувачів як частину картки:

Cards are usually displayed below the text body of a Chat message, but can situationally appear other places, such as dialogs. Each card can have a maximum size of 32 KB.

Тож залишилось лише реалізувати цю логіку:

У цьому коді для зберігання голосів використовуються віджети, тож кожна опція — це окрема секція, кожен голос — окремий віджет. Коли користувач голосує, то ми отримуємо всю картку, розібравши її на частинки можна відновити результати голосування та внести зміни.

Діаграма послідовності

Хотів привести діаграму послідовності, можливо вона допоможе вам краще розібратися у функціоналі:

Source Code

Код бота доступний на GitHub, реліз 5.1.0 відповідає коду з цієї статті.

Bender, реліз 5.1.0
Bender, білд 5.1.0

P.S.

З початку був реліз 5.0.0, але я потім подивився на той код, та вирішив навести в ньому лад, саме тому краще дивитися версію 5.1.0 :)

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.