В статье я опишу свое знакомство с новым для себя инструментом, подробно остановлюсь на коде бота, оставлю ссылку на полную версию бота и инструкцию как развернуть бота на Google Apps Script. Весь код на JavaScript с элементами Google API
Давно уже слышал про Google Apps Script, но все время откладывал возможность потестить для себя его функционал. Появилось немного свободного времени, на улице пасмурно и ко всему этому еще попалось интересное видео на эту тему - так сказать звезды сошлись. Ну, что же надо попробовать. И ведь попробовал. И был в шоке, от того как было интересно. Насколько затянуло, что даже во сне продолжал скрипты оформлять.
Как всегда, нового бота для тестирования придумать не смог, взял из уже имеющихся, конечно же этим ботом стал "Бот Обратной Связи". Функционал его понятен, и надо было уже показать пример как обходить настройку, когда пользователь запрещает пересылать сообщения. Очень много обращений по данному запрету.
* * *
Чем интересен Google Apps Script для владельца Telegram Bot:
- Размещение скрипта бота в Google, хостинг не требуется
- Хранение данных в Google Sheets (Google Таблицы)
- Отсутствует необходимость в доменном имени и соответственно в SSL сертификате
- Наличие online-редактора кода с подсветкой
- Есть встроенная система контроля версий (свой аналог Git)
- Разграничение доступа
- И многое чего еще я не пробовал ....
В ходе разработки я конечно же столкнулся с некоторыми непонятными для меня ситуациями*, такими как отсутствия поддержки Class Import/Export, либо я не до конца разобрался, но разбить проект на несколько файлов (отдельно для каждого класса) у меня не получилось. Странно, ведь как утверждается, работает все это дело на движке V8.
_____
>>> * Позже узнал как можно организовать архитектуру проекта, но про это в следующих статьях <<<
* * *
Кратко о функционале разрабатываемого бота
Если кратко про бот обратной связи, то это бот позволяющий его владельцу в режиме инкогнито общаться с пользователями Телеграм.
Технически не сложный бот, можно было бы его быстро накидать, но мне хотелось организовать небольшое окружение с разделением на сущности, также протестить хранение данных в таблице (CRUD - создание, чтение, редактирование, удаление). Данные хранить будем о пользователях, в дальнейшем при необходимости можно будет легко сделать рассылку от имени бота. Подключил еще языковые настройки - будет легко организовать мультиязычность бота. Также сделал при копировании сообщения (оно необходимо для обхода настройки запрета пересылки) сохранение форматирования, так как Телеграм форматирование присылает отдельно от текста. В общем и целом, работа была интересная.
* * *
Переходим к главному, к разработке нашего бота
Принцип работы в Google Apps Script заключается в создании проекта, размещении в нем скриптов, после развертывания проекта вы получите ссылку на "точку входа", на которую и будет приходить все данные от Телеграм через настроенный WeHook. Так называемая точка входа может принимать как GET так и POST, для получения данных и их обработки нужно создать почти одноименные функции в основном файле doGet()
и doPost()
. Про наш doPost() немного ниже.
А начнем мы, пожалуй, с описания настроек, их оказалось чуть больше чем обычно.
Расшифровка настроек:
sheet
- id Google Таблицы, где мы будем хранить данные ботаwebUrl
- адрес вашего приложения Google Apps Scripttoken
- токен вашего ботаuserNameBot
- username вашего ботаapiUrl
- адрес Telegram APIbotAdmin
- ваш личный id в TelegramlangParams
- языковые настройкиlinkCommands
- массив команд бота и их обработчикиdb
- настройки хранения данных, названия листов в таблице и порядковые номера столбцов в которых хранятся свойства сущностей
/**
* Настройки Бота
*/
const config = {
sheet: "1HbWBlyXMj..........PrVjzQqc_QF......bS_S5fY",
webUrl: "https://script.google.com/macros/s/AKfycbz0muZsFbS........2wONwoCg102OV5g/exec",
token: "540.....83:AAG4.......0DgsEo3QFx.....7F3o",
userNameBot: "......FeedBackBot",
apiUrl: "https://api.telegram.org/bot",
botAdmin: 00000000,
langParams: {
ru: {
admin: {
hello: "Начинаем ждать сообщений от пользователей",
answer: {
self: "Ответ на свое сообщение",
bot: "Ответ на сообщение бота",
button: {
reply: "Надо поставить сообщение пользователя в ответ.",
},
error: {
send: "Не удалось отправить сообщение пользователю"
}
}
},
user: {
hello: "Приветствую Вас, {name}.\nЯ очень жду вашего сообщения.\n------\nСпасибо."
}
}
},
linkCommands: [
{
template: /^\/start$/,
method: 'start'
}
],
db: {
users: {
table: "Users",
uid: 1,
name: 2,
userName: 3,
lang: 4,
created_at: 5,
updated_at: 6
}
}
}
* * *
Вспомогательные функции
Несколько функций я разместил вне классов, так как интерфейс онлайн-редактора позволяет их запускать без развертывания приложения (читай - сборка проекта, расскажу про это в инструкции с картинками):
function getMe() {
let response = UrlFetchApp.fetch(config.apiUrl + config.token + "/getMe");
console.log(response.getContentText());
}
function getWebHookInfo() {
let response = UrlFetchApp.fetch(config.apiUrl + config.token + "/getWebHookInfo");
console.log(response.getContentText());
}
function setWebHook() {
let response = UrlFetchApp.fetch(config.apiUrl + config.token + "/setWebHook?url=" + config.webUrl);
console.log(response.getContentText());
}
Исходя из названий функций, можно определить, что они делают запрос в Telegram Bot API и получают данные как о самом боте, так и о его webHook, и еще одна функция это для установки webHook для бота, ей мы воспользуемся один раз, сразу после первого развертывания проекта - у нас для этого уже появится webUrl
.
Метод UrlFetchApp.fetch()
- это метод, который идет уже под капотом Google Apps Script, он делает запрос по указанному вами адресу. Через него можно направлять и GET и POST запросы. В наших вспомогательных функциях мы передаем GET запросы.
* * *
Наша точка входа doPost()
Функция doPost()
по умолчанию обрабатывает POST запросы к нашему приложению, так как Telegram Bot API при использовании WebHook направляет данные через POST, то это как раз наш вариант:
/**
* Получаем данные от Телеграм
*/
function doPost(request) {
// получаем данные
let update = JSON.parse(request.postData.contents);
// направляем данные в объект WebHook
new WebHook(update);
}
То, что нас интересует лежит в объекте postData.contents
, мы их сразу обрабатываем, далее создаем объект класса WebHook и передаем в него объект с данными от Telegram преобразованный из "сырой" строки в JavaScript объект.
* * *
Class Helper
В нашем помощнике есть всего 2 статичных метода: isSet()
- проверяет на существование и isNull()
- проверяет на null, простые но часто используемые конструкции.
/**
* Класс Helper
*/
class Helper {
/**
* Проверяем на существование
*/
static isSet(variable) {
return typeof variable !== "undefined";
}
/**
* Проверяем на null
*/
static isNull(variable) {
return variable === null;
}
}
* * *
Class WebHook
Основной класс бота, в нем у нас лежит все необходимое окружение:
- Объект пользователя
- Объект языковых настроек
- Объект класса Bot с необходимыми методами для работы с Telegram Bot API
Методы класса:
constructor()
- определяем необходимые объектыroute()
- метод, который определяет исходя из пришедших данных куда их направлять для обработки. В основном это проверка на текстовые команды, в нашем наборе в настройках указана только одна команда/start
, но при необходимости можно добавить, а также проверка кто написал: пользователь или админ, от этого зависит каким методом будут копироваться сообщения (sendCopyToAdmin() или copyMessage()).checkCommand()
- проверяет на совпадение с шаблоном команды, в случае совпадения передает название метода для обработкиisAdmin()
- проверяет пользователя, является ли он владельцем бота исходя из настроекisBot()
- дает информацию сообщение, на которое идет ответ от администратора, принадлежит боту или пользователюstart()
- метод обработки команды /startprepareMethod()
- метод который преобразует строку типа video_note в VideoNotesendCopyToAdmin()
- вот он то самый метод, который в дуэте с методомroute()
позволяет обойти настройку Telegram, которая запрещает пересылать сообщения.
/**
* Класс WebHook
*/
class WebHook {
/**
* Создаем объект WebHook
*/
constructor(update) {
// создаем объект бота
this.bot = new Bot(config.token, update);
// создаем объект пользователя
this.user = new User(this.bot.getUserData());
// создаем объект языковых настроек
this.lang = new Lang(this.user.lang);
// получаем набор команд с шаблонами
this.linkCommands = config.linkCommands;
// запускаем роутер
this.route();
}
/**
* Получаем объект команды
*/
checkCommand(text) {
// текстовые ссылки
if (this.linkCommands.length > 0) {
// перебираем команды
for (let linkCommand of this.linkCommands)
// если есть совпадения
if (linkCommand.template.test(text)) {
// добавим флаг
linkCommand.result = true;
// вернем объект с методом
return linkCommand;
}
}
// если дошли до этой строчки то вернем флаг false
return {
result: false
};
}
/**
* Маршрутизируем
*/
route() {
// проверим на частный запрос
if(this.bot.data.message.chat.type != "private") {
// выйдем если это группа или канал
return;
}
// если это сообщение
if(Helper.isSet(this.bot.data.message)) {
// если это текстовое сообщение
if(Helper.isSet(this.bot.data.message.text)) {
// проверяем на команды
let command = this.checkCommand(this.bot.data.message.text);
// если есть совпадение по шаблону
if (command.result) {
// вызываем метод
this[command.method]();
// выходим
return;
}
}
// если пишет админ
if (this.isAdmin()) {
// если это ответ на сообщение
if (Helper.isSet(this.bot.data.message.reply_to_message)) {
// получаем текст из отвечаемого сообщения
let text_ = Helper.isSet(this.bot.data.message.reply_to_message.text)
? this.bot.data.message.reply_to_message.text // текстовое сообщение
: this.bot.data.message.reply_to_message.caption; // медиа сообщение
// если ответ самому себе
if (this.user.uid == this.bot.data.message.reply_to_message.from.id) {
// уведомляем админа, что ответ самому себе
this.bot.sendMessage(config.botAdmin, this.lang.getParam("admin.answer.self"));
} // если ответ на сообщение бота
else if (this.isReplyBot() && !/^USER_ID::[\d]+::/.test(text_)) {
// уведомляем, что ответ боту
this.bot.sendMessage(config.botAdmin, this.lang.getParam("admin.answer.bot"));
}
else {
// получить id пользователя из сообщения
let matches = text_.match(/^USER_ID::(\d+)::/);
// проверяем
if (matches) {
// все нормально отправляем копию сообщения пользователю
this.bot.copyMessage(matches[1], config.botAdmin, this.bot.data.message.message_id);
} else {
// уведомляем, что не удалось направить сообщение пользователю
this.bot.sendMessage(config.botAdmin, this.lang.getParam("admin.answer.error.send"));
}
}
} else {
// уведомление нажать кнопку ответить
this.bot.sendMessage(config.botAdmin, this.lang.getParam("admin.answer.button.reply"));
}
} else {
// Если это написал пользователь то отправляем копию админу
this.sendCopyToAdmin();
}
}
}
/**
* Проверяем на Админа
*/
isAdmin() {
// сравним текущего пользователя с админом из настроек
return config.botAdmin == this.user.uid;
}
/**
* Локальная проверка на бота
*/
isReplyBot() {
// вернем кто владелец сообщения на которое отвечаем
return this.bot.data.message.reply_to_message.from.is_bot;
}
/**
* Старт бота
*/
start() {
// определяем текст
let text = this.isAdmin() // проверяем кто стартанул
? this.lang.getParam("admin.hello") // если стартанул админ
: this.lang.getParam("user.hello", { // если стартанул пользователь
name: this.user.name // добавим для парсинга его имя
});
// выводим сообщение
this.bot.sendMessage(this.user.uid, text);
}
/**
* Отправляем копию
*/
sendCopyToAdmin() {
// создаем ссылку на просмотр профиля
let link = (this.user.userName.length > 0)
? "@" + this.user.userName // если есть username
: "<a href='tg://user?id=" + this.user.uid + "'>" + this.user.name + "</a>";
// дополнение к сообщению с id пользователя
let dop = "USER_ID::" + this.user.uid + "::\nот <b>" + this.user.name + "</b> | " + link + "\n-----\n";
// определяем данные по умолчанию
let typeMessage = this.bot.getMessageType(); // тип сообщения
let dopSend = false; // по умолчанию доп отправлять отдельно не нужно
let data = { // формируем данные сообщения
chat_id: String(config.botAdmin), // пользователь админ
disable_web_page_preview: true, // закроем превью ссылок
parse_mode: "HTML", // форматирование html
method: null // метод по умолчанию не определен
};
// если это текстовое сообщение
if (typeMessage == "text") {
// формируем доп с текстом
data.text = dop + this.bot.prepareMessageWithEntities(this.bot.getMessageText(), this.bot.getEntities());
// переопределяем метод
data.method = "sendMessage";
} else { // если это остальные типы сообщений
// проверяем нужно ли отправлять dop отдельным сообщением
dopSend = Helper.isNull(this.bot.getMessageText());
// заполняем данные
if(typeMessage == "location") {
// определяем координаты
data.longitude = this.bot.data.message.location.longitude;
data.latitude = this.bot.data.message.location.latitude;
} else {
// запоняем файлом
data[typeMessage] = this.bot.getMessageFileId();
}
// если не надо доп, значит описание не пустое
if (!dopSend) {
// дополняем описание
data.caption = dop + this.bot.prepareMessageWithEntities(this.bot.getMessageText(), this.bot.getEntities());
}
// переопределяем метод
data.method = "send" + this.prepareMethod(typeMessage);
}
// если метод определен
if (!Helper.isNull(data.method)) {
// и нужно отправить доп отдельным сообщением
if (dopSend) {
// отправляем админу доп
this.bot.sendMessage(config.botAdmin, dop);
}
// отправляем копию
this.bot.query({
method: "post",
payload: data
});
}
}
/**
* Преобразуем переданную строку в camelCase
*/
prepareMethod(method) {
return method.split('_') // разделяем по знаку _ в массив
.map(function(word,index){ // перебираем все значения
// преобразуем первый символ в верхний регистр, остальное в нижний
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
})
.join(''); // собираем в одно слово без пробелов
}
}
* * *
Class Bot
Обычный класс с необходимым набором методов для работы тестируемого бота. Добавил метод форматирование текста, так как при копировании сообщения мы имеем текст и форматирование отдельно друг от друга - очень удобная особенность Телеграм.
Методы класса:
constructor()
- принимаем и добавляем в свойства объекта класса необходимые и важные данные, такие как токен бота и объект от ТелеграмgetUserData()
- метод для выдачи данных пользователя переданных от ТелеграмgetEntities()
- получаем набор форматирования из объекта сообщенияgetMessageText()
- получаем текст из сообщения или описание медиаgetMessageType()
- получаем тип сообщения, очень удобная штукаgetMessageFileId()
- если это медиа сообщение, то метод нам поможет из него получить file_idprepareMessageWithEntities()
- метод форматирования текста по переданному набору EntitiessendMessage()
- один из методов отправки, описан отдельно в виду его частого использованияcopyMessage()
- этот метод помогает скопировать сообщение и отправить его в указанный чатquery()
- основной метод отправки в Телеграм данных, метод доставки данных определяется в наборе переданной ему конфигурации
/**
* Класс Бот
*/
class Bot {
/**
* Создаем объект класса
*/
constructor(token, data) {
// записываем токен бота
this.token = token;
// и полученный объект с данными от Телеграм
this.data = data;
}
/**
* Получаем данные пользователя
*/
getUserData() {
// вернем данные для создания обновления пользователя
return {
// его uid
uid: this.data.message.from.id ?? 0,
// его первое имя
firstName: this.data.message.from.first_name ?? "",
// его второе имя
lastName: this.data.message.from.last_name ?? "",
// его username
userName: this.data.message.from.username ?? "",
// его языковую настройку
lang: this.data.message.from.language_code ?? "ru"
}
}
/**
* Entities - форматировние
*/
getEntities() {
// если это сообщение
if (Helper.isSet(this.data.message)) {
// если это текствое сообщение
if(Helper.isSet(this.data.message.text)) {
// вернем текстовое форматирование если оно существует
return this.data.message.entities ?? null;
} else {
// если это не текствое сообщение, тогда вернем форматирование описания
return this.data.message.caption_entities ?? null;
}
} else {
// если это другой тип данных вернем null
return null;
}
}
/**
* MessageText - получаем текст или описание объекта
*/
getMessageText() {
// медиа объекты с возможным описанием
let medias = [
'audio',
'document',
'photo',
'animation',
'video',
'voice'
];
// если это текствое сообщение
if (Helper.isSet(this.data.message.text)) {
// вернем текст сообщения
return this.data.message.text ?? null;
} // если это медиа сообщение с описанием
else if (medias.includes(this.getMessageType())) {
// вернем описание объекта
return this.data.message.caption ?? null;
} else {
// если не подходит условия вернем null
return null;
}
}
/**
* Message Type
*/
getMessageType() {
// получаем объект сообщения
let message = this.data.message;
// начинаем проверки и при совпадении вернем тип сообщения
if (Helper.isSet(message.text)) {
return "text"; // текстовое сообщение
} else if (Helper.isSet(message.photo)) {
return "photo"; // картинка
} else if (Helper.isSet(message.audio)) {
return "audio"; // аудио файл
} else if (Helper.isSet(message.document)) {
return "document"; // документ
} else if (Helper.isSet(message.animation)) {
return "animation"; // анимация
} else if (Helper.isSet(message.sticker)) {
return "sticker"; // стикер
} else if (Helper.isSet(message.voice)) {
return "voice"; // голосовая заметка
} else if (Helper.isSet(message.video_note)) {
return "video_note"; // видео заметка
} else if (Helper.isSet(message.video)) {
return "video"; // видео файл
} else if (Helper.isSet(message.location)) {
return "location"; // местоположение
}
// по умолчанию вернем null
return null;
}
/**
* Message File Id
*/
getMessageFileId() {
// получаем объект сообщения
let message = this.data.message;
// определяем тип с вернем соответствующий file_id
if (Helper.isSet(message.photo)) {
// получаем массив картинок
let photo = message.photo;
// вернем самую последнюю - максимальный размер
return photo[photo.length-1].file_id;
} else if (Helper.isSet(message.audio)) {
// аудио файл
return message.audio.file_id;
} else if (Helper.isSet(message.document)) {
// документ
return message.document.file_id;
} else if (Helper.isSet(message.animation)) {
// анимация
return message.animation.file_id;
} else if (Helper.isSet(message.sticker)) {
// стикер
return message.sticker.file_id;
} else if (Helper.isSet(message.voice)) {
// голосовая заметка
return message.voice.file_id;
} else if (Helper.isSet(message.video_note)) {
// видео заметка
return message.video_note.file_id;
} else if (Helper.isSet(message.video)) {
// видео файл
return message.video.file_id;
}
// по умолчанию вернем null
return null;
}
/**
* Форматирование текста
*/
prepareMessageWithEntities(text, entities) {
// проверяем наличие форматирования
if (entities != null && entities.length > 0) {
// готовим переменную в нее будем добавлять
let prepareText = "";
// перебираем форматирование
entities.forEach(function(entity, idx, arr){
// добавляем все что между форматированием
if (entity.offset > 0) {
/*
* старт = если начало больше 0 и это первый элемент то берем сначала с нуля
* если не первый то берем сразу после предыдущего элемента
*
* длина = это разница между стартом и текущим началом
*/
// определяем начало
let start = (idx == 0)
? 0
: (arr[idx - 1].offset + arr[idx - 1].length);
// определяем длину
let length = entity.offset - start;
// добавляем
prepareText = prepareText + text.substr(start, length);
}
// выбираем текущий элемент форматирования
let charts = text.substr(entity.offset, entity.length);
// обрамляем в необходимый формат
if (entity.type == "bold") {
// полужирный
charts = "<b>" + charts + "</b>";
} else if (entity.type == "italic") {
// курсив
charts = "<i>" + charts + "</i>";
} else if (entity.type == "code") {
// код
charts = "<code>" + charts + "</code>";
} else if (entity.type == "pre") {
// inline код
charts = "<pre>" + charts + "</pre>";
} else if (entity.type == "strikethrough") {
// зачеркнутый
charts = "<s>" + charts + "</s>";
} else if (entity.type == "underline") {
// подчеркнутый
charts = "<u>" + charts + "</u>";
} else if (entity.type == "spoiler") {
// скрытый
charts = "<tg-spoiler>" + charts + "</tg-spoiler>";
} else if (entity.type == "text_link") {
// ссылка текстовая
charts = "<a href='" + entity.url + "'>" + charts + "</a>";
}
// добавляем в переменную
prepareText = prepareText + charts;
})
// добавляем остатки текста если такие есть
prepareText = prepareText + text.substr((entities[entities.length-1].offset + entities[entities.length-1].length));
// возвращаем результат
return prepareText;
}
// по умолчанию вернем не форматированный текст
return text;
}
/**
* Отправляем сообщение
*/
sendMessage(chat_id, text) {
// готовим данные
let data = {
method: "post",
payload: {
method: "sendMessage",
chat_id: String(chat_id),
text: text,
parse_mode: "HTML"
}
}
// вернем результат отправки
return this.query(data);
}
/**
* Отправляем копию сообщения
*/
copyMessage(to_id, from_id, message_id) {
// готовим данные
let data = {
method: "post",
payload: {
method: "copyMessage",
chat_id: String(to_id), // кому
from_chat_id: String(from_id), // откуда
message_id: message_id // что
}
}
// вернем результат отправки
return this.query(data);
}
/**
* Запрос в Телеграм
*/
query(data) {
return JSON.parse(UrlFetchApp.fetch(config.apiUrl + this.token + "/", data).getContentText());
}
}
* * *
Class User
Класс Пользователя, нам нужен только для тестирования по хранению данных в Google таблице. Как оказалось, это не сложно, при беглом взгляде у Google API для этого очень много инструментов. Надо будет потом написать класс по типу ActiveRecord, не искал, но скорее всего уже есть такие на GitHub. Но опять же мне для теста.
Методы класса:
constructor()
- в конструкторе сразу наполняем свойства пользователя полученными даннымиgetRowByUid()
- получаем номер строки по uidsave()
- добавляем или обновляем данные о пользователе
/**
* Класс Пользователь
*/
class User {
/**
* Создаем объект пользователя
*/
constructor(userData) {
// заполняем uid
this.uid = userData.uid;
// name сразу склеиваем из первого и второго имени
this.name = (userData.firstName + " " + userData.lastName).trim();
// заполняем lang из телеги
this.lang = userData.lang;
// username если есть
this.userName = userData.userName;
// сохраняем данные
this.save()
}
/**
* Получаем строку в таблице по uid
*/
getRowByUid(sheet, uid, range_ = "A1:A") {
// определяем диапазон ячеек в таблице
const range = sheet.getRange(range_);
// получаем через поиск по переданному uid
const result = range.createTextFinder(uid).matchEntireCell(true).findNext();
// вернем результат
return result // если он не null
? result.getRow() // вернем номер строки
: null; // или null
}
/**
* Обновляем или добавляем пользователя в таблицу
*/
save() {
// определяем таблицу и в ней лист
const sheet = SpreadsheetApp.openById(config.sheet).getSheetByName(config.db.users.table);
// получаем номер строки или null
const row = this.getRowByUid(sheet, this.uid);
// получаем текущую дату-время
const date = new Date();
// проверяем строку
if(row) { // если есть то обновляем данные пользователя - могли быть изменены
// обновляем имя пользователя
sheet.getRange(row, config.db.users.name).setValue(this.name);
// обновляем username
sheet.getRange(row, config.db.users.userName).setValue(this.userName);
// обновляем lang
sheet.getRange(row, config.db.users.lang).setValue(this.lang);
// обновляем дату-время последнего посещения
sheet.getRange(row, config.db.users.updated_at).setValue(date.toString());
} else {
// если строка не найдена, значит добавляем пользователя в лист
sheet.appendRow([this.uid, this.name, this.userName, this.lang, date.toString(), date.toString()]);
}
}
}
* * *
Class Lang
Этот класс позволит очень просто сделать нашего бота полиглотом. Текстовые настройки мы храним ближе к началу файла, чтобы было удобно вносить изменения.
Методы класса:
constructor()
- получаем набор текстовых настроек и устанавливаем пользовательскую настройкуsetLang()
- устанавливаем если присутствует пользовательский набор настроек или по умолчаниюgetParamByDot()
- этот метод позволяет удобно указывать какую настройку брать по типу "admin.answer.error.send", он по точке разбивает в массив и через рекурсию добирается до нужного значенияgetParam()
- метод возвращает окончательно сформированную текстовую часть с учетом подстановки динамических значений
/**
* Класс Lang
*/
class Lang {
/**
* Создаем объект Lang
*/
constructor(userLang = 'ru') {
// получаем данные из общих настроек
this.langParams = config.langParams;
// записываем языковую настроку пользователя
this.setLang(userLang);
}
/**
* Уставнавливаем параметр lang
*/
setLang(userLang) {
// если настроки по переданному параметру существуют
this.lang = Helper.isSet(this.langParams[userLang])
? userLang // то устанавливаем
: 'ru'; // иначе вернем по умолчанию
}
/**
* Получаем значение из массива
*/
getParamByDot(arr, obj) {
// получаем первый элемент массива
let name = arr.shift();
// проверяем есть ли еще в массиве другие параметры
if(arr.length > 0) {
// направляем на рекурсию
return this.getParamByDot(arr, obj[name]);
}
// вернем настройку
return obj[name];
}
/**
* Готовим значение
*/
getParam(param, data = {}) {
// получаем текстовую настройку
let text = this.getParamByDot(param.split('.'), this.langParams[this.lang]);
// если настройка не найдена
if (!Helper.isSet(text)) {
// то вернем заглушку
return "Unknown Text";
} // Если настройка найдена
else {
// проверяем переданы ли значения под замену
if (Object.keys(data).length > 0) {
// перебираем значения
for (let key in data) {
// создаем шаблон
let template = new RegExp('{' + key + '}', 'gi');
// заменяем
text = text.replace(template, data[key]);
}
}
// вернем настройку
return text;
}
}
}
* * *
Развертывание бота на площадке Google Apps Script
У таблицы необходимо получить ее id - он находится в адресной строке, далее пропишем его в настройках бота в параметре sheet
Сразу же переименуем лист таблицы в Users, в нем будет хранится информация о пользователях
Перейдем: Расширения > Apps Script
Откроется страница нового проекта "Проект без названия" (можете переименовать)
Заменим все что находится в файле Код.gs (открыт по умолчанию), на содержимое из
Если у вас еще нет данных бота, таких как токен бота и его username, то посмотрите мою статью по регистрации бота в Телеграм.
В коде из Нашего_Файла в настройках бота укажем id таблицы (sheet), данные бота (токен, username), ваш id (botAdmin) как владельца бота.
/**
* Настройки Бота
*/
const config = {
sheet: "1NcydPS8jth0.............1B4hZ0IOXPLu45Tabfm5A",
.....,
token: "54043.....:AAG4FcglR.........o3QFxvyqa-37F3o",
userNameBot: ".......FeedBackBot",
.....,
botAdmin: 000000000,
.....,
.....
}
Сохраняем все это дело, можно использовать быстрые клавиши CTRL + S
, запустим новое развертывание - это большая синяя кнопка справа вверху "Начать развертывание"
Откроется диалоговое окно, нажимаем на иконку "Шестеренка", выбираем "Веб-приложение"
Заполните поля и нажмите кнопку Начать развертывание
- Описание - название развертывания
- Запуск от имени - выберите
От моего имени
- У кого есть доступ - укажите
Все
, иначе Телеграм не сможет направить данные
При первом развертывания проекта, у вас запросят Предоставление прав, нажмите на синию кнопку.
Подробнее о предоставлении прав можно почитать в документации
В отображенном списке выберите аккаунт, который вы указали в поле Запуск от имени в настройках развертывания пару шагов назад
Google выдаст предупреждение, о том что указанное вами приложение не проверенное и будет остерегать вас давать разрешения, но это же ваш аккаунт и ваше приложение - предлагаю рискнуть и продолжить ... жмите Advanced - или на каком у вас там языке будет ссылка (зависит от выбранного вами языка интерфейса - у меня выдало on English)
Выдаст еще одно предупреждение - жмите Go to ......
В отображенной форме нажимайте кнопку Allow
Все, развертывание создано, из данных показанных в окне, нам нужно ссылка (URL) на веб-приложение, скопируйте ее, далее нужно будет ее добавить в настройки бота
/**
* Настройки Бота
*/
const config = {
sheet: "1NcydPS8jth0.............1B4hZ0IOXPLu45Tabfm5A",
webUrl: "https://script.google.com/macros/s/AKfycbxRQCxSPeUXf...........XMAZjU_8UmW3pi4zx2DAltn_/exec",
.....
}
Не забудьте сохранить изменения кода CTRL + S
, после сохранения нужно запустить установку webHook, используя уже готовую функцию. Для этого выберите в списке функцию под названием setWebHook() и нажмите кнопку Выполнить
В идеале мы должны получить примерно вот такой ответ от Телеграм
Можно запустить еще одну функцию: getWebHookInfo() - она выведет информацию о текущем состоянии настроенного webHook
Бот готов к использованию!
Открывайте его нажимайте Старт (/start
)
Дополнение
В случае если необходимо откорректировать код бота, после корректировки сохраните изменения, и в Управлении развертываниями отредактируйте текущее развертывание, назначив новую версию.
* * *
На этом думаю, что все, больше добавить нечего. С вас если не сложно предложения и комментарии, получилось ли у вас запустить этот пример бота.
Откройте по ссылке или QR бот @iMakeBot, нажмите кнопку Старт/Start.
Следуйте инструкциям бота.
Прям человеческое спасибо. Сайтов 100 просмотрела, не получалось. Сейчас бот ответил. Спасибо
Ирина, спасибо!)
Подскажите как у вас прошла установка? Сколько времени потребовалось? Все ли понятно было в инструкции?
Подскажите пожалуйста где дописать что бы после отправленного пользователем сообщения, в ответ сразу пришло сообщение от бота, типа: Ожидайте ответа...
class WebHook -> route()
Здравствуйте Можете объяснить? У вас в коде есть button. Это кнопка должна быть или я что то не так понял?
Это не кнопка, а текст для уведомления, вызывается по пути
admin.answer.button.reply
в class WebHook -> route()Почему то кнопки не появились. Просто отвечаю на сообщение путём свайпа влево
В этом боте нет кнопок. Вы напишите без свайпа - просто в бот, он вам выведет этот текст
А вы можете добавить небольшое дополнение к этому автоответу бота?
К сообщению одну кнопку по нажатию которой открывается инлайн меню этого бота в котором можно добавлять дополнительный контент (скажем telegra.ph ссылки). Некое подобие хелпа? типа этого бота: @graphrubot
И если отправить этот инлайн контент в переписку, то что бы бот не отправлял сообщение админу. Чисто для пользователя
Вы имеете в виду кнопку с параметром
switch_inline_query_current_chat
? Чтобы при нажатии на нее подставлялся текст: "@ваш_бот ", и выходили бы какие-то ваши заготовленные объекты (картинки, ссылки ... ) по инлайн-запросу, по типу FAQ?Ну это уже не в рамках этого бота. Возьму на заметку. Как раз искал пример для инлайн-бота.
Да, именно так, как в боте который я указал выше. Хотелось бы конечно это всё в рамках бота обратной связи сделать.
Как продолжение функционала бота обратной связи вполне логично.
Жду с нетерпеньем. Спасибо!
а вообще возможно подключить календарь к бд в которой есть столбец с датами и что бы я допустим нажимал на 22 декабря и календарь выводил инфу именно на этот день и так с другими числами, или это нереально ?
Возможно конечно, передавайте дату по кнопке, делайте выборку из бд и отправляйте в бот
оххх, для меня сложная задачка буду разбираться, спасибо большое
Не будет получаться, создайте на форуме ветку, чем смогу помогу
Хорошо, спасибо вам огромное
Супер!!!! даже лучше чем первый был бот обратной связи
подскажите пожалуйста, сколько ботов можно ставить на одном аккаунте гугл?
Спасибо. Специально не искал эту инфу, но думаю неограниченно.))
и еще, подскажите пожалуйста в какую строку именно нужно вставлять данный код
Не надо никуда это вставлять - это уже там где надо))
я перепутал, не тот код написал, сейчас исправил
В class WebHook -> route()
После вот этой строки
правильно же сделал?
а то что то не работает((
Нет, не правильно. В методе route(), под указанной строкой добавьте отправку уведомления админу
для меня это тяжеловато((
Здравствуйте, а как сделать, что бы в таблице в лист "user" прилетало не только айди, время, но и текст сообщения?
Делал первый раз все по инструкции.
Или направьте меня, где есть инструкция как выводитьв гугл таблицу вопросы заданные боту
Здравствуйте! Подскажите пожалуйста, есть ли возможность добавить еще админов в бота, то есть что бы сообщение от юзеров приходило не одному, а нескольким админам и так же была видна вся переписка
Можно создать группу, добавить бота в группу, и направлять сообщения в нее.
Красава! Просто лучший. Однозначно.
А возможно ли как то в этом боте реализовать возможность его запуска, только подписчиками определенного канала/группы? Или как в принципе такую возможность организовать? Нигде найти не могу, ничего подобного, только через платные сервисы. Не подскажите?
Можно в методе doPost() перед строкой new WebHook(update); поставить условие и проверять через метод Телеграм getChatMember() присутствие пользователя в супергруппе или канале, если пользователь не найден то вернуть ошибку и остановить выполнение скрипта
А для не программистов реальный кусочек кода не оформите? Потому, что в большинстве случаев такую информацию ищут люди далековатые от глубоких познай в области программирования, такие как я )) А вопрос довольно распространенный как я понял, и в этой статье был бы очень ценным дополнением, просто универсальный бот бы получился.
В любом случае, спасибо за ответ!
Попробуйте вот так, не проверял - на "коленке" написал:
В настройках добавьте
config.channel_id
Здравствуйте!
В настройках, в первых строках скрипта? Пробовал в таком формате как ботадмин: [botAdmin: 000000000,] только: [config.channel_id:-0000000000000,]выше и ниже этой строки, с запятой и без, с тире перед цифрами ID и без, ошибки начинают подчеркиваться, (без квадратных скобок естественно)
Что то я не то делаю походу...
Попробуйте в таком формате
Да, так ошибок нет, но бот работать перестал (
Если все правильно, то может просто проверка не проходит? То есть когда вы стартует бот он вас в группе ненаходит и останавливает работу
Пробовал и от участника и от не участника, не хочет стартовать даже от админа...
Развертывание\обновление, все по инструкции делал.
До этого работал?
Да и после отката изменений, сейчас тоже работает (Уже с убранным кодом проверки)
Id группы верный?
Надо посмотреть что возвращает result, не тестировал, так как надо разворачивать бот - поэтому и не отлаженный код
Если верить боту LeadConverter да
Здравствуйте. Я новичок в деле создания ботов, и хотел бы описать ситуацию, из которой ищу выход.
У меня есть некая БД на базе гугл таблиц, которую заполняю используя гугл формы. При отправке формы запускается скрипт, который всё раскладывает по своим местам и отправляет сообщение с необходимыми данными на электронку через MailApp.
Всё было хорошо, пока я не столкнулся с ограничением в 100 сообщений в день и теперь ищу выход из ситуации.
Можно ли на основе телеграм бота реализовать подобный функционал?
Т.е. мне не нужно, что бы пользователи что то у бота спрашивали и он отвечал, мне нужно, что бы он просто отправлял определённые сообщение указанным абонентам по команде от гугл скрипт.
Копать в эту сторону или в какую то другую?))
Спасибо.
Добрый день. Очень помогло, спасибо.
Хочу, чтобы он в случае если это просто сообщение от админа (не ответ на сообщение пользователя, не ответ боту и не ответ самому себе), рассылал это сообщение всем пользователям из таблицы. Я нуб, и сломал весь мозг. Подскажите как это можно сделать
Добрый день. Спасибо за полезные материалы. А возможно как-то закончить php вариант бота, с учетом обхода скрытия профилей? Просто там php тут gs, да и таблицы не нужны пока... А так было бы законченное решение на php ?