Инструкция: Как создавать ботов в Telegram
Инструкция: Как создавать ботов в Telegram
24 июня 2015 разработчики Telegram открыли платформу для создания ботов. Новость кого-то обошла стороной Хабр, однако многие уже начали разрабатывать викторины. При этом мало где указаны хоть какие-то примеры работающих ботов.
Прежде всего, бот для Telegram — это по-прежнему приложение, запущенное на вашей стороне и осуществляющее запросы к Telegram Bot API. Причем API довольное простое — бот обращается на определенный URL с параметрами, а Telegram отвечает JSON объектом.
Рассмотрим API на примере создания тривиального бота:
1. Регистрация
Прежде чем начинать разработку, бота необходимо зарегистрировать и получить его уникальный id, являющийся одновременно и токеном. Для этого в Telegram существует специальный бот — @BotFather.
Пишем ему /start и получаем список всех его команд.
Первая и главная — /newbot — отправляем ему и бот просит придумать имя нашему новому боту. Единственное ограничение на имя — в конце оно должно оканчиваться на «bot». В случае успеха BotFather возвращает токен бота и ссылку для быстрого добавления бота в контакты, иначе придется поломать голову над именем.
Для начала работы этого уже достаточно. Особо педантичные могут уже здесь присвоить боту аватар, описание и приветственное сообщение.
Не забудьте проверить полученный токен с помощью ссылки api.telegram.org/bot<TOKEN>/getMe, говорят, не всегда работает с первого раза.
2. Программирование
Создавать бота буду на Python3, однако благодаря адекватности этого языка алгоритмы легко переносятся на любой другой.
Telegram позволяет не делать выгрузку сообщений вручную, а поставить webHook, и тогда они сами будут присылать каждое сообщение. Для Python, чтобы не заморачиваться с cgi и потоками, удобно использовать какой-нибудь реактор, поэтому я для реализации выбрал tornado.web. (для GAE удобно использовать связку Python2+Flask)
Каркас бота:
URL = <span class="hljs-string">"https://api.telegram.org/bot%s/"</span> % BOT_TOKEN
MyURL = <span class="hljs-string">"https://example.com/hook"</span>
api = requests.Session()
application = tornado.web.Application([
(<span class="hljs-string">r"/"</span>, Handler),
])
<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">'__main__'</span>:
signal.signal(signal.SIGTERM, signal_term_handler)
<span class="hljs-keyword">try</span>:
set_hook = api.get(URL + <span class="hljs-string">"setWebhook?url=%s"</span> % MyURL)
<span class="hljs-keyword">if</span> set_hook.status_code != <span class="hljs-number">200</span>:
logging.error(<span class="hljs-string">"Can't set hook: %s. Quit."</span> % set_hook.text)
exit(<span class="hljs-number">1</span>)
application.listen(<span class="hljs-number">8888</span>)
tornado.ioloop.IOLoop.current().start()
<span class="hljs-keyword">except</span> KeyboardInterrupt:
signal_term_handler(signal.SIGTERM, <span class="hljs-keyword">None</span>)
Здесь мы при запуске бота устанавливаем вебхук на наш адрес и отлавливаем сигнал выхода, чтобы вернуть поведение с ручной выгрузкой событий.
Приложение торнадо для обработки запросов принимает класс tornado.web.RequestHandler, в котором и будет логика бота.
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Handler</span><span class="hljs-params">(tornado.web.RequestHandler)</span>:</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">post</span><span class="hljs-params">(self)</span>:</span>
<span class="hljs-keyword">try</span>:
logging.debug(<span class="hljs-string">"Got request: %s"</span> % self.request.body)
update = tornado.escape.json_decode(self.request.body)
message = update[<span class="hljs-string">'message'</span>]
text = message.get(<span class="hljs-string">'text'</span>)
<span class="hljs-keyword">if</span> text:
logging.info(<span class="hljs-string">"MESSAGE\t%s\t%s"</span> % (message[<span class="hljs-string">'chat'</span>][<span class="hljs-string">'id'</span>], text))
<span class="hljs-keyword">if</span> text[<span class="hljs-number">0</span>] == <span class="hljs-string">'/'</span>:
command, *arguments = text.split(<span class="hljs-string">" "</span>, <span class="hljs-number">1</span>)
response = CMD.get(command, not_found)(arguments, message)
logging.info(<span class="hljs-string">"REPLY\t%s\t%s"</span> % (message[<span class="hljs-string">'chat'</span>][<span class="hljs-string">'id'</span>], response))
send_reply(response)
<span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
logging.warning(str(e))
Здесь CMD — словарь доступных команд, а send_reply — функция отправки ответа, которая на вход принимает уже сформированный объект Message.
Собственно, её код довольно прост:
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_reply</span><span class="hljs-params">(response)</span>:</span>
<span class="hljs-keyword">if</span> <span class="hljs-string">'text'</span> <span class="hljs-keyword">in</span> response:
api.post(URL + <span class="hljs-string">"sendMessage"</span>, data=response)
Теперь, когда вся логика бота описана можно начать придумывать ему команды.
3. Команды
Перво-наперво, необходимо соблюсти соглашение Telegram и научить бота двум командам: /start и /help:
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">help_message</span><span class="hljs-params">(arguments, message)</span>:</span>
response = <span class="hljs-string">'chat_id'</span>: message[<span class="hljs-string">'chat'</span>][<span class="hljs-string">'id'</span>]
result = [<span class="hljs-string">"Hey, %s!"</span> % message[<span class="hljs-string">"from"</span>].get(<span class="hljs-string">"first_name"</span>),
<span class="hljs-string">"\rI can accept only these commands:"</span>]
<span class="hljs-keyword">for</span> command <span class="hljs-keyword">in</span> CMD:
result.append(command)
response[<span class="hljs-string">'text'</span>] = <span class="hljs-string">"\n\t"</span>.join(result)
<span class="hljs-keyword">return</span> response
Структура message[‘from’] — это объект типа User, она предоставляет боту информацию как id пользователя, так и его имя. Для ответов же полезнее использовать message[‘chat’][‘id’] — в случае личного общения там будет User, а в случае чата — id чата. В противном случае можно получить ситуацию, когда пользователь пишет в чат, а бот отвечает в личку.
Команда /start без параметров предназначена для вывода информации о боте, а с параметрами — для идентификации. Полезно её использовать для действий, требующих авторизации.
После этого можно добавить какую-нибудь свою команду, например, /base64:
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">base64_decode</span><span class="hljs-params">(arguments, message)</span>:</span>
response = <span class="hljs-string">'chat_id'</span>: message[<span class="hljs-string">'chat'</span>][<span class="hljs-string">'id'</span>]
<span class="hljs-keyword">try</span>:
response[<span class="hljs-string">'text'</span>] = b64decode(<span class="hljs-string">" "</span>.join(arguments).encode(<span class="hljs-string">"utf8"</span>))
<span class="hljs-keyword">except</span>:
response[<span class="hljs-string">'text'</span>] = <span class="hljs-string">"Can't decode it"</span>
<span class="hljs-keyword">finally</span>:
<span class="hljs-keyword">return</span> response
Для пользователей мобильного Telegram, будет полезно сказать @BotFather, какие команды принимает наш бот:
I: /setcommands
BotFather : Choose a bot to change the list of commands.
I: @******_bot
BotFather: OK. Send me a list of commands for your bot. Please use this format:
command1 - Description
command2 - Another description
I:
whoisyourdaddy - Information about author
base64 - Base64 decode
BotFather: Success! Command list updated. /help
C таким описанием, если пользователь наберет /, Telegram услужливо покажет список всех доступных команд.
4. Свобода
Как можно было заметить, Telegram присылает сообщение целиком, а не разбитое, и ограничение на то, что команды начинаются со слеша — только для удобства мобильных пользователей. Благодаря этому можно научить бота немного говорить по-человечески.
UPD: Как верно подсказали, такое пройдет только при личном общении. В чатах боту доставляются только сообщения, начинающиеся с команды (/<command>) (https://core.telegram.org/bots#privacy-mode)
- All messages that start with a slash ‘/’ (see Commands above)
- Messages that mention the bot by username
- Replies to the bot's own messages
- Service messages (people added or removed from the group, etc.)
Чтобы бот получал все сообщения в группах пишем @BotFather команду /setprivacy и выключаем приватность.
Для начала в Handler добавляем обработчик:
<span class="hljs-keyword">if</span> text[<span class="hljs-number">0</span>] == <span class="hljs-string">'/'</span>:
...
<span class="hljs-keyword">else</span>:
response = CMD[<span class="hljs-string">"<speech>"</span>](message)
logging.info(<span class="hljs-string">"REPLY\t%s\t%s"</span> % (message[<span class="hljs-string">'chat'</span>][<span class="hljs-string">'id'</span>], response))
send_reply(response)
А потом в список команд добавляем псевдо-речь:
RESPONSES =
<span class="hljs-string">"Hello"</span>: [<span class="hljs-string">"Hi there!"</span>, <span class="hljs-string">"Hi!"</span>, <span class="hljs-string">"Welcome!"</span>, <span class="hljs-string">"Hello, name!"</span>],
<span class="hljs-string">"Hi there"</span>: [<span class="hljs-string">"Hello!"</span>, <span class="hljs-string">"Hello, name!"</span>, <span class="hljs-string">"Hi!"</span>, <span class="hljs-string">"Welcome!"</span>],
<span class="hljs-string">"Hi!"</span>: [<span class="hljs-string">"Hi there!"</span>, <span class="hljs-string">"Hello, name!"</span>, <span class="hljs-string">"Welcome!"</span>, <span class="hljs-string">"Hello!"</span>],
<span class="hljs-string">"Welcome"</span>: [<span class="hljs-string">"Hi there!"</span>, <span class="hljs-string">"Hi!"</span>, <span class="hljs-string">"Hello!"</span>, <span class="hljs-string">"Hello, name!"</span>,],
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">human_response</span><span class="hljs-params">(message)</span>:</span>
leven = fuzzywuzzy.process.extract(message.get(<span class="hljs-string">"text"</span>, <span class="hljs-string">""</span>), RESPONSES.keys(), limit=<span class="hljs-number">1</span>)[<span class="hljs-number">0</span>]
response = <span class="hljs-string">'chat_id'</span>: message[<span class="hljs-string">'chat'</span>][<span class="hljs-string">'id'</span>]
<span class="hljs-keyword">if</span> leven[<span class="hljs-number">1</span>] < <span class="hljs-number">75</span>:
response[<span class="hljs-string">'text'</span>] = <span class="hljs-string">"I can not understand you"</span>
<span class="hljs-keyword">else</span>:
response[<span class="hljs-string">'text'</span>] = random.choice(RESPONSES.get(leven[<span class="hljs-number">0</span>])).format_map(
<span class="hljs-string">'name'</span>: message[<span class="hljs-string">"from"</span>].get(<span class="hljs-string">"first_name"</span>, <span class="hljs-string">""</span>)
)
<span class="hljs-keyword">return</span> response
Здесь эмпирическая константа 75 относительно неплохо отражает вероятность того, что пользователь всё-таки хотел сказать. А format_map — удобна для одинакового описания строк как требующих подстановки, так и без нее. Теперь бот будет отвечать на приветствия и иногда даже обращаться по имени.
5. Не текст.
Боты, как и любой нормальный пользователь Telegram, могут не только писать сообщения, но и делиться картинками, музыкой, стикерами.
Для примера расширим словарь RESPONSES:
RESPONSES[<span class="hljs-string">"What time is it?"</span>] = [<span class="hljs-string">"<at_sticker>"</span>, <span class="hljs-string">"date UTC"</span>]
И будем отлавливать текст <at_sticker>:
<span class="hljs-keyword">if</span> response[<span class="hljs-string">'text'</span>] == <span class="hljs-string">"<at_sticker>"</span>:
response[<span class="hljs-string">'sticker'</span>] = <span class="hljs-string">"BQADAgADeAcAAlOx9wOjY2jpAAHq9DUC"</span>
<span class="hljs-keyword">del</span> response[<span class="hljs-string">'text'</span>]
Видно, что теперь структура Message уже не содержит текст, поэтому необходимо модифицировать send_reply:
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">send_reply</span><span class="hljs-params">(response)</span>:</span>
<span class="hljs-keyword">if</span> <span class="hljs-string">'sticker'</span> <span class="hljs-keyword">in</span> response:
api.post(URL + <span class="hljs-string">"sendSticker"</span>, data=response)
<span class="hljs-keyword">elif</span> <span class="hljs-string">'text'</span> <span class="hljs-keyword">in</span> response:
api.post(URL + <span class="hljs-string">"sendMessage"</span>, data=response)
И все, теперь бот будет время от времени присылать стикер вместо времени:
6. Возможности
Благодаря удобству API и быстрому старту боты Telegram могут стать хорошей платформой для автоматизации своих действий, настройки уведомлений, создания викторин и task-based соревнований (CTF, DozoR и прочие).
Вспоминая статью про умный дом, могу сказать, что теперь извращений меньше, а работа прозрачнее.
7. Ограничения
К сожалению, на данный момент существует ограничение на использование webHook — он работает только по https и только с валидным сертификатом, что, например для меня пока критично за счет отсутствия поддержки сертифицирующими центрами динамических днс.
К счастью, Telegram также умеет работать и по ручному обновлению, поэтому не меняя кода можно создать еще одну службу Puller, которая будет выкачивать их и слать на локальный адрес:
<span class="hljs-keyword">while</span> <span class="hljs-keyword">True</span>:
r = requests.get(URL + <span class="hljs-string">"?offset=%s"</span> % (last + <span class="hljs-number">1</span>))
<span class="hljs-keyword">if</span> r.status_code == <span class="hljs-number">200</span>:
<span class="hljs-keyword">for</span> message <span class="hljs-keyword">in</span> r.json()[<span class="hljs-string">"result"</span>]:
last = int(message[<span class="hljs-string">"update_id"</span>])
requests.post(<span class="hljs-string">"http://localhost:8888/"</span>,
data=json.dumps(message),
headers=<span class="hljs-string">'Content-type'</span>: <span class="hljs-string">'application/json'</span>,
<span class="hljs-string">'Accept'</span>: <span class="hljs-string">'text/plain'</span>
)
<span class="hljs-keyword">else</span>:
logging.warning(<span class="hljs-string">"FAIL "</span> + r.text)
time.sleep(<span class="hljs-number">3</span>)
P.S. По пункту 7 нашел удобное решение — размещение бота не у себя, а на heroku, благо все имена вида *.herokuapp.com защищены их собственным сертификатом.
UPD: Telegram улучшили Бот Апи, из-за чего, теперь не обязательно иметь отдельную функцию для отправки сообщений при установленном вебхуке, а в ответ на POST запрос можно отвечать тем же сформированным JSON с ответным сообщением, где одно из полей устанавливается как ч 'method': 'sendMessage' (или любой другой метод, используемый ботом).