Google Chat Bot. Properties

Перед вами новий пост і знов піде мова про розробку ботів для Google Chat’ів. Сьогодні ми навчимося додавати статистику використання до нашого бота.

Відразу невеликий спойлер — ми будемо вести статистику в боті, не виходячи за межі нашого коду та не використовуючи інші сервіси. Ми познайомимось з таким функціоналом як Properties.

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

Тож ось вам перелік даних які будемо збирати, не питайте чому саме ці дані, бо у самурая немає цілі, є лише шлях:

  • основні події: повідомлення, кліки, додавання та вилучення зі спейсу
  • виклик slash-команд – які команди використовували та скільки разів
  • виклик функцій, які прив’язані до кнопок на картках

Збір та організація статистики

Перше питання, звідкіля брати потрібну інформацію? Вся інформація про події в нас доступна у об’єкті event який нам приходить до 4-х ключових функцій які в нас лежать у файлі Code.gs:

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
/**
 * Responds to a MESSAGE event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onMessage(event) {
  // ...
}
 
/**
 * Responds to a CARD_CLICKED event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onCardClick(event) {
  // ...
}
 
/**
 * Responds to an ADDED_TO_SPACE event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onAddToSpace(event) {
  // ...
}
 
/**
 * Responds to a REMOVED_FROM_SPACE event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onRemoveFromSpace(event) {
  // ...
}

Щоб зібрати дані для статистики нам треба лише вбудувати виклик функції, яка і буде збирати дані:

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
/**
 * Responds to a MESSAGE event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onMessage(event) {
  collectStatisticData(event)
  // ...
}
 
/**
 * Responds to a CARD_CLICKED event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onCardClick(event) {
  collectStatisticData(event)
  // ...
}
 
/**
 * Responds to an ADDED_TO_SPACE event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onAddToSpace(event) {
  collectStatisticData(event)
  // ...
}
 
/**
 * Responds to a REMOVED_FROM_SPACE event in Google Chat.
 *
 * @param {Object} event the event object from Google Chat
 */
function onRemoveFromSpace(event) {
  collectStatisticData(event)
  // ...
}

А ось саму функцію треба ще написати, вона повинна аналізувати наступні дані:

01
02
03
04
05
06
07
08
09
10
11
12
13
/**
 * Collect event data and count statistics for functions and commands
 *
 * @param {Object} event the event object from Google Chat
 */
function collectStatisticData(event) {
  // MESSAGE or CARD_CLICKED or ADDED_TO_SPACE or REMOVED_FROM_SPACE
  event.type
  // if user used a slash command
  event.message.slashCommand.commandId
  // if user clicked something on the card and call function
  event.common.invokedFunction
}

Тож наче все зрозуміло, тепер нам би розібратися де зберігати цю інформацію, і нам в цьому допоможуть Properties

Робота з Properties

Apps Script мають можливість використовувати 3 типи сховищ для службових даних:

  • ScriptProperties — сховище даних вашого додатку, воно потрібно щоб зберігати загальні налаштування скрипта на кшталт ключів доступу, кредів, тощо
  • UserProperties — сховище даних поточного користувача, воно прив’язано до вашого додатку, його використовують щоб зберігати налаштування користувача відносно вашого додатку, наприклад — вибір метричної системи або якоїсь іншої
  • DocumentProperties — це сховище пов’язане з відкритим документом, це не наш випадок, але і не розповісти про цього я не міг

При використанні слід пам’ятити про обмеження при використанні Properties Service — це 9KB на значення, та 500KB загалом на сховище. Також слід врахувати, що є обмеження на кількість read/write операцій — 50 000 на день для gmail акаунтів там 500 000 для Google Workspace.

Для реалізації мого задуму нам знадобляться наступні методи:

  • PropertiesService.getScriptProperties() — сховище даних вашого додатку, тут будемо зберігати загальну статистику використання
  • PropertiesService.getUserProperties() — сховище даних поточного користувача, тут будемо зберігати статистику користувача

Це key-value сховища, для роботи з якими є декілька методів, серед яких нам наразі потрібні лише getProperty(key) та setProperty(key, value).

Давайте додамо використання сховища ScriptProperties до нашої функції collectStatisticData(event):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * Collect event data and count statistics for functions and commands
 *
 * @param {Object} event the event object from Google Chat
 */
