В языке 1С все мы в той или иной степени спецы, но того же нельзя сказать применительно к Telegram (далее «телега»). Почему? Причина в высоком уровне входа в API. Лично я, когда почитал страницу на сайте телеги, подумал «и где же дока по апишке?» Оказалось, это и была «типа самодостаточная дока по апишке» в понимании разработчиков. Потом был период ковыряния интернета на предмет реализации той или иной функции средствами платформы 1С. Что-то находилось, но в основном только вопросы без ответов или адски колхозные решения.
Сейчас я научился понимать текст на странице, которую разрабы называют описанием API. Это понимание нашло реализацию в виде разнообразных ботов. Сначала реализация была для одного бота, потом, когда в одной конфигурации их стало несколько, пришлось адаптировать некоторые механизмы под работу с произвольным количеством ботов, появились общие модули, сформировалась концепция работы с телегой.
Смотрю уже не первый месяц ИС и вижу отдельные попытки исследования API Telegram и даже создания конструкторов ботов. Попытки не систематизированные, часто, неоптимальные или устаревшие, не использующие нынешние возможности платформы в полной мере. А для конструкторов просто не вижу перспектив. Если целевая аудитория конструктора - пользователи, то они не смогут написать обработчик какой-либо команды бота и будут ограничены возможностями конструктора, а если программисты, то для них наиболее оптимальной реализацией ботов будет исключение сервисных механизмов конструктора. Понятно, что разбираться в API телеги мало кому хочется, было бы удобно, если бы оно уже было разобрано и имелся бы набор функций для бота. Это и хочу предложить вам ниже. Теория, затем разбор архитектуры решения, почему именно так, а не иначе, и наконец, сам код. Долго думал, надо ли мне это, ведь можно снимать сливки с рынка, обладая информацией. Под публикацией вы найдёте выгрузку из куцей конфигурации, которой хватит для понимания и демонстрации содержания статьи (я поставил 0sm, но сайт поставил 1).
Что такое телеграм-бот? Упрощу описание: это просто запись в таблицах на серверах телеги, что есть такой «пользователь» телеги с типом «бот». Значит, ему могут писать, он может отвечать, его можно добавлять в группы и пр. Нюанс только в том, что обычный пользователь – человек и он с помощью клиента телеги сам может прочитать, ответить, добавиться в группу и пр, а тут нам надо каким-то образом читать/отправлять сообщения и мониторить прочие события, которые происходят с ботом.
В телеграме нет спама. Достигается это и благодаря ограничению общения ботов: они могут писать только тем пользователям, которые сами обращались к боту. Т.е. отправить сообщение вы можете кому угодно, но увидят его только те пользователи, у которых есть история общения.
Читать можно двумя способами: лазить на сервера и проверять/забирать новые сообщения (способ «getUpdates») или, наоборот, отправить телеге ссылку, куда она должна скидывать новые сообщения сама (способ «вебхук»).
Конечно, когда сообщения моментально передаются через вебхук, время реакции бота будет минимально. По методу getUpdates мы можем только постараться сократить время между запросами новых сообщений. С другой стороны, вебхуки требуют наличия контролируемого вами веб-сервера и настроенной связи между ним и 1С, что не каждому доступно.
Для getUpdates есть история. Пока я писал первого бота, я отправлял ему тестовые сообщения и всё было хорошо, но когда я запустил его в «прод», бот замолчал. Я начал исследовать и выяснилось, что новые сообщения вообще не приходят боту. Ну, думаю, часто бот отвечал и телега его забанила. Прошло несколько дней, ситуация не изменилась. Тогда я полез в доку и выяснил, что хотя у метода getUpdates все операнды опциональны, всё же стОит обратить внимание на первый из них:
оffset – идентификатор первого апдейта, начиная с которого будут переданы все поступившие апдейты. После того, как апдейт был успешно передан любым способом, начинает тикать счётчик. Спустя 24 часа этот апдейт (не сообщение) будет удалён с серверов телеги, но пока он там, мы за какой-то надобностью можем его получить, если будем указывать отрицательное значение этого параметра. На практике мне это не было нужно, т.к. все запросы от ботов я записываю в ИБ.
Получается, для каждого бота надо хранить ид последнего апдейта, прибавлять к нему 1 и запрашивать с получившегося ида апдейты. По-умолчанию сервера выдают 100 новых апдейтов. Количество можно указать в операнде limit от 0 до 100. На практике этим параметром можно пренебречь. Например, бот умер на неделю, ему наслали 1000 сообщений. Он первым запросом получит первые 100, следующий запрос будет получать вторую сотню и т.д.
Я реализовал ботов обработками. Одна обработка – один бот. Обработки запускаются отдельными рег.заданиями, чтобы было удобно включать и выключать ботов. Получается, формы им не нужны, параметры тоже, работают они на сервере.
Как описывалось выше, в случае со способом получения апдейтов getUpdates мы заинтересованы в как можно более стремительной реакции бота. Исходя из этого следует, что соединение с сервером телеги должно устанавливаться один раз и общение бота будет происходить в рамках этого соединения. Нюанс в том, что рег.задание создаёт фоновое задание каждые 3 секунды и новое фоновое задание создаётся в новой среде, которая не знает, что соединение уже установлено. Способ передачи HTTPСоединения между фоновыми заданиями я пока не нашёл. Создавал на форуме уже две темы и получал неприемлемые ответы. Поэтому пока могу предложить колхоз – установку соединения каждым фоновым заданием.
Если у вас база в режиме файл-сервер, то вам стоит исследовать код, который отвечает за выполнение рег.заданий в файловом режиме. Столкнулся с тем, что хотя у рег.задания задан интервал 3 секунды, выполняется он раз в минуту.
Чтобы описать ботов, сделал отдельный справочник «Боты», где все боты задаются предопределёнными элементами для удобства ссылок на них в коде. Первый необходимый реквизит – ид бота, он же «токен». Вторым реквизитом можно указать имя обработки бота. Выше я упоминал ид последнего апдейта, полученного ботом. Можно хранить его в реквизите справочника. Считать каждый раз запросом МАКСИМУМ() от всех ид апдейтов данного бота, хранящихся в ИБ будет более затратным по мощностям и времени.
Далее получается, что сообщения надо читать многим ботам. Так зачем плодить этот механизм? Так появилось отдельное рег.задание «Чтение апдейтов». Оно запускается каждые 3 секунды и читает апдейты для всех ботов, у которых указано, что им апдейты надо читать автоматически. Галка «Автоматическое получение апдейтов» – ещё один реквизит справочника.
Теперь обратим внимание на ограничения ботов. Например, у одного бота мне надо было, чтобы он отвечал на запросы только из личек, а в группах не общался, а у другого – чтобы он имел возможность работать в инлайн-режиме. Не надо городить в коде то, что может быть реализовано средствами самОй телеги. У ботов есть создатель - @botFather, который может это всё настроить. Рекомендую вам полазить по его настройкам и изучить их.
Ещё одно ограничение, которое может быть полезно, - это ограничение типов сообщений, получаемых ботом. Например, у меня есть бот, который читает только локи (location). Убрать лишний флуд можно операндом allowed_updates метода getUpdates и этот список типов сообщений тоже может храниться в реквизите справочника «Боты».
Полученные апдейты не всегда, но в большинстве случаев целесообразно хранить в ИБ. Это пригодится и для анализа запросов при разработке, и для разделения механизмов чтения и обработки апдейтов. Для цели хранения апдейтов можно создать регистр сведений «СообщенияБотам». Структура апдейта весьма витиевата, поэтому создавать под каждый его реквизит отдельный реквизит в РС нет смысла. В конце разработки бота вы заметите, что из всех реквизитов часто вам нужны только несколько: автор сообщения, ид сообщения, ид чата, текст сообщения (если это текстовое сообщение). Сравните с полным описанием структуры сообщения. Всё остальное, как например, ид сообщения, в ответ на которое пришло текущее сообщение, может быть получено из отдельного реквизита регистра, где хранится полный текст сообщения со всей структурой. Т.о. структура РС может быть следующего вида:
update_id
Бот - Поскольку в РС хранятся сообщения от всех ботов, то надо знать владельца этого апдейта.
МоментВремени
Пользователь - Я рекомендую сразу создать доп.реквизит TelegramID у сущностей ИБ, описывающих персонажей, которые могут общаться с ботом - контрагенты, физ.лица, сотрудники и т.п. В реквизите я храню саму сущность, это удобно для анализа, но это не принципиально.
message_id - Часто нужен, например, для ответа на сообщение.
chat_id - Нужен всегда - в какой чат отвечаем?
Обработан - Признак того, что данное сообщение было обработано ботом. Удобен для отладки, когда вылетает ошибка на обработке сообщения. При устранении ошибки запускаем заново бота и пытаемся обработать сообщение повторно.
ТипСообщения - Этот реквизит нужен для анализа типа сообщения и реакции на него.
ТекстСообщения - Если тип сообщения - message, то в реквизите текст сообщения.
Итак, у нас есть справочник, где хранятся настройки ботов, есть рег.задание, которое получает апдейты, есть обработка конкретного бота и рег.задание, которое запускает эту обработку. Что д.б. в обработке бота? В ней будет самое мясо – обработка новых непрочитанных сообщений.
Я не претендую на полный обзор всех возможностей ботов, но планирую описать наиболее интересные и полезные из них: отправку/получение файлов, inline-клавиатуры, «перманентные» клавиатуры, команды бота и некоторые другие возможности.
Начнём с самого простого – с команд бота. Сразу открою, что эти команды должны быть описаны у создателя ботов. Т.о. при вводе слеша первым символом сообщения пользователю будет показываться список предопределённых команд бота с заданным вами описанием. Это удобно. Учитывайте сразу, что пользователь может написать команду рАзНыМ регистром и добавить в конце @имя-вашего-бота. Последнее необходимо для точной идентификации бота-получателя команды. Например, в группу добавили трёх ботов. У каждого бота есть команда /start. Какому боту написали /start в группе? Именно @имя-вашего-бота после команды и уточняет получателя.
Поскольку список команд бота достаточно постоянен, его можно описать в справочнике команд ботов с указанием обработчика команды. При анализе сообщений будем фильтровать сообщения, начинающиеся со слеша и проверять по данному справочнику. Если нашли, вызываем обработчик и передаём ему сообщение.
Для анализа наличия команд удобно использовать свойство Message.Entities. Сервер телеги сам парсит пришедшее от пользователя текстовое сообщение и выделяет из него части вроде юзернеймов, команд бота, ссылок, хэштегов и пр. У каждого элемента этого массива есть свойство Type. Для команд бота у него будет значение "bot_command". Таким образом, вы можете не парсить ручками текст сообщения, а анализировать именно это свойство.
Теперь про клавиатуры. Они бывают двух типов - ReplyKeyboard, которые я называю "перманентными" и InlineKeyboard.
Перманентные клавиатуры - это непривязанные к сообщению кнопки в части экрана под строкой ввода сообщения. Клавиатуры можно сворвачивать и разворачивать. Перманентными я называю их потому, что они висят, пока их не уберёт сам бот. Я стараюсь использовать их под постоянные функции, которые должны быть доступны на протяжении всего общения с ботом. Однако, встречаются и интерактивные варианты клавиатур, например, у яндекс-бота. Принцип таких клавиатур прост - они отправляют название нажатой кнопки в виде сообщения от пользователя. Т.е. пользователь может написать "Погода" вручную, а может нажать кнопку "Погода" и результат для бота будет выглядеть одинаково. Такие кнопки всего лишь упрощают написание команд и анализировать вам придётся текст сообщений, учитывая произвольный регистр.
Куда интереснее дело обстоит с инлайн-клавиатурами. Это набор кнопок, которые привязаны к сообщению и выводятся под ним. Инлайн-клавиатура не может существовать без сообщения и сообщение д.б. от бота. Примечательно то, что по нажатию на кнопку во-первых, генерится апдейт с типом callback_query, а во-вторых, возвращаться может заданное вами значение. Даже если в качестве возвращаемого значения вы укажете строку, у вас будет гарантия по типу сообщения, что это не пользователь написал вручную, а была нажата такая-то кнопка клавиатуры, привязанной к такому-то сообщению.
Строго говоря, у апдейтов нет типов. Типы я ввёл для удобства дальнейшей обработки их ботами. На стадии записи апдейта в ИБ я проверяю наличие тех или иных параметров и устанавливаю тип апдейта. Например, если есть свойство callback_query, ставлю тип callbackquery, если у параметра message есть location, то ставлю тип location. Сразу становится понятно - прислали текст, или локу, или нажали инлайн-кнопку.
Как описать клавиатуры? У перманентных клавиатур кнопка имеет только один параметр - название (text). У инлайн - название (text) и значение (callback_data). На самом деле у них есть и другие параметры, открывающие потрясающие возможности, но их я описывать тут не буду пока. Значение инлайн-кнопок д.б. именно строковым, т.е. запихнуть туда ЗначениеВСтрокуВнутр() не получится. Рекоментую туда передавать УникальныйИдентификатор. И тут возникает хороший вопрос "зачем"...
Зачастую боты имеют многоуровневые меню. Например, сначала нам надо узнать у пользователя склад самовывоза, а потом время, когда он собирается приехать. Как это можно реализовать? Он даёт нам команду, что хочет забрать товар сегодня, мы в ответ выводим сообщение "да не вопрос, куда собираетесь ехать?" и с этим сообщением передаём инлайн-клавиатуру со списком возможных складов. Можно в качестве значения кнопки передавать уникальный идентификатор элемента справочника "МестаХранения" из ИБ, но это неудобно, т.к. при ответе надо понимать, на какой вопрос пришёл ответ.
Создаём РС "ВариантыОтветов", где измерением ставим реквизит "Идентификатор" с типом УникальныйИдентификатор, добавляем текстовый ресурс "Значение" и реквизит "ДатаСоздания". Ресурс будет содержать ЗначениеВСтрокуВнутр() от нужного нам значения, а реквизит нужен для последующего удаления данных сборщиком мусора, чтобы эти варианты не висели в ИБ вечно. В качестве значения кнопки передаём Идентификатор. Это позволит "привязать" кнопку к структуре, которая по ходу ответов от пользователя будет заполняться значениями его ответов.
Итак, пользователь выбрал склад, нам пришёл апдейт callbackquery, в этом апдейте указан код кнопки и из него мы понимаем, что за кнопка была нажата. Теперь можно изменить текст сообщения с клавиатурой на "Самовывоз со склада такого-то. Уточните время:" и клавиатуру заменить на предопределённые значения часов типа 09:00, 10:00 и т.д. При этом, выводя клавиатуру с часами есть несколько способов учесть ответ пользователя на предыдущий вопрос. Нам где-то надо хранить предыдущие ответы. Мы можем их включать в структуру из РС "ВариантыОтветов", а можем создать отдельный РС "СессииБота". В нём будет измерением ид чата (у приватных чатов он совпадает с ид пользователя телеги) и знакомые нам ресурс "Значение" и реквизит "ДатаСоздания". Если вам удобнее, можете вместо одного ресурса "Значение" создать отдельные ресурсы под каждое значение ответа пользователя. Из примера выше это будут "Склад" и "Время". Универсальность единого ресурса в том, что в нём можно хранить структуры разного состава реквизитов.
При поступлении апдейта callbackquery смотрим по РС "ВариантыОтветов", что пришло. Смотреть удобнее, когда с идентификатором проассоциировано не просто значение объекта ИБ, а структура, где указано, что за меню это было и значение ответа. Т.е. структура вида:
Операция: "ВыборСклада"
Значение: a763cfbb-f94f-4c67-8e13-0e96a3a7f353
Отсюда сразу понятно, что надо в РС "СессииБота" в сессии из соответствующего чата присвоить реквизиту "Склад" ссылку на элемент справочника "МестаХранения" с идентификатором "a763cfbb-f94f-4c67-8e13-0e96a3a7f353".
Когда пользователь выберет конкретное время, нам придёт callbackquery со ссылкой на запись в РС "ВариантыОтветов". Также, как и со складом, достаём из РС информацию, что это выбиралось время и что было выбрано такое-то конкретное время. Устанавливаем его в РС "СессииБота" и получаем заполненную структуру ответов от пользователя. Обновляем текст сообщения, чтобы отразить в нём результат выбора пользователя и убираем клавиатуру вообще.
Все клавиатуры - это JSON. Исходно это текст и его можно хранить в текстовых реквизитах. Вспоминаем, что в платформе 1С есть объекты и методы для работы с этим форматом: ЧтениеJSON, ПрочитатьJSON. Клавиатура - это массив строк из массива кнопок. В интернете полно генераторов, где можно задать массив и он будет преобразован в JSON, но можно и поэкспериментировать на примерах из 1С. Создайте массив из массивов структур с элементами text и callback_data и преобразуйте его с помощью кода в JSON:
СтруктураОтвета = Новый Структура;
СтруктураОтвета.Вставить("inline_keyboard", МассивСтрок);
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку(Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Нет,,,ЭкранированиеСимволовJSON.СимволыВнеASCII));
ЗаписатьJSON(ЗаписьJSON, СтруктураОтвета);
Должно получиться что-то вроде этого:
{
"inline_keyboard":
[
[
{"text":"Разрешить","callback_data":"%1"},
{"text":"Отклонить","callback_data":"%2"}
]
]
}
Поскольку в перманентных клавиатурах у кнопок есть только один реквизит text, разработчики решили нас порадовать упрощённой формой описания клавиатуры:
{
"keyboard":
[
[
"Разрешить",
"Отклонить"
]
]
}
Не всегда общение возможно с помощью клавиатур, иногда от пользователя ожидается текстовый ввод. Выходит, при поступлении текстового сообщения боту надо знать: это просто пользователю скучно или он отвечает на какой-то вопрос, заданный ему на каком-то этапе общения? Рассмотрим для примера функцию отправки отзыва о боте. Пользователь нажимает кнопку перманентной клавиатуры "Оставить отзыв". Мы видим, что есть такая кнопка, выводим ему "Следующим сообщением оставьте всё, что вы думаете обо мне" и ожидаем от пользователя сообщение. Это ожидание надо где-то зафиксировать. Я для этого создал РС "СессииКонтрагентов", очень похожий по структуре на "СессииБота". В нём хранятся текущие статусы общения с пользователем, если они есть. Например, после вывода сообщения пользователю про написание отзыва, следует записать в этом регистре, что в таком-то чате у нас теперь статус "ОжиданиеОтзыва". И когда придёт следующее сообщение, мы предварительно проверим статус и выполним соответствующую его обработку.
Следует отметить, что если общение с ботом ведётся в группах, то одновременно с ним могут общаться больше одного пользователя и по каждой ветке общения надо хранить текущий этап беседы. Это означает, что в РС СессииКонтрагентов надо добавить измерением ид сообщения: мы всегда будем знать, что у нас в таком-то чате, по такому-то сообщению сейчас такой-то этап общения. Привязки целого диалога к одному сообщению возможны в случае, когда мы меняем его текст и клавиатуру. Небольшая сложность поджидает нас при необходимости ввода текстовых ответов, как в примере выше с отправкой отзыва. Это легко решается переводом клиента в режим ответа на сообщение:
Параметры = Новый Структура("reply_markup, parse_mode", "{""force_reply"":true}", "Markdown");
НовоеСообщение = Боты.ОтправитьСообщение(Бот, Сообщение.Чат, "Укажите дату последнего медицинского осмотра в формате дд.мм.гггг:
|Например: _28.01.2018_",, Параметры);
В примере я передаю два параметра:
- parse_mode со значением Markdown говорит, что надо использовать при показе сообщения клиенту условное оформление (те самые подчёрки вокруг даты дают курсив);
- reply_markup со значением force_reply:true (JSON) говорит, что надо принудительно перевести клиента в режим ответа на это сообщение.
В результате клиент не задумывается о том, чтобы выделить нужное сообщение и выбрать "Reply", а вы получаете ответ, который привязан к вашему сообщению: в полученном сообщении надо анализировать реквизит Message.reply_to_message .
Теперь немного про отправку и приём файлов. Каждый залитый в телегу файл получает свой ид. Если вы хотите залить тот же файл ещё раз, выгоднее сделать это, указав тот самый ид файла. В этом случае перекачка выполняться не будет, а будет дана ссылка на файл на серверах телеги. Подробно об этом сказано здесь. Из трёх способов, о которых говорится по ссылке, как правило, всех интересует только один способ - как файл с диска или из ИБ залить в телегу. Сказано про это весьма лаконично:
Передайте файл, используя multipart/form-data как это обычно делает браузер.
Такая краткость вводила и меня в ступор поначалу. Потом начал гуглить, как же заливает файлы браузер и нашёл несколько статей, в.т.ч. и с реализацией на платформе 1С. К сожалению, примеры писались ещё под платформу 8.2, где всё было не столь радужно как в 8.3. Не буду подробно описывать, как происходит перекачка файлов, предоставлю вам эмоции первооткрывателей-гуглёжников. Во вложении содержится кусок кода для отправки различных видов медиа в чаты. В данной конфигурации он у вас не будет работать, потому что общий модуль "ПрисоединенныеФайлы" я просто дёрнул из БСП и он алярмит об ошибках. Практически все типовые конфигурации построены на базе БСП, поэтому часть функций по работе с присоединёнными файлами к объектам ИБ я не стал колхозить, а использовал стандартную функциональность. Если ваша конфигурация почему-то не использует БСП, можете написать свою функцию получения прикреплённого файла.
Один момент колхоза в реализации работы с файлами у меня есть. Как я говорил выше, если файл уже заливался на сервера телеги, то нам выгоднее использовать его ид, полученный после закачки. Я не стал создавать отдельный реквизит, потому как пришлось бы его создавать во всех справочниках "*ПрисоединенныеФайлы", а задействовал под хранение ида реквизит "Описание", который у меня не использовался. Если в вашей конфигурации он используется, тогда придётся создавать отдельный.
На этом пока остановлюсь. Ожидаю ваши вопросы и замечания в комментариях. Буду реагировать и дописывать либо, если вопросов будет много, сделаю отдельную публикацию.
Также, достаточно много информации по пониманию описания API содержится на ИС в комментариях к другим статьям про телегу.