Как всем уже давно известно, в популярном клиенте для обмена мгновенными сообщениями Telegram появилась возможность создавать специальных ботов, которые могут оказаться весьма полезными, будучи добавленными в групповые чаты. Боты могут показывать участникам конференции различную полезную информацию, будь то последние новости какого-либо интересного сайта, погоду или курс валют. Функциональность программ для ботов практически не ограничена, например, можно реализовать бота, который будет отправлять в приватный или групповой чат фотографию с подключенной к компьютеру Web-камеры или мониторить температуру процессора на сервере. К примеру, запустив программу бота на домашнем компьютере, можно будет следить за какими-нибудь его показателями удалённо, просто отправляя команды боту с любого устройства, на котором установлен клиент Telegram. Однако, существует несколько ограничений, которые реализованы на уровне Telegram Bot API:
- Во-первых, бот не может написать первым, он должен обязательно отозваться на команду пользователя;
- Во-вторых, боты не могут отвечать на сообщения друг-друга.
Это сделано для того, чтобы усложнить жизнь различным мошенникам и спаммерам и предотвратить возможность неконтролируемого флуда множества ботов в чатах. Создавая программу, всегда нужно помнить об этих ограничениях.
Дайджест-бот, отдающий накопленные сообщения за день в процессе работы. Скриншот из Desktop’ного клиента Telegram.
Для сообщества любителей мобильных гаджетов от Motorola — MotoFan.Ru существует весьма активная конференция в Telegram, участником которой я также являюсь. Мной было замечено, что для того, чтобы отметить значимое событие, произошедшее в течении дня, или какую-нибудь важную информацию, пользователи часто прибегают к использованию тега #digest. Подобным тегом отмечаются сообщения, которые по мнению участников конференции необходимо добавить в сводку текущий событий. При этом посетители группового чата, которые появляются в конференции достаточно редко, могут не читать огромное количество оставленных ранее сообщений, а выделить с помощью тега #digest только основную информацию. Но для этого им нужно воспользоваться поиском тега по сообщениям и попрыгать по листингу чата, что достаточно неудобно. Поэтому возникла идея создания дайджест-бота, который будет накапливать и сохранять все сообщения участников конференции, отмеченные тегом #digest за какой-либо срок, например, за неделю, а потом выдавать их скопом по команде /digest в чат. Это весьма удобно, в отличие от ручного поиска необходимых сообщений.
Содержание:
1. Знакомьтесь, Гаечка :3
2. Подготовка окружения к созданию бота на JavaScript и Node.js
3. Создаём профиль бота с помощью @BotFather
4. Реализация простейшего Hello World!-бота на JavaScript
5. Реализация дайджест-бота на JavaScript
5.1. Обработчик тега #digest
5.2. Обработчик команды /digest
5.3. Функция deleteObsoleteDigestMessages()
5.4. Генерация ответа бота на команду /digest
5.5. Заключение по дайджест-боту
6. Реализация валютного бота на JavaScript
7. Реализация упрощённого дайджест-бота на Python
8. Реализация бота, отправляющего случайную цитату с bash.im, на JavaScript
9. Реализация JavaScript-ботов, работающих со стикерами, мультимедийными объектами и интерактивными кнопками
9.1. Бот, отправляющий в ответ на стикер свой стикер
9.2. Бот, отправляющий в ответ на команду мультимедийный объект в чат
9.3. Бот, работающий с интерактивными кнопками
10. Заключение, полезные ссылки и ресурсы
1. Знакомьтесь, Гаечка :3
Из идеи, описанной выше, родилась Гаечка, программа, реализующая функциональность простейшего дайджест-бота.
Gadget Hackwrench — оригинальное имя персонажа из мультфильма Chip ‘n Dale Rescue Rangers, которое российскими локализаторами было переведено как Гаечка.
Позже Гаечке были добавлены некоторые другие возможности, которые часто были нужны участникам конференции MotoFan.Ru: например, демонстрация различных финансовых графиков, валютные котировки и курс драгоценных металлов по ЦБ РФ. Команда /digest стала понимать аргументы, например, /digest 2 выводит сводку событий за два дня, а /digest 7 за неделю. Но реализация этого функционала — это уже другая история, которая не будет затронута в рамках этой статьи. Поскольку исходный код бота был выложен под лицензией MIT, любой желающий может посмотреть на то как и каким образом работает программа и, кроме того, поучаствовать в её разработке. Ссылку на исходный код можно найти в конце статьи.
2. Подготовка окружения к созданию бота на JavaScript и Node.js
Благодаря тому, что команда разработчиков Telegram реализовала простой и хорошо документированный API для работы с ботами, привязки к различным языкам программирования и популярным современным технологиям не заставили себя ждать. Для реализации задуманной идеи я выбрал связку языка программирования JavaScript и фреймворка Node.js, поскольку для Node.js был доступен хорошо документированный пакет node-telegram-bot-api, являющийся абстракцией и удобной надстройкой над официальным Telegram Bot API. С языком программирования JavaScript я уже сталкивался и немного знаком с ним вкупе с технологией QtQuick/QML. Именно поэтому мной и был выбран Qt Creator в качестве IDE для разработки. Эта среда предоставляет базовую поддержку JavaScript, которую с лихвой хватило для удобной навигации по коду проекта. Кроме самого JavaScript’а меня прельстила простота установки необходимых зависимостей в Node.js с помощью специального менеджера пакетов, а также лёгкость разворачивания приложения на любом GNU/Linux-сервере. Реализация дайджест-бота на языке программирования Python отошла в «долгий ящик» и была написана лишь по просьбе одного из участников конференции, её можно будет посмотреть ниже. Выбор языка и технологии для имплементации вашего бота это дело вкуса, вы сами должны решить что для вас наиболее предпочтительно.
Работа с JavaScript кодом дайджест-бота в Qt Creator IDE (кликабельно).
Итак, подготовим окружение, необходимое для запуска и написания бота. Предполагается, что вы работаете в любом deb-based дистрибутиве GNU/Linux и имеете навык работы с консолью. Пользователи дистрибутивов, отличных от deb-based могут самостоятельно найти аналоги устанавливаемых пакетов в своих репозиториях и инсталлировать их с помощью соответствующего системного пакетного менеджера. Пользователи MS Windows тоже могут произвести инсталляцию необходимых программ, но идеологически правильнее будет развернуть виртуальную машину с любым GNU/Linux дистрибутивом в специальной программе виртуализации, например, в бесплатном VMWare Player.
Установку Node.js и сопутствующего ему пакетного менеджера можно произвести следующими командами, введя их в терминале:
1 2 |
sudo apt-get install nodejs sudo apt-get install npm |
Затем, необходимо создать директорию, в которой можно будет ставить эксперименты и разворачивать бота и перейти в неё. Это тоже проще всего сделать из терминала:
1 2 |
mkdir ~/Deploy/ cd ~/Deploy/ |
Тильда (~) перед названием директории означает обращение к вашему домашнему каталогу. Вы можете создать директорию для экспериментов в любом удобном для вас месте. Теперь в свежесозданный каталог нужно установить необходимые пакеты с помощью пакетного менеджера Node.js:
1 2 |
npm install node-telegram-bot-api npm install request |
Каталог со всеми необходимыми компонентами для запуска бота, эмулятор терминала Konsole.
Заметьте, что пакеты установятся локально в ту директорию, в которой вы находитесь. В нашем случае это ~/Deploy/. Вы можете установить их глобально в систему, для этого у npm install существует специальный ключ −−global. Пакет node-telegram-bot-api, как было сказано выше, необходимая обёртка над официальным Telegram Bot API, связывающая его с Node.js, а пакет request может пригодиться нам для получения различных файлов из Интернета. В общем случае он не нужен и обычно уже идёт в стандартной поставке вместе с Node.js, но это зависит от дистрибутива. Скорее всего вам вовсе не потребуется эта зависимость, но на всякий случай установим и её.
Итак, окружение готово к работе. Теперь ваш компьютер является сервером для различных приложений на Node.js, частным случаем которых являются боты для Telegram. При желании бота можно будет перенести на какой-нибудь вменяемый хостинг, поддерживающий Node.js и запускать его оттуда. Для отладки Node.js приложений можно воспользоваться специальной облачной Web-платформой Cloud9 IDE.
И ещё нужно отметить кое-что важное, существует две разновидности ботов для Telegram: WebHook-боты с подписанным HTTPS-сертификатом и так называемые Polling-боты. Первых ботов дёргает главный сервер Telegram’а, а вторые сами постоянно ходят на сервер и сканируют чаты на предмет новых сообщений в них. Для реализации ботов первого типа нужен специальный валидный HTTPS-сертификат, причём самоподписанный не годится, поэтому рассматривать в этой статье мы их не будем. Для экспериментов и для простеньких ботов с головой хватит и Polling-режима, задержка в полсекунды не слишком критична на мой взгляд. Эти режимы регламентируются официальным Telegram Bot API и имеют полную реализацию во всех популярных пакетах-обёртках, в node-telegram-bot-api в том числе. При желании и наличии подходящего сертификата вы можете переделать Polling-бота в WebHook-бота, использующего HTTPS-сертификат, следуя официальной документации.
На этом теоретическая часть закончена. Переходим к следующим действиям.
3. Создаём профиль бота с помощью @BotFather
Для экспериментов нам потребуется «физическая сущность» бота в сети Telegram, его профиль, который можно добавлять в групповые чаты или просто начать с ним персональную беседу. Бот должен быть связан с запущенной на вашем компьютере программой посредством специального индивидуального и уникального токена. Заметьте, что этот токен предоставляет доступ к «физической сущности» вашего бота, а следовательно должен быть секретным. К счастью, если токен скомпрометирован, функционал Telegram позволяет его отозвать.
Пример создания профиля бота с помощью @BotFather. Установка изображения аватара в профиль.
@BotFather является специальным ботом, который может создавать профили других ботов, а так же выдавать их токены. Для создания собственного профиля будущего бота, просто начните разговор с @BotFather, после чего он добавится вам в список контактов. Небольшой совет: отправив сообщение, содержащее «@BotFather» в любой чат, вы получите ссылку на профиль бота и возможность начать разговор с ним, просто нажав кнопку «Send Message».
Для создания бота @BotFather‘у в чат необходимо отправить команду /newbot, после чего задать его имя, которое будет отображено в профиле и username, через который пользователи Telegram смогут получить ссылку на профиль. Заметьте, что username обязательно должен заканчиваться на «_bot», например, @MySuperTest_bot. Выберите тот username, который нравится вам и не занят. После того, как вы отправите всю необходимую информацию, @BotFather должен выдать вам уникальный токен бота. Рекомендую записать его в текстовый файл, сохранить и спрятать куда-нибудь в укромное местечко.
Общие команды бота вместе с описанием, установленные с помощью /setcommands и быстрый доступ к ним.
Далее, отправляя @BotFather‘у команды, начинающиеся с «/set» можно настроить различные параметры бота, например: установить ему имя, аватар, текст в профиль и описание. Всё достаточно интуитивно и прозрачно, но есть пара важных моментов.
Разберёмся сначала с командой /setcommands; она устанавливает команды, которые будут отображены в чате по нажатию специальной пиктограммы быстрого доступа (см. изображение выше). Кроме того, пользователь может быстро выбрать необходимую ему команду бота во всплывающей подсказке, просто набрав в чате «/». Весьма важно заполнить эту информацию, пример формата заполнения следующий:
1 2 3 4 |
digest - Get digest from current chat. rouble - Get current exchange rate of RUB. grivna - Get current exchange rate of UAH. help - Extended help for bot commands. |
Рекомендую сохранить текст описания команд в какой-нибудь файл, так как при изменении этого списка вам придётся его отправлять заново.
Следующие команды, на которые надо обратить особое внимание, это /setjoingroups и /setprivacy. Первая команда даёт возможность пользователям добавлять вашего бота в групповые чаты (если мне не изменяет память, по дефолту эта возможность включена), а вторая запрещает боту просматривать все сообщения в группе, кроме команд. Обязательно отключите эту способность, так как для дайджест-бота критически важна возможность мониторинга всех сообщений пользователей на предмет присутствия в них тега #digest. В общем, выставите настройки так, чтобы пользователи могли добавлять бота в групповые чаты (/setjoingroups в ENABLED) и бот мог мониторить сообщения в них (/setprivacy в DISABLED).
Теперь у нас есть токен и настроенный профиль. Самое время взяться за написание Hello World!-бота!
4. Реализация простейшего Hello World!-бота на JavaScript
Перейдём в созданную ранее директорию ~/Deploy/, создадим там файл «HelloWorldBot.js» с помощью любого, удобного вам текстового редактора с поддержкой и подсветкой синтаксиса JavaScript. Вставим в этот файл следующий текст:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
var TelegramBot = require('node-telegram-bot-api'); var token = 'WRITE_YOUR_TOKEN_HERE'; var botOptions = { polling: true }; var bot = new TelegramBot(token, botOptions); bot.getMe().then(function(me) { console.log('Hello! My name is %s!', me.first_name); console.log('My id is %s.', me.id); console.log('And my username is @%s.', me.username); }); bot.on('text', function(msg) { var messageChatId = msg.chat.id; var messageText = msg.text; var messageDate = msg.date; var messageUsr = msg.from.username; if (messageText === '/say') { sendMessageByBot(messageChatId, 'Hello World!'); } console.log(msg); }); function sendMessageByBot(aChatId, aMessage) { bot.sendMessage(aChatId, aMessage, { caption: 'I\'m a cute bot!' }); } |
Вместо WRITE_YOUR_TOKEN_HERE не забудьте вписать выданный вам токен, соблюдая синтаксис, то есть аккуратно заключив его в кавычки. Сохраним файл и пройдёмся по коду с комментариями.
Самая первая строка указывает на то, что необходимо использовать пакет node-telegram-bot-api, уже развёрнутый нами в директорию ~/Deploy/. Далее идёт описание необходимых боту аргументов и опций: это ваш токен и опция polling, определяющая разновидность бота. В данном случае эта опция говорит о том, что бот будет мониторить чат сам, без помощи главного сервера Telegram. Далее создаётся объект bot, с которым мы и будем работать. Вызов функции getMe().then() происходит при успешной авторизации токена бота и выдачи управления программе. При этом в качестве аргумента функция getMe().then() принимает функцию function(me) первым аргументом которой является объект, с помощью которого можно получить различную информацию о вашем боте и, в данном случае, вывести её в консоль. Далее, самая главная функция bot.on(‘text’, …) вызывается когда бот видит чьё-либо сообщение в чате. В качестве первого аргумента она принимает тип отслеживаемого сообщения (об этом можно подробнее почитать в документации node-telegram-bot-api), а в качестве второго аргумента она принимает функцию function(msg) первым аргументом которой является объект, с помощью которого можно «вынуть» различную информацию из полученного сообщения. Именно этим и занимаются нижеследующие строки. В переменной messageChatId сохраняется id чата, из которого пришло сообщение; в messageText — текст; в messageDate — дата, в формате: секунды, прошедшие с UNIX-time; в messageUsr — ник участника, отправившего сообщение.
Далее текст сообщения проверяется на соответствие команде /say; и если пользователь действительно отправил эту команду, вызывается функция sendMessageByBot(), являющаяся обёрткой метода bot.sendMessage(). В функцию sendMessageByBot() необходимо передать два аргумента: первый — id чата, в который нужно отправить сообщение, а второй — собственно, текст отправляемого сообщения. В нашем случае id чата, в который нужно доставить сообщение, это id чата из которого и была получена команда /say. Далее для отладки можно вывести объект msg со всеми его полями прямо в стандартный вывод консоли.
Надеюсь, с этим всё понятно. Теперь необходимо просто запустить нашего бота на исполнение. Для этого в терминале, находясь в директории ~/Deploy/, набираем команду:
nodejs HelloWorldBot
В консоль при удачном соединении и успешной авторизации будет выведена информация о профиле вашего бота. Если этого не произошло, то вы где-то ошиблись. Проверьте интернет-соединение на компьютере, где запускаете программу и корректный ввод токена.
Демонстрация Hello World!-бота, отвечающего на команду /say.
Если всё получилось, программа останется на выполнении. Теперь в клиенте Telegram можно начать диалог с ботом или добавить его в группу. Если боту отправить команду /say, то он выдаст вам в чат сообщение, содержащие текст «Hello World!» (см. изображение выше). При этом на любые сообщения бот должен реагировать выводом полей объекта msg в консоль.
Для остановки работы бота и выхода из приложения просто нажмите сочетание клавиш «Ctrl + C», когда окно терминала будет в фокусе.
На этом всё, теперь можно переходить к созданию более сложного дайджест-бота.
5. Реализация дайджест-бота на JavaScript
Для реализации функциональсти, описаной в самом начале статьи, было придумано следующее: имеется глобальный лист, образно именуемый стеком, куда будут записаны сообщения с тегом #digest. Глобальный он потому, что при каждом вызове функции bot.on(‘text’, …) в него происходит (или не происходит) добавление соответствующего сообщения или его модификация. Тут вся сложность в том, что бота можно добавить в несколько чатов сразу и в каждом он должен корректно отработать, собирая все сообщения из чатов с вышеупомянутым тегом и корректно (то есть каждому чату именно ему принадлежащие сообщения) выводить соответствующую информацию. В начале планировалось создавать индивидуальные листы, содержащие сообщения для каждого чата индивидуально, но позже пришла мысль использовать лишь один лист, добавляя в него кроме, непосредственно, сообщения ещё и различную дополнительную информацию вроде id чата и даты, благо node-telegram-bot-api позволяет это сделать. Да и возни будет меньше, так как ответ станет формироваться на выходе, где в вывод будут добавлены лишь те сообщения, которые соответствуют id того чата, из которого пришла команда /digest.
Простая схема работы дайджест-бота с листом.
Далее, нужно помнить, что наш стек может неплохо так раздуться, если в него постоянно будут добавлять сообщения из разных чатов. Поэтому необходимо придумать механизм, который будет периодически очищать стек, удаляя неактуальные сообщения. Пусть этот механизм будет работать следующим образом: пользователь, который ввёл команду /digest последним, реформирует стек, удаляя из него устаревшую информацию. Механизм удаления пусть будет следующий: у последней команды /digest берётся время и из этого времени вычитается какая-то фиксированная величина-задержка в секундах, к примеру сутки или неделя, после чего с помощью цикла перебираются все сообщения в стеке и используя сравнение по времени, вычисляется граница (в конкретном случае количество устаревших сообщений «снизу», или порядковый номер граничного сообщения), которая разделяет стек на две части из устаревших и актуальных сообщений. Далее лист режется прямиком по этой границе на две половинки, часть с устаревшими сообщениями выкидывается вместе с ними, а часть с актуальными сообщениями становится новым стеком. Но тут, конечно, нужно помнить, что имеется целых три различных варианта развития событий:
- Во-первых, это вариант, описанный выше, то есть граница чётко определена, лист делится на две части, старый стек замещается новым, а старые сообщения выбрасываются;
- Во-вторых, если получилось так, что количество устаревших сообщений равно нулю, то не нужно производить никаких манипуляций со стеком и оставить то, что есть, так как все сообщения в нём актуальны;
- В-третьих, если количество устаревших сообщений равно размеру стека, то есть получилось так, что все сообщения устарели, стек нужно полностью обнулить, а сообщения выкинуть.
Ситуация, когда пользователи постоянно пишут сообщения с тегом #digest, но не вызывают команду /digest маловероятна, поэтому опустим этот случай. При желании, конечно, можно перенести механизм (в конкретном случае одну функцию) и в обработчик, который срабатывает при получении #digest-тега, но таким образом реформация стека будет происходить уж больно часто. В описаной выше функциальности любой пользователь любого чата неявно реформирует стек при каждом вызове команды /digest, удаляя все устаревшие сообщения всех чатов, за которыми следит бот.
Программа-симулятор логики работы со стеком, описанной выше.
После того, как эта функциональность была обдумана в голове, мне захотелось проверить её в боевых действиях. Но, увы, меня подвёло моё плохое интернет-соединение. Поэтому вечером, будучи в «offline», мой воспалённый идеей мозг, накидал простенький симулятор логики на C++ и Qt (см. изображение выше). Два окна с текстом в них — имитируют два активных чата, в которые добавлен бот, реагирующий на команду /digest и тег #digest. Кнопки «Start/Stop Discussion» запускают симуляцию активной беседы в необходимом чате (простой генератор случайных чисел), иногда среди обычных сообщений попадаются имеющие тег #digest, которые старательно складываются ботом в структуру, записываемую в контейнер QList. При этом я могу симулировать поведение пользователя, вводя в текстовые поля сообщения или команду /digest, реформируя QList и получая ответ бота. Исходные тексты этой программки можно найти здесь. Утром, после того как моё интернет-соединение пришло в норму, я просто начал портировать уже готовое решение на JavaScript.
Я не буду публиковать здесь полный листинг реализации дайджест-бота, он слишком большой. Лучше ниже дам ссылку на исходные коды актуальной версии. Но рассмотреть работу некоторых важных механизмов в исходном коде, я думаю, будет полезно.
5.1. Обработчик тега #digest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
bot.on('text', function(msg) { ... if (messageText.indexOf('#digest') >= 0) { var normalMessage = normalizeMessage(messageText); if (!(isBlank(normalMessage))) { var messageInfoStruct = { 's_chatID': messageChatId, 's_date': messageDate, 's_message': normalMessage, 's_username': globalUserNameIs }; globalStackListDigestMessages.push(messageInfoStruct); // Send message by bot. sendMessageByBot(messageChatId, 'Added!'); } } ... } |
Тут всё просто. Если сообщение содержит тег #digest, то начинает выполнятся тело условия. В переменную normalMessage помещается нормализированное сообщение, которое возвращает функция normalizeMessage(); она модифицирует полученное сообщение, например, удаляет из него тег, «разряжает» пробелы, меняет регистр первой буквы на заглавную и проводит ещё несколько различных манипуляций. Если сообщение после нормализации вернётся не пустым (например, сообщение, состоящее из одного тега #digest, после нормализации будет пустым), то создаётся простенькая структурка, в которую добавляется вся необходимая информация, после чего структурка закидывается в стек-лист. Затем отправляется сообщение в чат об успешном добавлении информации в стек.
5.2. Обработчик команды /digest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
bot.on('text', function(msg) { ... if (messageText === '/digest') { // Digest delay. // 45 sec for debug. // 43 200 for 12-hours. // 86 400 for 24-hours. // 172 800 for 48-hours. var hourDelay = 86400; var bSendDigest = false; if (globalStackListDigestMessages.length > 0) { // Delete all obsolete digest messages from globalStackListDigestMessages bSendDigest = deleteObsoleteDigestMessages(messageDate - hourDelay); } // Generate Bot Answer if (bSendDigest) { ... } else { sendNoDigestMessages(messageChatId); } } ... } |
Формально обработчик можно разделить на две части, собственно, работу по актуализации стека и генерацию ответа на команду. Последнее мы рассмотрим немного позже. Как уже было сказано выше, здесь вводится задержка, в конкретном случае суточная, результат вычитания которой из даты принятого сообщения отправляется в функцию deleteObsoleteDigestMessages(), от возвращаемого значения которой зависит будет ли сгенерирован ответ, или же будет выполнена функция sendNoDigestMessages(), отправляющая в чат информацию об отсутствии актуальных сообщений на данный момент.
5.3. Функция deleteObsoleteDigestMessages()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
function deleteObsoleteDigestMessages(aObsoleteDate) { var stackSize = globalStackListDigestMessages.length; var position = 0; for (var i = 0; i < stackSize; ++i) { if (globalStackListDigestMessages[i].s_date < aObsoleteDate) { position++; } } // All stack digest messages are obsolete. // Drop stack. if (position == stackSize) { globalStackListDigestMessages = [ ]; return false; } // All stack digest messages are relevant. // Print them. if (position == 0) { return true; } // Replace current digest stack by sliced. globalStackListDigestMessages = globalStackListDigestMessages.slice(position); // Return true if stack not empty return stackSize > 0; } |
Реализация уже описанной выше работы со стеком. Комментарии говорят сами за себя, что либо объяснять здесь я считаю излишним.
5.4. Генерация ответа бота на команду /digest
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
if (bSendDigest) { var botAnswer = ''; var endLineString = ';\n'; var stackSize = globalStackListDigestMessages.length; // Count of digest messages from one chat. var countOfDigestMessagesByChat = getCountDigestMessagesOfChat(messageChatId); // Append answer string. for (var i = 0; i < stackSize; ++i) { if (globalStackListDigestMessages[i].s_chatID === messageChatId) { botAnswer += catchPhrases.digestMarker + globalStackListDigestMessages[i].s_message + endLineString; } } // Delete last new line and semicolon characters (;\n). botAnswer = botAnswer.substring(0, botAnswer.length - 2); // Add dot to end of line. if (botAnswer.substr(botAnswer.length - 1) !== '.') { botAnswer += '.'; } // Check countOfDigestMessagesByChat. if (countOfDigestMessagesByChat > 0) { // Send botAnswer to chat with catchPhrases chunks. sendMessageByBot(messageChatId, getDigestReportHeader() + botAnswer); } else { sendNoDigestMessages(messageChatId); } } |
Генерация ответа очень проста: в строку botAnswer в цикле добавляются все подходящие сообщения из стека, вместе с форматированием. Функция getCountDigestMessagesOfChat() возвращает в переменную countOfDigestMessagesByChat количество сообщений с тегом для чата из которого была получена команда. Условие внутри цикла регламентирует добавление в botAnswer лишь тех сообщений, id которых совпадает с id чата, из которого была получена команда /digest. Потом в конец botAnswer добавляется точка, если это нужно и botAnswer отправляется в чат, если переменная countOfDigestMessagesByChat больше нуля.
5.5. Заключение по дайджест-боту
Эти функции помогут понять устройство бота и подскажут, как реализовать его самостоятельно. Мою реализацию и полный исходный код вы можете посмотреть по этой ссылке. Там же найдёте и простую инструкцию по установке и разворачиванию. Бот Гаечка, описанный в первом пункте этой статьи, является усложнённой имплементацией дайджест-бота.
Update 06-MAR-2017: Прочитать ещё немного информации по поводу дайджест-бота Gadget Hackwrench можно в разделе этой статьи.
Далее перейдём к описанию простого валютного бота.
6. Реализация валютного бота на JavaScript
Смысл валютного бота состоит в том, чтобы брать актуальные валютные котировки с сайта ЦБ РФ и отправлять их в чат. Создадим файл «Currency.js» в директории ~/Deploy/ и заполним его следующим содержимым:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
var TelegramBot = require('node-telegram-bot-api'); var http = require('http'); var token = 'WRITE_YOUR_TOKEN_HERE'; var botOptions = { polling: true }; var bot = new TelegramBot(token, botOptions); var options = { host: "www.cbr.ru", port: 80, path: "/scripts/XML_daily.asp?" }; var content = ""; bot.getMe().then(function(me) { console.log('Hello! My name is %s!', me.first_name); console.log('My id is %s.', me.id); console.log('And my username is @%s.', me.username); }); bot.on('text', function(msg) { var messageChatId = msg.chat.id; var messageText = msg.text; var messageDate = msg.date; var messageUser = msg.from.username; if (messageText.indexOf('/currency') === 0) { updateGlobalCurrencyList(messageChatId); } }); function sendMessageByBot(aChatId, aMessage) { bot.sendMessage(aChatId, aMessage, { caption: 'I\'m a cute bot!' }); } function updateGlobalCurrencyList(aMessageChatId) { var req = http.request(options, function(res) { res.setEncoding("utf8"); res.on("data", function(chunk) { content += chunk; }); res.on("end", function() { sendMessageByBot(aMessageChatId, shittyParseXML(content)); }); }); req.end(); } function generateBotAnswer(aCurrencyList) { var currencyTable = 'CURRENCY:\n'; currencyTable += '1 USD = ' + aCurrencyList.USD + ' ' + 'RUB' + ';\n'; currencyTable += '1 EUR = ' + aCurrencyList.EUR + ' ' + 'RUB' + ';\n'; currencyTable += '1 UAH = ' + aCurrencyList.UAH + ' ' + 'RUB' + ';\n'; currencyTable += '1 KZT = ' + aCurrencyList.KZT + ' ' + 'RUB' + ';\n'; currencyTable += '1 BYR = ' + aCurrencyList.BYR + ' ' + 'RUB' + ';\n'; currencyTable += '1 GBP = ' + aCurrencyList.GBP + ' ' + 'RUB' + '.'; return currencyTable; } function shittyParseXML(aAllXml) { var currencyList = { 'USD': 0.0, 'EUR': 0.0, 'UAH': 0.0, 'KZT': 0.0, 'BYR': 0.0, 'GBP': 0.0 }; currencyList.USD = getCurrentValue('USD', aAllXml); currencyList.EUR = getCurrentValue('EUR', aAllXml); currencyList.UAH = getCurrentValue('UAH', aAllXml); currencyList.KZT = getCurrentValue('KZT', aAllXml); currencyList.BYR = getCurrentValue('BYR', aAllXml); currencyList.GBP = getCurrentValue('GBP', aAllXml); return generateBotAnswer(currencyList); } function getCurrentValue(aCurrency, aString) { var nominal = parseFloat(replaceCommasByDots(getStringBelow(aString.indexOf(aCurrency), 1, aString))); var value = parseFloat(replaceCommasByDots(getStringBelow(aString.indexOf(aCurrency), 3, aString))); return (value / nominal).toFixed(4); } function removeTags(aString) { return aString.replace(/(<([^>]+)>)/ig, ''); } function getStringBelow(aStart, aBelow, aString) { var textSize = aString.length; var countOfLineEndings = 0; var getLineWith = 0; for (var i = aStart; i < textSize; ++i) { if (countOfLineEndings === aBelow) { getLineWith = i; break; } if (aString[i] === '\n') { countOfLineEndings++; } } return getLineFromXml(getLineWith, aString); } function replaceCommasByDots(aString) { return aString.replace(',', '.'); } function getLineFromXml(aStart, aString) { var textSize = aString.length; var targetString = ''; for (var i = aStart; i < textSize; ++i) { if (aString[i] === '\n') { break; } targetString += aString[i]; } return removeTags(targetString.trim()); } |
Конечно же не забываем вместо WRITE_YOUR_TOKEN_HERE вписать свой токен. Далее сохраним файл и пройдёмся по коду с комментариями.
Демонстрация работы валютного бота, ответ на команду /currency.
Во-первых, нам нужно подключить пакет http, поскольку потребуется получить XML-файл с котировками, расположенный на сервере ЦБ РФ по адресу http://www.cbr.ru/scripts/XML_daily.asp?. Опции доступа к этому хосту описаны в переменной options. В глобальную переменную content мы будем сохранять ответ сервера, содержащий XML. Далее по коду отлично видно, что бот мониторит все сообщения и в том случае, если кто-то отправит команду /currency, начнёт выполняться функция updateGlobalCurrencyList(). В ней создаётся асинхронный HTTP-запрос, в результате выполнения которого в переменную content загрузятся XML-данные. По окончанию запроса необходимо распарсить XML, и отправить его в чат, что и происходит в функции res.on(«end», …). К сожалению, в языке программирования JavaScript и фреймворке Node.js отсутствуют стандартные средства для работы с XML. Поэтому для парсинга я решил не тянуть лишнюю зависимость в Node.js, а попытаться найти закономерности в файле и распарсить его костылём. Предупреждаю сразу, что лучше всего всё сделать по уму и воспользоваться библиотекой. Функция shittyParseXML() принимает в качестве единственного аргумента сырые XML-данные и работает следующим образом: нам требуется в XML найти какие-нибудь уникальные значения, чтобы привязаться к ним и работать от них. Такие данные действительно есть, это поле «CharCode», которое характеризует уникальный идентификатор валюты. Если от строки, содержащей идентификатор отступить вниз на одну, мы получим валютный номинал, а если отступить на три, то получим значение. Разделив значение на валютный номинал, мы получим другое значение, которое будет соответствовать эквиваленту одной какой-нибудь валюты. Пройдя по цепочке вызовов из shittyParseXML() обозначим наиболее интересные функции и их описание:
- getStringBelow(i, j, data_string) — собственно, спуститься от строки «i» вниз на «j» в буфере «data_string»;
- getLineFromXml(i, data_string) — возвращает строку под номером «i» в буфере «data_string»;
- removeTags(string) — принимает строку «string» с тегами и возвращает строку без тегов;
- replaceCommasByDots(string) — функция заменяет в строке «string» запятые на точки, что очень важно для глобальной функции parseFloat().
Интересный факт: этот велосипедный парсер отлично справился с XML-кой, содержащей котировки похожего формата, полученной с одного украинского финансового сайта. Но это скорее всего счастливая случайность и совпадение, чем ожидаемое поведение.
Теперь можно запустить валютного бота на исполнение. Для запуска, находясь в директории ~/Deploy/, набираем команду:
nodejs Currency
Если завести диалог с ботом и послать ему команду /currency, то в ответ он должен отправить в чат курс валют сегодняшнего дня по ЦБ РФ (см. изображение выше). Выключить бота можно сочетанием клавиш «Ctrl + C» в окне терминала.
Далее следует реализация ещё одного простенького дайджест-бота на языке программирования Python. Если вам это нисколько не интересно, можете полностью пропустить следующий раздел.
7. Реализация упрощённого дайджест-бота на Python
Один из участников конференции MotoFan.Ru, скрывающийся под ником @J()KER, попросил меня разобраться с twx.botapi и накидать реализацию какого-нибудь бота на языке программирования Python; twx.botapi — одна из двух популярных реализаций Telegram Bot API, которая максимально использует ООП (объектно-ориентированное программирование), но, к сожалению, имеет весьма скудную документацию. Вторая реализация называется pyTelegramBotAPI, она несколько проще для понимания и её документация снабжена примерами, хорошо демонстрирующими ту или иную фичу реализации. К сожалению, о pyTelegramBotAPI я узнал уже после того, как написал бота на twx.botapi. Но выбор подходящей реализации Telegram Bot API, это скорее всего дело обычного вкуса; кому-то twx.botapi может понравится больше, следовательно наш бот будет использовать именно его.
Я не в первый раз сталкиваюсь с языком программирования Python, раньше я работал с ним вкупе с технологией PyQt. Это очень приятный и выразительный язык, хотя меня немного напрягает неявная типизация совмещённая со строгой и зависимость от отступов. Мои знания Python весьма поверхностны, однако SODD (Stack Overflow Driven Development) и всеобъемлющая документация помогают очень быстро разобраться и сделать работающий прототип буквально за 20-30 минут. Просто портируем основную логику JavaScript дайджест-бота, написанного ранее, на язык программирования Python. Естественно, будет использоваться Polling-режим.
Реализация twx.botapi позволяет использовать как Python второй версии, так и третьей. Мы для написания бота будем использовать Python 3, для запуска которого требуется немного настроить окружение (далее предполагается что вы работаете в deb-based дистрибутиве). В начале устанавливаем собственно сам Python 3 и его пакетный менеджер:
sudo apt-get install python3 python3-pip
Затем с помощью Python’овского пакетного менеджера устанавливаем пакет twx.botapi:
pip3 install twx.botapi
Теперь можно зайти в любую удобную директорию, например, в ~/Deploy/ и создать в ней текстовой файл с именем «DigestBot.py», в который вставить и сохранить следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
#!/usr/bin/python # -*- coding: utf-8 -*- from __future__ import unicode_literals from twx.botapi import TelegramBot import traceback class DigestBot(object): token = 'WRITE_YOUR_TOKEN_HERE' stack_list = [] admin = 'exlmoto' def __init__(self): self.bot = TelegramBot(self.token) self.bot.get_me() last_updates = self.bot.get_updates(offset=0).wait() try: self.last_update_id = list(last_updates)[-1].update_id except IndexError: self.last_update_id = None print('last update id: {0}'.format(self.last_update_id)) def process_message(self, message): text = message.message.text chat = message.message.chat text = text.strip() digest_tag = '#digest' print('Got message: \33[0;32m{0}\33[0m from chat: {1}'.format(text, chat)) try: if text == '/digest': bot_answer = 'There is your digest:\n' try: for struct in self.stack_list: if struct['chat_id'] == chat.id: bot_answer += struct['chat_message'] bot_answer += '\n' bot_answer = bot_answer[:-1] bot_answer += '.' self.bot.send_message(chat.id, bot_answer) except Exception: self.bot.send_message(chat.id, 'Unknow error. Sorry.') if text == '/stackView': list_answer = 'There is my stack list:\n' try: if message.message.sender.username == self.admin: for (index, d) in enumerate(self.stack_list): list_answer += str(index + 1) list_answer += ' ' + str(d['chat_id']) list_answer +=' ' + d['chat_message'] list_answer += '\n' list_answer = list_answer[:-1] self.bot.send_message(chat.id, list_answer) else: raise Exception('You do not access for this function.') except Exception as ex_acc: answer = ex_acc.args self.bot.send_message(chat.id, answer) if digest_tag in text: try: text = text.replace(digest_tag, '') text = text.strip() struct = { 'chat_id': chat.id, 'chat_message': text } self.stack_list.append(struct.copy()) self.bot.send_message(chat.id, 'Done. I append your digest-message in my stack list.') except Exception: self.bot.send_message(chat.id, 'There is error. Sorry.') except Exception: pass def run(self): print('Main loop started') while True: updates = self.bot.get_updates(offset=self.last_update_id).wait() try: for update in updates: if int(update.update_id) > int(self.last_update_id): self.last_update_id = update.update_id self.process_message(update) except Exception as ex: print(traceback.format_exc()) if __name__ == '__main__': try: DigestBot().run() except KeyboardInterrupt: print('Exiting...') |
Только не забудьте WRITE_YOUR_TOKEN_HERE заменить на полученный токен бота. Итак, пробежимся по коду.
Строка from __future__ import unicode_literals говорит о том, что в нашей программе будет использоваться Unicode; это действительно так: сообщения из чата могут приходить на различных языках. Далее идёт подключение пакета twx.botapi и если скрипт запущен как независимая программа (выражение if __name__ == ‘__main__’), начинается построение класса DigestBot с полями token, admin и листом stack_list. Первый метод, который проинициализирует должным образом переменные, называется __init__(self), он является неким аналогом конструктора, но называть его конструктором несколько неправильно, так как к моменту вызова этого метода объект уже будет создан и вы будете иметь ссылку на созданный экземпляр класса. В этом методе в API отправляется ваш токен, вызывается метод get_me(), тестирующий авторизационный токен вашего бота и с помощью метода get_updates() в переменную last_updates возвращается массив объектов обновления. Если этот массив не пустой, то в поле класса last_update_id устанавливается update_id последнего элемента массива last_updates.
Далее у экземпляра класса вызывается метод run(), в котором реализован главный бесконечный цикл, в теле которого: в переменную updates возвращается массив объектов обновления начиная со значения переменной last_update_id определённой в __init__(self), затем во вложенном цикле выполняется проверка значений различных update_id, и если update_id прилетевшего события больше, чем тот, что был сохранён в поле класса, то поле класса будет обновлено, а сообщение обработано методом process_message(self, message). В теле которого в различные переменные выдирается сообщение и происходят стандартные преобразования, описанные выше в разделе про создание дайджест-бота на JavaScript: при получении тега #digest в лист добавляется структура с информацией о сообщении; при получении команды /digest идёт процесс формирования ответа. В этой версии дайджест-бота стек-лист никем не модифицируется, но при желании, конечно, можно реализовать аналог того, что было написано на JavaScript; просто я решил не раздувать листинги исходного кода. Дату сообщения, судя по документации twx.botapi, можно выдрать с помощью message.message.date в аналогичном UNIX-формате. Ещё стоит упомянуть обработку команды /stackView, которая отображает всё содержимое листа с данными изо всех чатов. Поэтому этой командой может воспользоваться только администратор, ник которого прописан в поле класса admin.
Для запуска дайджест-бота в консоли следует перейти в директорию с файлом «DigestBot.py» и набрать следующую команду в терминале:
python3 DigestBot.py
Завершить процесс можно сочетанием клавиш «Ctrl + C» в окне терминала.
Далее будет описано создание бота на JavaScript, который отправляет случайную цитату в чат с bash.im, главного цитатника Рунета.
8. Реализация бота, отправляющего случайную цитату с bash.im, на JavaScript
Пользователи конференций часто хотят развлечься и прочитать случайную цитату из главного цитатника Рунета, сайта bash.im. Давайте реализуем такой функционал у бота. Сперва в директории ~/Deploy/ создадим файл с именем «Bash.js» и вставим в него следующее содержимое:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
var TelegramBot = require('node-telegram-bot-api'); var http = require('http'); var iconv = require('iconv-lite'); var token = 'WRITE_YOUR_TOKEN_HERE'; var botOptions = { polling: true }; var bot = new TelegramBot(token, botOptions); var options = { host: "bash.im", port: 80, path: "/forweb/" }; bot.getMe().then(function(me) { console.log('Hello! My name is %s!', me.first_name); console.log('My id is %s.', me.id); console.log('And my username is @%s.', me.username); }); bot.on('text', function(msg) { var messageChatId = msg.chat.id; var messageText = msg.text; var messageDate = msg.date; var messageUser = msg.from.username; if (messageText.indexOf('/bash') === 0) { sendRandomBashImQuote(messageChatId); } }); function sendRandomBashImQuote(aMessageChatId) { http.get(options, function(res) { res.pipe(iconv.decodeStream('win1251')).collect(function(err, decodedBody) { var content = getQuoteBlockFromContent(decodedBody); content = removeAllMarkUp(content[1]); sendMessageByBot(aMessageChatId, content); }); }); } function removeAllMarkUp(aString) { var cleanQuote = replaceAll(aString, "<' + 'br>", '\n'); cleanQuote = replaceAll(cleanQuote, "<' + 'br />", '\n'); cleanQuote = replaceAll(cleanQuote, '"', '\"'); cleanQuote = replaceAll(cleanQuote, '<', '<'); cleanQuote = replaceAll(cleanQuote, '>', '>'); return cleanQuote; } function replaceAll(aString, aFingString, aReplaceString) { return aString.split(aFingString).join(aReplaceString); } function getQuoteBlockFromContent(aString) { var quoteBlock = aString.replace('<\' + \'div id="b_q_t" style="padding: 1em 0;">', '__the_separator__'); quoteBlock = quoteBlock.replace('<\' + \'/div><\' + \'small>', '__the_separator__'); return quoteBlock.split('__the_separator__'); } function sendMessageByBot(aChatId, aMessage) { bot.sendMessage(aChatId, aMessage, { caption: 'I\'m a cute bot!' }); } |
Естественно, WRITE_YOUR_TOKEN_HERE заменяем своим токеном. Сохраним файл и немного рассмотрим его.
Демонстрация бота, отправляющего случайную цитату с bash.im
Случайную цитату можно получить по ссылке bash.im/forweb/. К сожалению, необходимая информация выдаётся в кодировке Windows-1251, поэтому нам нужно преобразовать ответ сервера в UTF-8. Для работы с кодировками существует несколько пакетов, например, node-iconv, но нам подойдёт самый компактный из них — iconv-lite. Устанавливаем его в директорию ~/Deploy/ с помощью пакетного менеджера Node.js:
npm install iconv-lite
В листинге кода выше, этот пакет подключается третьей строкой. Далее всё достаточно стандартно: при отправке команды /bash в чат, бот вызывает у себя функцию sendRandomBashImQuote(), в которой создаёт запрос и конвертирует полученную информацию в UTF-8 (см. документацию для более подробного изучения работы этой функции). Далее из результата ответа сервера, хранящегося в переменной decodedBody уже в правильной кодировке, вычленяется блок, отвечающий за саму цитату с помощью функции getQuoteBlockFromContent(). Эта функция возвращает массив, который сохраняется в переменную content, второй элемент которого (по индексу [1]) отправляется в функцию removeAllMarkUp, отвечающую за удаление всей HTML-разметки. Результат выполнения функции снова записывается в content и после отправляется в чат.
Для запуска бота, находясь в директории ~/Deploy/, следует выполнить команду:
nodejs Bash
Теперь, если завести диалог с ботом и написать команду /bash в чат, то в ответ он должен отправить случайную цитату с вышеупомянутого сайта (см. изображение выше). Если часто отсылать команду /bash, то можно заметить, что цитата не изменилась. За выдачу рандомной цитаты отвечает сервер bash.im, поэтому вряд ли что-то здесь можно сделать. Разве что прикрутить задержку. Выключить бота можно нажав сочетание клавиш «Ctrl + C» в окне терминала.
В следующем разделе статьи будут описаны примеры исходного кода JavaScript-ботов, работающих с такими объектами, как изображения, видео, аудио, стикеры и интерактивные кнопки.
9. Реализация JavaScript-ботов, работающих со стикерами, мультимедийными объектами и интерактивными кнопками
Помимо отправки и получения простой текстовой информации, боты могут анализировать и другие события, вроде получения изображений или стикеров. Соответственно, так же как и пользователи, они могут отправлять различные мультимедийные объекты в чат. Кроме того, у ботов имеется интересная возможность создавать у пользователей в клиенте Telegram под полем ввода специальные интерактивные кнопки, нажимая на которые участник конференции может выбрать какое-либо необходимое ему действие. Ниже мы рассмотрим примеры таких ботов, а в заключительной части статьи я перечислю ресурсы с полезной информацией, которая вам пригодится, если вы заинтересуетесь созданием ботов для Telegram.
9.1. Бот, отправляющий в ответ на стикер свой стикер.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
// StickerBot.js var TelegramBot = require('node-telegram-bot-api'); var token = 'WRITE_YOUR_TOKEN_HERE'; var botOptions = { polling: true }; var bot = new TelegramBot(token, botOptions); var stickersList = [ 'BQADAgADfgADEag0Bb5mxH0gvtktAg', 'BQADAgADowADEag0BQs_xQSkcIFKAg', 'BQADAgAD1wADEag0BTEYGb09JERjAg', 'BQADAgAD5wADEag0BZAwDWvpwGrtAg', 'BQADAgADxQADEag0BRBpCE1JOT4sAg', 'BQADAgADwwADEag0BbGlUZ12nxZ8Ag', 'BQADAgADvwADEag0Bf5nBjEjQyUYAg', 'BQADAgADyQADEag0BYauZXVnHFqOAg' ]; bot.getMe().then(function(me) { console.log('Hello! My name is %s!', me.first_name); console.log('My id is %s.', me.id); console.log('And my username is @%s.', me.username); }); bot.on('sticker', function(msg) { var messageChatId = msg.chat.id; var messageStickerId = msg.sticker.file_id; var messageDate = msg.date; var messageUsr = msg.from.username; sendStickerByBot(messageChatId, stickersList[getRandomInt(0, stickersList.length)]); console.log(msg); }); function getRandomInt(aMin, aMax) { return Math.floor(Math.random() * (aMax - aMin + 1)) + aMin; } function sendStickerByBot(aChatId, aStickerId) { bot.sendSticker(aChatId, aStickerId, { caption: 'I\'m a cute bot!' }); } |
У каждого стикера, загруженного на серверы Telegram, существует его уникальный идентификатор file_id, который можно увидеть, выведя объект msg в консоль. Отправляя этот идентификатор в качестве одного из аргументов функции bot.sendSticker(), мы по сути отправляем этот стикер в чат с помощью бота. Логика программы простая: в ответ на любой стикер, полученный из чата, бот отправит в ответ случайный стикер из листа stickersList, где перечислены их уникальные идентификаторы. Заметьте, что аргументом API-метода может быть и подходящий под понятие стикера файл (например, формата webp с разрешением 512×512).
9.2. Бот, отправляющий в ответ на команду мультимедийный объект в чат.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// MultimediaBot.js var TelegramBot = require('node-telegram-bot-api'); var request = require('request'); var token = 'WRITE_YOUR_TOKEN_HERE'; var botOptions = { polling: true }; var bot = new TelegramBot(token, botOptions); bot.getMe().then(function(me) { console.log('Hello! My name is %s!', me.first_name); console.log('My id is %s.', me.id); console.log('And my username is @%s.', me.username); }); bot.on('text', function(msg) { var messageChatId = msg.chat.id; var messageText = msg.text; if (messageText === '/photo') { var photo = request('http://www.google.com/images/srpr/logo3w.png'); bot.sendPhoto(messageChatId, photo, { caption: 'Image:' }); } if (messageText === '/audio') { var audio = __dirname + '/audio.ogg'; bot.sendAudio(messageChatId, audio, { caption: 'Audio:' }); } if (messageText === '/file') { var url2 = 'http://exlmoto.ru/wp-content/Packages/stransball2.mgx'; var file = request(url2); bot.sendDocument(messageChatId, file, { caption: 'Document:' }); } }); |
Telegram Bot API позволяет отправлять как локальные мультимедийные объекты и файлы, так и удалённые. Для того, чтобы иметь возможность отправлять удалённые объекты, полученные по URL, требуется подключить пакет request, о котором и говорилось в начале статьи. Список всех доступных send-методов можно посмотреть здесь. Помимо этого можно скачать файл в любую удобную директорию, например, рядом с js-файлом и потом уже отправить его оттуда, как это реализовано здесь.
9.3. Бот, работающий с интерактивными кнопками.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// KeysBot.js var TelegramBot = require('node-telegram-bot-api'); var token = 'WRITE_YOUR_TOKEN_HERE'; var botOptions = { polling: true }; var bot = new TelegramBot(token, botOptions); bot.getMe().then(function(me) { console.log('Hello! My name is %s!', me.first_name); console.log('My id is %s.', me.id); console.log('And my username is @%s.', me.username); }); bot.on('text', function(msg) { var messageChatId = msg.chat.id; var messageText = msg.text; if (messageText === '/keys') { var opts = { reply_to_message_id: msg.message_id, reply_markup: JSON.stringify({ keyboard: [ ['Yes'], ['No'] ] }) }; bot.sendMessage(messageChatId, 'Do you love me?', opts); } if (messageText === 'Yes') { bot.sendMessage(messageChatId, 'I\'m too love you!', { caption: 'I\'m bot!' }); } if (messageText === 'No') { bot.sendMessage(messageChatId, ':(', { caption: 'I\'m bot!' }); } }); |
Бот в ответ на команду /keys отправляет специальное сообщение-ответ с особым параметром-клавиатурой. Пользователь может нажать на какую-нибудь из интерактивных кнопок, тем самым отправляя текст этой кнопки в чат, после чего бот снова обработает сообщение и в зависимости от варианта ответа произведёт какие-либо действия. Подобный режим весьма полезен для ботов, которые работают не с групповыми чатами, а с отдельными пользователями в индивидуальных диалогах. Выскакивающая клавиатура может раздражать пользователей в конференциях, поэтому используйте эту функциональность осторожно.
Демонстрация работы валютного бота @RubBot, использующего интерактивные кнопки.
Примером удачной реализации такого бота служит @RubBot, который отправляет необходимую котировку, в зависимости от нажатой пользователем кнопки.
10. Заключение, полезные ссылки и ресурсы
В этой статье были описаны варианты создания различных ботов на языках программирования JavaScript и Python. Я буду рад, если материал поможет вам разобраться во всех тонкостях работы с Telegram Bot API и поспособствует реализации ваших идей. Как было сказано в самом начале статьи, с помощью ботов можно организовать удобное получение и обработку различной информации. Для тех, кто заинтересовался этой темой, я предлагаю воспользоваться полезными ссылками, опубликованными ниже. Небольшой совет: обязательно читайте документацию реализации Telegram Bot API, используемой вами, и множество вопросов отпадут сами собой. Хочу выразить огромную благодарность создателю Telegram-конференции сообщества MotoFan.Ru — @ZorgeR‘у, который помог мне в имплементации различных аспектов дайджест-бота Гаечки и не раз наставлял меня на правильный и верный путь.
- Репозиторий с исходным кодом дайджест-бота на JavaScript и инструкцией по его установке;
- Исходный код простого дайджест-бота на Python;
- Официальная документация Telegram Bot API;
- Документация и официальный репозиторий реализации Telegram Bot API на языке JavaScript под Node.js — node-telegram-bot-api;
- Документация реализации Telegram Bot API на языке Python — twx.botapi;
- Официальный репозиторий реализации Telegram Bot API на языке Python — twx.botapi;
- Документация и официальный репозиторий реализации Telegram Bot API на языке Python — pyTelegramBotAPI;
- Отличная статья про реализацию JavaScript-ботов на Node.js и node-telegram-bot-api от Catethysis.
Update 18-MAR-2016: Официальное Telegram Bot API теперь предоставляет возможность использования специальных inline-ботов, которые могут искать различную информацию в Интернете и позволять пользователю выбирать подходящие варианты. Простой пример такого бота можно посмотреть здесь (исходный код), у него должен быть особый атрибут inline-бота, установить который можно с помощью команды /setinline, которую следует отправить @BotFather боту.
Хо-хо! NoPH8 знаменито! (первый скрин).
А так — спасибо, утащил в закладки.
Классная статья. Хочу тоже реализовать своего бота с рандомной выдачей картинок. Вы натолкнули на мысль. По сути я могу ведь использовать морду сайта. Грубо: создаю поддомен, куда помещаю 200 фото. Каждая фотография обернута в класс. Мне вот интересно, что если я обращаюсь к домену, беру оттуда один класс в рандомном порядке и выдаю его содержимое в сообщение телеграмм. Такое можно организовать? И еще вопрос, при этом не нужно отдельного серва, а можно использовать сервер телеграмма (polling)? Если будет возможность, прокомментируйте мое сообщение. И вообще хотелось бы посоветоваться по некоторым вопросам. Просто Вы единственный человек в интернете, у кого я нашел подробную статью на русском языке. Причем используется язык программирования яваскрипт.
Большое спасибо за отзыв.
Да, ничего не помешает реализовать такого бота. Только я бы сделал гораздо проще, без классов. К примеру, загрузил эти 200 фотографий в корень pics.exlmoto.ru, переименовал каждую картинку в 1..200.jpg, а дальше отправлял бы в чат случайную картинку посредством такого кода (модифицирован пример 9.2):
При этом верхнюю границу в функции getRandomInt() можно не хардкодить, а вычислять динамически в зависимости от количества файлов в директории на вашем сервере. Это можно сделать, например, таким образом. Вы сможете загружать фотографии в эту директорию, соблюдая правило именования 1..200.jpg и они будут подхватываться ботом динамически. При желании вообще можно опустить правило именования: сделать специальный обновляемый массив, элементы которого будут являться именами картинок в каталоге. Потом можно будет просто брать необходимое имя файла из этого массива по случайному индексу и отправлять картинку с таким именем в чат. В таком случае вам лишь потребуется просто добавлять картинки на сервер, не заботясь о чём-либо.
Нет. Любой бот, как Polling, так и HTTPS, является серверной программой и должен где-то «крутиться». На серверах Telegram’а, к сожалению, нельзя размещать своих ботов. Для Javascript-ботов, базирующихся на node-telegram-bot-api вам необходим хостинг с поддержкой Node.js; в качестве тестовой площадки для запуска ботов можно использовать Cloud9 IDE.
А если я хочу на своем сервере держать картинки? Мне нужно ajax запросы писать?
Просто сделайте так, чтобы сервер мог отдавать картинку по прямой ссылке. Без всяких AJAX’ов. На мой взгляд это самый простой подход.
Могли бы вы мне сделать бота за вознагрождение
Контакты для связи и обсуждения ТЗ
@tizin
lekaavlov [at] gmail [d0t] com
Отписал в Telegram.
Здравствуйте, нужно дописать бота на js. Добавить пару функций, за оплату. Напишите пожалуйста @ziziboba
Написал.
Благодарю за столь подробный ответ.
Подскажи пожалуйста. У метода sendMessage в оригинальном апи есть параметр parse_mode, значение которого может быть, например «HTML» — отвечает за разметку. В этой js-обертке не могу заставить его работать. Передаю в объекте третьим параметром метода, но не срабатывает. Как быть?
разобрался. Забыл просто передать параметром в другую функцию. Спасиб за статью.
Спасибо за комментарий. Заметьте, что официальное Bot API часто обновляется, а node-telegram-bot-api не всегда за ним успевает.
Добрый вечер.
Вот у меня такая проблема созрела, написал по аналогии бота который перебирает в масиве картинки и отдает при клике (‘yes’)
Вот ссылка на код, https://github.com/JackieChan1/telegramBot/blob/master/index.js .
При одном подключении все работает хорошо, если приконектилось двое отдает и пытаются перебирать фотки, то тому кто первый подключился работает ОК, а у того кто второй подключился не работает, и отдает «Unhandled rejection Error: 400 {«ok»:false,»error_code»:400,»description»:»[Error]: Bad Request: message not found»}». Не могу понять как это пофиксить. Может что то подскажите?
К сожалению на ум ничего не приходит, возможно, баг в node-telegram-bot-api.
при запуске на сервере nodejs бот убивается со следующей информацией:
Unhandled rejection Error: 409 {«ok»:false,»error_code»:409,»description»:»[Error]: Conflict: terminated by other long poll or webhook»}
что не так, в какую сторону смотреть
Судя по всему, бот с таким токеном уже где-то запущен.
а можно по подробннее, это первые шаги вообще в запуске чего то под nodejs с использование командной строки
Что именно подробнее? Эта ошибка — признак того, что бот с таким токеном где-то уже запущен, к примеру, на другом терминале. Можете показать код, что именно вы запускаете?
Запускаю в screen следующий код:
пока открыта консоль он работает, при переводе его в screen он работает минут 20 и падает, техподдержка хостинга где запущен node.js прислала код ошибки:
Unhandled rejection Error: 409 {“ok”:false,”error_code”:409,”description”:”[Error]: Conflict: terminated by other long poll or webhook”}
при входе в screen я вижу ошибку:
at afterTimeout (/h/…/node_modules/node-telegram-bot-api/node_modules/bluebird/js/main/timers.js:16:15)
at timeoutTimeout (/h/…/node_modules/node-telegram-bot-api/node_modules/bluebird/js/main/timers.js:59:9)
at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)
так как это первый опыт то не понятно что падает и где, по вашей ссылке установил npm install node-telegram-bot (1.9.0)
токен сменил в @botfather
Может стоит отказаться от использования screen? Здесь описаны способы запуска процесса в фоне. Лично я для этого использую юнит в systemd.
Техподдержка предложила варианты:
Вы можете запустить необходимые приложение в screen или tmux — оба пакета установлены на Вашем VPS. Вы также можете запустить процесс в фоне средствами bash:
$ command &
попробую Ваше решение. Спасибо.
Будем рады, если вы зарегистрируете своих ботов в каталоге botoboom.com
Интересный сервис, спасибо, попробовал зарегистрировать.
Кстати, исправьте ошибку: если попытаться загрузить аватар больше, чем 512×512, то на форме с ошибкой список языков отображается некорректно.
Добрый день! спасибо за отличный туториал, но естть просьба, сделайте туториал по залитию этого всего на хостинги, например heroku
просто это как оказалось куда сложнее, чем использовать node-telegram-bot-api, ибо его надо тащить всегда и везде за собой, да и создание node js проектов дело забавное
Спасибо за отзыв!
Если у меня появится свободное время, то обязательно напишу.
Здравствуйте, скажите в какой статье есть информация о том, чтобы бот допустим проверял поступили ли деньги на кошелек, и если поступили, дал ответ пользователю, который допустим хотел заказать телефон у меня? заранее спасибо
Если будет возможно с помощью Telegram-бота «достучаться» до транзакций в кошельке, то это можно будет сделать. Единственное — пользователь должен как-то помечать транзакцию, чтобы была возможность идентифицировать какой именно пользователь положил деньги в кошелёк и кому именно бот должен отвечать.
Здравствуйте, хотел бы обратиться к вам за помощью в написании бота, не хочу раскрывать свою идею тут, пожалуйста напишите мне на мыло terlovoyartem [аt] gmail [d0t] com, если вам не трудно, заранее спасибо
Отписался.
Здравствуйте, убедительная просьба написать мне в телеграм, если это возможно!? Уж очень нужна ваша помощь, без вас я походу не справлюсь..
Заранее спасибо.
Мой юзер в телеграме @bread_ik
Отписался.
Еще раз благодарю за статью, и много раз еще поблагодарю) Благодаря вам, практически дописал своего бота. Но снова и снова возникают вопросы. Написал эхо сервер ботаЮ подобный тому, что у вас со стикерами. Но вот загвоздка. Что у вас со стикерами, что у меня — бот работает исключительно в личке. То есть, я подключаю бота в чат, и там эхо-сервер не работает.. То есть он вообще не мониторит сообщения. Даже если я в функцию эхо-сервера вставлю console.log(msg); он мне не выведет никакой информации о сообщениях. Получается по дефолту нельзя штоль мониторить чат ботам?!
Если можно, то как мне сделать так, чтобы бот в чатах тоже отрабатывал эхо-сервер? Подскажите, пожалуйста.
Спасибо за отзыв. Для того, чтобы бот имел доступ к сообщениям в группах, нужно выставить это в настройках бота через @BotFather, команда /setprivacy, описана в этом разделе статьи.
Спасибо! Я если что, буду вам вопросы сюда писать по мере поступления. Потому что есть вещи достаточно специфические, и гугл не помогает. Вот еще один такой вопрос, допустим я хочу в описании к боту на команду «/about» добавить ссылку на искходный код гитхаба. Также в команде «/comandlist»прописать все доступные команды, но чтобы они были не текстом, а ссылкой (сразу чтобы нажал и команда выполнилась, как в botFather). И никак. И нагуглить не получилось.
Не совсем понятно, что именно вы хотите сделать. Ссылка на GitHub в профиле бота легко добавляется с помощью @BotFather, смотрите как пример профиль той же @Digest_bot. Команды, перечисленные в /setcommands, и так вызываются сразу, нужно лишь нажать «/», а затем кликнуть на нужный пункт во всплывающем меню. Если вы хотите организовать справку по командам, по типу той, что организована в @Digest_bot (отправьте боту команду /help), то не вижу никаких затруднений, просто добавляйте к нужным командам знак «/», Telegram их автоматически подсветит и сделает доступными для нажатия.
Продолжаю писать своего бота, и снова столкнулся со следующей проблемой. Не могу вернуть нормально погоду. Суть такая, я использую пакет request, чтобы возвращать погоду. Вообще это в идеале лучше бы так. Нужно бы как-то создать стэк, в котором хранить результат запроса (вывод погоды). Сам стэк обновлять каждые три часа. А при нажатии на ‘/weather’ выводить данные из стэка, если они там есть, а если их нет, то выполнять функцию самому. Это идеальный вариант, как это сделать гугл мне не помог.. Однако пытался неидеальный вариант: вызываю отдельно функцию, он возвращает underfined, подумал, что из-за того, что запрос не успевает выполниться, и он тупо не успевает обработать ответ, чтобы его выдать. Решил через promise. Однако, всегда попадаю в error.. Вот мой коде
Не работает, а вот пример рабочего кода. Он работает, и все ок. Но не подходит мне, потому что я хочу использовать пакет cheerio для каждодневного вывода погоды. Поэтому мне нужно вытаскивать из функции условие if().
Подскажите, пожалуйста, как лучше быть и в какую сторону смотреть или почитать? в идеале вообще со стэком, чтобы запрос моментально выполнялся, а в стэк результат приходил асинхронно раз в три часа.
Не в идеале, хотя бы как мне попасть в true результат promise? Если будет удобно мой ник в телеграмме @Romkaa, но можно и в комментариях. По возможности ваших знаний, желания и времени подскажите мне, пожалуйста.
Я бы сделал обновление погоды через таймер. То есть следующим механизмом: в массиве/стеке сохраняются записи погоды, которые через каждые три часа обновляются с помощью таймера. Команда /weather просто получает доступ к нужному элементу в стеке, если он там есть. Если стек пуст, то тупо выводятся данные, например, вашим вторым способом и помимо вывода кешируются в стек. Что-то вроде такого псевдокода получится:
Благодарю. Из этого всего вот, что я не понял:
function weatherFunction(var sendMessage) — вы в параметре функции создаете переменную? Или опечатка? Не проходит такое.
И еще он у меня обновляет стэк, конечно же. Но при этом и в чат выводит тоже. После каждого обновления стэка автоматически постит в чат..
Да, была опечатка. Поправил код выше, теперь должно быть более понятно.
Я все никак не успокоюсь. Я понял ход мыслей. Мы передаем параметр, потом запускаем функцию либо с true, либо с false. Все хорошо, но вот последний момент, который никак не выходит. Когда вызываю ‘/weather’ он все равно перезаписывает массив. То есть
Всегда попадаем в else(), массив при этом обнуляется и в консольку падает ‘StackEmpty’. При каждом запуске ‘/weather’
Роман, я сейчас проверил, всё работает, смотрите ниже код:
Все, я закончил написание бота и все работает) конечно, еще нужно рефакторить, однако вчера арендовал сервер, установил node.js и запустил. Так или иначе, это был бесценный опыт, который я приобрел во многом, благодаря Вам. Большое спасибо.
Не за что. Очень рад, что вам пригодилась изложенная мною информация.
Здравствуйте. Хотя бы в общем скажите как сделать: есть файл excel. в первом столбце коды, во втором описание кода. нужен бот такой: вводишь код, а он в ответ описание кода. Как это сделать?
Достаточно легко, например, можно задействовать любой пакет, работающий с XLS (Excel files) для Node.js отсюда: https://www.npmjs.com/browse/keyword/xls
Далее парсить необходимый XLS, написать логику, которая будет вытаскивать нужное и т. д.
msg.from.first_name — кейпас чтобы поулчить имя
Интересно ботов обсуждаем, а ботам зпрешено писать в чате. Дискриминация однако xdd)
Это сделано с целью предотвратить спам и «зацикливание» типа один бот пишет второму, он отвечает, на его ответ отвечает первый, а на ответ первого — второй и так далее.
Ребята,Автор статьи,пожалуйста спасите!!!Уже неделю исчу и никак не могу найти уже схожу сума.Кто нибудь,пожалуйста дайте пример самого простого hello бота для (группы)чата на JS,чтобы он просто говорил привет или helo каждому новому(вступившему в группу) участнику.Очень нужна помощь.Огромное спасибо тем кто откликнется.
Вот простейший пример такого бота:
Более сложный вариант, который не отвечает приветствием на добавление самого себя в группу и использует список пользователей, если вступивших в группу несколько, можно посмотреть здесь: DigestBot/DigestBot.js:326-343.
Огромное спасибо. Спаситель.
Не стоит благодарности.