Руководство по aiogram

Используемая версия aiogram: 3.0 beta 7

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

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

Так, например, во всех или почти во всех главах токен бота будет указываться прямо в исходных текстах.
Это плохой подход, поскольку может привести к раскрытию токена, если вы забудете его удалить перед заливкой
кода в публичный репозиторий (например, 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.0b7" > requirements.txt
[groosha@main 01_quickstart]$ echo "python-dotenv==0.21.1" >> 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, чтобы он нам не мешал.

Если для написания ботов вы используете PyCharm, рекомендую также установить сторонний плагин
Pydantic для поддержки автодополнения кода
в телеграмных объектах.

Первый бот¶

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

bot.py

import asyncio
import logging
from aiogram import Bot, Dispatcher, types
from aiogram.filters.command import Command

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

# Хэндлер на команду /start
@dp.message(Command("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(Command("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, Command("test2"))

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

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

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

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


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

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

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

@dp.message(Command("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. Перепишем предыдущую функцию:

# не забудьте про импорт
from aiogram.enums.dice_emoji import DiceEmoji

@dp.message(Command("dice"))
async def cmd_dice(message: types.Message, bot: Bot):
    await bot.send_dice(-100123456789, emoji=DiceEmoji.DICE)

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

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


@dp.message(Command("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.

Напишем простой диалоговый 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 мин

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

Штош. В этой статье я расскажу вам, как создать 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

Привет! Как-то захотел я сделать своего бота в телеграме. Ну, знаете. Есть свой канал, пара чатов, знаю Python. Но скажу честно, первые попытки были заброшены. Оказалось, что в разработке ботов не было единого подхода, а найти хороший фреймворк оказалось сложнее, чем казалось. Или я не особо внимательный. Но в начале 2021 я нашел свой идеал. Это AIOGram.

А теперь я решил совместить приятное с полезным, так что буду писать какие-то нововведения для бота параллельно документируя их для сайта. Такая себе коллаборация с самим собой для получения побочных продуктов)

Создаём простой проект в PyCharm, ждём завершения и открываем main.py. Его содержимое нас не интересует, так что удаляем. Вместо этого пишем:

from aiogram import Bot, Dispatcher, executor, types

Дабы почувствовать всю прелесть разработки в PyCharm – используем Context Action для импорта фреймрворка. Ставим указатель на aiogram и нажимаем Alt + Enter, и выбираем Install Package.

Если что-то не получилось, то необходимые библиотеки можно установить через Python Packages.

В будущем мы создадим файлик requirements.txt, в котором будут прописаны все необходимые библиотеки для автоматической установки. А токен и другие важные переменные вынесем в .env файл. Но для простоты эти шаги пока опустим.

Далее пишем следующее:

API_TOKEN = ‘BOT TOKEN HERE’

bot = Bot(token=API_TOKEN)

dp = Dispatcher(bot)

if __name__ == ‘__main__’:

    executor.start_polling(dp, skip_updates=True)

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

Так и пишите в телеге @BotFather

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

А polling не даст завершить работу бота пока приложение активно.

Запустить можно уже, но интереса никакого. Предлагаю добавить эхо функцию.

@dp.message_handler()

async def echo(message: types.Message):

    await message.answer(message.text)

Т.е. теперь dispatcher для любого полученного сообщения будет выполнять функцию echo. Если что-то не особо понятно, то полный код будет в конце статьи.

Проверим? Нажимаем Ctrl +Shift + F10 для запуска скрипта.

И вправду эхо

Расширяем функционал

Кстати, message_handler не только текст принимает. А любые сообщения, плюс мы можем указывать свои фильтры. Вроде сообщений от админа, личные, группа и т.д. Поддерживает он и команды. Например, эта функция будет срабатывать только при вводе команды /start. Только разместите её выше, чем echo, это важно. В ином случае сообщение перехватит эхо и до этой функции /start не дойдёт.

@dp.message_handler(commands=[‘start’])

async def send_welcome(message: types.Message):

    await message.reply(«Hello world!»)

Уже интереснее

Добавим ещё аналогичную help, но ради интереса пропишем её ниже, чем echo:

@dp.message_handler(commands=[‘help’])

async def send_help(message: types.Message):

    await message.reply(«Чем я могу помочь?»)

Неа, не получилось

Как видите, старт так и срабатывает, а вот help нет. Как я и говорил. Обрабатывать мы можем только одну функцию за раз, и если какой-то обработчик соответствует, то дальнейшие будут отброшены.

Именно поэтому, когда мы пишем /start не срабатывает echo. И именно поэтому не сработал help. Потому что echo собирает все сообщения, которые не /start. Переместим help куда-то выше и попробуем снова.

Прекрасно!

Кстати, можно указать ещё и фильтр на тип сообщения. Стикер, аудио, файл. А можно даже прописывать особые состояния, благодаря которым бот будет ожидать от пользователя конкретных действий. И, конечно же, мы всё это попробуем дальше. А пока вы можете прописать набор каких-то команд и насладиться своим собственным ботом (пока активна IDE, как только выключите, то и бот отвечать перестанет)

На этом пока всё, но это только начало!

Полный код показанного сегодня:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

from aiogram import Bot, Dispatcher, executor, types

API_TOKEN = ‘TOKEN!’

bot = Bot(token=API_TOKEN)

dp = Dispatcher(bot)

@dp.message_handler(commands=[‘start’])

async def send_welcome(message: types.Message):

    await message.reply(«Hello world!»)

@dp.message_handler(commands=[‘help’])

async def send_welcome(message: types.Message):

    await message.reply(«Чем я могу помочь?»)

@dp.message_handler()

async def echo(message: types.Message):

    await message.answer(message.text)

if __name__ == ‘__main__’:

    executor.start_polling(dp, skip_updates=True)

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

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

  • 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-разработчик
Узнать больше

Понравилась статья? Поделить с друзьями:
  • Вариативные модули классное руководство
  • Инструкция по кадровому делопроизводству в оао ржд
  • Руководство по эксплуатации стиральной машины candy aquamatic
  • Инструкция по замене цепи грм змз 409
  • Хендай сантамо руководство