Как написать телеграмм бота на python aiogram

О совместимости версий

Код в главах сейчас использует aiogram 3.0 beta3. Возможна несовместимость с другими версиями.

Некоторые детали сознательно упрощены!

Автор этой книги убеждён, что помимо теории должна быть и практика. Чтобы максимально упростить повторение
приведённого далее кода, пришлось пойти на использование подходов, пригодных только для локальной разработки
и обучения.

Так, например, во всех или почти во всех главах токен бота будет указываться прямо в исходных текстах.
Это плохой подход, поскольку может привести к раскрытию токена, если вы забудете его удалить перед заливкой
кода в публичный репозиторий (например, GitHub).

Или иногда в качестве хранилищ данных будут использоваться структуры, расположенные исключительно в
оперативной памяти (словари, списки…). В действительности такие объекты нежелательны, поскольку остановка
бота приведёт безвозвратной потере данных.

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

Важно помнить, что автор ставит перед собой цель объяснить именно работу с Telegram Bot API при помощи
aiogram, а не вообще весь Computer Science во всём его многообразии.

Терминология¶

Чтобы разговаривать в одних и тех же понятиях, введём некоторые термины, дабы в дальнейшем не путаться:

  • ЛС — личные сообщения, в контексте бота это диалог один-на-один с пользователем, а не группа/канал.
  • Чат — общее название для ЛС, групп, супергрупп и каналов.
  • Апдейт — любое событие из этого списка:
    сообщение, редактирование сообщения, колбэк, инлайн-запрос, платёж, добавление бота в группу и т.д.
  • Хэндлер — асинхронная функция, которая получает от диспетчера/роутера очередной апдейт
    и обрабатывает его.
  • Диспетчер — объект, занимающийся получением апдейтов от Telegram с последующим выбором хэндлера
    для обработки принятого апдейта.
  • Роутер — аналогично диспетчеру, но отвечает за подмножество множества хэндлеров.
    Можно сказать, что диспетчер — это корневой роутер.
  • Фильтр — выражение, которое обычно возвращает True или False и влияет на то, будет вызван хэндлер или нет.
  • Мидлварь — прослойка, которая вклинивается в обработку апдейтов.

Установка¶

Для начала давайте создадим каталог для бота, организуем там virtual environment (далее venv) и
установим библиотеку aiogram.
Проверим, что установлен Python версии 3.9 (если вы знаете, что установлен 3.9 и выше, можете пропустить этот раздел):

