В этом посте рассмотрим одну фичу, которая может быть полезна дорогому читателю
Недавно встал вопрос с множественным выбором. Бот предлагает пользователю список возможных вариантов, а пользователь в свою очередь имеет возможность выбрать несколько из этих вариантов. При том реализовано это через инлайн-клавиатуру
Ожидаемый результат работы флагов будет примерно следующий:
Краткая вводная дана, переходим к порядку реализации.
Создаем бота через @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" мы получаем от бота нашу клаву в виде сообщения
Заключение
Я описала функционал, который может быть полезен в тех или иных ботах. Его конечно можно модифицировать или усовершенствовать и допилить под ваши нужды. Но, надеюсь, общая идея ясна.
censor2005
Если мне не изменяет память, то предыдущую клавиатуру можно не удалять, а обновлять с помощью updateMessage - обновление будет более плавным, чем отправка и удаление. Только нужно убедиться, что предыдущее сообщение не удалено пользователем. Также вроде можно обновить только inline-клавиатуру
clackx
Всё верно, вот только убедиться никак нельзя. Бот не знает, что сообщение удалено, ведь удаляется оно только на стороне пользователя, т.о. не происходит никаких ошибок/исключений при попытке изменить такое сообщение.
Но в целом совет правильный, если просто изменять предыдущее сообщение, это будет выглядеть гораздо приятнее и даже с некоторой долей магии.
Nadjuscha Автор
Сказывается моя невнимательность или fuzzy brain в конце рабочего дня, но я не нашла в документации инфы про обновление клавы.
editMessage есть, но шифтнуть одну клаву другой вроде нельзя.
Если нашли что-то, тыкните пожалуйста)
censor2005
Посмотрите метод editMessageReplyMarkup
Для обнаружения того, что сообщение удалено, можно читать ответ от выполнения метода editMessage - он возвращает JSON, в котором вроде есть информация об успешности или неуспешности зарпоса:
Nadjuscha Автор
Спасибо за совет^^ Выглядит действительно лучше
Добавила функцию edit_inline()
И немного изменила doPost()
censor2005
Нужно на всякий случай проверять, успешно ли редактирование. Если сообщение было удалено, редактирование вернёт ошибку, в таком случае нужно повторно отправить сообщение с клавиатурой с помощью sendMessage