# Туториал по tg_bot и aiogram-3
[Все проводится по гайду](https://mastergroosha.github.io/aiogram-3-guide/)
## Установка виртуального окружения venv и версионности git
инициализируем git
```
git init
```
установим виртуальную среду **venv** в папку **venv**
```sh
python -m venv venv
```
запишем в файл зависимостей первую запись для установки **aiogram**
```sh
echo "aiogram<4.0" > requirements.txt
```
добавим туда же **pydantic-settings**
```sh
echo "pydantic-settings" >> requirements.txt
```
активируем виртуальную среду
```sh
source venv/bin/activate
```
для выхода из **venv** можно использовать:
```sh
deactivate
```
Добавим файл .gitignore и файл с секретами .env (.env - укажем в .gitignore)
```sh
# Игнорирование виртуальной среды Python
venv/
.venv/
myenv/
#Игнорирование рабочих каталогов
bin/
include/
lib/
lib64/
# Игнорирование файлов с окружением
.env
.gitignore
pyvenv.cfg
# Игнорирование скомпилированных файлов Python
__pycache__/
**/__pycache__/
```
Установим наконец наши зависимости
```sh
pip install -r requirements.txt
```
## Первый бот
```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())
```
### Создадим файл конфигурации и переопределим файл секретов в него
Итак, создадим рядом с **bot.py** отдельный файл **config_reader.py** о следующим содержимым
config_reader.py
```py
from pydantic_settings import BaseSettings, SettingsConfigDict
from pydantic import SecretStr
class Settings(BaseSettings):
# Желательно вместо str использовать SecretStr
# для конфиденциальных данных, например, токена бота
bot_token: SecretStr
# Начиная со второй версии pydantic, настройки класса настроек задаются
# через model_config
# В данном случае будет использоваться файла .env, который будет прочитан
# с кодировкой UTF-8
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')
# При импорте файла сразу создастся
# и провалидируется объект конфига,
# который можно далее импортировать из разных мест
config = Settings()
```
Теперь немного отредактируем наш bot.py:
```py bot.py
# импорты
from config_reader import config
# Для записей с типом Secret* необходимо
# вызывать метод get_secret_value(),
# чтобы получить настоящее содержимое вместо '*******'
bot = Bot(token=config.bot_token.get_secret_value())
```
## Работа с сообщениями
Разберёмся, как применять различные типы форматирования к сообщениям и работать с медиафайлами.
### ТЕКСТ
В распоряжении у разработчика имеется три способа разметки текста: HTML, Markdown и MarkdownV2.
Наиболее продвинутыми из них считаются HTML и MarkdownV2
> Часто требуется получить id user-а (user_id), получить его можно так:
```py
user_id = message.from_user.id
```
#### Форматированный вывод
За выбор форматирования при отправке сообщений отвечает аргумент ``parse_mode``, например:
```py
from aiogram import F
from aiogram.types import Message
from aiogram.filters import Command
from aiogram.enums import ParseMode
# Если не указать фильтр F.text,
# то хэндлер сработает даже на картинку с подписью /test
@dp.message(F.text, Command("test"))
async def any_message(message: Message):
await message.answer(
"Hello, world!",
parse_mode=ParseMode.HTML
)
await message.answer(
"Hello, *world*\!",
parse_mode=ParseMode.MARKDOWN_V2
)
```
В aiogram можно задать параметры бота по умолчанию. Для этого создайте объект DefaultBotProperties и передайте туда нужные настройки:
```py
from aiogram.client.default import DefaultBotProperties
bot = Bot(
token=config.bot_token.get_secret_value(),
default=DefaultBotProperties(
parse_mode=ParseMode.HTML
# тут ещё много других интересных настроек
)
)
# где-то в функции...
await message.answer("Сообщение с HTML-разметкой")
# чтобы явно отключить форматирование в конкретном запросе,
# передайте parse_mode=None
await message.answer(
"Сообщение без какой-либо разметки",
parse_mode=None
)
```
#### Экранирование ввода
Второе чуть сложнее, но более продвинутое: воспользоваться специальным инструментом,
который будет собирать отдельно текст и отдельно информацию о том, какие его куски должны быть отформатированы.
```py
from aiogram.filters import Command
from aiogram.utils.formatting import Text, Bold
@dp.message(Command("hello"))
async def cmd_hello(message: Message):
content = Text(
"Hello, ",
Bold(message.from_user.full_name)
)
await message.answer(
**content.as_kwargs()
)
```
В примере выше конструкция ****content.as_kwargs()** вернёт аргументы **text, entities, parse_mode** и подставит их в вызов **answer()**
Упомянутый инструмент форматирования довольно комплексный, [официальная документация](https://core.telegram.org/bots/api#formatting-options) демонстрирует удобное отображение сложных конструкций
#### Сохранение форматирования
Представим, что бот должен получить форматированный текст от пользователя и добавить туда что-то своё, например, отметку времени.
Напишем простой код:
```py
# новый импорт!
from datetime import datetime
@dp.message(F.text)
async def echo_with_time(message: Message):
# Получаем текущее время в часовом поясе ПК
time_now = datetime.now().strftime('%H:%M')
# Создаём подчёркнутый текст
added_text = html.underline(f"Создано в {time_now}")
# Отправляем новое сообщение с добавленным текстом
await message.answer(f"{message.text}\n\n{added_text}", parse_mode="HTML")
```
НО! ``message.text`` возвращает просто текст, без каких-либо оформлений.
Чтобы получить текст в нужном форматировании, воспользуемся альтернативными свойствами: ``message.html_text`` или ``message.md_text``.
#### Работа с entities
**Telegram** сильно упрощает жизнь разработчикам, выполняя предобработку сообщений пользователей на своей стороне. Например, некоторые сущности, типа **e-mail**, **номера телефона, юзернейма** и др. можно не доставать регулярными выражениями, а извлечь напрямую из объекта **Message** и поля **entities**, содержащего массив объектов типа [MessageEntity](https://core.telegram.org/bots/api#messageentity).
Здесь кроется важный подвох. *Telegram возвращает не сами значения, а их начало в тексте и длину*.
```py
@dp.message(F.text)
async def extract_data(message: Message):
data = {
"url": "",
"email": "",
"code": ""
}
entities = message.entities or []
for item in entities:
if item.type in data.keys():
# Неправильно
# data[item.type] = message.text[item.offset : item.offset+item.length]
# Правильно
data[item.type] = item.extract_from(message.text)
await message.reply(
"Вот что я нашёл:\n"
f"URL: {html.quote(data['url'])}\n"
f"E-mail: {html.quote(data['email'])}\n"
f"Пароль: {html.quote(data['code'])}"
)
```
#### Команды и их аргументы
В составе aiogram есть фильтр Command(), упрощающий жизнь разработчика.
Реализуем последний пример в коде:
```py
@dp.message(Command("settimer", prefix="/!")) # добавим дополнительные префиксы для оперделения команды
async def cmd_settimer(
message: Message,
command: CommandObject
):
# Если не переданы никакие аргументы, то
# command.args будет None
if command.args is None:
await message.answer(
"Ошибка: не переданы аргументы"
)
return
# Пробуем разделить аргументы на две части по первому встречному пробелу
try:
delay_time, text_to_send = command.args.split(" ", maxsplit=1)
# Если получилось меньше двух частей, вылетит ValueError
except ValueError:
await message.answer(
"Ошибка: неправильный формат команды. Пример:\n"
"/settimer