В этом посте рассмотрим одну фичу, которая может быть полезна дорогому читателю

Недавно встал вопрос с множественным выбором. Бот предлагает пользователю список возможных вариантов, а пользователь в свою очередь имеет возможность выбрать несколько из этих вариантов. При том реализовано это через инлайн-клавиатуру

Ожидаемый результат работы флагов будет примерно следующий:

Клавиатура с двумя выбранными вариантами
Клавиатура с двумя выбранными вариантами

Краткая вводная дана, переходим к порядку реализации.

Создаем бота через @BotFather, создаем новую ГТ и открываем AppsScripts (подробнее про создание ботов можно почитать тут.

Начало

Как правило, я делю скрипт на несколько логических кусочков и сохраняю в разные файлы

Настоятельно рекомендую поступать также.

В файл Global записываю глобальные переменные или константы.

const API = "Ваш АПИ";
const DOC = SpreadsheetApp.openById("Ваш ИД");
const Test = DOC.getSheetByName("Ваше название листа");
//в кавычки пропишите свои значения

Здесь

  • API бота;

  • ссылка на текущий гугл док;

  • лист этого дока, с которым мы будем непосредственно работать.

Ссылка на гугл док расположена в адресной строке браузера

Выделенное является ид гугл таблицы
Выделенное является ид гугл таблицы

Клавиатура и ее отправка в чат

В файле Keyboards создадим нашу клавиатуру. Клавиатуру я записываю в отдельную переменную (в данном случае FLAGS)

Моя клава выглядит так:

let FLAGS =
  {
      "inline_keyboard": [
        [{"text": "➖ Уточнение деталей у клиента по вакансии устно", "callback_data": "0"}],
        [{"text": "➖ Уточнение деталей у клиента по вакансии письменно", "callback_data": "1"}],
        [{"text": "➖ Составление карты поиска", "callback_data": "2"}],
        [{"text": "➖ Размещение вакансий на различных источниках", "callback_data": "3"}],
        [{"text": "➖ Поиск на открытых источниках", "callback_data": "4"}],
        [{"text": "➖ Сорсинг", "callback_data": "5"}],
        [{"text": "➖ Телефонное интервью", "callback_data": "6"}],
        [{"text": "➖ Собеседование с видео по zoom/Skype", "callback_data": "7"}],
        [{"text": "➖ Технический скрининг на собеседование", "callback_data": "8"}],
        [{"text": "➖ Контроль выхода кандидата ", "callback_data": "9"}],
        [{"text": "➖ Контроль ИС", "callback_data": "10"}]
      ],
      "resize_keyboard": true
    }; 

Сейчас самое интересное…

Какая логика? Мы отправляем клаву в чат по запросу пользователя, по команде, например. Далее пользователь кликает на один из вариантов (кнопку), который хочет выбрать. Мы запоминаем его выбор и отправляем новую клавиатуру, где в тексте выбранной кнопки будет изменен флаг.

Внимательный читатель заметил, что в названиях кнопок есть смайлики. Они-то и будут нашими флагами, которые отражают одно из значений true/false, 1/0, да/нет.

Так как мы записываем клаву в таблицу, сначала будем проверять, есть ли уже эта клава в таблице и в какой именно ячейке она записана. Искать будем по ключу chat_id

function getInd(chat_id,sheet) { //возвращает индекс строки, в кот нах-ся ид
  let lr = sheet.getLastRow();
  let chat_id_arr = sheet.getRange(1,1,lr).getValues();
  chat_id_arr = chat_id_arr.flat();
  let ind = chat_id_arr.indexOf(chat_id);
  
  return ind;
}

Функция выше возвращает id строки таблицы, в которой записан искомый ид чата. Id строки в таблице и номер строки - не одно и то же. Когда мы забираем все значения из таблицы и записываем их в массив (методом getValues()), значения в массиве начинаются с 0, тогда как в таблице отсчет ведется с 1. Просто помним об этом)

Следующая функция ищет chat_id в таблице и возвращает соответствующую этому ид клаву. В ином случае возвращает дефолтную клаву.

function getKeyboard(chat_id) { //забирает клаву из табл
  let ind = getInd(chat_id,Test); 
  let lc = Test.getLastColumn();
  let cur_keyboard = [];
  let keys = [];
  let KEYBOARD = {};
  
  if (ind > 0) { //если ind=-1, chat_id не был найден в таблице
    keys = Test.getRange(ind+1,2,1,lc).getValues();
    keys = keys.flat();
    for (i=0; i<keys.length; i++) {
      cur_keyboard.push([{"text":keys[i], "callback_data":i}]);
    }
    if (cur_keyboard != "") {
      KEYBOARD = 
        {
          "inline_keyboard": cur_keyboard,
          "resize_keyboard": true
        }
    } else {
      KEYBOARD = FLAGS;
    }
  } else {
    KEYBOARD = FLAGS;
  }
  return KEYBOARD
}

