Гугл таблица как бд для телеграм бота

Kate

Administrator
Команда форума
В этом посте рассмотрим в деталях, как непосредственно использовать гугл таблицы в качестве базы данных.

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

Ниже представлен скрин из чата с ботом.

23bcc9c1571e20137df4758e959c2099.png

Дисклеймер: если вы здесь в первый раз, пожалуйста ознакомьтесь с первым постом, где более детально разобраны скрипты в гугл таблицах (тыц).


Начнем с создания контейнера и написания скрипта в нем. Создаем новый Spreadsheet.

a98cf9537542573553146baca3005499.png

В качестве забираемых значений укажу 4 вопроса на листе Questions.

27d455a4e43e15d358084519668fd0ad.png

И варианты ответов для каждого вопроса на листе Answers. Данные из двух таблиц связаны по ИД – первая колонка.

В таблицу Users буду вносить ответы от пользователей, но об этом позже.

Переходим к скрипту. Укажу в качестве глобальных переменных следующие значения:

const doc = SpreadsheetApp.getActive();
const questionsSheet = doc.getSheetByName("Questions");
const answersSheet = doc.getSheetByName("Answers");
const usersSheet = doc.getSheetByName("Users");
Так как скрипт мы разворачиваем в том же документе (контейнере), то нам достаточно использовать метод getActive(). Если вы ссылаетесь на другой Spreadsheet, то можно на него сослаться методами .openByUrl() или .openById().

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

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

function sendQuestions() {
const questionsArr = questionsSheet.getRange(1,1,questionsSheet.getLastRow(), questionsSheet.getLastColumn()).getValues()

Logger.log(questionsArr)
}
Запустить функцию можно из меню, выбрав имя функции и кликнув Run

Меню

Меню
Результатом выполнения функции будет массив в логере.

Логер

Логер
Разберем строку в функции по частям.

const questionsArr = questionsSheet.getRange(1,1,questionsSheet.getLastRow(), questionsSheet.getLastColumn()).getValues()
Здесь мы объявляем переменную для записи в нее возвращаемого массива. Далее указываем лист, из которого будем забирать значения questionsSheet. Для листа выбираем метод .getRange() и указываем ячейки, к которым функция должна обратиться.

Перевожу написанное в скобках метода getRange на понятный язык:

getRange(номер строки начала диапазона
, номер столбца начала диапазона
, номер строки конца диапазона
, номер столбца конца диапазона)
Методами листа .getLastRow() и .getLastColumn() получаем номер последней строки и столбца, которые будут равны 4 и 2 соответственно. При этом методы возвращают последние столбец и строку, в которых указано значение (ячейка не пуста) или есть валидация.

Границы заданного диапазона

Границы заданного диапазона
В то же время можно указать в скобках questionsSheet.getRange("A1:B4").

Возвращаемся к функции и к методу диапазона .getValues(). Здесь обратим внимание на то, что существует также метод .getValue(), применять который следует, если мы обращаемся к одной ячейке, например questionsSheet.getRange(3,4) или questionsSheet.getRange("D3").

getValues() при этом возвращает двумерный массив, а getValue() - значение.

*Попробуйте обратиться к разным диапазонам с использованием разного синтаксиса и понаблюдайте за возвращаемыми значениями. После нескольких попыток обращение к диапазонам станет интуитивным.

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

По задумке бот отправляет все вопросы одновременно с кнопками. Сначала попробуем просто отправить вопросы без вариантов ответов.

Обратимся к функции send().

function send(msg, chat_id) {
const payload = {
'method': 'sendMessage',
'chat_id': String(chat_id),
'text': msg,
'parse_mode': 'HTML'
}
const data = {
'method': 'post',
'payload': payload,
'muteHttpExceptions': true
}
UrlFetchApp.fetch('https://api.telegram.org/bot' + token + '/', data);
}
В функцию мы передаем текст отправляемого ботом сообщения и ид чата, в который это сообщение будет отправлено. Внутри функции объявляем объект payload и указываем передаваемые параметры. Я использую только необходимые в рамках задачи параметры (method, chat_id, text, parse_mode), но их может быть и больше (ссылка на апи телеграмма для метода sendMessage).

В переменную data передаем payload (передаваемые параметры для метода апи "post") и указываем сам метод post.

В строке UrlFetchApp мы обращаемся к АПИ телеги по ссылке (таким же образом можно обратиться к любому другому доступному АПИ). В этой же строке мы передаем значение переменной token и data. Про token чуть позже.

Вернемся к функции sendQuestions() и вызовем из нее send() для каждого элемента массива questionsArr.