function collectStatisticData(event) {
  const scriptProperties = PropertiesService.getScriptProperties();
 
  let eventCounter = scriptProperties.getProperty(event.type) || 0;
 
  scriptProperties.setProperty(event.type, parseInt(eventCounter) + 1);
 
  // Increment slash command counter if applicable
  if (event.message && event.message.slashCommand) {
    let slashCommandCounter = scriptProperties.getProperty('SLASH_' + event.message.slashCommand.commandId) || 0
    scriptProperties.setProperty('SLASH_' + event.message.slashCommand.commandId, parseInt(slashCommandCounter) + 1)
  }
 
  // Increment invoked function counter if applicable
  if (event.common && event.common.invokedFunction) {
    let functionCounter = scriptProperties.getProperty('FNC_' + event.common.invokedFunction) || 0
    scriptProperties.setProperty('FNC_' + event.common.invokedFunction, parseInt(functionCounter) + 1)
  }
}

Виглядає досить «дивно», та ми ще повернемося до цього функціоналу, давайте подивимось де ми можемо подивитися на дані які ми зберігаємо. Для цього треба перейти до вкладки Project Settings, і проскроліть до розділу Script Properties:

А тепер трошки оптимізуємо наявний спосіб збереження, я пропоную зберігати дані у JSON форматі:

01
02
03
04
05
06
07
08
09
10
{
  events: {
    MESSAGE: 0,
    CARD_CLICKED: 0,
    ADDED_TO_SPACE: 0,
    REMOVED_FROM_SPACE: 0,
  },
  commands: {},
  functions: {},
}

Тепер внесемо зміни до функції збору статистики:

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
/**
 * Collect event data and count statistics for functions and commands
 *
 * @param {Object} event the event object from Google Chat
 */
function collectStatisticData(event) {
  const scriptProperties = PropertiesService.getScriptProperties();
 
  let statistics = scriptProperties.getProperty('STATS');
 
  if (statistics) {
    statistics = JSON.parse(statistics)
  } else {
    statistics = {
      events: {
        MESSAGE: 0,
        CARD_CLICKED: 0,
        ADDED_TO_SPACE: 0,
        REMOVED_FROM_SPACE: 0,
      },
      commands: {},
      functions: {},
    }
  }
 
  statistics[event.type]++
 
  // Increment slash command counter if applicable
  if (event.message && event.message.slashCommand) {
    const { commandId } = event.message.slashCommand
    statistics.commands[commandId] = (statistics.commands[commandId] || 0) + 1
  }
 
  // Increment invoked function counter if applicable
  if (event.common && event.common.invokedFunction) {
    const functionName = event.common.invokedFunction
    statistics.functions[functionName] = (statistics.functions[functionName] || 0) + 1
  }
 
  scriptProperties.setProperty('STATS', JSON.stringify(statistics))
}

Зверніть увагу в цьому прикладі, що дані які ми отримуємо зі сховища то є текст, і нам ще треба його розпарсити. Звісно перед зберіганням треба JSON знов перетворити на текст. І це ще досить непогано, бо у попередній версії цієї функції нам треба була кожного разу парсити текст, щоб працювати з цифрами 😯

Додайте такий самий функціонал, але вже в якості сховища використайте userProperties:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
/**
 * Collect event data and count statistics for functions and commands
 *
 * @param {Object} event the event object from Google Chat
 */
function collectStatisticData(event) {
  const userProperties = PropertiesService.getScriptProperties();
 
  let userStatistics = userProperties.getProperty('STATS');
 
  //
  // ...
  //
 
  userProperties.setProperty('STATS', JSON.stringify(userStatistics))
}

На цьому збір статистики ми закінчимо, та будемо переходити до команди відображення.

Розширення slash-команд

Для відображення статистики я не хочу створювати окрему команду для бота, т.к. тоді вона буде з’являтися у переліку з іншими slash-командами, а це не дуже зручно, та ще буде збивати користувачів з пантелику. Тому я хочу сховати команду як аргумент до вже існуючої команди /bender stats. Для цього слід внести зміни до функції slashBender():

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
/**
 * @param {Object} event the event object from Google Chat
 */
function slashBender (event) {
 
  if (event.message.argumentText && event.message.argumentText.length) {
    switch (event.message.argumentText.trim()) {
      case 'stats':
        return slashBenderStats(event)
    }
  }
 
  //
  // ...
  //
 
}

Статистика

Нарешті перейдемо до відображення статистики, за це в нас якраз буде відповідати функція slashBenderStats():

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
/**
 * @param {Object} event the event object from Google Chat
 */
