---
title: "🤖 Howto Make Telegram Bot"
date: 2023-05-28T04:02:05+03:00
draft: true
tags: [python, tutorial, telegram, bot]
---

# Как создать чат-бота для Telegram с помощью Python

> Статья является полной копией страницы
https://pythonru.com/primery/python-telegram-bot.

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

Время переходить к делу и узнать наконец, как создавать ботов в Telegram.

## Шаг № 0: немного теории об API Telegram-ботов

Начать руководство стоит с простого вопроса: как создавать чат-ботов в Telegram?

Ответ очень простой: для чтения сообщений отправленных пользователями
и для отправки сообщений назад используется API HTML. Это требует использования URL:

```text
https://api.telegram.org/bot/METHOD_NAME
```

`METHOD_NAME` — это метод, например, `getUpdates`, `sendMessage`, `getChat` и т.п.

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

Токен выглядит приблизительно так:

```text
123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
```

Для выполнения запросов используются как GET, так и POST запросы.
Многие методы требуют дополнительных параметров
(методу `sendMessage`, например, нужно передать `chat_id` и `текст`).  
Эти параметры могут быть переданы как строка запроса URL,
`application/x-www-form-urlencoded` и `application-json` (кроме загрузки файлов).  
Еще одно требование — кодировка `UTF-8`.

После отправки запроса к API, вы получаете ответ в формате JSON.
Например, если извлечь данные с помощью метода `getME`, ответ будет такой:

```text
GET https://api.telegram.org/bot/getMe
```

```text
{
    ok: true,
    result: {
        id: 231757398,
        first_name: "Exchange Rate Bot",
        username: "exchangetestbot"
    }
}
```

Список всех типов данных и методов API Telegram-бота можно найти здесь
https://core.telegram.org/bots/api.

Следующий вопрос: как получать пользовательские сообщения?

Есть два варианта.

Первый — вручную создавать запросы с помощью метода `getUpdates`.
В качестве объекта вы получите массив объектов `Update`.
Этот метод работает как технология длинных опросов (long polling),
когда вы отправляете запрос, обрабатываете данные и начинаете повторяете процесс.
Чтобы избежать повторной обработки одних и тех же данных рекомендуется использовать параметр
`offset`.

Второй вариант — использовать `webhooks`.
Метод `setWebhook` нужно будет применить только один раз.
После этого Telegram будет отправлять все обновления на конкретный URL-адрес,
как только они появятся.
Единственное ограничение — необходим HTTPS, но можно использовать и сертификаты,
заверенные самостоятельно.

Как выбрать оптимальный метод? Метод `getUpdates` лучше всего подходит, если:

1. Вы не хотите или не можете настраивать HTTPS во время разработки.
2. Вы работаете со скриптовыми языками, которые сложно интегрировать в веб-сервер.
3. У бота высокая нагрузка.
4. Вы меняете сервер бота время от времени.

Метод с Webhook лучше подойдет в таких случаях:

1. Вы используете веб-языки (например, PHP).
2. У бота низкая нагрузка, и нет смысла делать запросы вручную.
3. Бот на постоянной основе интегрирован в веб-сервер.
4. В этом руководстве будет использоваться метод `getUpdates`.

Еще один вопрос: как создать зарегистрировать бота?

`@BotFather` используется для создания ботов в Telegram.
Он также отвечает за базовую настройку
(описание, фото профиля, встроенная поддержка и так далее).

