Google Chat Bot. Properties

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

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

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

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

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

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

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

/**
 * 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) {
  // ...
}

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

/**
 * 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)
  // ...
}

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

/**
 * 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):

/**
 * 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 форматі:

{
  events: {
    MESSAGE: 0,
    CARD_CLICKED: 0,
    ADDED_TO_SPACE: 0,
    REMOVED_FROM_SPACE: 0,
  },
  commands: {},
  functions: {},
}

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

/**
 * 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:

/**
 * 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():

/**
 * @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():

/**
 * @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() мала наступний вигляд:

/**
 * 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.