Как написать Telegram бота: практическое руководство

TelegramВ последнее время Telegram у всех на слуху. Нужно отдать должное отделу маркетинга этого приложения, шумиху подняли на славу. Одной из основных «фишек» Telegram является его якобы защищённость — по словам Павла Дурова вся переписка между пользователями шифруется. Более того, ни одна спец.служба мира не будет иметь доступ к вашим сообщениям. Но в данной статье речь не об этом. Сегодня хотелось бы поговорить о не менее крутой фишке в Telegram, а именно о ботах. Помимо того, что в сети уже полно информации о различного рода Telegram ботах (github бот, например), мессенджер открыл своё API для разработчиков, и теперь каждый может создать своего собственного бота с блэкджеком и плюшками.

В статье я приведу пример написания онлайн бота с использованием Python и Django фреймворка. То есть мы «запилим» полноценное веб-приложение, которое будет крутиться на удалённом хосте и принимать команды от пользователей. Весь исходный текст доступен в моём github репозитории.

Документация, описывающая процесс взаимодействия с ботами Telegram находится тут. Чтобы не изобретать велосипед, я нашел неплохую Python библиотеку, реализующую все основные функции ботов — telepot. Как я уже упоминал ранее, для того, чтобы обслуживать пользователей нашего бота мы будет разрабатывать веб-приложение, используя Django фреймворк.

Как создать Telegram бота?

Для начала нам необходимо зарегистрировать в Telegram нашего будущего бота. Это делается следующим образом:

  • Необходимо установить приложение Telegram на телефон или компьютер. Скачать приложение можно тут
  • Добавляем к себе в контакт-лист бота с именем BotFather
  • Запускаем процедуру «общения» с ботом нажатием кнопки Start. Далее перед нами предстанет список команд точно как на скриншоте.
  • Для того, чтобы создать нового бота необходимо выполнить команду /newbot и следовать инструкциям. Обратите внимание, что username для бота должен всегда содержать в конце слово bot. Например, DjangoBot или Django_bot.

Telegram bot

  • Для нашего бота я выбрал имя PythonPlanetBot, так как его основная функция заключается в парсинге RSS feed сайта Python Planet и выдача информации о последних постах пользователю 🙂

Python Planet бот

После создания бота, обратите внимание на строку с текстом:

Use this token to access the HTTP API:

За которой следует т.н. token по которому мы будем манипулировать нашим ботом. Помимо функции создания telegram бота, BotFather также имеет ряд других возможностей:

  • Присвоить боту описание
  • Установить аватар
  • Поменять token

и так далее. Полное описание доступных команд можно увидеть на первом скриншоте.

Приступаем к кодированию

Как я ранее уже упоминал, мы будем писать веб-приложение на Django. Но стоит отметить, что это делать необязательно. Можно обойтись и обычным Python скриптом, правда в этом случае необходимо будет периодически опрашивать Telegram на предмет новых запросов от пользователей бота (используя метод getUpdates) и увеличивая offset для получения самых последних данных без повторений.  В Telegram существует два взаимоисключающих метода получения команд/сообщений для вашего бота.

  • Использование вызова API метода getUpdates
  • Установка Webhook

Установка Webhook заключается в передаче боту специального URL адреса на который будет поступать POST запрос каждый раз, когда кто-то начнёт посылать сообщения боту. Именно этот вариант мы и будем использовать для взаимодействия между ботом и его пользователем. Для того, чтобы задать URL, необходимо использовать API метод setWebhook. Отмечу, что URL должен начинаться с https, то есть иметь защищённое SSL соединение с валидным сертификатом. Telegram разрешает использовать самоподписанный сертификат, правда для этого необходимо в методе setWebhook передавать также публичный ключ в PEM формате (ASCII base64). Либо же можно получить валидный бесплатный SSL сертификат от Let’s Encrypt.

Подробнее о getUpdates и setWebhook можно почитать соответственно здесь и тут.

Итак, вернёмся к python библиотеке для работы с Telegram — telepot. На текущий момент самой последней её версий является 6.7. Устанавливаем её в виртуальное окружение python virtualenv:

pip install telepot