Чтобы обратиться к каждому элементу можно воспользоваться циклами типа for или while. Но я предпочитаю использовать методы массивов и стрелочные функции, которые умещаются в одну строку.

Таким образом, вместо конструкции

for (let i=0; i<questionsArr.length; i++) {
send(questionsArr[1],chat_id)
}
я могу написать

questionsArr.forEach(e => send(e[1],chat_id))
Итак, в качестве текста сообщения я забираю из вложенного массива элемент на позиции [1] (на позиции 0 указан ид вопроса).

Нам не достает только чат ид, который я укажу в качестве аргумента функции sendQuestions() и получу следующее:

function sendQuestions(chat_id) {
const questionsArr = questionsSheet.getRange(1,1,questionsSheet.getLastRow(), questionsSheet.getLastColumn()).getValues();

Logger.log(questionsArr);

questionsArr.forEach(e => send(e[1],chat_id));
}
Наконец, функцию sendQuestions(chat_id) мы вызовем, когда пользователь запустит бота по команде /start. Пропишем эту логику.

Сначала добавлю функцию, которая парсит возвращаемый из телеграма json пакет.

function doPost(e) {
const update = JSON.parse(e.postData.contents);
let msgData = {}
if (update.hasOwnProperty('message')) {
msgData = {
id : update.message.message_id,
chat_id : update.message.chat.id,
user_name : update.message.from.username,
text : update.message.text,
is_msg : true
};
}
}
Результатом функции является объект msgData с ключами id, chat_id, user_name, text, is_msg. Возвращаемые ключи вы можете определить сами, для простоты я указала только те, которые нужны для задачи.

Здесь же я передам в функцию отправки вопросов значение ключа chat_id.

function doPost(e) {
const update = JSON.parse(e.postData.contents);
let msgData = {}
if (update.hasOwnProperty('message')) {
msgData = {
id : update.message.message_id,
chat_id : update.message.chat.id,
user_name : update.message.from.username,
text : update.message.text,
is_msg : true
};
}
sendQuestions(msgData.chat_id);
}
Как создать бота и получить токен уже описывалось здесь. Берем этот токен и записываем в глобальную переменную token. Далее деплоим приложение (смотреть ссылку выше) и записываем URL веб приложения в переменную appLink.

И наконец функция api_connector() для установки веб хука.

function api_connector() {
const appLink = "Ваш URL";
UrlFetchApp.fetch("https://api.telegram.org/bot"+token+"/setWebHook?url="+ appLink);
}
В этой функции мы опять же отправляем запрос на сервер по адресу https://api.telegram.org и дополнительно указываем метод и/или параметры. В данном случае используется метод setWebhook и параметр url, куда мы передаем значение переменной appLink.

Запускаем эту функцию по кнопке Run.

После успешной установки вебхука запускаем бота в телеграме. В ответ получаем вопросы

cbc2f82a0ad646769dfc597e59a5e2f2.png

Весь код целиком:

const doc = SpreadsheetApp.getActive();
const questionsSheet = doc.getSheetByName("Questions");
const answersSheet = doc.getSheetByName("Answers");
const usersSheet = doc.getSheetByName("Users");

const token = "Ваш токен"

function api_connector () {
const App_link = "Ваш URL";
UrlFetchApp.fetch("https://api.telegram.org/bot"+token+"/setWebHook?url="+App_link);
}


function doPost(e) {
const update = JSON.parse(e.postData.contents);
let msgData = {}
if (update.hasOwnProperty('message')) {
msgData = {
id : update.message.message_id,
chat_id : update.message.chat.id,
user_name : update.message.from.username,
text : update.message.text,
is_msg : true
};
}

sendQuestions(msgData.chat_id);
}

function send(msg, chat_id) {
const payload = {
'method': 'sendMessage',
'chat_id': String(chat_id),
'text': msg,
'parse_mode': 'HTML'
}
const data = {
'method': 'post',
'payload': payload,
'muteHttpExceptions': true
}
UrlFetchApp.fetch('https://api.telegram.org/bot' + token + '/', data);
}


function sendQuestions(chat_id) {
const questionsArr = questionsSheet.getRange(1,1,questionsSheet.getLastRow(), questionsSheet.getLastColumn()).getValues();

Logger.log(questionsArr)

questionsArr.forEach(e => send(e[1],chat_id))

}


Далее я расскажу как забирать данные из ячеек и превратить их в кнопки, разместив их под сообщениями, которые мы успешно отправляли до этого.

Ячейки гт в кнопки тг - превращение​

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

Таблица Answers