[groosha@main lesson_01]$ python3.9
Python 3.9.9 (main, Jan 11 2022, 16:35:07) 
[GCC 11.1.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> exit()
[groosha@main lesson_01]$ 

Теперь создадим файл requirements.txt, в котором укажем используемую нами версию aiogram. Также нам понадобится
библиотека python-dotenv для файлов конфигурации.

О версиях aiogram

В этой главе используется aiogram 3.x, перед началом работы рекомендую заглянуть в
канал релизов библиотеки и проверить наличие более новой версии. Подойдёт любая
более новая, начинающаяся с цифры 3, поскольку aiogram 2.x более рассматриваться не будет и считается устаревшим.

[groosha@main 01_quickstart]$ python3.9 -m venv venv
[groosha@main 01_quickstart]$ echo "aiogram==3.0.0b3" > requirements.txt
[groosha@main 01_quickstart]$ echo "python-dotenv==0.20.0" >> requirements.txt
[groosha@main 01_quickstart]$ source venv/bin/activate
(venv) [groosha@main 01_quickstart]$ pip install -r requirements.txt 
# ...здесь куча строк про установку...
Successfully installed ...тут длинный список...
[groosha@main 01_quickstart]$

Обратите внимание на префикс «venv» в терминале. Он указывает, что мы находимся в виртуальном окружении с именем «venv».
Проверим, что внутри venv вызов команды python указывает на всё тот же Python 3.9:

(venv) [groosha@main 01_quickstart]$ python
Python 3.9.9 (main, Jan 11 2022, 16:35:07) 
[GCC 11.1.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> exit()
(venv) [groosha@main 01_quickstart]$ deactivate 
[groosha@main 01_quickstart]$ 

Последней командой deactivate мы вышли из venv, чтобы он нам не мешал.

Первый бот¶

Давайте создадим файл bot.py с базовым шаблоном бота на aiogram:

bot.py

import asyncio
import logging
from aiogram import Bot, Dispatcher, types

# Включаем логирование, чтобы не пропустить важные сообщения
logging.basicConfig(level=logging.INFO)
# Объект бота
bot = Bot(token="12345678:AaBbCcDdEeFfGgHh")
# Диспетчер
dp = Dispatcher()

# Хэндлер на команду /start
@dp.message(commands=["start"])
async def cmd_start(message: types.Message):
    await message.answer("Hello!")

# Запуск процесса поллинга новых апдейтов
async def main():
    await dp.start_polling(bot)

if __name__ == "__main__":
    asyncio.run(main())

Первое, на что нужно обратить внимание: aiogram — асинхронная библиотека, поэтому ваши хэндлеры тоже должны быть асинхронными,
а перед вызовами методов API нужно ставить ключевое слово await, т.к. эти вызовы возвращают корутины.

Асинхронное программирование в Python

Не стоит пренебрегать официальной документацией!
Прекрасный туториал по asyncio доступен на сайте Python.

Если вы в прошлом работали с какой-то другой библиотекой для Telegram, например, pyTelegramBotAPI, то концепция
хэндлеров (обработчиков событий) вам сразу станет понятна, разница лишь в том, что в aiogram хэндлерами управляет диспетчер.
Диспетчер регистрирует функции-обработчики, дополнительно ограничивая перечень вызывающих их событий через фильтры.
После получения очередного апдейта (события от Telegram), диспетчер выберет нужную функцию обработки, подходящую по всем
фильтрам, например, «обработка сообщений, являющихся изображениями, в чате с ID икс и с длиной подписи игрек». Если две
функции имеют одинаковые по логике фильтры, то будет вызвана та, что зарегистрирована раньше.

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

Рассмотрим следующий код:

# Хэндлер на команду /test1
@dp.message(commands=["test1"])
async def cmd_test1(message: types.Message):
    await message.reply("Test 1")

# Хэндлер на команду /test2
async def cmd_test2(message: types.Message):
    await message.reply("Test 2")

Давайте запустим с ним бота:
Команда /test2 не работает

Хэндлер cmd_test2 не сработает, т.к. диспетчер о нём не знает. Исправим эту ошибку
и отдельно зарегистрируем функцию:

# Хэндлер на команду /test2
async def cmd_test2(message: types.Message):
    await message.reply("Test 2")

# Где-то в другом месте, например, в функции main():
dp.message.register(cmd_test2, commands=["test2"])

Снова запустим бота:
Обе команды работают

Синтаксический сахар¶

Для того чтобы сделать код чище и читабельнее, aiogram расширяет возможности стандартных объектов Telegram.
Например, вместо bot.send_message(...) можно написать message.answer(...) или message.reply(...). В последних
двух случаях не нужно подставлять chat_id, подразумевается, что он такой же, как и в исходном сообщении.
Разница между answer и reply простая: первый метод просто отправляет сообщение в тот же чат, второй делает «ответ» на
сообщение из message:

@dp.message(commands=["answer"])
async def cmd_answer(message: types.Message):
    await message.answer("Это простой ответ")


@dp.message(commands=["reply"])
async def cmd_reply(message: types.Message):
    await message.reply('Это ответ с "ответом"')

Разница между message.answer() и message.reply()

Более того, для большинства типов сообщений есть вспомогательные методы вида
«answer_{type}» или «reply_{type}», например:

@dp.message(commands=["dice"])
async def cmd_dice(message: types.Message):
    await message.answer_dice(emoji="🎲")

что значит ‘message: types.Message’ ?

Python является интерпретируемым языком с сильной, но динамической типизацией,
поэтому встроенная проверка типов, как, например, в C++ или Java, отсутствует. Однако начиная с версии 3.5
в языке появилась поддержка подсказок типов, благодаря которой
различные чекеры и IDE вроде PyCharm анализируют типы используемых значений и подсказывают
программисту, если он передаёт что-то не то. В данном случае подсказка types.Message соообщает
PyCharm-у, что переменная message имеет тип Message, описанный в модуле types библиотеки
aiogram (см. импорты в начале кода). Благодаря этому IDE может на лету подсказывать атрибуты и функции.

При вызове команды /dice бот отправит в тот же чат игральный кубик. Разумеется, если его надо отправить в какой-то
другой чат, то придётся по-старинке вызывать await bot.send_dice(...). Но объект bot (экземпляр класса Bot) может быть
недоступен в области видимости конкретной функции. В aiogram 3.x объект бота, которому пришёл апдейт, неявно
прокидывается в хэндлер и его можно достать как аргумент bot. Предположим, вы хотите по команде /dice
отправлять кубик не в тот же чат, а в канал с ID -100123456789. Перепишем предыдущую функцию:

@dp.message(commands=["dice"])
async def cmd_dice(message: types.Message, bot: Bot):
    await bot.send_dice(-100123456789, emoji="🎲")

Иногда при запуске бота может потребоваться передать одно или несколько дополнительных значений. Это может быть
объект конфигурации, список администраторов группы, отметка времени и что угодно ещё. Для этого достаточно передать
параметры как дополнительные именованные (!) аргументы функции start_polling(...) (для вебхуков есть аналогичный
способ). В хэндлерах для получения этих значений достаточно указать их как те же аргументы. Более того, изменение таких
объектов в одних хэндлерах влияют на их содержимое в других. Рассмотрим на примере:

@dp.message(commands=["add_to_list"])
async def cmd_add_to_list(message: types.Message, mylist: list[int]):
    mylist.append(7)
    await message.answer("Добавлено число 7")


@dp.message(commands=["show_list"])
async def cmd_show_list(message: types.Message, mylist: list[int]):
    await message.answer(f"Ваш список: {mylist}")

Теперь список mylist можно читать и писать в разных хэндлерах. Существует также ещё один вариант, более подходящий
в других ситуациях. Речь, конечно же, о мидлварях, про которые подробно рассказывается
в соответствующей главе.

Аргумент mylist может быть изменён между вызовами

Файлы конфигурации¶

Чтобы не хранить токен прямо в коде (вдруг вы захотите залить своего бота в публичный репозиторий?) можно вынести
подобные данные в отдельный конфигурационный файл. Существует хорошее и адекватное мнение,
что для прода достаточно переменных окружения, однако в рамках этой книги мы будем пользоваться отдельными файлами .env,
чтобы немного упростить себе жизнь и сэкономить читателям время на разворачивание демонстрационного проекта.

Итак, создадим рядом с bot.py отдельный файл config_reader.py со следующим содержимым

config_reader.py

from pydantic import BaseSettings, SecretStr


class Settings(BaseSettings):
    # Желательно вместо str использовать SecretStr 
    # для конфиденциальных данных, например, токена бота
    bot_token: SecretStr

    # Вложенный класс с дополнительными указаниями для настроек
    class Config:
        # Имя файла, откуда будут прочитаны данные 
        # (относительно текущей рабочей директории)
        env_file = '.env'
        # Кодировка читаемого файла
        env_file_encoding = 'utf-8'


# При импорте файла сразу создастся 
# и провалидируется объект конфига, 
# который можно далее импортировать из разных мест
config = Settings()

Теперь немного отредактируем наш bot.py:

bot.py

# импорты
from config_reader import config

# Для записей с типом Secret* необходимо 
# вызывать метод get_secret_value(), 
# чтобы получить настоящее содержимое вместо '*******'
bot = Bot(token=config.bot_token.get_secret_value())

Наконец, создадим файл .env (с точкой в начале), где опишем токен бота:

.env

BOT_TOKEN = 0000000000:AaBbCcDdEeFfGgHhIiJjKkLlMmNn

Если всё сделано правильно, то при запуске python-dotenv подгрузит переменные из файла .env, pydantic
их провалидирует и объект бота успешно создастся с нужным токеном.

На этом мы закончим знакомство с библиотекой, а в следующих главах рассмотрим другие «фишки» aiogram и Telegram Bot API.

#Руководства

  • 14 сен 2022

  • 0

Разбираемся, как написать чат-бота с помощью библиотеки aiogram. Весь код — внутри статьи.

Иллюстрация: Polina Vari для Skillbox Media

Антон Яценко

Изучает Python, его библиотеки и занимается анализом данных. Любит путешествовать в горах.

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

В этой статье мы научимся с нуля создавать чат-ботов с помощью Python: выберем лучшую библиотеку и напишем на ней эхо-бота, который отвечает на сообщения пользователя точно такими же сообщениями. Это первая часть урока по чат-ботам в Telegram — во второй части мы добавим новые фичи.

Для создания Telegram-ботов на Python существует несколько десятков библиотек. Они различаются популярностью, размером комьюнити и функциональностью. Рассмотрим самые популярные.

aiogram

Современная библиотека, набирающая популярность. Работает с асинхронным подходом к выполнению кода. Это позволяет не останавливать работу бота в ожидании ответа пользователя. У aiogram подробная документация и большое русскоязычное комьюнити. В этой и последующих статьях цикла мы будем работать как раз с этой библиотекой.

python-telegram-bot

Одна из первых библиотек для создания ботов. Отличается от aiogram синхронным подходом к работе, то есть при ожидании ответа от пользователя выполнение кода останавливается.

TeleBot

Библиотека для создания простых ботов, позволяющая работать с асинхронным и синхронным подходом на выбор. Подходит для небольших проектов. Подробнее можно узнать в документации.

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

Для этого нам необходимо:

  • установить Python и настроить виртуальное окружение;
  • зарегистрировать бота в специальном Telegram-канале @BotFather;
  • установить библиотеку aiogram;
  • написать код эхо-бота, связав его по API с Telegram.

На macOS или Linux. Python установлен в эти операционные системы изначально. Чтобы проверить его наличие, откройте терминал и введите команду:

python --version

Если Python установлен, то терминал покажет его версию:

Скриншот: aiogram / Skillbox Media

На Windows требуется установка Python. Сделать это проще всего по нашей инструкции.

После установки и проверки Python требуется установить виртуальное окружение с помощью virtualenv. Это специальный инструмент, который позволяет изолировать друг от друга проекты в разработке, независимо устанавливая для них библиотеки и пакеты. Удобно, когда вы работаете над разными приложениями одновременно.

virtualenv устанавливается через терминал:

sudo pip3 install virtualenv

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

mkdir telegram_bot
cd telegram_bot

Команда mkdir создаст папку telegram_bot, а команда cd переведёт нас в неё. Теперь в этой директории будут храниться файлы проекта, связанные с нашим ботом.

Развернём виртуальное окружение внутри папки telegram_bot:

virtualenv venv -p python3

Теперь его активируем. Если этого не сделать, то оно не будет работать.

source venv/bin/activate

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


Для создания бота необходимо воспользоваться Telegram и ботом @BotFather. Откройте мессенджер и введите название бота в поисковой строке:

Скриншот: aiogram / Skillbox Media

Открываем его, жмём кнопку «Запустить» и вводим команду /newbot:

Скриншот: aiogram / Skillbox Media

Теперь напишем название и юзернейм для нашего бота. Назовём его echo_skillbox_bot (теперь это имя занято, так что вам надо будет придумать своё). В ответ придёт наш токен, который мы будем использовать для подключения к API Telegram.

Скриншот: aiogram / Skillbox Media

Этот токен мы сохраняем — он потребуется нам в будущем.


Для установки aiogram воспользуемся менеджером пакетов pip. Вводим в терминал:

pip install aiogram

Важно! Библиотека устанавливается в созданное ранее виртуальное окружение, связанное с папкой telegram_bot. Если вы решите создать нового бота в другой директории на компьютере, то установку будет необходимо провести заново, иначе aiogram не будет работать.


Писать код на Python лучше всего в IDE, а не в окне терминала. В проекте ниже мы будем использовать бесплатный редактор Visual Studio Code, но вы можете воспользоваться любым удобным для вас инструментом.

Откроем IDE и создадим файл main.py. Для этого проекта нам потребуется только он. Импортируем из aiogram нужные классы и модуль:

from aiogram import Bot, Dispatcher, executor, types

Разберёмся, что каждый из них делает. Начнём с классов:

  • Bot определяет, на какие команды от пользователя и каким способом отвечать;
  • Dispatcher позволяет отслеживать обновления;
  • Executor запускает бота и выполняет функции, которые следует выполнить.

Модуль types позволит нам использовать базовые классы для аннотирования, то есть восприятия сообщений. Например, мы будем использовать types.Message, позволяющий работать с приёмом текстовых сообщений пользователя. Подробно об этом можно прочесть в документации.

Импортируем наш токен, который поможет коммуницировать с API Telegram:

API_TOKEN = '5602787567:AAGYv7NrSjwyW7qPs_yvu70C060zrcfZDbQ' #В одинарных кавычках размещаем токен, полученный от @BotFather.

Теперь необходимо инициализировать объекты bot и Dispatcher, передав первому наш токен. Если их не инициализировать, то код не будет работать.

bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)

Настроим приветственное окно для нового пользователя, которое будет появляться при нажатии команды /start. Для этого создаём message_handler и прописываем функцию ответа:

@dp.message_handler(commands=['start']) #Явно указываем в декораторе, на какую команду реагируем. 
async def send_welcome(message: types.Message):
   await message.reply("Привет!nЯ Эхо-бот от Skillbox!nОтправь мне любое сообщение, а я тебе обязательно отвечу.") #Так как код работает асинхронно, то обязательно пишем await.

Теперь при нажатии на кнопку Начать или при вводе команды /start пользователь будет получать от бота приветственное сообщение.

Разберёмся в коде:

  • message_handler — это декоратор, который реагирует на входящие сообщения и содержит в себе функцию ответа. Декоратор — это «обёртка» вокруг функций, позволяющая влиять на их работу без изменения кода самих функций. В нашем случае мы управляем функцией, считая команды пользователя;
  • commands=[‘start’] — это команда, которая связана с декоратором и запускает вложенную в него функцию;
  • async def send_welcome — создаёт асинхронную функцию, которая принимает в себя сообщение пользователя message, определяемое через тип Message. Саму функцию можно назвать любым образом. Мы выбрали send_welcome, чтобы название было понятным и осмысленным;
  • await message.reply — определяет ответ пользователя, используя await из-за асинхронности работы библиотеки.

Теперь создадим событие, которое будет обрабатывать введённое пользователем сообщение:

@dp.message_handler() #Создаём новое событие, которое запускается в ответ на любой текст, введённый пользователем.
async def echo(message: types.Message): #Создаём функцию с простой задачей — отправить обратно тот же текст, что ввёл пользователь.
   await message.answer(message.text)

Так как бот должен реагировать на любое текстовое сообщение от пользователя, то скобки в @dp.message_handler мы оставляем пустыми. Параметр message не отличается от использованного в предыдущих шагах.

Для ответа мы также используем метод message, указывая, что возвращаем исходный текст, принятый в message.

Остаётся последний этап — настроить получение сообщений от сервера в Telegram. Если этого не сделать, то мы не получим ответы бота. Реализовать получение новых сообщений можно с помощью поллинга. Он работает очень просто — метод start_polling опрашивает сервер, проверяя на нём обновления. Если они есть, то они приходят в Telegram. Для включения поллинга необходимо добавить две строчки:

if __name__ == '__main__':
   executor.start_polling(dp, skip_updates=True)

Всё, теперь код нашего бота полностью готов:

from aiogram import Bot, Dispatcher, executor, types
 
API_TOKEN = '5602787567:AAGYv7NrSjwyW7qPs_yvu70C060zrcfZDbQ'
 
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)
 