Самый простой вариант взаимодействия с Telegram ботом на Python выглядит следующим образом:

import telepot
token = '123456'
TelegramBot = telepot.Bot(token)
print TelegramBot.getMe()

Переменной token присваиваем значение токена, полученного при создании бота через BotFather. В итоге после выполнения этих команд мы получим:

{u'username': u'PythonPlanetBot', u'first_name': u'Python Planet Bot', u'id': 199266571}

Поздравляю! Мы вызывали самый простой API запрос getMe, который возвращает информацию о боте: username, id, first_name.

Добавим нашего бота к себе в контакт-лист и пошлём ему первую стандартную команду /start

Telegram Bot

Выполняем код:

TelegramBot.getUpdates()
[{u'message': {u'date': 1459927254, u'text': u'/start', u'from': {u'username': u'adilkhash', u'first_name': u'Adil', u'id': 31337}, u'message_id': 1, u'chat': {u'username': u'adilkhash', u'first_name': u'Adil', u'type': u'private', u'id': 7350}}, u'update_id': 649179764}]

Процесс общения с telegram ботом происходит по HTTPS; для передачи данных используется JSON. Метод getUpdates возвращает список/массив из объектов типа Update. Внутри Update находится объект Message. Для стандартного взаимодействия с ботом нас фактически интересует именно объект Message, у которого мы считываем атрибут text, хранящий в себе текст, переданный боту и объект chat, в котором лежит информация о пользователе, инициировавшем общение с нашим Telegram ботом. Также имеется параметр update_id, который служит в качестве offset параметра при вызове метода getUpdates. То есть update_id+1 вернёт все сообщения, поступившие после последнего update_id, при этом все предыдущие сообщения будут удалены.

TelegramBot.getUpdates(649179764+1)
[{u'message': {u'date': 1459928527, u'text': u'hello bro', u'from': {u'username': u'adilkhash', u'first_name': u'Adil', u'id': 31337}, u'message_id': 13, u'chat': {u'username': u'adilkhash', u'first_name': u'Adil', u'type': u'private', u'id': 7350}}, u'update_id': 649179765}]

На этапе написания простейшего Telegram бота нам этих вызовов достаточно. Приступим к написанию Django приложения для обслуживания наших пользователей.

Простая функция парсинга RSS фида Planet Python выглядит вот так:

# -*- coding: utf8 -*-
from xml.etree import cElementTree

import requests


def parse_planetpy_rss():
    """Parses first 10 items from http://planetpython.org/rss20.xml
    """
    response = requests.get('http://planetpython.org/rss20.xml')
    parsed_xml = cElementTree.fromstring(response.content)
    items = []

    for node in parsed_xml.iter():
        if node.tag == 'item':
            item = {}
            for item_node in list(node):
                if item_node.tag == 'title':
                    item['title'] = item_node.text
                if item_node.tag == 'link':
                    item['link'] = item_node.text

            items.append(item)

    return items[:10]

Здесь я использую python библиотеку requests для работы с HTTP в самом простейшем варианте без обработки ошибок. Django «вьюшка» выглядит следующим образом:

TOKEN = '<Our_Bot_Token>'
TelegramBot = telepot.Bot(TOKEN)

def _display_help():
    return render_to_string('help.md')


def _display_planetpy_feed():
    return render_to_string('feed.md', {'items': parse_planetpy_rss()})


class CommandReceiveView(View):
    def post(self, request, bot_token):
        if bot_token != TOKEN:
            return HttpResponseForbidden('Invalid token')

        commands = {
            '/start': _display_help,
            'help': _display_help,
            'feed': _display_planetpy_feed,
        }

        try:
            payload = json.loads(request.body.decode('utf-8'))
        except ValueError:
            return HttpResponseBadRequest('Invalid request body')
        else:
            chat_id = payload['message']['chat']['id']
            cmd = payload['message'].get('text')  # command

            func = commands.get(cmd.split()[0].lower())
            if func:
                TelegramBot.sendMessage(chat_id, func(), parse_mode='Markdown')
            else:
                TelegramBot.sendMessage(chat_id, 'I do not understand you, Sir!')

        return JsonResponse({}, status=200)

    @method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):
        return super(CommandReceiveView, self).dispatch(request, *args, **kwargs)