В этом руководстве будет использоваться библиотека
[pyTelegramBotAPI](https://github.com/eternnoir/pyTelegramBotAPI).

## Шаг № 1: реализовать запросы курсов валют

Начать стоит с написания Python-скрипта,
который будет реализовывать логику конкретных запросов курсов валют.
Использовать будем `PrivatBank API`.

```text
https://api.privatbank.ua/p24api/pubinfo?json&exchange&coursid=5
```

Пример ответа:

```text
[
    {
        ccy:"USD",
    base_ccy:"UAH",
        buy:"25.90000",
        sale:"26.25000"
    },
    {
    ccy:"EUR",
        base_ccy:"UAH",
        buy:"29.10000",
        sale:"29.85000"
    },
    {
        ccy:"RUR",
        base_ccy:"UAH",
        buy:"0.37800",
        sale:"0.41800"
    },
    {
        ccy:"BTC",
        base_ccy:"USD",
        buy:"11220.0384",
        sale:"12401.0950"
    }
]
```

Создадим файл `pb.py` со следующим кодом:

```python
import re  
import requests  
import json  
  
  
URL = 'https://api.privatbank.ua/p24api/pubinfo?json&exchange&coursid=5'  
  
  
def load_exchange():  
    return json.loads(requests.get(URL).text)  
  
  
def get_exchange(ccy_key):  
    for exc in load_exchange():  
        if ccy_key == exc['ccy']:  
            return exc  
    return False  
  
  
def get_exchanges(ccy_pattern):  
    result = []  
    ccy_pattern = re.escape(ccy_pattern) + '.*'  
    for exc in load_exchange():  
        if re.match(ccy_pattern, exc['ccy'], re.IGNORECASE) is not None:  
            result.append(exc)  
    return result
```

Были реализованы три метода:

1. `load_exchange`: загружает курсы валют по указанному URL-адресу
и возвращает их в формате словаря(dict).
2. `get_exchange`: возвращает курсы валют по запрошенной валюте.
3. `get_exchanges`: возвращает список валют в соответствии с шаблоном
(требуется для поиска валют во встроенных запросах).

## Шаг № 2: создать Telegram-бота с помощью `@BotFather`

Необходимо подключиться к боту `@BotFather`, чтобы получить список чат-команд в Telegram.
Далее нужно набрать команду `/newbot` для инструкций выбора название и имени бота.
После успешного создания бота вы получите следующее сообщение:

```text
Done! Congratulations on your new bot. You will find it at telegram.me/. 
You can now add a description, about section and profile picture for your bot, see /help for a list of commands. 
By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. 
Just make sure the bot is fully operational before you do this.

Use this token to access the HTTP API:
 (here goes the bot’s token)

For a description of the Bot API, see this page: https://core.telegram.org/bots/api
```

Его нужно сразу настроить.  
Необходимо добавить описание и текст о боте
(команды `/setdescription` и `/setabouttext`),
фото профиля (`/setuserpic`), включить встроенный режим (`/setinline`),
добавить описания команд (`/setcommands`).  
Потребуется использовать две команды: `/help` и `/exchange`. Стоит описать их в `/setcommands`.

Теперь, когда настройка закончена, можно переходить к написанию кода.
Прежде чем двигаться дальше, рекомендуется почитать об
[API](https://core.telegram.org/bots/api#authorizing-your-bot)
и ознакомиться с документацией библиотеки, чтобы лучше понимать то, о чем пойдет речь дальше.

## Шаг № 3: настроить и запустить бота

Начнем с создания файла `config.py` для настройки:

```python
TOKEN = ''  # заменить на токен своего бота
TIMEZONE = 'Europe/Moscow'
TIMEZONE_COMMON_NAME = 'Moscow'
```

В этом файле указаны: токен бота и часовой пояс,
в котором тот будет работать
(это понадобится в будущем для определения времени обновления сообщений.
API Telegram не позволяет видеть временную зону пользователя,
поэтому время обновления должно отображаться с подсказкой о часовом поясе).

Создадим файл `bot.py`. Нужно импортировать все необходимые библиотеки,
файлы с настройками и предварительно созданный `pb.py`.
Если каких-то библиотек не хватает, их можно установить с помощью `pip`.

```python
import telebot
import config
import pb
import datetime
import pytz
import json
import traceback


P_TIMEZONE = pytz.timezone(config.TIMEZONE)
TIMEZONE_COMMON_NAME = config.TIMEZONE_COMMON_NAME
```

Создадим бота с помощью библиотеки `pyTelegramBotAPI`.
Для этого конструктору нужно передать токен:

```python
bot = telebot.TeleBot(config.TOKEN)
bot.polling(none_stop=True)
```

## Шаг № 4: написать обработчик команды `/start`

Теперь чат-бот на Python работает и постоянно посылает запросы с помощью метода `getUpdates`.
Параметр `none_stop` отвечает за то, чтобы запросы отправлялись,
даже если API возвращает ошибку при выполнении метода.

Из переменной бота возможно вызывать любые методы API Telegram-бота.

Начнем с написания обработчика команды `/start`
и добавим его перед строкой `bot.polling(none_stop=True)`:

```python
@bot.message_handler(commands=['start'])  
def start_command(message):  
    bot.send_message(  
        message.chat.id,  
        'Greetings! I can show you exchange rates.\n'
        'To get the exchange rates press /exchange.\n'
        'To get help press /help.'  
    )
```

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

В нашем случае если условие `commands=['start']` равно `True`,
тогда будет вызвана функция `start_command`.
Объект сообщения (десериализованный тип `Message`) будет передан функции.
После этого вы просто запускаете `send_message` в том же чате с конкретным сообщением.

Это было просто, не так ли?

## Шаг № 5: создать обработчик команды `/help`

Давайте оживим обработчик команды `/help`
с помощью встроенной кнопки со ссылкой на ваш аккаунт в Telegram.
Кнопку можно озаглавить **«Message the developer»**.

```python
@bot.message_handler(commands=['help'])  
def help_command(message):  
    keyboard = telebot.types.InlineKeyboardMarkup()  
    keyboard.add(  
        telebot.types.InlineKeyboardButton(  
            'Message the developer', url='telegram.me/artiomtb'  
        )  
    )  
    bot.send_message(  
        message.chat.id,  
        '1) To receive a list of available currencies press /exchange.\n'
        '2) Click on the currency you are interested in.\n'
        '3) You will receive a message containing information regarding the source and the target currencies, '
        'buying rates and selling rates.\n'
        '4) Click “Update” to receive the current information regarding the request. '
        'The bot will also show the difference between the previous and the current exchange rates.\n'
        '5) The bot supports inline. Type @ in any chat and the first letters of a currency.',  
        reply_markup=keyboard  
    )
```

Как видно в примере выше, был использован дополнительный параметр
(`reply_markup`) для метода `send_message`.
Метод получил встроенную клавиатуру (`InlineKeyboardMarkup`)
с одной кнопкой (`InlineKeyboardButton`) и следующим текстом:
«Message the developer» и `url='telegram.me/artiomtb'`.

Результат выше выглядит вот так:

**::TODO::**

## Шаг № 6: добавить обработчик команды `/exchange`

Обработчик команды `/exchange` отображает меню выбора валюты
и встроенную клавиатуру с 3 кнопками: `USD`, `EUR` и `RUR`
(это валюты, поддерживаемые API банка).

```python
@bot.message_handler(commands=['exchange'])  
def exchange_command(message):  
    keyboard = telebot.types.InlineKeyboardMarkup()  
    keyboard.row(  
        telebot.types.InlineKeyboardButton('USD', callback_data='get-USD')  
    )  
    keyboard.row(  
        telebot.types.InlineKeyboardButton('EUR', callback_data='get-EUR'),  
        telebot.types.InlineKeyboardButton('RUR', callback_data='get-RUR')  
    )  
  
    bot.send_message(  
        message.chat.id,   
        'Click on the currency of choice:',  
        reply_markup=keyboard  
    )
```

Вот как работает `InlineKeyboardButton`.
Когда пользователь нажимает на кнопку, вы получаете `CallbackQuery`
(в параметре `data` содержится `callback-data`) в `getUpdates`.
Таким образом вы знаете, какую именно кнопку нажал пользователь,
и как ее правильно обработать.

Вот как выглядит работа команды `/exchange`:

**::TODO::**

## Шаг № 7: написать обработчик для кнопок встроенной клавиатуры

В библиотеке pyTelegramBotAPI есть декоратор `@bot.callback_query_handler`,
который передает объект `CallbackQuery` во вложенную функцию.

```python
@bot.callback_query_handler(func=lambda call: True)  
def iq_callback(query):  
    data = query.data  
    if data.startswith('get-'):  
        get_ex_callback(query)
```

Давайте реализуем метод `get_ex_callback`:

```python
def get_ex_callback(query):  
    bot.answer_callback_query(query.id)  
    send_exchange_result(query.message, query.data[4:])
```

Метод `answer_callback_query` нужен, чтобы убрать состояние загрузки,
к которому переходит бот после нажатия кнопки.
Отправим сообщение `send_exchange_query`.
Ему нужно передать `Message` и код валюты (получить ее можно из `query.data`.
Если это, например, `get-USD`, передавайте `USD`).

Реализуем `send_exchange_result`:

```python
def send_exchange_result(message, ex_code):  
    bot.send_chat_action(message.chat.id, 'typing')  
    ex = pb.get_exchange(ex_code)  
    bot.send_message(  
        message.chat.id, serialize_ex(ex),  
        reply_markup=get_update_keyboard(ex),  
    parse_mode='HTML'  
    )
```

Все довольно просто.

Сперва отправим состояние ввода в чат,
так чтобы бот показывал индикатор **«набора текста»**,
пока API банка получает запрос.
Теперь вызовем метод `get_exchange` из файла `pb.py`,
который получит код валюты (например, USD).
Также нужно вызвать два новых метода в `send_message`: `serialize_ex`,
сериализатор валюты и `get_update_keyboard`
(который возвращает клавиатуре кнопки «Update« и «Share»).

```python
def get_update_keyboard(ex):  
    keyboard = telebot.types.InlineKeyboardMarkup()  
    keyboard.row(  
        telebot.types.InlineKeyboardButton(  
            'Update',  
        callback_data=json.dumps({  
                't': 'u',  
        'e': {  
                    'b': ex['buy'],  
            's': ex['sale'],  
            'c': ex['ccy']  
                }  
            }).replace(' ', '')  
        ),  
    telebot.types.InlineKeyboardButton('Share', switch_inline_query=ex['ccy'])  
    )  
    return keyboard
```

Запишем в `get_update_keyboard` текущий курс валют в `callback_data` в форме JSON.
JSON сжимается, потому что максимальный разрешенный размер файла равен 64 байтам.

Кнопка `t` значит тип, а `e` — обмен. Остальное выполнено по тому же принципу.

У кнопки **Share** есть параметр `switch_inline_query`.
После нажатия кнопки пользователю будет предложено выбрать один из чатов,
открыть этот чат и ввести имя бота и определенный запрос в поле ввода.

Методы `serialize_ex` и дополнительный `serialize_exchange_diff` нужны,
чтобы показывать разницу между текущим и старыми курсами валют после нажатия кнопки `Update`.

```python
def serialize_ex(ex_json, diff=None):  
    result = '' + ex_json['base_ccy'] + ' -> ' + ex_json['ccy'] + ':\n\n' + \  
             'Buy: ' + ex_json['buy']  
    if diff:  
        result += ' ' + serialize_exchange_diff(diff['buy_diff']) + '\n' + \  
                  'Sell: ' + ex_json['sale'] + \  
                  ' ' + serialize_exchange_diff(diff['sale_diff']) + '\n'  
    else:  
        result += '\nSell: ' + ex_json['sale'] + '\n'  
    return result


def serialize_exchange_diff(diff):  
    result = ''  
    if diff > 0:  
        result = '(' + str(diff) + ' <img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="↗️" src="https://s.w.org/images/core/emoji/2.3/svg/2197.svg">" src="https://s.w.org/images/core/emoji/2.3/svg/2197.svg">" src="https://s.w.org/images/core/emoji/2.3/svg/2197.svg">" src="https://s.w.org/images/core/emoji/72x72/2197.png">" src="https://s.w.org/images/core/emoji/72x72/2197.png">)'  
    elif diff < 0:  
        result = '(' + str(diff)[1:] + ' <img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="↘️" src="https://s.w.org/images/core/emoji/2.3/svg/2198.svg">" src="https://s.w.org/images/core/emoji/2.3/svg/2198.svg">" src="https://s.w.org/images/core/emoji/2.3/svg/2198.svg">" src="https://s.w.org/images/core/emoji/72x72/2198.png">" src="https://s.w.org/images/core/emoji/72x72/2198.png">)'  
    return result
```

Как видно, метод `serialize_ex` получает необязательный параметр `diff`.
Ему будет передаваться разница между курсами обмена в формате
`{'buy_diff': ..., 'sale_diff': ...}`.
Это будет происходить во время сериализации после нажатия кнопки `Update`.
Когда курсы валют отображаются первый раз, он нам не нужен.

Вот как будет выглядеть результат выполнения после нажатия кнопки USD:

**::TODO::**

## Шаг № 8: реализовать обработчик кнопки обновления

Теперь можно создать обработчик кнопки `Update`.
После дополнения метода `iq_callback_method` он будет выглядеть следующим образом:

```python
@bot.callback_query_handler(func=lambda call: True)  
def iq_callback(query):  
    data = query.data  
    if data.startswith('get-'):  
        get_ex_callback(query)  
    else:  
        try:  
            if json.loads(data)['t'] == 'u':  
                edit_message_callback(query)  
        except ValueError:  
            pass
```

Если данные обратного вызова начинаются с `get-`
(`get-USD`, `get-EUR` и так далее), тогда нужно вызывать `get_ex_callback`, как раньше.
В противном случае стоит попробовать разобрать строку JSON и получить ее ключ `t`.
Если его значение равно `u`, тогда нужно вызвать метод `edit_message_callback`.  
Реализуем это:

```python
def edit_message_callback(query):  
    data = json.loads(query.data)['e']  
    exchange_now = pb.get_exchange(data['c'])  
    text = serialize_ex(  
        exchange_now,  
    get_exchange_diff(  
            get_ex_from_iq_data(data),  
        exchange_now  
        )  
    ) + '\n' + get_edited_signature()  
    if query.message:  
        bot.edit_message_text(  
            text,  
        query.message.chat.id,  
        query.message.message_id,  
        reply_markup=get_update_keyboard(exchange_now),  
        parse_mode='HTML'  
    )  
    elif query.inline_message_id:  
        bot.edit_message_text(  
            text,  
        inline_message_id=query.inline_message_id,  
        reply_markup=get_update_keyboard(exchange_now),  
        parse_mode='HTML'  
    )
```

Как это работает? Очень просто:

1. Загружаем текущий курс валюты (`exchange_now = pb.get_exchange(data['c'])`).
2. Генерируем текст нового сообщения путем
сериализации текущего курса валют с параметром `diff`,
который можно получить с помощью новых методов (о них дальше).
Также нужно добавить подпись — `get_edited_signature`.
3. Вызываем метод `edit_message_text`,
если оригинальное сообщение не изменилось.
Если это ответ на встроенный запрос, передаем другие параметры.

Метод `get_ex_from_iq_data` разбирает JSON из `callback_data`:

```python
def get_ex_from_iq_data(exc_json):  
    return {  
        'buy': exc_json['b'],  
        'sale': exc_json['s']  
    }
```

Метод `get_exchange_diff` получает старое и текущее значение курсов валют
и возвращает разницу в формате `{'buy_diff': ..., 'sale_diff': ...}`:

```python
def get_exchange_diff(last, now):  
    return {  
        'sale_diff': float("%.6f" % (float(now['sale']) - float(last['sale']))),  
        'buy_diff': float("%.6f" % (float(now['buy']) - float(last['buy'])))  
    }
```

`get_edited_signature` генерирует текст «Updated…»:

```python
def get_edited_signature():  
    return 'Updated ' + \  
           str(datetime.datetime.now(P_TIMEZONE).strftime('%H:%M:%S')) + \  
           ' (' + TIMEZONE_COMMON_NAME + ')'
```

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

**::TODO::**

И вот так — если изменились:

**::TODO::**

## Шаг № 9: реализовать встроенный режим

Реализация встроенного режима значит,
что если пользователь введет `@ + имя бота` в любом чате,
это активирует поиск введенного текста и выведет результаты.
После нажатия на один из них бот отправит результат от вашего имени (с пометкой «via bot»).

```python
@bot.inline_handler(func=lambda query: True)  
def query_text(inline_query):  
    bot.answer_inline_query(  
        inline_query.id,  
        get_iq_articles(pb.get_exchanges(inline_query.query))  
    )
```

Обработчик встроенных запросов реализован.

Библиотека передаст объект `InlineQuery` в функцию `query_text`.
Внутри используется функция `answer_line`, которая должна получить `inline_query_id`
и массив объектов (результаты поиска).

Используем `get_exchanges` для поиска нескольких валют, подходящих под запрос.
Нужно передать этот массив методу `get_iq_articles`,
который вернет массив из `InlineQueryResultArticle`:

```python
def get_iq_articles(exchanges):  
    result = []  
    for exc in exchanges:  
        result.append(  
            telebot.types.InlineQueryResultArticle(  
                id=exc['ccy'],  
            title=exc['ccy'],  
            input_message_content=telebot.types.InputTextMessageContent(  
                    serialize_ex(exc),  
            parse_mode='HTML'  
        ),  
            reply_markup=get_update_keyboard(exc),  
            description='Convert ' + exc['base_ccy'] + ' -> ' + exc['ccy'],  
            thumb_height=1  
        )  
        )   
    return result
```

Теперь при вводе `@exchangetestbost + пробел` вы увидите следующее:

**::TODO::**

Попробуем набрать `usd`, и результат мгновенно отфильтруется:

**::TODO::**

Проверим предложенный результат:

**::TODO::**

Кнопка «Update» тоже работает:

**::TODO::**

Отличная работа! Встроенный режим работает!