@dp.message_handler(commands=['start'])
async def send_welcome(message: types.Message):
   await message.reply("Привет!nЯ Эхо-бот от Skillbox!nОтправь мне любое сообщение, а я тебе обязательно отвечу.")
 
@dp.message_handler()
async def echo(message: types.Message):
   await message.answer(message.text)
 
if __name__ == '__main__':
   executor.start_polling(dp, skip_updates=True)

Сохраняем его в нашей папке telegram_bot под именем main.py.


Для запуска бота нам необходим терминал. Открываем его и переходим в нашу папку telegram_bot. После этого вводим команду:

python3 main.py

В ответ терминал пришлёт сообщение, что обновления успешно пропущены:

Скриншот: aiogram / Skillbox Media

Находим нашего бота в Telegram по имени @echo_skillbox_bot и запускаем его, нажав на кнопку Начать. В ответ на это или на команду /start нам придёт приветственное сообщение:

Скриншот: aiogram / Skillbox Media

Попробуем написать что-то:

Скриншот: aiogram / Skillbox Media

Как мы видим — всё работает. Бот возвращает нам наши сообщения.

Расширять функциональность бота, указывая для разных команд пользователя разные ответы. Например, добавить раздел помощи, который будет появляться по команде /help. Или настроить запуск кода на виртуальном сервере, чтобы бот работал независимо от вашего компьютера.

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

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

Участвовать

Научитесь: Профессия Python-разработчик
Узнать больше

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

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

  1. Настройка
  2. Hello, bot!
  3. Docker
  4. Деплой на AWS
  5. Заключение

Настройка