function slashBenderStats(event) {
  let defaultStats = {
    events: {
      MESSAGE: 0,
      CARD_CLICKED: 0,
      ADDED_TO_SPACE: 0,
      REMOVED_FROM_SPACE: 0,
    },
    commands: {},
    functions: {},
  }
 
  let userStats = PropertiesService.getUserProperties().getProperty('STATS')
    ? JSON.parse(PropertiesService.getUserProperties().getProperty('STATS'))
    : defaultStats
 
  let scriptStats = PropertiesService.getScriptProperties().getProperty('STATS')
    ? JSON.parse(PropertiesService.getScriptProperties().getProperty('STATS'))
    : defaultStats
 
  let widgets = []
 
  widgets.push(
    widgetDivider()
  )
 
  widgets.push(
    widgetTextParagraph('<b>Scripts statistics</b>')
  )
 
  widgets.push(
    widgetTextParagraph('Events')
  )
 
  Object.keys(scriptStats.events).forEach((key) => {
    widgets.push(
      widgetTextParagraph(`${key}: ${scriptStats.events[key]}`)
    )
  })
 
  widgets.push(
    widgetTextParagraph('Commands')
  )
 
  Object.keys(scriptStats.commands).forEach((key) => {
    widgets.push(
      widgetTextParagraph(`ID ${key}: ${scriptStats.commands[key]}`)
    )
  })
 
  widgets.push(
    widgetTextParagraph('Functions')
  )
 
  Object.keys(scriptStats.functions).forEach((key) => {
    widgets.push(
      widgetTextParagraph(`${key}: ${scriptStats.functions[key]}`)
    )
  })
 
  widgets.push(
    widgetDivider()
  )
 
  widgets.push(
    widgetTextParagraph('<b>User statistics</b>')
  )
 
  widgets.push(
    widgetTextParagraph('Commands')
  )
 
  Object.keys(userStats.commands).forEach((key) => {
    widgets.push(
      widgetTextParagraph(`ID ${key}: ${userStats.commands[key]}`)
    )
  })
 
  widgets.push(
    widgetTextParagraph('Functions')
  )
 
  Object.keys(userStats.functions).forEach((key) => {
    widgets.push(
      widgetTextParagraph(`${key}: ${userStats.functions[key]}`)
    )
  })
 
  return {
    'cardsV2': [{
      'cardId': 'bender',
      'card':
      {
        'sections': [
          {
            'header': 'Stats',
            'collapsible': false,
            'widgets': widgets
          }
        ]
      }
    }]
  }
}

Таким чином за запитом /bender stats ми отримаємо ось таку статистику:

Для оформлення картки зі статистикою я використовував функції-помічники widgetTextParagraph() та widgetDivider(), окрім цього у коді на GitHub я ще додав декілька подібних функцій, може вони і вам стануть у нагоді ;)

Deployment

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

Після цього вас запитають, щоб ви скрипт перемкнули на використання проєкту у GCP:

Для цього слід зайти на сторінку проєкту у консолі Google та скопіювати project number:

Тепер поверніться та внесіть цей номер у налаштування проєкту:

Так, тепер знов спробуйте створіть деплоймент:

Після створення скопіюйте Deployment ID, та пропишіть його у налаштуваннях Google Chat API у вашому проєкті. Все, тепер вашим ботом зможуть користуватися інші користувачі з тих кому ви надали доступ.

Пам’ятайте, що після цього кроку, для додавання кожної нової функції до бота вам треба буде створювати новий деплоймент, і лише після цього новий функціонал буде доступним у боті.

Домашнє завдання

Цього разу завдання буде на «покурити код та розібратися», тож давайте по порядку.

Перше — це знов таки хочу звернути вашу увагу на файл Widgets.gs, перелік віджетів у ньому збільшився, бо мені кортіло оформити статистику трохи цікавіше, ніж на скрині вище.

І друге — для роботи зі статистикою я зробив окремий клас StatisticsManager, щоб функція collectStatisticData() мала наступний вигляд:

01
02
03
04
05
06
07
08
09
10
/**
 * Collect event data and count statistics for functions and commands
 *
 * @param {Object} event the event object from Google Chat
 */
function collectStatisticData(event) {
  const statistics = new StatisticsManager()
  statistics.collectData(event)
  statistics.save()
}

Тож дякую за увагу, не ігноруйте завдання, та чекайте на наступні статті з цієї серії.

Source Code

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

Bender, реліз 3.0.0
Bender, білд 4.0.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.