пятница, 10 июля 2015 г.

Инструкция: Как создавать ботов в Telegram / Хабрахабр

Инструкция: Как создавать ботов в Telegram

24 июня разработчики 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.

Каркас бота:

URL = "https://api.telegram.org/bot%s/" % BOT_TOKEN  MyURL = "https://example.com/hook"    api = requests.Session()  application = tornado.web.Application([      (r"/", Handler),  ])    if __name__ == '__main__':      signal.signal(signal.SIGTERM, signal_term_handler)      try:          set_hook = api.get(URL + "setWebhook?url=%s" % MyURL)          if set_hook.status_code != 200:              logging.error("Can't set hook: %s. Quit." % set_hook.text)              exit(1)          application.listen(8888)          tornado.ioloop.IOLoop.current().start()      except KeyboardInterrupt:          signal_term_handler(signal.SIGTERM, None)  

Здесь мы при запуске бота устанавливаем вебхук на наш адрес и отлавливаем сигнал выхода, чтобы вернуть поведение с ручной выгрузкой событий.

Приложение торнадо для обработки запросов принимает класс tornado.web.RequestHandler, в котором и будет логика бота.

class Handler(tornado.web.RequestHandler):          def post(self):              try:                  logging.debug("Got request: %s" % self.request.body)                  update = tornado.escape.json_decode(self.request.body)                  message = update['message']                  text = message.get('text')                  if text:                      logging.info("MESSAGE\t%s\t%s" % (message['chat']['id'], text))                        if text[0] == '/':                          command, *arguments = text.split(" ", 1)                          response = CMD.get(command, not_found)(arguments, message)                          logging.info("REPLY\t%s\t%s" % (message['chat']['id'], response))                          send_reply(response)              except Exception as e:                  logging.warning(str(e))  

Здесь CMD — словарь доступных команд, а send_reply — функция отправки ответа, которая на вход принимает уже сформированный объект Message.

Собственно, её код довольно прост:

def send_reply(response):      if 'text' in response:          api.post(URL + "sendMessage", data=response)  


Теперь, когда вся логика бота описана можно начать придумывать ему команды.

3. Команды


Перво-наперво, необходимо соблюсти соглашение Telegram и научить бота двум командам: /start и /help:

def help_message(arguments, message):      response = {'chat_id': message['chat']['id']}      result = ["Hey, %s!" % message["from"].get("first_name"),                "\rI can accept only these commands:"]      for command in CMD:          result.append(command)      response['text'] = "\n\t".join(result)      return response  


Структура message['from'] — это объект типа User, она предоставляет боту информацию как id пользователя, так и его имя. Для ответов же полезнее использовать message['chat']['id'] — в случае личного общения там будет User, а в случае чата — id чата. В противном случае можно получить ситуацию, когда пользователь пишет в чат, а бот отвечает в личку.

Команда /start без параметров предназначена для вывода информации о боте, а с параметрами — для идентификации. Полезно её использовать для действий, требующих авторизации.

После этого можно добавить какую-нибудь свою команду, например, /base64:

def base64_decode(arguments, message):      response = {'chat_id': message['chat']['id']}      try:          response['text'] = b64decode(" ".join(arguments).encode("utf8"))      except:          response['text'] = "Can't decode it"      finally:          return 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 добавляем обработчик:

if text[0] == '/':      ...  else:      response = CMD["<speech>"](message)      logging.info("REPLY\t%s\t%s" % (message['chat']['id'], response))      send_reply(response)  

А потом в список команд добавляем псевдо-речь:

RESPONSES = {      "Hello": ["Hi there!", "Hi!", "Welcome!", "Hello, {name}!"],      "Hi there": ["Hello!", "Hello, {name}!", "Hi!", "Welcome!"],      "Hi!": ["Hi there!", "Hello, {name}!", "Welcome!", "Hello!"],      "Welcome": ["Hi there!", "Hi!", "Hello!", "Hello, {name}!",],  }  def human_response(message):      leven = fuzzywuzzy.process.extract(message.get("text", ""), RESPONSES.keys(), limit=1)[0]      response = {'chat_id': message['chat']['id']}      if leven[1] < 75:          response['text'] = "I can not understand you"      else:          response['text'] = random.choice(RESPONSES.get(leven[0])).format_map(              {'name': message["from"].get("first_name", "")}          )      return response  

Здесь эмпирическая константа 75 относительно неплохо отражает вероятность того, что пользователь всё-таки хотел сказать. А format_map — удобна для одинакового описания строк как требующих подстановки, так и без нее. Теперь бот будет отвечать на приветствия и иногда даже обращаться по имени.

5. Не текст.


Боты, как и любой нормальный пользователь Telegram, могут не только писать сообщения, но и делиться картинками, музыкой, стикерами.

Для примера расширим словарь RESPONSES:

RESPONSES["What time is it?"] = ["<at_sticker>", "{date} UTC"]  

И будем отлавливать текст <at_sticker>:

if response['text'] == "<at_sticker>":          response['sticker'] = "BQADAgADeAcAAlOx9wOjY2jpAAHq9DUC"          del response['text']  

Видно, что теперь структура Message уже не содержит текст, поэтому необходимо модифицировать send_reply:

def send_reply(response):      if 'sticker' in response:          api.post(URL + "sendSticker", data=response)      elif 'text' in response:          api.post(URL + "sendMessage", data=response)  

И все, теперь бот будет время от времени присылать стикер вместо времени:



6. Возможности


Благодаря удобству API и быстрому старту боты Telegram могут стать хорошей платформой для автоматизации своих действий, настройки уведомлений, создания викторин и task-based соревнований (CTF, DozoR и прочие).

Вспоминая статью про умный дом, могу сказать, что теперь извращений меньше, а работа прозрачнее.

7. Ограничения


К сожалению, на данный момент существует ограничение на использование webHook — он работает только по https и только с валидным сертификатом, что, например для меня пока критично за счет отсутствия поддержки сертифицирующими центрами динамических днс.

К счастью, Telegram также умеет работать и по ручному обновлению, поэтому не меняя кода можно создать еще одну службу Puller, которая будет выкачивать их и слать на локальный адрес:

while True:              r = requests.get(URL + "?offset=%s" % (last + 1))              if r.status_code == 200:                  for message in r.json()["result"]:                      last = int(message["update_id"])                      requests.post("http://localhost:8888/",                                    data=json.dumps(message),                                    headers={'Content-type': 'application/json',                                             'Accept': 'text/plain'}                       )              else:                  logging.warning("FAIL " + r.text)              time.sleep(3)  


P.S. По пункту 7 нашел удобное решение — размещение бота не у себя, а на heroku, благо все имена вида *.herokuapp.com защищены их собственным сертификатом.


Sent from my iPhone

Комментариев нет:

Отправить комментарий