Таблица Answers
Это таблица Answers с тремя колонками:

  • ид текста вопроса, к которому относится ответ
  • текст кнопки
  • маркер верного ответа
Дополним существующий скрипт обработкой данных из этой таблички и формированием объекта типа InlineKeyboardMarkup согласно документации Telegram bot api.

Вернемся к функции sendQuestions(chat_id) из последнего поста

function sendQuestions(chat_id) {
const questionsArr = questionsSheet.getRange(1,1,questionsSheet.getLastRow(), questionsSheet.getLastColumn()).getValues();

Logger.log(questionsArr)

questionsArr.forEach(e => send(e[1],chat_id))
}
И добавим строку для получения всех значений из вкладки Answers.

const answersArr = answersSheet.getDataRange().getValues();
Как было отмечено в комментариях к прошлой статье, есть более понятный способ получения всех данных из листа, а именно метод .getDataRange().

Предлагаю строку

const questionsArr = questionsSheet.getRange(1,1,questionsSheet.getLastRow(), questionsSheet.getLastColumn()).getValues();
тоже изменить на

const questionsArr = questionsSheet.getDataRange().getValues();
В итоге получаем

function sendQuestions(chat_id) {
const questionsArr = questionsSheet.getDataRange().getValues();
const answersArr = answersSheet.getDataRange().getValues();

Logger.log(questionsArr)
Logger.log(answersArr)

//questionsArr.forEach(e => send(e[1],chat_id))
}
Последнюю строку в функции я пока закомментировала, так как взаимодействовать с чатом мы будем позже.

Запускаем функцию и получаем в логере два массива - с вопросами и ответами.

4e2b3ae255157acb67370c0da29666d9.png

Приступаем к трансформации массива с вариантами ответов в объект keyboard.

На данном этапе я объявлю одноименную переменную, в которой укажу ключ inline_keyboard со значением массива из кнопок.

const keyboard = {
"inline_keyboard": arr
}
Значение arr - это двумерный массив. Вложенный массив включает в себя объект с описанием характеристик кнопки. Пример, как может выглядеть arr

const arr =
[
[{"text":"button1", "otherKey1":"value1", "otherKey2":"value2"}],
[{"text":"button2", "otherKey1":"value1", "otherKey2":"value2"}]
]
* Если в вашем проекте одна или несколько статичных клавиатур, вы можете задать их в виде переменных непосредственно в коде, например

const keyboard1 = {
"inline_keyboard":
[
[{"text": "buttonText1", "otherKey1":"value1", "otherKey2":"value2"}],
[{"text": "buttontext2", "otherKey1":"value1", "otherKey2":"value2"}]
]
}

const keyboard2 = {
"inline_keyboard":
[
[{"text": "buttonText3", "otherKey1":"value1", "otherKey2":"value2"}],
[{"text": "buttontext4", "otherKey1":"value1", "otherKey2":"value2"}],
[{"text": "buttontext5", "otherKey1":"value1", "otherKey2":"value2"}]
]
}
Ключ text обязательный и его значение выводится непосредственно в телеграме. Остальные доступные ключи можно посмотреть в документации, в этой задаче я дополнительно буду использовать только callback_data.

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

Вернемся к обработке массива с вопросами в строке questionsArr.forEach(e => send(e[1],chat_id)). Для этого перепишу ее немного иначе

questionsArr.forEach(e => {
//send(e[1],chat_id)
})
В теле стрелочной функции объявим наш arr для объекта клавиатуры и сам объект keyboard

questionsArr.forEach(e => {
let arr = answersArr.filter(el => el[0] == e[0])

const keyboard = {
"inline_keyboard": arr
}

//send(e[1],chat_id)
})

В новой добавленной строке кода я объявляю переменную массива, в нее записываю значения из массива с ответами, которые предварительно фильтрую по признаку, что ид вопроса из таблицы Questions равно ид вопроса из таблицы Answers. Использую, соответственно, метод массива filter().

Внутри метода я ссылаюсь на элемент массива answersArr используя обозначение el, и сравниваю значение el на позиции 0 с элементом массива questionsArr также на позиции 0.

Результат фильтрации выведу в логере

questionsArr.forEach(e => {
let arr = answersArr.filter(el => el[0] == e[0])
Logger.log(arr)

const keyboard = {
"inline_keyboard": arr
}

//send(e[1],chat_id)
})
61a7b46f0ce0179cfd1a874566b2e88a.png

*Такой же результат можно получить вложенными циклами

for (let i=0; i<questionsArr.length; i++) {
let arr = new Array();
for (let j=0; j<answersArr.length; j++) {
if (questionsArr[0] == answersArr[j][0]) arr.push(answersArr[j])
}
Logger.log(arr)
}

