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 защищены их собственным сертификатом.

Комментарии (16)


  1. TyVik
    10.07.2015 10:42

    На днях тоже писал своего бота, но только на flask. Столкнулся с проблемой, что запросы ко мне просто не приходят, ошибки было 2: попытка сделать бота на поддомене, в то время как сертификат был только на домен, и нужно указывать все промежуточные сертификаты. Нашёл ещё интересную библиотечку для работы с ботом — github.com/datamachine/twx.botapi (кто-нибудь ей пользовался?). Но вот так и не понял как же удобней этого бота разрабатывать локально, на заглушках?


    1. M_Muzafarov Автор
      10.07.2015 10:46

      Разрабатывать локально — я просто генерировал json, который должен приходить в хук и слал его локально себе curl'om. Вполне рабочий вариант.


  1. Ogoun
    10.07.2015 12:19

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


    1. Ingener74
      10.07.2015 13:04
      -1

      Тут есть пример отправления изображения, аудио, видео, и стикеры посути отправляются также, осторожно там есть мат))
      github.com/Ingener74/Telegram-Bot/blob/master/TelegramBox.py


  1. kapuletti
    10.07.2015 17:05
    +1

    Если тут бывают разработчики Телеграма, сообщение для них:

    Собственно хотелось бы увидеть в сл. обновлении BOT API, так это получение статуса пользователя, точнее, онлайн ли он, если нет, когда он был в сети последний раз.


  1. dolphin4ik
    10.07.2015 18:44
    +2

    Ждём первую текстовую РПГ. Помню в ICQ ботов было море, на любой вкус: анекдоты, новости, погода, анонимные чаты… эх Пашка «вернул 2007»


  1. L0NGMAN
    11.07.2015 00:39

    Telegram Bot на php: github.com/akalongman/php-telegram-bot


    1. zelenin
      11.07.2015 10:05

      github.com/search?l=PHP&o=desc&q=telegram+bot&s=updated&type=Repositories&utf8=%E2%9C%93


  1. fingoldo
    11.07.2015 17:34
    -2

    Допустим, я хочу слать сообщения одному из своих друзей из списка контактов в Телеграме.
    Бота я зарегистрировал. Как теперь выцепить параметр chat_id друга для метода sendMessage?
    Хочу отметить, что у Telegram документация ужасного качества, из неё совершенно не понятно, как что-то полезное сделать.


    1. zelenin
      11.07.2015 21:23
      +2

      абсолютно ясная документация.
      chat_id выцепить из объекта Update, полученного с помощью getUpdates(), после того, как ваш друг напишет вашему боту.


      1. fingoldo
        13.07.2015 11:59

        Разобрался, спасибо. Тем не менее, в документации не помешал бы пример обращения к боту/отсылки ответа.


        1. zelenin
          13.07.2015 12:03

          все-таки это документация для программистов — примеры для новичков напишут на хабре


    1. hormold
      13.07.2015 02:05

      Боты могут писать только тем, кто первым им написал. Что бы не спамили с ботов


      1. SonkoDmitry
        14.07.2015 13:31
        +1

        Да еще есть такой нюанс, бот не может писать боту. У нас была идея, реализовать на ботах техсаппорт. Человек пишет главному боту, тот пересылает сообщение промежуточному, промежуточный пишет саппорта. Наоборот аналогично. Оказалось что нельзя, получаем ошибку «Error: Bad Request: user not found». Как ответила ТП телеграма на это:

        Using the current bot API, it is not possible.

        The bot API is still in its infancy right now. There are many potential features to consider and implement. We'll be studying what people do with their bots for a while to see which directions will be most important for the platform.


  1. afanasyevsanya
    03.08.2015 23:07

    Ты говоришь, что тебе удалось использовать heroku для хостинга бота + ты используешь webHook. Когда я попробовал хостить бота на heroku, приложение падает через 60 секунд с ошибкой: Error R10 (Boot timeout) -> Web process failed to bind to $PORT within 60 seconds of launch. Погуглив, я получил ответ, что heroku не может слушать определенный порт и надо использовать переменную process.env.PORT в нее heroku поставит тот порт, который посчитает нужным. Соответсвенно порт мне присваевается не правильный и telegram выдает соответствующую ошибку.

    Скажи сталкивался ли ты с этим? Как решал?


    1. M_Muzafarov Автор
      04.08.2015 10:26

      Ну собственно, как Хероку пишет — так и решал:

      from os import environ
      def main():
      ...
          application.listen(environ["PORT"])
      

      Проблем не возникало.

      Правда мне бесплатный хероку надоел своими фризами и я впоследствии весь код переписал на Flask для GAE. До 10 ботов нахаляву можно спокойно запустить и они не спят.