CommandReceiveView ждёт POST запрос на себя, парсит его и отвечает исходя из заданной команды. Полноценное Django приложение можно найти по этой ссылке. Стоит отметить в коде использование ещё одного API вызова — sendMessage. Этот метод отправляет сообщение заданному пользователю, используя при этом chat_id и сам текст сообщения. Chat_id — это уникальный идентификатор чата между пользователем и ботом (его идентификатор есть в ответе на запрос getUpdates). У Telegram ботов есть одно ограничение, они не могут посылать сообщения пользователям, которые предварительно не инициировали общение с ним. По-видимому это сделано дабы избежать массового создания спам-ботов.

Я предполагаю, что вы уже клонировали мой репозиторий, настроили окружение и установили все необходимые зависимости: Django, requests, telepot. Если же вы не знаете как это сделать, то совсем скоро я напишу цикл статей о разработке веб-приложений на Python, включая разбор экосистемы: разработка, настройка, деплой. Если вам это интересно, то отпишитесь, пожалуйста, в комментариях к этой статье. Хочется получить обратную связь 🙂

Итак, веб-приложение на Django запущено. Как же начать тестировать бота? А всё очень просто — необходимо симулировать действия Telegram сервиса. Для этого нам понадобится HTTP клиент и тело запроса. В качестве HTTP клиента я часто использую Chrome плагин под названием Postman, а тело запроса мы возьмём напрямую из данных, полученных с помощью API вызова getUpdates.

После запуска runserver, URL на который необходимо посылать запрос выглядит следующим образом:

http://127.0.0.1:8000/planet/bot/BOT_TOKEN/

где BOT_TOKEN — это токен нашего бота. Смотрим скриншот:

Postman REST Client telegram-bot-postman

А давайте-ка отправим команду feed для получения списка новостей из Planet Python:

Postman и TelegramPostman и Telegram

На скриншотах видно, что бот адекватно отреагировал на нашу команду вывести список последних 10 постов.

Следующим шагом является деплой нашего Django приложения на удалённый хост и последующий вызов метода setWebhook для передачи URL на который будет посылаться POST запрос от сервиса Telegram каждый раз при поступлении команд боту от пользователей. Об этом мы поговорим в следующей заметке.

Чтобы не пропустить обновления, подпишитесь на мой Твиттер 🙂

ОБНОВЛЕНИЕ: Часть 2. Разворачиваем Telegram бота на сервере

04/10/2016: Написал небольшую заметку о нововведении в Telegram: создание игр в Telegram.