Начиная с третьей строки в логере, выводятся отфильтрованные значения по каждому из ид вопросов - 1, 2, 3 и 4.

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

questionsArr.forEach(e => {
let arr = answersArr.filter(el => el[0] == e[0])
arr = arr.map(el => [{"text":el[1], "callback_data":el[1]}])
Logger.log(arr)

const keyboard = {
"inline_keyboard": arr
}

//send(e[1],chat_id)
})

Внутри метода map() ссылаюсь на элемент массива arr через el, беру значение на позиции 1 и присваиваю его ключам text и callback_data. То есть элемент [1.0, Джейк пес, ] превратится в [{"text":"Джейк пес", "callback_data":"Джейк пес"}]

И пример того, как это будет в циклах без применения методов (ну за исключением одного)

for (let i=0; i<questionsArr.length; i++) {
let arr = new Array();
for (let j=0; j<answersArr.length; j++) {
if (questionsArr[0] == answersArr[j][0]) {
const val = answersArr[j][1]
const button = {"text":val, "callback_data":val}
arr.push([button])
}
}
Logger.log(arr)
}
*Метод .push() вставляет указанный в скобках элемент в конец массива

И вывод в логере модифицированного массива arr

b41d31f0396c01158a1620ff8a24bdf1.png

Вся функция sendQuestions() ниже

function sendQuestions() {
const questionsArr = questionsSheet.getDataRange().getValues();
const answersArr = answersSheet.getDataRange().getValues();

Logger.log(questionsArr)
Logger.log(answersArr)

//Альтернатива 1
/*
for (let i=0; i<questionsArr.length; i++) {
let arr = new Array();
for (let j=0; j<answersArr.length; j++) {
if (questionsArr[0] == answersArr[j][0]) {
const val = answersArr[j][1]
const button = {"text":val, "callback_data":val}
arr.push([button])
}
}
Logger.log(arr)
}
*/
//конец Альтернативы 1
//Альтернатива 2
questionsArr.forEach(e => {
let arr = answersArr.filter(el => el[0] == e[0])
arr = arr.map(el => [{"text":el[1], "callback_data":el[1]}])
Logger.log(arr)
//конец Альтернативы 2
const keyboard = {
"inline_keyboard": arr
}
//send(e[1],chat_id)
})
}
На этом этапе мы готовы отправить клавиатуру с сообщением, но нам нужна функция, которая эта делает. Внесем некоторые правки в существующую функцию send().

У нас появился новый передаваемый параметр, имя ключа этого параметра reply_markup, а значение keyboard:

'reply_markup' : JSON.stringify(keyboard)
Не забудем указать keyboard в параметрах функции. Таким образом, получим

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

Сохраняем проект и деплоим (Как это делать?). Не забываем запустить функцию api_connector() с новым значением appLink

function api_connector() {
const appLink = "Ваш URL";
UrlFetchApp.fetch("https://api.telegram.org/bot"+token+"/setWebHook?url="+ appLink);
}

Идем в чат, отправляем любой текст и… видим результат!

1c8c584ee4f7df9f0b1453fef145f593.png

Бот ответил отправив все вопросы с кнопками вариантов ответов.

Ниже весь код, который у нас есть на текущий момент

const doc = SpreadsheetApp.getActive();
const questionsSheet = doc.getSheetByName("Questions");
const answersSheet = doc.getSheetByName("Answers");
const usersSheet = doc.getSheetByName("Users");

const token = "Ваш токен"

function api_connector () {
const App_link = "Ваш URL";
UrlFetchApp.fetch("https://api.telegram.org/bot"+token+"/setWebHook?url="+App_link);
}

function doPost(e) {
const update = JSON.parse(e.postData.contents);
let msgData = {}
if (update.hasOwnProperty('message')) {
msgData = {
id : update.message.message_id,
chat_id : update.message.chat.id,
user_name : update.message.from.username,
text : update.message.text,
is_msg : true
};
}

sendQuestions(msgData.chat_id);
}


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

function sendQuestions(chat_id) {
const questionsArr = questionsSheet.getDataRange().getValues();
const answersArr = answersSheet.getDataRange().getValues();

Logger.log(questionsArr)
Logger.log(answersArr)

questionsArr.forEach(e => {
let arr = answersArr.filter(el => el[0] == e[0])
arr = arr.map(el => [{"text":el[1], "callback_data":el[1]}])
Logger.log(arr)

const keyboard = {
"inline_keyboard": arr
}

send(e[1], chat_id, keyboard)
})
}

 
Сверху