Telegram ботом сегодня уже никого не удивишь. Даже на платформе 1Сных http-сервисов люди создают разные интересные проекты: как для развлечения, так и вполне реальные утилитарные решения, вроде администрирования кластера через inline-клавиатуру. Однако, я пока еще не встречал разработок или статей, рассказывающих о работе с другой веткой этого, если так можно сказать, telegram-стека - о Mini Apps.
Так вот я сейчас о них и расскажу.
Что такое Mini Apps?
Telegram Mini Apps - это технология, которая позволяет очень просто и нативно запускать самописные веб-приложения прямо внутри Telegram, а также предоставляет API для связи между вашим приложением и мессенджером, чтобы все было быстро и отзывчиво.
Да, да, я опять буду показывать свой проект :P
Для использования приложения в качестве Mini App, оно должно быть доступно по какому-либо URL. Этот URL прописывается в BotFather (автор рассчитывает, что вы уже знакомы с процессом создания ботов), после чего приложение становится доступно по отдельной кнопке на нижней панели в диалоге с ботом.
Что касается API, то работа с ним осуществляется при помощи подключения js файла от самого Telegram в HTML
<script src="https://telegram.org/js/telegram-web-app.js"></script>
Для большего понимания давайте создадим небольшое приложение, но так, чтобы это отражало работу с данным механизмом из 1С.
Мегамагазин
Идея такая: будет справочник с полями: Наименование, Описание, Картинка - номенклатура в вакууме. Данные из этого справочника мы оформим в HTML и позволим пользователю выбирать позиции в этаком импровизированном онлайн магазине.
Начнем с HTML. Для начала я создам целиком страницу вместе с несколькими карточками, а потом вынесу карточки отдельно, дабы была возможность клепать их сколько угодно. Вот такой файлик у меня получился:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/redirect.php?url=aHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L25wbS9ib290c3RyYXBANS4zLjIvZGlzdC9jc3MvYm9vdHN0cmFwLm1pbi5jc3M=" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
<style>
body{
padding: auto;
padding-bottom: 20px;
padding-top: 20px;
}
.card{
text-align: center;
margin: auto;
}
</style>
</head>
<body>
<div class="card" style="width: 18rem;">
<img src="https://catherineasquithgallery.com/uploads/posts/2021-03/1614610387_167-p-zadnii-fon-dlya-fotoshopa-238.jpg" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
<div class="card" style="width: 18rem;">
<img src="https://catherineasquithgallery.com/uploads/posts/2021-03/1614610387_167-p-zadnii-fon-dlya-fotoshopa-238.jpg" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
<div class="card" style="width: 18rem;">
<img src="https://catherineasquithgallery.com/uploads/posts/2021-03/1614610387_167-p-zadnii-fon-dlya-fotoshopa-238.jpg" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
</body>
</html>
Пока, думаю, оригинальность дизайна для нас не очень важна, поэтому я использовал базовые карточки из Bootstrap 5
Теперь вынесем одиночную карточку отдельно и немного поменяем под свои нужды. Для примера я захардкожу ее в отдельный метод и буду заменять переменные с @ на нужные мне данные, а картинку отправлять Base64 строкой. Но вы можете организовать это так, как вам удобно. Желательно, конечно, реализовать это при помощи Ajax
<div class="card" style="width: 18rem;">
<img src="data:image/png;base64, @Картинка64" class="card-img-top" alt="@Наименование">
<div class="card-body">
<h5 class="card-title">@Наименование</h5>
<p class="card-text">@Описание</p>
<button class=""btn btn-primary"" id=""@Номер"">В корзину</button>
</div>
</div>
Остальной документ вынесу точно так же:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/redirect.php?url=aHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L25wbS9ib290c3RyYXBANS4zLjIvZGlzdC9jc3MvYm9vdHN0cmFwLm1pbi5jc3M=" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
<style>
body{
padding: auto;
padding-bottom: 20px;
padding-top: 20px;
}
.card{
text-align: center;
margin: auto;
}
</style>
</head>
<body>
@Карточки
</body>
</html>
Теперь перейдем в 1С. Добавлю обычный справочник для товаров: Описание - Строка и Картинка - ХранилищеЗначений. Теперь необходимо написать обработчик для http-сервиса, который будет хостить наше приложение. Первыми создадим 2 метода, которые будут просто возвращать наши HTML макеты:
Функция ВернутьОсновнойДокумент()
Возврат
"<!DOCTYPE html>
|<html>
|<head>
|<meta charset=""utf-8"">
|<meta name=""viewport"" content=""width=device-width, initial-scale=1"">
|<link href=""https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"" rel=""stylesheet"" integrity=""sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"" crossorigin=""anonymous"">
|<script src=""https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"" integrity=""sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"" crossorigin=""anonymous""></script>
|
|<style>
|
|body{
|padding: auto;
|padding-bottom: 20px;
|padding-top: 20px;
|}
|
|.card{
|text-align: center;
|margin: auto;
|}
|
|
|</style>
|
|</head>
|<body>
|
|@Карточки
|
|</body>
|</html>";
КонецФункции
Функция ВернутьКарточку()
Возврат
"<div class=""card"" style=""width: 18rem;"">
|<img src=""data:image/png;base64, @Картинка64"" class=""card-img-top"" alt=""@Наименование"">
|<div class=""card-body"">
|<h5 class=""card-title"">@Наименование</h5>
|<p class=""card-text"">@Описание</p>
|<button class=""btn btn-primary"" id=""@Номер"">В корзину</button>
|</div>
|</div>";
КонецФункции
И сборщик этих макетов в единый документ по выборке из справочника Товары:
Функция СобратьДокумент() Экспорт
HTMLКарточек = "";
HTMLОбщий = ВернутьОсновнойДокумент();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Товары.Наименование КАК Наименование,
| Товары.Описание КАК Описание,
| Товары.Картинка КАК Картинка
|ИЗ
| Справочник.Товары КАК Товары";
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
КартинкаДД = ВыборкаДетальныеЗаписи.Картинка.Получить();
КартинкаB64 = Base64Строка(КартинкаДД);
Карточка = ВернутьКарточку();
Карточка = СтрЗаменить(Карточка, "@Наименование", ВыборкаДетальныеЗаписи.Наименование);
Карточка = СтрЗаменить(Карточка, "@Описание" , ВыборкаДетальныеЗаписи.Описание);
Карточка = СтрЗаменить(Карточка, "@Картинка64" , КартинкаB64);
HTMLКарточек = HTMLКарточек + Карточка + Символы.ПС + Символы.ПС;
КонецЦикла;
HTMLОбщий = СтрЗаменить(HTMLОбщий, "@Карточки", HTMLКарточек);
Возврат HTMLОбщий;
КонецФункции
Попробуем заполнить несколько товаров и закинуть это на http-сервис.
Выглядит уже неплохо, но пока ничего не делает. Добавим наше приложение в Telegram через BotFather, чтобы убедится в правильности размеров элементов, и перейдем к работе с API мини приложений.
У BotFather необходимо выбрать бота из списка и нажать Bot Settings
Затем Menu Button.
Там будет всего один вариант - Configure menu button, при выборе которого нам предложат отправить URL своего приложения и надпись, которая будет красоваться на кнопке.
Это один из нескольких доступных способов предоставления доступа к приложению - он создает постоянную кнопку возле поля ввода сообщения. Однако, кнопку открытия можно отправлять и как кнопку клавиатуры под сообщением при помощи прямой ссылки. Но мы это пока рассматривать не будем.
Вот так это теперь выглядит на ПК:
И так на телефоне
Теперь необходимо сделать это все интерактивным.
telegram-web-app.js
Подключаемый скрипт для Mini App создает особый объект работы с мессенджером, если веб-приложение открыто внутри Telegram. Необходимо добавить его в свой макет.
И вызвать объект. Вот пример скрипта с несколькими базовыми настройками:
let tg = window.Telegram;
if(tg != undefined){
if (tg.WebApp != undefined && tg.WebApp.initData != undefined){
let safe = tg.WebApp.initData;
tg.WebApp.backgroundColor = '#3d3d3d';
tg.WebApp.headerColor = '#212121';
tg.WebApp.expand();
}
}
Сперва мы объявляем переменную tg, куда помещаем объект для работы с Telegram. Далее необходима проверка на существование этого объекта, так как если данное приложение будет запущено не внутри мессенджера, то объект определен не будет и обращение через точку вызовет исключение. Внутри проверки у нас есть три строки: цвет фона, цвет шапки и метод expand(), который позволяет растянуть окно приложения на всю доступную высоту, так как по умолчанию шторка веб-приложения вылетает не в полный размер.
Но куда важнее переменная safe, которой присваивается tg.WebApp.initData.
Одной из ключевых особенностей скрипта telegram-web-app.js является возможность уже при запуске приложения получить информацию о пользователе, работающем с ним. Для этого существуют два метода
WebApp.initData и WebApp.initDataUnsafe
Честно говоря, существование initDataUnsafe вызывает много вопросов, так как очевидно, что передавать на сервер "достоверную" информацию, спокойно фальсифицируемую руками в отладчике, нельзя. Мы и не будем. Рассмотрим лучше, что нам возвращает initData
query_id=xxxx-4QbxxxxxxBuFAEjT&user=%7B%22id%22%3Axxxxxxx%2C%22first_name%22%3A%22Anton%22%2C%22last_name%22%3A%22Titowets%22%2C%22username%22%3A%22xxxx%22%2C%22language_code%22%3A%22ru%22%2C%22allows_write_to_pm%22%3Atrue%7D&auth_date=1702715987&hash=986xxxx9aa131c473bd830de5e7670cf24df15c0781611043f65babd7b960
P.S. веб-отладчик можно включить в настройках desktop версии Telegram, или использовать отладку по USB для мобильной
Оно возвращает простую строку, где находятся данные о пользователе. Но это не только данные сами по себе - их можно проверить на достоверность. Хотя чтобы такое провернуть, нужно решить буквально целый ребус
Но процедуру я уже написал раньше, так что теперь не придется.
Функция ПараметрыЗапросаВСоответствие(Знач СтрокаПараметров) Экспорт
СоответствиеВозврата = Новый Соответствие;
КоличествоЧастей = 2;
МассивПараметров = СтрРазделить(СтрокаПараметров, "&", Ложь);
Для Каждого Параметр Из МассивПараметров Цикл
МассивКлючЗначение = СтрРазделить(Параметр, "=");
Если МассивКлючЗначение.Количество() = КоличествоЧастей Тогда
СоответствиеВозврата.Вставить(МассивКлючЗначение[0], МассивКлючЗначение[1]);
КонецЕсли;
КонецЦикла;
Возврат СоответствиеВозврата;
КонецФункции
Функция ОбработатьДанныеTMA(Знач СтрокаДанных, Знач Токен) Экспорт
СтрокаДанных = РаскодироватьСтроку(СтрокаДанных, СпособКодированияСтроки.КодировкаURL);
СтруктураДанных = ПараметрыЗапросаВСоответствие(СтрокаДанных);
Ключ = "WebAppData";
Хэш = "";
Результат = HMACSHA256(ПолучитьДвоичныеДанныеИзСтроки(Ключ), ПолучитьДвоичныеДанныеИзСтроки(Токен));
ТЗ = Новый ТаблицаЗначений;
ТЗ.Колонки.Добавить("Ключ");
ТЗ.Колонки.Добавить("Значение");
Для Каждого Данные Из СтруктураДанных Цикл
НоваяСтрока = ТЗ.Добавить();
НоваяСтрока.Ключ = Данные.Ключ;
НоваяСтрока.Значение = Данные.Значение;
КонецЦикла;
ТЗ.Сортировать("Ключ");
СоответствиеВозврата = Новый Соответствие;
DCS = "";
Для Каждого СтрокаТЗ Из ТЗ Цикл
Если СтрокаТЗ.Ключ <> "hash" Тогда
DCS = DCS + СтрокаТЗ.Ключ + "=" + СтрокаТЗ.Значение + Символы.ПС;
СоответствиеВозврата.Вставить(СтрокаТЗ.Ключ, СтрокаТЗ.Значение);
Иначе
Хэш = СтрокаТЗ.Значение;
КонецЕсли;
КонецЦикла;
DCS = Лев(DCS, СтрДлина(DCS) - 1);
Подпись = HMACSHA256(Результат, ПолучитьДвоичныеДанныеИзСтроки(DCS));
Финал = ПолучитьHexСтрокуИзДвоичныхДанных(Подпись);
Если Финал = вРег(Хэш) Тогда
Ответ = Истина;
Иначе
Ответ = Ложь;
КонецЕсли;
СоответствиеВозврата.Вставить("passed", Ответ);
Возврат СоответствиеВозврата;
КонецФункции
Функция ОбработатьДанныеTMA() возвращает соответствие с полученными параметрами и результат проверки под ключом passed. Единственное, что вам необходимо будет достать, так это несколько методов из БСП: тут используются функции для работы с криптографией HMAC SHA-256, которые есть в модуле РаботаВМоделиСервисаБТС. Список необходимых методов:
Однако, вам не придется этого делать, если вы воспользуетесь TelegramEnterprise, так как там это уже есть:
TelegramEnterprise - базовая open-source библиотека интеграции с Telegram
- Множество реализованных методов для работы с Telegram API
- Простая установка - нужно лишь забрать 2 общих модуля
- Бесплатно и с открытым исходным кодом на GitHub
*Конец рекламной паузы*
Теперь мы знаем, как получить данные и можем их обрабатывать. Тут есть два пути:
1. Мы можем забить на объект Telegram
У нас уже есть информация о пользователе, а её отправку можно повесить, например, на какую-нибудь кнопку. Как только на эту кнопку нажмут, мы тут же узнаем, кто это сделал. Но это уже не вопросы работы с TMA, а просто стандартные методы работы через Ajax
//Функция отправки GET запроса
function ping(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url , true);
xhr.onload = function() {
if (xhr.status === 200)
{
resolve(true);
}
else
{
resolve(false);
}
};
xhr.onerror = function() {
resolve(false);
};
xhr.send();
});
}
//Обработка нажатия
myCoolButton.onclick = function(event) {
let safe = window.Telegram.WebApp.initData;
ping(url + '?' + safe).then(function(success){
console.log('Aright!');
});
}
Далее мы можем сохранить у себя полученные данные или отправить сообщение ботом при помощи нового http-запроса из 1С.
Процедура Привет(Знач Данные) Экспорт //Данные - строка initData
ДанныеПользователя = ОбработатьДанныеTMA(Данные);
Если ДанныеПользователя["passed"] Тогда
ИнформацияПользователя = ДанныеПользователя["user"];
ИнформацияПользователя = МетодыРаботыHttp.JsonВСтруктуру(
ПолучитьДвоичныеДанныеИзСтроки(ИнформацияПользователя));
ID = МетодыРаботыHttp.ЧислоВСтроку(ИнформацияПользователя["id"]);
МетодыРаботыTelegram.ОтправитьСообщение(ID,"Привет " + ИнформацияПользователя["first_name"]);
КонецЕсли;
КонецПроцедуры
Этот способ хорош тем, что позволяет использовать уже существующие приложения, которые помимо TMA работают и сами по себе. Лично я этот способ и использовал, так как у меня был готовый работающий сайт, отправляющий сообщения боту по кнопке. Осталось лишь добавить
window.Telegram.WebApp.close()
для красивого закрытия шторки приложения, после того как пользователь сделал выбор (это показано на гифке в начале). При этом, данный функционал никак не мешает жить приложению в качестве обычного сайта дальше - объект вне телеграма не создается, функция не выполняется.
2. Использовать telegram-web-app дальше
Для того, чтобы наше приложение меньше походило на мобильную версию сайта и больше на TMA, мы можем использовать еще некоторые стандартные функции. Работать с ними довольно просто - в качестве основного элемента управления выступает огромная кнопка подтверждения, которая сама создастся и адаптируется под нужный размер, если включить ее отображение через функцию
coolButton = window.Telegram.WebApp.MainButton;
coolButton.show();
coolButton.text = 'Оформить';
Мы все так же работаем с WebApp объекта Telegram и обращаемся там к MainButton. У нее есть несколько полей и методов (все можно посмотреть тут), основные из которых методы show() и hide() для показа/скрытия, enable() и disable() для активности/неактивности, поля с говорящими названиями text, color и textcolor. Оформленный выше код у меня используется вот так:
Очень кустарно, но без мухлежа - все из 1С. Дальше я буду приводить код цивильно в отдельных блоках, но просто знайте, что для использования его необходимо будет подключить в HTML вашего приложения - как внешний файл, ну или хотя бы текстом, как я здесь. В нормальной ситуации и HTML, конечно, не должен формироваться в 1С, но так просто нагляднее.
После добавления кода, кнопка появляется:
Теперь нам необходимо сделать механизм оформления. Для этого немного отредактируем карточки, добавив в них чекбокс "нахождения в корзине". Никто не мешает вместо него использовать для этих целей и счетчик, и отдельный список сохранения выбранных товаров.
Ради наглядности, скрывать свой чекбокс я не буду
Функция ВернутьКарточку()
Возврат
"<div class=""card"" style=""width: 18rem;"">
|<img src=""data:image/png;base64, @Картинка64"" class=""card-img-top"" alt=""@Наименование"">
|<div class=""card-body"">
|<h5 class=""card-title"">@Наименование</h5>
|<p class=""card-text"">@Описание</p>
|<button class=""btn btn-primary"" id=""@Номер"">В корзину</button>
//Тут новый чекбокс---------
|<input type=""checkbox"" id=""cbx_@Номер"" name=""cbx_@Номер"" />
|<label for=""cbx_@Номер"">Выбрано</label>
//--------------------------
|</div>
|</div>";
КонецФункции
А на кнопку "В корзину" повешу активацию этого чекбокса.
let allButtons = document.querySelectorAll('button');
allButtons.forEach(function(item, i, arr) {
item.onclick = function(event){
let id = item.id;
let thatCheckbox = document.getElementById('cbx_' + id);
if(thatCheckbox.checked){
item.textContent = 'В корзину';
}else{
item.textContent = 'Убрать из корзины';
}
thatCheckbox.checked = !thatCheckbox.checked;
}
});
Остается лишь обработать нажатие на кнопку "Оформить". У всех стандартных элементов TMA есть единая точка входа обработки событий - Telegram.WebApp.onEvent. Для нашей кнопки это выглядит так.
Telegram.WebApp.onEvent('mainButtonClicked', function(){
let goodsArr = []; // Массив для выбранных товаров
//Обход всех элементов
allButtons.forEach(function(item, i, arr) {
let id = item.id;
let thatCheckbox = document.getElementById('cbx_' + id);
if(thatCheckbox.checked){
goodsArr.push(id); //Если выбран - добавляем
}
});
let tgdata = window.Telegram.WebApp.initData;
//Создаем объект с полями order - массив товаров и user - со строкой initData
let reqdata = {'order': goodsArr, 'user': tgdata};
//Отправляем методом POST, конвертируя объект в JSON
post('https://api.athenaeum.digital/node/bot/goods_order', JSON.stringify(reqdata)).then(function(success){
window.Telegram.WebApp.close(); //Закрываем после ответа
});
});
А в 1С полученные данные уже обрабатываются:
Функция ОтправитьСписокПокупок(Данные, Токен) Экспорт
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(Данные);
ОтветОбъект = ПрочитатьJSON(ЧтениеJSON);
ДанныеПользователя = ОбработатьДанныеTMA(ОтветОбъект["user"], Токен);
ИнформацияПользователя = ДанныеПользователя["user"];
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(ИнформацияПользователя);
ИнформацияПользователя = ПрочитатьJSON(ЧтениеJSON);
Если ДанныеПользователя["passed"] Тогда
ТекстСообщения = "Ваш заказ:" + Символы.ПС;
Для Каждого Товар Из ОтветОбъект["order"] Цикл
Наименование = Справочники.Товары.НайтиПоКоду(Товар).Наименование;
ТекстСообщения = ТекстСообщения + Наименование + Символы.ПС;
КонецЦикла;
МетодыРаботыTelegram.ОтправитьСообщение(МетодыРаботыHttp.ЧислоВСтроку(ИнформацияПользователя["id"])
, ТекстСообщения, Токен);
КонецЕсли;
Возврат "ok";
Конецфункции
И вот наш магазин готов!
Мы отправляем запрос в 1С и от туда же отправляем ответ, так что, разумеется, там можно не только сформировать сообщение пользователю, но и создать документы, сделать записи в регистры, выполнить доп. обработку, etc.
Тут я рассказал не про все доступные возможности, но про основные - наиболее вероятные и полезные в реальном использовании. Небольшая, но полная документация по всей системе TMA есть тут:
https://core.telegram.org/bots/webapps
А тут весь код модуля нашего магазина (кроме методов БСП для HMAC SHA256 и отправки сообщения в Телеграм из TelegramEnterprise):
Ну а пока это все, спасибо за внимание!
Мой GitHub: https://gitub.com/Bayselonarrend Лицензия MIT: https://mit-license.org