И разумеется, нужна функция для сохранения клавиатуры в таблице гугл

function setKeyboard(chat_id,vote) { //записывает текст клавы в табл
  let ind = getInd(chat_id,Test);
  let KEYBOARD = getKeyboard(chat_id);
  let key = KEYBOARD.inline_keyboard[vote][0].text; 
  let flag = key.split(' ');
  switch (flag[0])
    {
      case '✅' : flag.splice(0,1,'➖'); break;
      case '➖' : flag.splice(0,1,'✅'); break;
    }
  flag = flag.join(' ');
  KEYBOARD.inline_keyboard[vote][0].text = flag;
  let new_arr = [];
  new_arr = KEYBOARD.inline_keyboard.flat();
  
  if (ind < 0) { //если ид нет в табл
    let new_ind = Test.getLastRow()+1; //строка для записи нового ид
    Test.getRange(new_ind,1).setValue(chat_id);
    for (i=0; i<new_arr.length; i++) {
      Test.getRange(new_ind,2+i).setValue(new_arr[i].text)
    }
  } else {
      for (i=0; i<new_arr.length; i++) {
      Test.getRange(ind+1,2+i).setValue(new_arr[i].text)
    }
  }
}

Здесь я сначала разделяю текст кнопки, которая была нажата, по пробелам, получая массив. Далее изменяю первый элемент (смайлик) на противоположное значение.

Объединяю все снова в один текст для этой кнопки и записываю в таблицу.

Сохраненная в таблице клавиатура будет выглядеть так

Кнопки клавы записываю в одну строку, чтобы не множить строки таблицы
Кнопки клавы записываю в одну строку, чтобы не множить строки таблицы

Соответственно, если ботом пользуются несколько человек в разных чатах, то для каждого чата будет выделена строка в таблице под свою клавиатуру. Идентификатор является ид чата (первая колонка)

Также запилю функцию для отправки клавы пользователя в виде сообщения

function sendKeyboard(chat_id) { //вернет клаву в виде текста
  let ind_keyboard = getInd(chat_id,Test);
  let msg = Test.getRange(ind_keyboard+1,2,1,Test.getLastColumn()).getValues();
  msg = msg.flat();
  msg = msg.join(",\n");

  return msg
}

Функции отправки сообщений ботом

Бот будет общаться с пользователем с помощью двух функций send() и send_key().

С первой из них уже было знакомство в этой статье, вторую рассмотрим здесь.