Откройте Telegram, найдите @BotFather и начните беседу. Отправьте команду /newbot и следуйте инструкциям. Вы получите:

  • свой токен;
  • адрес Telegram API (https://api.telegram.org/bot);
  • ссылку на документацию.

Обязательно сохраните токен, так как это ключ для взаимодействия с ботом.

Примечание Хранение токена должно быть локальным: ни в коем случае не выгружайте его в общий доступ, например в GitHub-репозиторий .

Далее начните беседу. Введите в поисковой строке имя бота и нажмите /start. Отправьте любое сообщение: оно станет первым обновлением, которое получит Телеграм бот.

Установка Python

Для написания Telegram-бота на Python, нужно установить сам язык. Если вы пользуетесь Windows, скачать Python можно с официального сайта. Версия важна. Нам подойдет Python не ниже версии 3.7. Если же у вас Linux или macOS, то, скорее всего, у вас стоит Python 3.6. Как обновиться, можете почитать здесь.

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

Установка pip

Это менеджер пакетов. В версиях выше Python 2.7.9 и Python 3.4, а также на macOS/Linux он уже есть. Проверить это можно командой pip --version в терминале. Если же по каким-то причинам он отсутствует, установить его можно при помощи команды:

$ sudo apt-get install python-pip

Установка aiogram

Установить данный фреймворк для Telegram Bot API с помощью pip:

pip install aiogram

Hello, bot!

Давайте напишем простенькую программу приветствия. Для начала следует импортировать библиотеки и создать экземпляры Телеграм бота и диспетчера:

from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils import executor
TOKEN = "ваш токен от бота здесь"
bot = Bot(token=TOKEN)
dp = Dispatcher(bot)

Теперь напишем обработчик текстовых сообщений, который будет обрабатывать входящие команды /start и /help:

@dp.message_handler(commands=['start', 'help'])
async def send_welcome(msg: types.Message):
    await msg.reply_to_message(f‘Я бот. Приятно познакомиться,
                               {msg.from_user.first_name}’)

Добавим ещё один обработчик для получения текстовых сообщений. Если бот получит «Привет», он также поздоровается. Все остальные сообщения будут определены, как нераспознанные:

@dp.message_handler(content_types=['text'])
async def get_text_messages(msg: types.Message):
   if msg.text.lower() == 'привет':
       await msg.answer('Привет!')
   else:
       await msg.answer('Не понимаю, что это значит.')

Запускаем Telegram бота, написанного на Python, следующим образом:

if __name__ == '__main__':
   executor.start_polling(dp)

Примечание Так мы задаём боту непрерывное отслеживание новых сообщений. Если бот упадёт, а сообщения продолжат поступать, они будут накапливаться в течение 24 часов на серверах Telegram, и в случае восстановления бота прилетят ему все сразу.

Ну вот и всё, простенький бот в Телеграмме на языке Python готов.

Docker

Сейчас мало кто не слышал про Docker, но если вдруг не слышали — вот хорошая статья. Для нашего проекта потребуется самый простой Dockerfile:

FROM python:3.8
# set work directory
WORKDIR /usr/src/app/
# copy project
COPY . /usr/src/app/
# install dependencies
RUN pip install --user aiogram
# run app
CMD ["python", "bot.py"]

Каталог проекта должны при этом содержать следующие файлы:

  • bot.py;
  • Dockerfile.

Для локальных тестов достаточно установить Docker (linux, mac, windows), после чего в папке проекта собрать и запустить контейнер с помощью команд:

docker build -t my_app
docker run -d my_app

my_app — это просто название нашего контейнера, вместо которого можно использовать другое имя.

-d — специальный флаг, который запускает контейнер в фоне и позволяет дальше работать в терминале. Это называется detached mode.

Деплой на AWS

Прежде всего нам понадобится аккаунт на Docker Hub. Это аналог GitHub, только не с исходниками кода, а с уже созданными контейнерами. Работа с Docker Hub выглядит достаточно просто:

  1. Локально или с помощью пайплайнов собрали контейнер.
  2. Загрузили его на докер хаб.
  3. В любом удобном месте скачали его. Это может быть локальная машина, VPS сервер или облачный провайдер по типу AWS.
  4. Запустили.

Пройдёмся по этим шагам. Везде, где указано <docker_hub_username>, надо вставлять свой юзернейм, использованный при регистрации на докерхабе. Если это ваша первая публикация на докерхаб, для начала потребуется залогиниться с помощью docker login.

Билдим контейнер:

docker build -t <docker_hub_username>/my_app

Загружаем его на докерхаб:

docker push <docker_hub_username>/my_app

Для проверки успешности загрузки можете запустить контейнер из Docker Hub с помощью команды:

docker run -d <docker_hub_username>/my_app

Далее загрузим наш контейнер в AWS Elastic Beanstalk. Для этого потребуется аккаунт на AWS. Если его нет, необходимо зарегистрироваться. Вас попросят ввести данные карты для верификации, но переживать не стоит, ведь мы воспользуемся бесплатным годовым триалом. Чтобы поиграться, этого более чем достаточно, а вот если вы захотите вывести проект в продакшен, следует перейти на VPS — это даст больше контроля и гибкости.

  • Переходим в Elastic Beanstalk, на вкладку Applications, и создаём новое приложение:

Elastic Beanstalk вкладка Applications

  • Называем приложение, теги оставляем пустыми:

Называем приложение в Elastic Beanstalk

  • Создаём для приложения environment:

environment для будущего Telegram-бота

  • Выбираем Worker environment:

Worker environment для будущего Telegram-бота

  • В качестве платформы выбираем Docker:

Docker для Telegram-бота на Python

  • В пункте Application code нужно загрузить JSON-файл с конфигурацией Docker-образа. Сам файл:
Dockerrun.aws.json
{
 "AWSEBDockerrunVersion": "1",
 "Image": {
   "Name": "<docker_hub_username>/my_app",
   "Update": "true"
 },
"Ports": [
 {
   "ContainerPort": 5000,
   "HostPort": 8000
 }
]
}

Application code

  • Создаём окружение:

Создаём окружение

  • AWS начинает создавать окружение, просто ждём завершения процесса:

AWS начинает создавать окружение

  • Если всё прошло успешно, вы увидите индикатор успешного запуска приложения:

Docker для Телеграм бота

Проверяем работу нашего Telegram bot:

Проверка Телеграм бота

Успех!

Заключение

Поздравляем! Теперь вы знаете, как писать роботов для Telegram на Python.

Бота можно дополнять другими функциями, например, добавить отправку файлов, опрос или клавиатуру.

Кстати, в телеграмме есть аж целых два типа клавиатур:

  1. Классическая RelpyKeyboardMarkup, у которой кнопки располагаются под полем ввода сообщения:
    RelpyKeyboardMarkup
  2. Более современная InlineKeyboardMarkup, которая привязывается к конкретному сообщению:
    InlineKeyboardMarkup

Но и это полностью рабочий Телеграм-бот на Python: дополните словарём, и получите полноценную беседу. Также можете опробовать функциональность нашего Telegram-бота.

В «настоящих проектах» не обойтись без базы данных. Тут на помощь приходит docker-compose, который позволяет объединить несколько контейнеров в один сервис. Таким образом, например, можно создать приложение и положить его в контейнер, а базу данных, как отдельный сервис, поместить в другой контейнер, и с помощью docker-compose наладить между ними связь.

Также для более серьёзной разработки лучше использовать выделенный виртуальный сервер (VPS): он даёт гораздо больше гибкости и свободы, чем тот же AWS. А самое главное, он более приближён к «боевой» разработке. Схема работы тут будет даже проще, чем с AWS: вам просто нужно установить Docker, спуллить образ с Docker Hub и запустить его.

Время на прочтение
11 мин

Количество просмотров 39K

Штош. В этой статье я расскажу вам, как создать Telegram бота, который получает текущую погоду по IP адресу. Мы будем использовать язык Python и асинхронную библиотеку для взаимодействия с Telegram Bot API — aiogram.

Итак, как же вы можете создать такого бота?

TL;DR

Склонируйте репозиторий shtosh-weather-bot и пройдите по инструкции в README.

Выбираем погодный сервис с бесплатным API

Данные о текущей погоде нам нужно откуда-то брать. Еще желательно, чтобы это было бесплатно. У сайта OpenWeatherMap есть нужный нам API текущей погоды. Бесплатно можно посылать 1000 запросов в день.

https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={API key}

Кстати, если вы ищете какой-то application user interface для своего проекта, рекомендую репозиторий public-apis.

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

Заходим в My API keys и видим здесь тот самый ключ. Можете взять мой, мне не жалко.

Итак, давайте сформируем запрос. Я выбрал координаты Нью-Йорка, просто потому что хочу и могу. Не забудем добавить флаг units со значением metric, чтобы температура показывалась в градусах Цельсия. По умолчанию дается в Кельвинах.

https://api.openweathermap.org/data/2.5/weather?lat=40,7143&lon=-74,006&appid=8537d9ef6386cb97156fd47d832f479c&units=metric

Вот такой json мы получаем.

{
  "coord": {
    "lon": -74.006,
    "lat": 40.7128
  },
  "weather": [
    {
      "id": 802,
      "main": "Clouds",
      "description": "scattered clouds",
      "icon": "03d"
    }
  ],
  "base": "stations",
  "main": {
    "temp": 31.36,
    "feels_like": 31.23,
    "temp_min": 28.86,
    "temp_max": 33.94,
    "pressure": 1013,
    "humidity": 39
  },
  "visibility": 10000,
  "wind": {
    "speed": 5.14,
    "deg": 290
  },
  "clouds": {
    "all": 40
  },
  "dt": 1661375748,
  "sys": {
    "type": 2,
    "id": 2039034,
    "country": "US",
    "sunrise": 1661336121,
    "sunset": 1661384510
  },
  "timezone": -14400,
  "id": 5128581,
  "name": "New York",
  "cod": 200
}

Создаем бота и устанавливаем все необходимое

Создайте Telegram бота с помощью BotFather и возьмите его токен.

Из названия видео вы могли догадаться, что мы будем использовать язык Python и библиотеку aiogram. Я надеюсь, с установкой Python у вас не возникнет проблем. С aiogram тоже.

pip install aiogram

Лирическое отступление

Я много позаимствовал у проекта Алексея Голобурдина — автора YouTube канала «Диджитализируй!» Проблема в том, что его проект предназначен только для macOS устройств, потому что координаты берутся с помощью инструмента командной строки whereami. Пример вывода:

Latitude: 45.424807, 
Longitude: -75.699234
Accuracy (m): 65.000000
Timestamp: 2019-09-28, 12:40:20 PM EDT

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

Я решил, что можно доработать идею и охватить максимальное количество пользователей, пренебрегая точностью информации.

Пишем код. Файл конфигурации

Итак, файл config.py содержит константы:

  • Токен бота BOT_API_TOKEN

  • Ключ OpenWeather WEATHER_API_KEY

  • Запрос текущей погоды CURRENT_WEATHER_API_CALL

config.py

BOT_API_TOKEN = ''
WEATHER_API_KEY = ''

CURRENT_WEATHER_API_CALL = (
        'https://api.openweathermap.org/data/2.5/weather?'
        'lat={latitude}&lon={longitude}&'
        'appid=' + WEATHER_API_KEY + '&units=metric'
)

Конечно, такие данные, как токены и ключи нужно хранить в переменных окружения, но это пет-проект, деплоить я его не буду, поэтому особо не заморачиваюсь.

Получаем координаты

Для получения координат я создал отдельный модуль. Датакласс Coordinates содержит широту и долготу с типами float.

from dataclasses import dataclass

@dataclass(slots=True, frozen=True)
class Coordinates:
    latitude: float
    longitude: float

По IP адресу их можно найти с помощью ipinfo.io/json. Получается вот такой ответ.

{
  "ip": "228.228.228.228",
  "city": "Moscow",
  "region": "Moscow",
  "country": "RU",
  "loc": "55.7522,37.6156",
  "org": "Starlink",
  "postal": "101000",
  "timezone": "Europe/Moscow",
  "readme": "https://ipinfo.io/missingauth"
}

Нас интересует ключ "loc" сокращенно от location. Опять капитан очевидность. Делаем запрос с помощью функции urlopen модуля request библиотеки urllib. Возвращаем словарь с помощью json.load()

from urllib.request import urlopen
import json

def _get_ip_data() -> dict:
    url = 'http://ipinfo.io/json'
    response = urlopen(url)
    return json.load(response)

В функции получения координат парсим этот словарь и возвращаем датакласс координат.

def get_coordinates() -> Coordinates:
    """Returns current coordinates using IP address"""
    data = _get_ip_data()
    latitude = data['loc'].split(',')[0]
    longitude = data['loc'].split(',')[1]

    return Coordinates(latitude=latitude, longitude=longitude)

coordinates.py

from urllib.request import urlopen
from dataclasses import dataclass
import json


@dataclass(slots=True, frozen=True)
class Coordinates:
    latitude: float
    longitude: float


def get_coordinates() -> Coordinates:
    """Returns current coordinates using IP address"""
    data = _get_ip_data()
    latitude = data['loc'].split(',')[0]
    longitude = data['loc'].split(',')[1]

    return Coordinates(latitude=latitude, longitude=longitude)


def _get_ip_data() -> dict:
    url = 'http://ipinfo.io/json'
    response = urlopen(url)
    return json.load(response)

Парсим ответ OpenWeather API

Далее рассмотрим модуль api_service. В нем происходит вся суета с погодой. Температура измеряется в градусах Цельсия, чему соответствует псевдоним float числа.

from typing import TypeAlias

Celsius: TypeAlias = float

Как известно, градусы Фаренгейта были созданы только для того, чтобы Рэй Брэдбери смог красиво назвать свою антиутопию.

В ответе API направление ветра дается в градусах. Я решил привести их в более удобный формат. Для этого я создал перечисление основных направлений ветра.

from enum import IntEnum

class WindDirection(IntEnum):
    North = 0
    Northeast = 45
    East = 90
    Southeast = 135
    South = 180
    Southwest = 225
    West = 270
    Northwest = 315

В функции парсинга округление по 45 градусов выглядит таким образом: делим градусы на 45, округляем и умножаем обратно на 45. Результат может округлиться до 360 градусов, поэтому обрабатываем этот случай.

def _parse_wind_direction(openweather_dict: dict) -> str:
    degrees = openweather_dict['wind']['deg']
    degrees = round(degrees / 45) * 45
    if degrees == 360:
        degrees = 0
    return WindDirection(degrees).name

Все данные о погоде будут храниться в датаклассе. При желании вы можете добавить сюда остальную информацию из ответа OpenWeather, например атмосферное давление, часовой пояс, минимальную и максимальную зафиксированную в данный момент температуру.

@dataclass(slots=True, frozen=True)
class Weather:
    location: str
    temperature: Celsius
    temperature_feeling: Celsius
    description: str
    wind_speed: float
    wind_direction: str
    sunrise: datetime
    sunset: datetime

В остальном ничего интересного в модуле не происходит, просто парсинг json.

api_service.py

from typing import Literal, TypeAlias
from urllib.request import urlopen
from dataclasses import dataclass
from datetime import datetime
from enum import IntEnum
import json

from coordinates import Coordinates
import config

Celsius: TypeAlias = float


class WindDirection(IntEnum):
    North = 0
    Northeast = 45
    East = 90
    Southeast = 135
    South = 180
    Southwest = 225
    West = 270
    Northwest = 315


@dataclass(slots=True, frozen=True)
class Weather:
    location: str
    temperature: Celsius
    temperature_feeling: Celsius
    description: str
    wind_speed: float
    wind_direction: str
    sunrise: datetime
    sunset: datetime


def get_weather(coordinates=Coordinates) -> Weather:
    """Requests the weather in OpenWeather API and returns it"""
    openweather_response = _get_openweather_response(
        longitude=coordinates.longitude, latitude=coordinates.latitude
    )
    weather = _parse_openweather_response(openweather_response)
    return weather


def _get_openweather_response(latitude: float, longitude: float) -> str:
    url = config.CURRENT_WEATHER_API_CALL.format(latitude=latitude, longitude=longitude)
    return urlopen(url).read()


def _parse_openweather_response(openweather_response: str) -> Weather:
    openweather_dict = json.loads(openweather_response)
    return Weather(
        location=_parse_location(openweather_dict),
        temperature=_parse_temperature(openweather_dict),
        temperature_feeling=_parse_temperature_feeling(openweather_dict),
        description=_parse_description(openweather_dict),
        sunrise=_parse_sun_time(openweather_dict, 'sunrise'),
        sunset=_parse_sun_time(openweather_dict, 'sunset'),
        wind_speed=_parse_wind_speed(openweather_dict),
        wind_direction=_parse_wind_direction(openweather_dict)
    )


def _parse_location(openweather_dict: dict) -> str:
    return openweather_dict['name']


def _parse_temperature(openweather_dict: dict) -> Celsius:
    return openweather_dict['main']['temp']


def _parse_temperature_feeling(openweather_dict: dict) -> Celsius:
    return openweather_dict['main']['feels_like']


def _parse_description(openweather_dict) -> str:
    return str(openweather_dict['weather'][0]['description']).capitalize()


def _parse_sun_time(openweather_dict: dict, time: Literal["sunrise", "sunset"]) -> datetime:
    return datetime.fromtimestamp(openweather_dict['sys'][time])


def _parse_wind_speed(openweather_dict: dict) -> float:
    return openweather_dict['wind']['speed']


def _parse_wind_direction(openweather_dict: dict) -> str:
    degrees = openweather_dict['wind']['deg']
    degrees = round(degrees / 45) * 45
    if degrees == 360:
        degrees = 0
    return WindDirection(degrees).name

Делаем сообщения для бота

В модуле messages собраны сообщения для бота по командам. Сообщение о погоде /weather содержит локацию, описание погоды, температуру и ее ощущение.

from coordinates import get_coordinates
from api_service import get_weather

def weather() -> str:
    """Returns a message about the temperature and weather description"""
    wthr = get_weather(get_coordinates())
    return f'{wthr.location}, {wthr.description}n' 
           f'Temperature is {wthr.temperature}°C, feels like {wthr.temperature_feeling}°C'

Сообщение о ветре /wind показывает его направление и скорость в метрах в секунду.

def wind() -> str:
    """Returns a message about wind direction and speed"""
    wthr = get_weather(get_coordinates())
    return f'{wthr.wind_direction} wind {wthr.wind_speed} m/s'

Ну и сообщение о времени восхода и заката солнца /sun_time. Здесь datetime объект форматируется в часы и минуты, остальное в данном случае неважно.

def sun_time() -> str:
    """Returns a message about the time of sunrise and sunset"""
    wthr = get_weather(get_coordinates())
    return f'Sunrise: {wthr.sunrise.strftime("%H:%M")}n' 
           f'Sunset: {wthr.sunset.strftime("%H:%M")}n'

Нужно заметить, что при каждом вызове функции создается новый API запрос. Почему это нужно заметить? Потому что сначала я сделал бота с одним запросом и недоумевал, почему информация не меняется через время. Потому что в идеале делать один запрос в 5 или 10 минут, за это время погода не особо меняется, да и данные OpenWeather тоже не обновляются каждую секунду.

messages.py

from coordinates import get_coordinates
from api_service import get_weather


def weather() -> str:
    """Returns a message about the temperature and weather description"""
    wthr = get_weather(get_coordinates())
    return f'{wthr.location}, {wthr.description}n' 
           f'Temperature is {wthr.temperature}°C, feels like {wthr.temperature_feeling}°C'


def wind() -> str:
    """Returns a message about wind direction and speed"""
    wthr = get_weather(get_coordinates())
    return f'{wthr.wind_direction} wind {wthr.wind_speed} m/s'


def sun_time() -> str:
    """Returns a message about the time of sunrise and sunset"""
    wthr = get_weather(get_coordinates())
    return f'Sunrise: {wthr.sunrise.strftime("%H:%M")}n' 
           f'Sunset: {wthr.sunset.strftime("%H:%M")}n'

Inline клавиатура

Можно было сделать reply клавиатуру, но мне больше по душе Inline. 3 кнопки для 3 команд.

from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup

BTN_WEATHER = InlineKeyboardButton('Weather', callback_data='weather')
BTN_WIND = InlineKeyboardButton('Wind', callback_data='wind')
BTN_SUN_TIME = InlineKeyboardButton('Sunrise and sunset', callback_data='sun_time')

4 клавиатуры для 4 команд, добавляется команда помощи. В чем суть? После сообщения погоды нам не нужно показывать ее кнопку. Такая же логика для всех других команд, кроме помощи. Для нее выводятся кнопки всех 3 команд.

WEATHER = InlineKeyboardMarkup().add(BTN_WIND, BTN_SUN_TIME)
WIND = InlineKeyboardMarkup().add(BTN_WEATHER).add(BTN_SUN_TIME)
SUN_TIME = InlineKeyboardMarkup().add(BTN_WEATHER, BTN_WIND)
HELP = InlineKeyboardMarkup().add(BTN_WEATHER, BTN_WIND).add(BTN_SUN_TIME)

inline_keyboard.py

from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup

BTN_WEATHER = InlineKeyboardButton('Weather', callback_data='weather')
BTN_WIND = InlineKeyboardButton('Wind', callback_data='wind')
BTN_SUN_TIME = InlineKeyboardButton('Sunrise and sunset', 
                                    callback_data='sun_time')

WEATHER = InlineKeyboardMarkup().add(BTN_WIND, BTN_SUN_TIME)
WIND = InlineKeyboardMarkup().add(BTN_WEATHER).add(BTN_SUN_TIME)
SUN_TIME = InlineKeyboardMarkup().add(BTN_WEATHER, BTN_WIND)
HELP = InlineKeyboardMarkup().add(BTN_WEATHER, BTN_WIND).add(BTN_SUN_TIME)

Главный модуль бота

Ну и в главном модуле бота присутствует стандартная настройка, хэндлеры сообщений и коллбэков для inline кнопок, ничего сверхъестественного.

Нужно хоть что-нибудь рассказать. Под стандартной настройкой aiogram подразумевается следующий блок кода:

import logging

from aiogram import Bot, Dispatcher, executor, types

import config

logging.basicConfig(level=logging.INFO)

bot = Bot(token=config.BOT_API_TOKEN)
dp = Dispatcher(bot)

Хэндлер для сообщений /start и /weather выглядит следующим образом. Все работает с помощью магии декораторов aiogram.

@dp.message_handler(commands=['start', 'weather'])
async def show_weather(message: types.Message):
    await message.answer(text=messages.weather(),
                         reply_markup=inline_keyboard.WEATHER)

Хэндлер коллбэка для инлайн-кнопки погоды:

@dp.callback_query_handler(text='weather')
async def process_callback_weather(callback_query: types.CallbackQuery):
    await bot.answer_callback_query(callback_query.id)
    await bot.send_message(
        callback_query.from_user.id,
        text=messages.weather(),
        reply_markup=inline_keyboard.WEATHER)

Запускаем скрипт с помощью такой конструкции:

if __name__ == '__main__':
    executor.start_polling(dp, skip_updates=True)

bot.py

import logging

from aiogram import Bot, Dispatcher, executor, types

import inline_keyboard
import messages
import config

logging.basicConfig(level=logging.INFO)

bot = Bot(token=config.BOT_API_TOKEN)
dp = Dispatcher(bot)


@dp.message_handler(commands=['start', 'weather'])
async def show_weather(message: types.Message):
    await message.answer(text=messages.weather(),
                         reply_markup=inline_keyboard.WEATHER)


@dp.message_handler(commands='help')
async def show_help_message(message: types.Message):
    await message.answer(
        text=f'This bot can get the current weather from your IP address.',
        reply_markup=inline_keyboard.HELP)


@dp.message_handler(commands='wind')
async def show_wind(message: types.Message):
    await message.answer(text=messages.wind(), 
                         reply_markup=inline_keyboard.WIND)


@dp.message_handler(commands='sun_time')
async def show_sun_time(message: types.Message):
    await message.answer(text=messages.sun_time(), 
                         reply_markup=inline_keyboard.SUN_TIME)


@dp.callback_query_handler(text='weather')
async def process_callback_weather(callback_query: types.CallbackQuery):
    await bot.answer_callback_query(callback_query.id)
    await bot.send_message(
        callback_query.from_user.id,
        text=messages.weather(),
        reply_markup=inline_keyboard.WEATHER
    )


@dp.callback_query_handler(text='wind')
async def process_callback_wind(callback_query: types.CallbackQuery):
    await bot.answer_callback_query(callback_query.id)
    await bot.send_message(
        callback_query.from_user.id,
        text=messages.wind(),
        reply_markup=inline_keyboard.WIND
    )


@dp.callback_query_handler(text='sun_time')
async def process_callback_sun_time(callback_query: types.CallbackQuery):
    await bot.answer_callback_query(callback_query.id)
    await bot.send_message(
        callback_query.from_user.id,
        text=messages.sun_time(),
        reply_markup=inline_keyboard.SUN_TIME
    )


if __name__ == '__main__':
    executor.start_polling(dp, skip_updates=True)

Запускаем бота

Смотрим логирование, вы должны увидеть 3 сообщения:

INFO:aiogram:Bot: superultramegaweatherbot [@superultramegaweatherbot]
WARNING:aiogram:Updates were skipped successfully.
INFO:aiogram.dispatcher.dispatcher:Start polling.

Пока что все работает, давайте посмотрим по IP из Германии.

Бывают такие случаи, когда запрос долго обрабатывается. Я не обрабатывал ошибки и не делал для них сообщений, бот просто ничего не делает в таких случаях. Я посчитал, что уже и так хорошо. Как говорится:

  • Лучшее — враг хорошего

  • Работает — не трогай

  • Еще сотня фраз для оправдания лени

  • Еще тысяча успокаивающих фраз для перфекционистов

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

https://web.telegram.org/k/#@WeathersBot

https://web.telegram.org/k/#@WeathersBot

Штош. Спасибо за прочтение. Надеюсь на отзывы, комментарии и критику.


GitHub репозиторий shtosh-weather-bot

Back to top

Edit this page

Toggle table of contents sidebar

Опасно

This version is still in development!

MIT License
PyPi status
PyPi Package Version
Downloads
Supported python versions
Telegram Bot API
Tests
Codecov

aiogram is a modern and fully asynchronous framework for
Telegram Bot API written in Python 3.8 using
asyncio and
aiohttp.

Make your bots faster and more powerful!

Documentation:
  • 🇺🇸 English

  • 🇺🇦 Ukrainian

Опасно

Breaking News:

aiogram 3.0 has breaking changes.

It breaks backward compatibility by introducing new breaking changes!

Features#

  • Asynchronous (asyncio docs, PEP 492)

  • Has type hints (PEP 484) and can be used with mypy

  • Supports PyPy

  • Supports Telegram Bot API 6.5 and gets fast updates to the latest versions of the Bot API

  • Telegram Bot API integration code was autogenerated and can be easily re-generated when API gets updated

  • Updates router (Blueprints)

  • Has Finite State Machine

  • Uses powerful magic filters <https://docs.aiogram.dev/en/dev-3.x/dispatcher/filters/magic_filters.html#magic-filters>

  • Middlewares (incoming updates and API calls)

  • Provides Replies into Webhook

  • Integrated I18n/L10n support with GNU Gettext (or Fluent)

Simple usage#

import asyncio
import logging

from aiogram import Bot, Dispatcher, Router, types
from aiogram.filters import Command
from aiogram.types import Message

# Bot token can be obtained via https://t.me/BotFahter
TOKEN = "42:TOKEN"

# All handlers should be attached to the Router (or Dispatcher)
router = Router()


@router.message(Command(commands=["start"]))
async def command_start_handler(message: Message) -> None:
    """
    This handler receive messages with `/start` command
    """
    # Most event objects have aliases for API methods that can be called in events' context
    # For example if you want to answer to incoming message you can use `message.answer(...)` alias
    # and the target chat will be passed to :ref:`aiogram.methods.send_message.SendMessage`
    # method automatically or call API method directly via
    # Bot instance: `bot.send_message(chat_id=message.chat.id, ...)`
    await message.answer(f"Hello, <b>{message.from_user.full_name}!</b>")


@router.message()
async def echo_handler(message: types.Message) -> None:
    """
    Handler will forward received message back to the sender

    By default, message handler will handle all message types (like text, photo, sticker and etc.)
    """
    try:
        # Send copy of the received message
        await message.send_copy(chat_id=message.chat.id)
    except TypeError:
        # But not all the types is supported to be copied so need to handle it
        await message.answer("Nice try!")


async def main() -> None:
    # Dispatcher is a root router
    dp = Dispatcher()
    # ... and all other routers should be attached to Dispatcher
    dp.include_router(router)

    # Initialize Bot instance with a default parse mode which will be passed to all API calls
    bot = Bot(TOKEN, parse_mode="HTML")
    # And the run events dispatching
    await dp.start_polling(bot)


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    asyncio.run(main())

Contents#

  • Installation
    • Stable (2.x)
      • From PyPI
      • From Arch Linux Repository
    • Development build (3.x)
      • From PyPI
      • From GitHub
      • From AUR
  • Bot API
    • Bot
    • Client session
      • Use Custom API server
      • Base
      • aiohttp
    • Types
      • Inline mode
      • Available types
      • Telegram Passport
      • Getting updates
      • Stickers
      • Payments
      • Games
      • Internals
    • Methods
      • Available methods
      • Payments
      • Stickers
      • Games
      • Getting updates
      • Telegram Passport
      • Updating messages
      • Inline mode
    • Enums
      • BotCommandScopeType
      • ChatAction
      • ChatMemberStatus
      • ChatType
      • ContentType
      • DiceEmoji
      • InlineQueryResultType
      • InputMediaType
      • MaskPositionPoint
      • MenuButtonType
      • MessageEntityType
      • ParseMode
      • PollType
      • StickerType
      • TopicIconColor
      • UpdateType
    • How to download file?
      • Download file manually
      • Download file in short way
    • How to upload file?
      • Upload from file system
      • Upload from buffer
      • Upload from url
  • Handling events
    • Observer
      • EventObserver
      • TelegramEventObserver
    • Router
      • Router
      • Event observers
      • Nested routers
    • Dispatcher
      • Dispatcher
      • Simple usage
      • Handling updates
    • Class based handlers
      • BaseHandler
      • CallbackQueryHandler
      • ChosenInlineResultHandler
      • ErrorHandler
      • InlineQueryHandler
      • MessageHandler
      • PollHandler
      • PreCheckoutQueryHandler
      • ShippingQueryHandler
      • ChatMemberHandler
    • Filtering events
      • Builtin filters
      • Writing own filters
      • Combining Filters
    • Middlewares
      • Base theory
      • Basics
      • Arguments specification
      • Examples
      • Facts
    • Finite State Machine
      • Usage example
      • Read more
    • Flags
      • Via decorators
      • Via handler registration method
      • Via filters
      • Use in middlewares
      • Use in utilities
  • Utils
    • Keyboard builder
      • Usage example
      • Base builder
      • Inline Keyboard
      • Reply Keyboard
    • Translation
      • Installation
      • Make messages translatable
      • Configuring engine
      • Deal with Babel
    • Chat action sender
      • Sender
      • Middleware
    • WebApp
      • Usage
      • Functions
      • Types
    • Callback answer
      • Simple usage
      • Advanced usage
      • Description of objects
  • Changelog
    • 3.0.0b7 [UNRELEASED DRAFT] (2023-02-25)
      • Improved Documentation
    • 3.0.0b7 (2023-02-18)
      • Features
      • Bugfixes
      • Misc
    • 3.0.0b6 (2022-11-18)
      • Features
      • Bugfixes
      • Improved Documentation
      • Misc
    • 3.0.0b5 (2022-10-02)
      • Features
      • Bugfixes
      • Improved Documentation
      • Deprecations and Removals
      • Misc
    • 3.0.0b4 (2022-08-14)
      • Features
      • Bugfixes
      • Misc
    • 3.0.0b3 (2022-04-19)
      • Features
      • Bugfixes
      • Misc
    • 3.0.0b2 (2022-02-19)
      • Features
      • Bugfixes
      • Misc
    • 3.0.0b1 (2021-12-12)
      • Features
      • Bugfixes
      • Misc
    • 3.0.0a18 (2021-11-10)
      • Features
      • Bugfixes
      • Misc
    • 3.0.0a17 (2021-09-24)
      • Misc
    • 3.0.0a16 (2021-09-22)
      • Features
      • Misc
    • 3.0.0a15 (2021-09-10)
      • Features
      • Bugfixes
      • Misc
    • 3.0.0a14 (2021-08-17)
      • Features
      • Bugfixes
      • Improved Documentation
      • Misc
    • 2.14.3 (2021-07-21)
    • 2.14.2 (2021-07-26)
    • 2.14 (2021-07-27)
    • 2.13 (2021-04-28)
    • 2.12.1 (2021-03-22)
    • 2.12 (2021-03-14)
    • 2.11.2 (2021-11-10)
    • 2.11.1 (2021-11-10)
    • 2.11 (2021-11-08)
    • 2.10.1 (2021-09-14)
    • 2.10 (2021-09-13)
    • 2.9.2 (2021-06-13)
    • 2.9 (2021-06-08)
    • 2.8 (2021-04-26)
    • 2.7 (2021-04-07)
    • 2.6.1 (2021-01-25)
    • 2.6 (2021-01-23)
    • 2.5.3 (2021-01-05)
    • 2.5.2 (2021-01-01)
    • 2.5.1 (2021-01-01)
    • 2.5 (2021-01-01)
    • 2.4 (2021-10-29)
    • 2.3 (2021-08-16)
    • 2.2 (2021-06-09)
    • 2.1 (2021-04-18)
    • 2.0.1 (2021-12-31)
    • 2.0 (2021-10-28)
    • 1.4 (2021-08-03)
    • 1.3.3 (2021-07-16)
    • 1.3.2 (2021-05-27)
    • 1.3.1 (2018-05-27)
    • 1.3 (2021-04-22)
    • 1.2.3 (2018-04-14)
    • 1.2.2 (2018-04-08)
    • 1.2.1 (2018-03-25)
    • 1.2 (2018-02-23)
    • 1.1 (2018-01-27)
    • 1.0.4 (2018-01-10)
    • 1.0.3 (2018-01-07)
    • 1.0.2 (2017-11-29)
    • 1.0.1 (2017-11-21)
    • 1.0 (2017-11-19)
    • 0.4.1 (2017-08-03)
    • 0.4 (2017-08-05)
    • 0.3.4 (2017-08-04)
    • 0.3.3 (2017-07-05)
    • 0.3.2 (2017-07-04)
    • 0.3.1 (2017-07-04)
    • 0.2b1 (2017-06-00)
    • 0.1 (2017-06-03)
  • Contributing
    • Developing
      • Use virtualenv
      • Activate the environment
      • Setup project
      • Making changes in code
      • Format the code (code-style)
      • Run tests
      • Docs
      • Docs translations
      • Describe changes
      • Complete
    • Star on GitHub
    • Guides
    • Take answers
    • Funding

Асинхронный Telegram бот на языке Python 3 с использованием библиотеки aiogram

Для понимания уроков необходимо хотя бы базовое знание языка Python версии 3.

Код из всех уроков доступен на GitHub.

Важно! Автор не является профессионалом, в уроках от вас не требуется поступать точно так же. Данный учебник является дружеской рекомендацией, поэтому обо всех ошибках и недочетах можно и нужно писать в комментариях или обсуждении. Советы, как поступить было бы лучше, тоже приветствуются.

Q&A:

Q: Почему aiogram, а не, например, pyTelegramBotAPI?
A: Автор сам начинал знакомство с разработкой Телеграм ботов, используя pyTelegramBotAPI, однако поведение библиотеки перестало удовлетворять на больших проектах, у неё странная многопоточность, FSM приходилось создавать самостоятельно (есть даже порт FSM из aiogram в pyTelegramBotAPI), плохо реализованное логгирование, aiogram позволяет создавать middleware, например то же логгирование, антифлуд, ну и просто, почему нет?

Оглавление:

  • Урок 1. Быстрый старт. Эхо-бот
  • Урок 2. Медиа, разметка, эмоджи и щепотка логирования
  • Урок 3. Машина состояний и то самое логгирование
  • Урок 4. Платежи в Telegram
  • Урок 5. Клавиатуры и кнопки

Телеграм используют более пятисот миллионов людей по всему миру. Компании с его помощью упрощают заказ товаров или услуг, дают консультации. Для этого используют ботов — автоматические программы. Их пишут на разных языках программирования. Рассмотрим, как создать бот на самом популярном в феврале 2022 года языке — Python.

Установите интерпретатор Python

Python — динамически типизированный, интерпретируемый язык. Программы, написанные на нём, не компилируются в исполняемые файлы. Поэтому, чтобы запускать Python-программы, устанавливают его интерпретатор.

🖥️ Windows

  1. Перейдите на официальную страницу загрузки Python.

    как установить интерпретатор Python

    Выберите последнюю версию Python

  2. Пролистайте вниз страницы и скачайте Windows Installer.

    64-bit в интерпретаторе Python

    Выберите 64-bit

  3. Откройте установщик и поставьте галочку, нажмите Install Now.

    как установить интерпретатор Python на Windows

    Поставьте галочку перед установкой напротив Add Python to PATH

  4. После откройте командную строку cmd.exe и установите виртуальное окружение с помощью команды
pip install virtualenv

❗ Вызов интерпретатора в командной строке Windows отличается от других ОС. Используйте команду py вместо python3.

🖥️ Linux

Если вы используете ОС Linux, вероятно, Python уже установлен. Чтобы проверить, откройте терминал с помощью команды:

python3 --version

Если вы видите что-то вроде Python 3.x.x, то Python есть. В противном случае используйте установленный менеджер пакетов. В основном это apt. Введите команду:

sudo apt install python3

Установите виртуальное окружение с помощью команды:

sudo apt install python3-venv

🖥️ MacOS

На макбуках Python часто тоже уже есть. Откройте терминал и проверьте с помощью команды:

python3 --version

Ответ Python 3.x.x, а не ошибка, тоже подтверждает, что Python установлен. Если нет — используйте менеджер пакетов brew. Введите команду:

brew install python3

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

pip install virtualenv

Создайте папки проекта и виртуального окружения

Откройте терминал Linux или MacOS, командную строку Windows. Перейдите в директорию, в которой вы хотите создать проект бота. Последовательно

введите команды:

mkdir myBot
cd myBot
python3 -m venv env
# 

или, если вы используете Windows:

py -m venv env #

Эти команды создадут папку проекта myBot внутри текущей рабочей директории. А в ней — папку с виртуальным окружением.

Активируйте окружение, выберите и установите библиотеки

Чтобы активировать виртуальное окружение на MacOS или Linux, используйте команду

source ./env/bin/activate

И команду

source.envbinactivate.bat

для Windows.

Чтобы создавать боты, используют разные библиотеки. Самые популярные: python-telegram-bot с синхронным подходом, aiogram с асинхронным.

Асинхронность позволяет отвлекаться от задач и не ждать ввода от пользователя, поэтому мы будем использовать библиотеку aiogram. Документация по ней — на docs.aiogram.

Используйте менеджер Python-пакетов (pip). Чтобы установить библиотеку aiogram, введите команду:

pip install aiogram

Зарегистрируйте бота и получите API-ключ

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

Нажмите «Запустить».

как добавить нового бота в телеграм

как зарегистрировать бота в телеграме

Введите команду /newbot и отображаемое имя бота

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

валидация бота в телеграм

Никому не сообщайте полученный токен

Примеры ботов

🤖 Эхобот

Для начала в папке проекта создайте несколько файлов.

как создать эхобота

Handlers.py будет хранить функции — обработчики сообщений и команд, main.py нужен для запуска

Напишите в main.py код:

from aiogram import Bot, Dispatcher, executor
import handlers

API_TOKEN = 'вставьте сюда ваш токен'

# создаем экземпляры бота и диспетчера
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)