20/10/2016: Намедни я создал telegram канал для разработчиков, если интересно следить за новостями, статьями и видео из мира разработки ПО, подписывайтесь 🙂

 

  • Alexander Ivanov

    Было бы не плохо прочитать про Django деплой на примере рабочего приложения. Так сказать: «Что, зачем и почему?»

  • c6h10o5

    Серьёзно? Передавать токен от бота в качестве параметра url?

    • Адиль

      Да, а что здесь не так? Токен знает только автор бота и сам сервис телеграм, когда мы передаём URL при вызове setWebhook, тем самым мы избавляемся от разного рода пауков, которые могли бы посылать мусор в запросах к боту.

      Вот что пишет сам телеграм:

      If you’d like to make sure that the Webhook request comes from Telegram, we recommend using a secret path in the URL, e.g. https://www.example.com/. Since nobody else knows your bot‘s token, you can be pretty sure it’s us.

  • Виталик

    Доброго времени суток. При попытке выполнить код

    import telepot
    token = »
    TelegramBot = telepot.Bot(token)
    print TelegramBot.getMe()
    Вываливается ошибка.

    self, «Failed to establish a new connection: %s» % e)urllib3.exceptions.NewConnectionError: : Failed to establish a new con

    • Приветствую! У вас нормально работает API хост телеграма?

      • Виталик

        Добрый день. Я вообще новичёк. Хочу развернуть своего бота при помощи Джанго. Делаю по инструкции. в одном случае упираюсь в
        self, «Failed to establish a new connection: %s» % e)urllib3.exceptions.NewConnectionError: : Failed to establish a new con

        если делаю всё по нововой то натыкаеться на syntax error в строке
        print TelegramBot.getMe().

        Бота хочу развернуть на pythonanywhere.com.

        Посоветуйте, что почитать по теме? Информации в интернете много и про Deploy Django и про ботов телеграмма. Самого простого бота удалось поставить через getUpdates.

        Как проверить API хост телеграмма?

        • Syntax Error может быть связан с тем, что код запускается на python3, тогда нужно исправить на:

          print(TelegramBot.getMet())

        • Vladimir

          Виталий, сам недавно с этим столкнулся, решив попробовать pythonanywhere.com (я про недоступность хоста)
          Дело в том, что для бесплатных аккаунтов доступ в интернет у них осуществляется через прокси, поэтому, чтобы Ваш бот мог обращаться к серверу Telegram, Вам нужно в библиотеке Telepot в файле api.py заменить строчку
          ‘default’: urllib3.PoolManager(num_pools=3, maxsize=10, retries=False, timeout=30),
          на
          ‘default’: urllib3.ProxyManager(«http://proxy.server:3128», num_pools=3, maxsize=10, retries=False, timeout=30),

          Ну или перейти на платный аккаунт. Я для себя пока выбрал первый вариант и всерьез подумываю о Digital Ocean.

          • менять код сторонней библиотеки плохая практика. наверняка есть способ и в клиентском коде (т.е. вашем) указать прокси-сервер.

          • Vladimir

            Адиль, спасибо за комментарий! Конечно, это было неверное решение, прошу простить мою неопытность. Вот, как нужно правильно: https://github.com/nickoala/telepot/issues/83

            Посыпаю голову пеплом, что сразу не сообразил заглянуть в Issues библиотеки, можно было догадаться, что не я один за прокси оказался.
            Кстати, есть еще вариант просто откатиться на версию Telepot <=7.1, но это тоже из рубрики "вредные советы".

      • Виталик

        Добрый день.
        Я создаю виртуально окружение ставлю туда telepot и запускаю код.
        import telepot
        token = »
        TelegramBot = telepot.Bot(token)
        print TelegramBot.getMe()

        File «/home/vetos/botProject/vetBotTelepot/bot_venv/local/lib/python2.7/site-packages/urllib3/connection.py», line 151, in _new_conn
        self, «Failed to establish a new connection: %s» % e)
        urllib3.exceptions.NewConnectionError: : Failed to establish a new connection:

        • А пинг до api.telegram.org проходит?

          ping api.telegram.org

          • Виталик

            From ip-10-182-115-102.ec2.internal (10.182.115.102) icmp_seq=1 Destination Port Unreachable
            Наверное не проходит

          • В вашей сети заблокирован доступ к API Telegram.

          • Виталик

            Спасибо буду разбираться с сервером.

          • Виталик

            Подскажите.
            Если использую если библиотеку telebot. То метод get_me() из нее отрабатывает без ошибок. В этой библиотеке тоже используется api.telegram.org.

  • Виталик

    Еще вопрос. На каком все таки питоне нужно запускать вашего бота на 2.7 или уже на 3.4?
    Спасибо.

  • Виталик

    Подскажите момент.
    делаю запрос через POSTMAN http://127.0.0.1:8000/planet/bot/BOT_TOKEN/ (вместо BOT_TOKEN вставляю свой токен )
    пишет Invalid token
    Куда заглянуть?

    • Вам необходимо прописать Token в настройки проекта. https://github.com/adilkhash/planetpython_telegrambot/blob/master/blog_telegram/settings.py#L96

      • Виталик

        Только там?
        Я еще прописывал в blog_telegram/.env-template +TELEGRAM_BOT_TOKEN=»

        Заработало только после того как вернул (в ручную прописал) в blog_telegram/settings.py
        TOKEN = »
        TelegramBot = telepot.Bot(TOKEN)

        • .env-template нужно переименовывать в .env, тогда не нужно будет менять ничего в settings.py

  • Vladimir

    Адиль, прокомментируйте, пож-та, еще по такому вопросу. В документации и примерах к Telepot сказано, что для передачи сообщений боту следует использовать модуль Queue для организации очереди, а также handler для обработки разных типов сообщений. В примерах всегда можно увидеть bot.message_loop(…). Несколько я понимаю, именно там происходит парсинг JSON. Вы же, как я вижу, парсите JSON отдельно. Я допускаю, что для простого бота может не требоваться очередь, определение типа сообщения и прочие штуки, но возникает вопрос, зачем вообще тогда нужен Telepot в простом боте, ведь можно просто на запрос от сервера API отвечать своим JSON с текстом ответа, который и придет пользователю. Что-то я, похоже, запутался с необходимым и достаточным.

    • Насколько я понимаю, ознакомившись поверхностно с Queue, этот механизм необходимо для избежания побочных эффектов от использования Webhook механизма, в частности случаев, когда сообщения могут приходить не по порядку. Queue в этом плане гарантирует порядок. В простейшем боте как таковой необходимости в Telepot может даже и не быть, всё возможно написать средства простого http-клиента типа requests. Telepot же позиционирует себя как не просто библиотека, а фреймворк для написания Telegram ботов (да ещё и с поддержкой asyncio).

      • Vladimir

        Ну т.е. я правильно понимаю, что использование Telepot в такого рода простейших ботах — это overkill? Ну вроде как разворачивать Django для статического сайта-визитки никому неизвестного ООО «Рога и копыта» с посещаемостью 1 ген. дир в неделю? А можно ли как-то в таком случае предвосхитить при какой нагрузке на бота понадобится Queue, asyncio и вот это все? Просто сделал простенького бота для друзей, нагрузка никакая и сейчас волноваться нечего. Но функционал потихоньку растет и есть вероятность роста нагрузки (друзья могут поделиться со своими друзьями и т.п.) и не хотелось бы в один прекрасный день получить подвисающего и теряющего сообщения боты, при том что быстро переписать его под со всеми этими крутыми штуками мне пока квалификации и времени совсем не хватит — возиться буду долго и мучительно. Хотелось бы Ваших советов опытного питониста!

        • Если у Вас веб-приложение как в примере этой статьи, то функциональную часть бота (вычисление чего-то, скачивание, отправка и тд) можно вынести в систему очередей сообщений вроде Celery или RQ. О них я также писал статьи:

          https://khashtamov.com/2016/02/celery-best-practices/
          https://khashtamov.com/2016/04/python-rq-howto/

          В этом случае Ваше веб-приложение будет отвечать лишь за приём команд, а дальнейшие махинации над ними будут делать отдельные процессы-воркеры Celery/RQ, таким образом снижая нагрузку на веб-сервер (например, gunicorn, uwsgi, mod_wsgi и тд)

          Я не думаю, что Telelpot overkill, если у Вашего бота есть потенциал. Asyncio вообще тема отдельной статьи и даже не одной, надеюсь в ближайшее время напишу и про это.

          • Vladimir

            Спасибо, с удовольствием прочитал бы статьи по asyncio!

  • старый зенит

    Пытаюсь выполнить следующий код
    import telepot
    token = ‘*****’ # тут токен моего бота
    TelegramBot = telepot.Bot(token)
    print TelegramBot.getme()

    вываливается ошибка
    print TelegramBot.getme()
    ^
    SyntaxError: invalid syntax
    в чём проблема?
    python 3.5 стоит

    • код работает только с python 2.x

    • danya stdfx

      В python 3.x аргументы функции print нужно ставить в скобки

  • а почему вы импортируете cElementTree, он разве не предназначен для работы на языке С

    • Этот модуль написан на Си (и быстрее поэтому), работаю с ним по привычке уже.

  • Ильдар

    Кто нибудь может написать мне в телеграм? +7 926 725 15 92
    Кто нибудь кто понимаю, помощь нужна. Заранее спасибо.

  • Pingback: Блогу исполнился 1 год! — Персональный блог Адиля Хаштамова()

  • Alexei Lavrov

    Ребят подскажите как настроить обычный постинг сообщений от бота? без привязки к другим сайтам просто типа напоминалки чтоб срабатывал???