function send_key (msg, chat_id, api, keyboard)
{
  var payload = {
    'method': 'sendMessage',
    'chat_id': String(chat_id),
    'text': msg,
    'parse_mode': 'HTML',
    reply_markup : JSON.stringify(keyboard)
  }
  var data = {
    "method": "post",
    "payload": payload
  }
  UrlFetchApp.fetch('https://api.telegram.org/bot' + api + '/', data);

Функция отправляет инлайн-клавиатуру в чат вместе с сообщением. Соответственно на входе нам нужны текст сообщения, ид чата, куда мы эти сообщение и клаву отправляем, апи бота и сама клава.

Вызываем функции send() и send_key() из тела функции doPost().

Стандартная функция doPost() для коммуникации с ботом получает от нас одну из двух команд и выполняет два соотвутствующих этим командам действия:

  • отправляет клаву в чат;

  • отправляет сообщение, текст которого содержит выбор пользователя

function doPost(e)
{
  let update = JSON.parse(e.postData.contents);
  if (update.hasOwnProperty('message'))
  {
    let msg = update.message;
    let chat_id = msg.chat.id;
    let text = msg.text;
    
    if (text == "/getkeyboard") {
      let keyboardToSend = getKeyboard(chat_id);
      Demo.send_key("Галочки", chat_id, API, keyboardToSend)
    }
    if (text == "/save") {
      Demo.send("Клавиатура сохранена: \n" + sendKeyboard(chat_id), chat_id, API)
    }
  }

  if (update.hasOwnProperty('callback_query')) {
    let chat_id = update.callback_query.message.chat.id;
    let vote = update.callback_query.data;
    let msg_id = update.callback_query.message.message_id;

    if (vote >= 0 && vote <= 11) {
      setKeyboard(chat_id,vote);
      Demo.send_key("Ваш выбор: ",chat_id,API,getKeyboard(chat_id));
    }
  }
}

В функции выше у нас имеются два условия

  • update.hasOwnProperty('message')

  • update.hasOwnProperty('callback_query')

В первый if мы попадаем при условии, что пользователь написал боту сообщение, а во второй, - когда пользователь нажал на нопку.

В переменную vote я записываю значение callback_data нажатой кнопки. callback_data мы указали в переменной FLAGS для каждой кнопки, и эти значения - числа от 0 до 10.

Эти же числа выполняют роль номера элемента массива при работе c кнопками в функции setKeyboard().

Сохраняем и деплоим

Создадим файл Api connector и запишем сюда функцию установки вебхука

function api_connector ()
{
  let App_link = " ";
  //App_link указываем свой и обновляем после каждого деплоя
  UrlFetchApp.fetch("https://api.telegram.org/bot"+API+"/setWebHook?url="+App_link); 
}

Укажем App_link (ссылка появляется в окне после деплоя) и запустим функцию api_connector.

Тестируем бота

Я отправила боту сообщение "/getkeyboard", на что он мне вернул клаву без галочек

Далее результат клика по одной из кнопок

Я бы еще удаляла предыдущую клаву, чтобы у нас оставалась только одна активная.

К списку функций в файл functions допишем следующую функцию

function del_inline(chat_id, msg_id) {
  var payload = {
    'method': 'editMessageReplyMarkup',
    'chat_id': String(chat_id),
    'message_id': String(msg_id)
  }
  var Data = {
    "method": "post",
    "payload": payload
  }
  UrlFetchApp.fetch('https://api.telegram.org/bot' + API + '/', Data); 
}

И добавим вызов этой функции в doPost()

Проверяем работу бота снова

Предыдущая клава успешна удалена.

По команде "/save" мы получаем от бота нашу клаву в виде сообщения

Заключение

Я описала функционал, который может быть полезен в тех или иных ботах. Его конечно можно модифицировать или усовершенствовать и допилить под ваши нужды. Но, надеюсь, общая идея ясна.

Комментарии (6)


  1. censor2005
    26.10.2021 13:18

    Если мне не изменяет память, то предыдущую клавиатуру можно не удалять, а обновлять с помощью updateMessage - обновление будет более плавным, чем отправка и удаление. Только нужно убедиться, что предыдущее сообщение не удалено пользователем. Также вроде можно обновить только inline-клавиатуру


    1. clackx
      26.10.2021 21:43

      Всё верно, вот только убедиться никак нельзя. Бот не знает, что сообщение удалено, ведь удаляется оно только на стороне пользователя, т.о. не происходит никаких ошибок/исключений при попытке изменить такое сообщение.

      Но в целом совет правильный, если просто изменять предыдущее сообщение, это будет выглядеть гораздо приятнее и даже с некоторой долей магии.


    1. Nadjuscha Автор
      27.10.2021 00:28

      Сказывается моя невнимательность или fuzzy brain в конце рабочего дня, но я не нашла в документации инфы про обновление клавы.
      editMessage есть, но шифтнуть одну клаву другой вроде нельзя.

      Если нашли что-то, тыкните пожалуйста)


      1. censor2005
        27.10.2021 07:10
        +1

        Посмотрите метод editMessageReplyMarkup

        Для обнаружения того, что сообщение удалено, можно читать ответ от выполнения метода editMessage - он возвращает JSON, в котором вроде есть информация об успешности или неуспешности зарпоса:

        On success, if the edited message is not an inline message, the edited Message is returned, otherwise True is returned.


        1. Nadjuscha Автор
          30.10.2021 22:54
          +1

          Спасибо за совет^^ Выглядит действительно лучше
          Добавила функцию edit_inline()

          function edit_inline(chat_id, msg_id) {
            let keyboard = getKeyboard(chat_id);
            var payload = {
              'method': 'editMessageReplyMarkup',
              'chat_id': String(chat_id),
              'message_id': String(msg_id),
              'reply_markup': JSON.stringify(keyboard)
            }
            
            var Data = {
              "method": "post",
              "payload": payload
            }
            UrlFetchApp.fetch('https://api.telegram.org/bot' + API + '/', Data); 
          }

          И немного изменила doPost()


          1. censor2005
            31.10.2021 22:14

            Нужно на всякий случай проверять, успешно ли редактирование. Если сообщение было удалено, редактирование вернёт ошибку, в таком случае нужно повторно отправить сообщение с клавиатурой с помощью sendMessage