# запускаем программу
if __name__ == '__main__':
# указание skip_updates=True
# пропустит команды,
# которые отправили
# до старта бота
executor.start_polling(dp, skip_updates=True)

Разберем построчно:

from aiogram import Bot, Dispatcher, executor
import handlers

Здесь из библиотеки aiogram мы импортируем классы Bot, Dispatcher — класс, который регистрирует, на какие команды/сообщения и какой функцией отвечать. И executor — он запускает бота и выполняет функции, зарегистрированные в диспетчере. На второй строке мы импортируем модуль handlers.py — в нём хранят обработчики.

Затем объявите переменную, в которую нужно вставить токен от @BotFather. Создайте экземпляр класса бота, в него передайте токен. И экземпляр диспетчера, в него передайте только что созданный бот.

Далее откройте файл handlers.py и напишите в нём пару команд, которые будут обрабатывать запросы:

from aiogram import types

# функция, обрабатывающая команду /start
async def start(message: types.Message):
await message.answer("Привет!nНапиши мне что-нибудь!")

# функция, которая отвечает на сообщение
# текстом
async def echo(message: types.Message):
await message.answer("Сам ты: " + message.text)

Здесь из модуля aiogram импортируем типы, с помощью которых преобразуем текст сообщения или файлы в структуру данных. Это может быть Message — сообщение, Audio — аудиозапись, Animation — анимация.

Далее определите асинхронные функции, чтобы обрабатывать команды /start и отвечать на сообщения.

Зарегистрируйте эти функции в диспетчере. Для этого в файле main.py добавьте перед запуском программы:

# регистрируем функции
dp.register_message_handler(h.start, commands=["start"])
dp.register_message_handler(h.echo)

В итоге файл main.py выглядит так:

from aiogram import Bot, Dispatcher, executor
import handlers

API_TOKEN = 'вставьте сюда ваш токен'

# создаем бота и диспетчер
bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)

# регистрируем функции
dp.register_message_handler(handlers.start, commands=["start"])
dp.register_message_handler(handlers.echo)

# запускаем программу
if __name__ == '__main__':
# указание skip_updates=True
# пропустит команды,
# которые отправили
# до старта бота
executor.start_polling(dp, skip_updates=True)

ка запустить Эхобот в телеграме

Чтобы проверить работу, введите в терминале python3 main.py или py main.py на Windows. Откройте бота в телеграме и запустите его

🤖 Криптобот

Чтобы написать криптобота, который будет сообщать текущую цену BTC, LTC и DASH, используйте бесплатный API-сервис SoChain. Еще потребуется дополнительная библиотека, чтобы создать асинхронные запросы aiohttp, но ее устанавливают вместе с aiogram.

Создайте новый модуль utils.py. Добавьте в него url-адрес API-сервиса. Чтобы узнавать цену, используйте метод Get Prices. Он возвращает json-объект с данными о цене из нескольких источников. Поэтому напишите функцию, которая вычисляет среднее значение:

BASE_URL = "https://sochain.com/api/v2/" # API URL

# функция, чтобы рассчитать цену
def calculate_price(data):
prices = [float(entity["price"]) for entity in data["data"]["prices"]]
return f"{(sum(prices) / len(prices)):.2f} USD"

Далее откройте файл handlers.py и напишите код:

from aiogram import types

# импортируем библиотеку aiohttp
import aiohttp

# импортируем из utils все данные
from utils import *

# обработчик команды /start
async def start(message: types.Message):
await message.answer("Привет!nНапиши мне акроним криптовалюты, чтобы узнать текущую цену")

# обработчик команды /help
async def help(message: types.Message):
await message.answer("Доступные сети:n" + "n".join(networks))

# обработчик запроса цены
async def get_price(message: types.Message):
session = aiohttp.ClientSession()

# создаем GET запрос по закрепленному за методом get_price url
async with session.get(BASE_URL + f"get_price/{message.text}/USD") as resp:

# получаем ответ в формате json
data = await resp.json()

# если статус запроса — успешно
if data["status"] == "success":

# рассчитываем цену и отправляем пользователю
price = calculate_price(data)
await message.answer(price)
else:
# сообщаем о том, что произошла ошибка
await message.answer("Произошла ошибка")

Теперь измените main.py файл: зарегистрируйте функции. Затем протестируйте бота:

from aiogram import Bot, Dispatcher, executor
import handlers

API_TOKEN = 'вставьте сюда ваш токен'

bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)

dp.register_message_handler(handlers.start, commands=["start"])
dp.register_message_handler(handlers.help, commands=["help"])
dp.register_message_handler(handlers.get_price)

if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

как запустить криптобота в телегараме

Когда пользователь укажет неподдерживаемую сеть ETH, бот вернет цену биткоина, потому что так устроен API-сервис. Если он не распознал сеть, то использует BTC

Чтобы исправить ошибку, создайте клавиатуру, которая возвращает нужные значения. Напишите специальный класс Middleware: в нём описываются проверки до и после обработки запроса. Либо проверьте вхождение внутри функции. Последний вариант проще и легче.

Если пользователь отправит неподдерживаемый акроним криптосети, вы сообщите ему об этом. Для этого в файле utils.py определите список поддерживаемых сетей:

networks = ["BTC", "LTC", "DASH"] # &lt;-- поддерживаемые сети
BASE_URL = "https://sochain.com/api/v2/" # API URL

def calculate_price(data):
prices = [float(entity["price"]) for entity in data["data"]["prices"]]
return f"{(sum(prices) / len(prices)):.2f} USD"

Теперь отредактируйте функцию расчета цены криптовалюты в файле handlers.py. Для этого добавьте проверку вхождения сети в список поддерживаемых:

async def get_price(message: types.Message):
network = message.text.upper() # приводим сообщение к верхнему регистру

# выполняем проверку вхождения
if network not in networks:
await message.answer("Вы указали неподдерживаемую криптовалюту")
return

session = aiohttp.ClientSession()

async with session.get(BASE_URL + f"get_price/{message.text}/USD") as resp:
data = await resp.json()
if data["status"] == "success":
price = calculate_price(data)
await message.answer(price)
else:
await message.answer("Произошла ошибка")

Когда пользователь укажет неподдерживаемую сеть ETH, бот сообщит об этом

Код всех файлов

utils.py

networks = ["BTC", "LTC", "DASH"] # поддерживаемые сети
BASE_URL = "https://sochain.com/api/v2/" # API URL

def calculate_price(data):
prices = [float(entity["price"]) for entity in data["data"]["prices"]]
return f"{(sum(prices) / len(prices)):.2f} USD"

handlers.py

import aiohttp
from aiogram import types
from utils import *

async def start(message: types.Message):
await message.answer("Привет!nНапиши мне акроним криптовалюты, чтобы узнать текущую цену")

async def help(message: types.Message):
await message.answer("Доступные сети:n" + "n".join(networks))

async def get_price(message: types.Message):
network = message.text.upper()

if network not in networks:
await message.answer("Вы указали неподдерживаемую криптовалюту")
return

session = aiohttp.ClientSession()

async with session.get(BASE_URL + f"get_price/{message.text}/USD") as resp:
data = await resp.json()
if data["status"] == "success":
price = calculate_price(data)
await message.answer(price)
else:
await message.answer("Произошла ошибка")

main.py

from aiogram import Bot, Dispatcher, executor
import handlers

API_TOKEN = 'вставьте сюда ваш токен'

bot = Bot(token=API_TOKEN)
dp = Dispatcher(bot)

dp.register_message_handler(handlers.start, commands=["start"])
dp.register_message_handler(handlers.help, commands=["help"])
dp.register_message_handler(handlers.get_price)

if __name__ == '__main__':
executor.start_polling(dp, skip_updates=True)

Главное о телеграм-боте на Python

  • Чтобы создать бота, установите интерпретатор Python. На Linux и MacOS он часто уже есть.
  • Создайте папку с проектом myBot, в ней — папку с окружением, активируйте его, установите библиотеку aiogram. Она позволяет обрабатывать запросы, даже если вы ждете, пока пользователь введет информацию.
  • Регистрируйте бота и управляйте его настройками с помощью @BotFather.

Python-разработчик: новая работа через 9 месяцев

Получится, даже если у вас нет опыта в IT

Узнать больше

В онлайн-университете Skypro обучаем профессии Python-разработчика за 10 месяцев. Узнаете, как писать чистый код, серверы для магазина, приложения, сервиса или игры, разрабатывать сложную архитектуру сервисов. Выполните практические задания с реальными инструментами и сделаете четыре проекта для портфолио. Развиваем только нужные навыки, необходимые для старта в профессии.

Понравилась статья? Поделить с друзьями:
  • Как написать телеграмм бота на php
  • Как написать телеграмм бота на java
  • Как написать телеграм сокращенно
  • Как написать телеграм бота пайтон
  • Как написать телеграм бота на пайтоне