Photo by charlesdeluvio on Unsplash

Уверен, небольшие pet-проекты полезны не только для прокачивания навыков, но и для отвлечения от рабочей рутины и - нередко - для решения небольших практических задач. 

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

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

Про использование Python понятно из сабжа, ну а добиться максимально быстрого запуска нам позволят встроенные CI/CD процессы от Amvera, которые буквально из кода соберут работающий сервис. 


Функциональность и архитектура бота

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

Чтобы все выглядело чисто и SOLID’но, естественно нужно начать с проектирования решения.

Забегая вперед, получилось не так SOLID’но, как хотелось бы - расценивайте это как возможность похоливарить и предложить схему получше.

Вот что я накидал в draw.io думая о боте и пытаясь решить о возможных расширениях функциональности в дальнейшем:

Layered-архитектура
Layered-архитектура

Основные используемые библиотеки:

  • PTB (Python Telegram Bot)

  • APS (Advanced Python Scheduler) - на самом деле в рамках python-telegram-bot[job-queue]

Что можно сразу заметить: на схеме нет инфраструктурных компонентов. “А как же PostrgreSQL?”, справедливо спросите вы. В нашем случае PostgreSQL нужен для хранения джобов на расписании - а это реализуется в рамках упомянутого выше APS, внутри которого эти слои и имплементированы. 

Итак, что же может делать бот:

  1. Конвертировать заданную сумму и валюту в рубли

    • Курс на основе официального курса ЦБ РФ

    • Простейшие математические операции (сложение, вычитание, умножение и деление)

  2. Подписаться на регулярные уведомления по запрошенной конвертации

    • И, конечно, отписаться от этих уведомлений

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

Если не нужны регулярные уведомления

Если хочется запустить бот, но без уведомлений и (как следствие) без использования PostgreSQL, то - как водится в SOLID'ной архитектуре - можно буквально в одной строке это скорректировать:

#вместо:
subscriber = PTBScheduler()

#сделать:
subscriber = None

Ну а теперь переходим к самому интересному - развертыванию бота.


Развертывание бота в Amvera Cloud

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

1. Пререквизиты

Перед стартом проверьте пререквизиты:

  1. У вас есть аккаунт в Amvera Cloud

  2. На вашей машине установлен Git

Предполагаются базовые знания и навыки в следующих топиках:

  • Git

  • Docker

  • Python


2. Предварительная подготовка

Выделены следующие этапы подготовки:

  1. Создание бота в ТГ (через BotFather)

  2. Запуск и проверка PostgreSQL

Итак, поехали.

2.1. Создание бота в ТГ

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

В коде предусмотрена возможность работы в inline-режиме, но его нужно активировать с помощью того же BotFather. Для этого:

1. Выберите своего бота из списка в BotFather

2. Нажмите ‘Bot Settings’:

3. Нажмите ‘Inline Mode’:

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

Там же можно настроить placeholder - то что будет отображаться в inline-режиме, например:

Ну не красота ли?
Ну не красота ли?

2.2. Запуск и проверка PostgreSQL

Полноценное описание о запуске PostgreSQL в Amvera есть в соответствующем детальном гайде.

Достаточно просто настраивается запуск самой БД:

А уже в отдельном проекте - pgAdmin - можно к ней подключиться:

1. Создать новый сервер:

2. Ввести данные БД для подключения:

Помните, что адрес подключения (хост) можно найти в настройках проекта с PostgreSQL:

После успешного подключения в интерфейсе будет видна БД:


3. Развертывание бота

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

  1. Скачать репозиторий с кодом

  2. Создать проект в Amvera

  3. Загрузить исходники в Amvera

    1. Загрузка через Git (рекомендуемый способ)

    2. Загрузка файлов на сайте

  4. Проверка бота

Итак, поехали.

3.1. Скачать репозиторий с кодом

Доступны 2 варианта:

  1. С использованием командной строки

  2. Без использования командной строки

Рекомендуется конечно же первый вариант.

3.1.1. С использование командной строки

Я подразумеваю, что вы воспользовались инструкцией для установки Git, которую я упоминал ранее. В этом случае использование командной строки (а нам нужна только утиллита Git) будет выглядеть одинаково для всех платформ (Linux-Windows-MacOS).

1. Открываем командную строку:

2. Я предварительно создал папку bot, в которой и будет размещен код. Создайте и перейдите в нужную вам папку:

mkdir bot
cd bot
pwd

3. Скопируйте ссылку на репозиторий и выполните загрузку:

Поздравляю, вы великолепны :)

3.1.2. Без использования командной строки

Без командной строки все выполняется не менее просто:

1. Создайте папку, в которой будет располагаться код

2. В браузере перейдите по ссылке на репозиторий

3. Скачайте архив с кодом:

4. Распакуйте архив в созданную на шаге 1 папку

Поздравляю, вы великолепны :)

3.2. Создать проект в Amvera

Ниже пошаговый гайд по созданию проекта:

1. Залогиньтесь в Amvera и перейдите на вкладку проектов

2. Нажмите “Создать”:

3. Заполняем общую информацию и нажимаем “Далее”:

  • Название - можно выбрать любое какое нравится.

  • Тип сервиса - приложение.

  • Тарифный план - самый минимальный подойдет для целей такого бота. Что удобно в Amvera - в дальнейшем можно будет расширить мощности буквально в пару кликов.

4. Шаг с загрузкой данных пропустим, просто нажав “Далее” - его можно выполнить позднее:

5. Задаем настройки конфигурации (по сути - конфигурации сборки проекта) и скачиваем amvera.yml:

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

Выбираем:

  • Окружение: docker

  • Инструмент: docker

  • Dockerfile: Dockerfile (название файла в репозитории с кодом)

  • persistenceMount: /app/data

Про persistenceMount расскажу чуть подробнее несколько позже. 

Окружения Amvera

У Amvera есть широкий список окружений:

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

В случае с Python доступен де-факто стандарт: pip. 

У вас может возникнуть справедливый вопрос: “почему же окружение выбрано Docker, а не Python”. На самом деле можно выбрать и Python: для этого только потребуется предварительно создать список используемых библиотек - командой: 

pip freeze > requirements.txt

В моем случае использование Docker имеет с одной стороны наследственный характер (раньше не было таких сервисов как Amvera, и все равно приходилось писать Dockerfile самому), с другой - бОльший контроль над созданием образа. Это позволяет производить более тонкую настройку, и встраивать другие процессы - например, регулярную проверку образов на уязвимости (к примеру, с помощью snyk и подобных сервисов).

И последнее что важно отметить в блоке с Docker: можно использовать и уже построенный образ, размещенный на публично доступном ресурсе (тот же dockerhub).

И отдельно отмечу варианты с db:

Таким образом, установить можно далеко не только PostgreSQL, но и кучу других БД. 

Безусловно не стоит забывать про вариант с Docker - там полная свобода: можно использоваться абсолютно любой публично доступный образ. 

Нажимаем “Завершить” для создания проекта

6. В созданном проекте переходим на “Переменные”, предварительно определившись с их наполнением:

Это специальный раздел, в котором задаются переменные окружения - по сути, конфигурационные параметры. В данном исходном коде предусмотрены следующие параметры: 

  • Относящиеся к боту:

    • BOTNAME - название бота, заданное в BotFather

    • TOKEN - токен, выданный ранее BotFather

    • START_MESSAGE - сообщение, которое будет выведено при вводе команды /start

    • HELP_MESSAGE - сообщение, которое будет выведено при вводе команды /help

    • PERSISTENCE_FILE - путь до файла, в котором будут сохраняться промежуточные объекты для inline-клавиатуры

  • Относящиеся к расписанию:

    • JOB_PERSISTENCE - флаг (1 или 0), подключаем ли базу данных для хранения джобов

    • PSQL_URL - строка подключения к базе данных 

  • Относящиеся к логике разбора запросов пользователей:

    • REGEXP - регулярка, которая позволяет проверить корректность введенного выражения (особенно актуально, если там есть вычисления)

  • Относящиеся к парсеру источника курса валют:

    • URL - адрес, из которого можно забрать актуальные данные по курсу валют

    • TIMEOUT - таймаут ожидания ответа при запросе по указанному URL (на случай неполадок в сети)

Разберем некоторые из них детальнее:

PERSISTENCE_FILE

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

Для корректной обработки нажатия в каждой кнопке необходимо передать контекст:

  • Какое действие будет произведено (подписка или отписка)

  • Если подписка - то на какое выражение (в данном примере: 1 EUR)

  • Если подписка - то какой тип (каждый день / каждую неделю / каждый месяц)

Этот контекст хранится в Python dict-структуре, которая сериализуется в файл. И переменная PERSISTENCE_FILE как раз указывает путь до этого файла. Если не сохранять состояние в файл, то при перезагрузке бота весь контекст по всем кнопкам потеряется. 

Необходимо задать значение: data/callback_persistence.pickle

Контейнеры

Немного из базовой части Docker: важно помнить, что наше приложение запускается в виде контейнера (из образа), который существует пока приложение работает. Перезагрузка приложения = удаление контейнера и создание нового, то есть “откат” к состоянию в образе. Способы сохранения какого-либо состояния следующие:

  • С помощью примонтированных persistence-областей (тот самый persistenceMount), 

  • С помощью внешних систем, выступающих в качестве Persistence Storage: например, тот же PostgreSQL. 

Наличие PERSISTENCE_FILE напрямую связано с persistenceMount, который упоминался выше в конфигурации. 

А теперь следите за руками:

1. В конфигурации проекта Amvera мы указали: /app/data

  • Это означает, что папка /app/data является примонтированной, и данные хранящиеся в ней не будут стираться вне зависимости от рестарта приложения

2. В Dockerfile в репозитории с кодом указано: WORKDIR /app

  • Это означает, что все команды в файловой системе (в том числе навигация из Python) производится относительно этой стартовой точки (директория /app)

3. Значение переменной окружения PERSISTENCE_FILE мы указываем: data/callback_persistence.pickle

  • Чтобы получить полный путь, нужно добавить стартовую точку, то есть получится: /app/data/callback_persistence.pickle

  • Таким образом, файл находится внутри примонтированной области, а значит он будет сохранен между рестартами приложения

JOB_PERSISTENCE

Числовой флаг (1 или 0), регулирующий хранение джобов уведомлений. Если стоит 0, то все хранится только в памяти, и между перезапусками бота весь контекст теряется.

Если возможность потерять все джобы на расписании допустима, то можно смело оставлять 0. Важное следствие: PostgreSQL в этом случае не будет использоваться.

PSQL_URL

В боте для подключения к базе данных используется SQL Alchemy - возможно, самый распространенный инструмент работы с БД из Python. 

Строка подключения к PostgreSQL в этом случае должна выглядеть в следующем формате:

postgresql://<user>:<password>@<db_host>[:<db_port>]/<dbname>

Вспоминаем все настройки, которые задавали при подключении к БД из pgAdmin:

В моем случае получается так:

postgresql://tgbot:***@amvera-dikirilov-***-rw/converter

Вместо *** конечно же должны быть пароль и полный хост соответственно.

REGEXP

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

На текущем этапе мне не хотелось в рамках pet-проекта заниматься полноценной реализацией польской записи, поэтому было использовано простое (но привносящее уязвимости) решение: функция eval. Для минимизации уязвимости и в целом проверки, что оно сработает (если выражение неверное - Exception), нужна эта регулярка. 

Даже с проверкой регуляркой я не рекомендую использовать eval() в продуктивных решениях. 

Для проверки использую следующее выражение: ^([-+/]?\d+(.\d+)?)$

URL

Бот умеет парсить XML, которая любезно предоставляется ЦБ по следующему адресу:

http://www.cbr.ru/scripts/XML_daily.asp

TIMEOUT

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

Я обычно выставляю значение 5 - уж точно не упремся при выгрузке ~100+ курсов валют.

7. Вносим переменные в проект

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

В переменные окружения попадают как переменные, так и секреты:

С точки зрения приложения разницы между ними нет.

С точки зрения хранения и видимости - разница есть. 

Правило очень простое: любая чувствительная информация должна быть секретом: пароли, токены, etc.

По итогу должно получиться примерно так (список конечно же прокручивается, просто только это умещается в окне 13-дюймового устройства):


3.3. Загрузка и запуск бота

Для загрузки кода в Amvera можно воспользоваться как Git, так и загрузкой непосредственно на сайте. 

Рекомендуется, конечно же, вариант 1 - с него и начнем.

3.3.1. Загрузка через Git

Ранее мы уже загрузили репозиторий к себе:

Далее пошаговый гайд:

1. Подключить Amvera репозиторий как удаленный. Команду для этого можно найти на вкладке “Репозиторий” в проекте. В моем случае это выглядит так:

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

В командной строке:

В процессе скорее всего потребуются логин и пароль от аккаунта Amvera. 

2. Выполнить merge веток в своем репозитории, для этого необходимо выполнить команды:

git pull amvera master

В процессе откроется редактор, в котором нужно будет заполнить сообщение для коммита с мерджем. Скорее всего это будет vi(m). Меня устраивает дефолтное сообщение, поэтому просто выхожу с сохранением (набрав :wq):

Работа в редакторе vi(m) - это отдельная тема (в том числе, для шуток и мемов). Для быстрого ознакомления с минимально необходимым можно посмотреть сюда.

Мой фаворит среди мемов
Мем из интернетов
Мем из интернетов

Если знаете еще более "цепляющие" - кидайте в комменты

После этого произойдет мердж файлов из репозитория Amvera в наш репозиторий:

Как мы видели, кроме файла конфигурации Amvera.yml там ничего нет (папка .git есть во всех репозиториях, там служебная информация) - только этот файл и загрузился. 

Возможная ошибка

Сразу стоит отметить, что если вы не настраивали Git на своей машине (как я не делал этого на удаленном сервере), то скорее всего вы получите такую ошибку:

Благо, здесь сразу представлена инструкция, что с этим делать: задать почту и имя пользователя Git с помощью команд:

git config --global user.email “you@example.com”
git config --global user.name “Your Name”

Можно и без global, если эти параметры хотите задать только для этого репозитория.

3. Загрузка исходников в Amvera

А теперь мы перешли к финальному шагу, который не просто загрузит файлы в Amvera, но и запустит процесс сборки.

По сути нам нужно просто актуализировать репозиторий в Amvera, что делается одной командой:

git push remote amvera master

После выполнения команды на вкладке “Репозиторий” проекта можно увидеть новые файлы, а также автоматически запустившуюся сборку по факту коммита:

Проверим, что сборка успешна:

Последние логи (“Pushing image…” и “Pushed…”) говорят о том, что образ успешно сформирован и загружен во внутреннее хранилище образов. Далее проект автоматически запускается - на дальнейших шагах проверим, что работает сам бот.

3.3.2. Загрузка файлов на сайте

Альтернативный способ загрузки - прямо в браузере. Для этого необходимо зайти в проект на вкладку “Репозиторий” и нажимаем “Загрузить данные”:

Поскольку amvera.yml уже есть в репозитории Amvera, а при загрузке мы только добавляем файлы, а не синхронизируем как в случае с git, нам не нужно предварительно скачивать этот файл.

Появится окно, куда необходимо перенести все файлы из репозитория:

При загрузке файлов не нужно выбирать .git* папки и файлы, то есть:

Нажимаем “Готово”:

Если сборка не началась автоматически, переходим на вкладку “Конфигурация”, листаем вниз и нажимаем “Собрать”:

Проверим, что сборка успешна:

Последние логи (“Pushing image…” и “Pushed…”) говорят о том, что образ успешно сформирован и загружен во внутреннее хранилище образов. Далее проект автоматически запускается - на дальнейших шагах проверим, что работает сам бот. 


3.4. Проверка бота

Для начала убедимся, что бот успешно запустился. На вкладке “Лог приложения” должны появиться строки примерно как тут:

После “Start polling” лога можно непосредственно тестировать бот:

А также в inline-режиме: 

Из примечательного: как только появятся сообщения с inline-клавиатурой, в разделе с data появится наш файл, который хранит состояние с контекстом кнопок:

Помимо этого надо проверить, что и подключение к БД произошло успешно.

Для этого зайдем в pgAdmin и проверим, что появилась таблица apscheduler_jobs:

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

Вместо заключения

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

Прелесть CI/CD-процесса, любезно предоставляемого Amvera, раскрывается в дальнейшей работе: сопровождении и доработках проекта. Как только изменение готово, одной командой (git push amvera master) можно сразу все раскатить и пользоваться.

One more thing

Кстати, удобно сделать как минимум 2 бота (для TEST и PROD), которые будут соответствовать 2-м разным проектам в Amvera. Оба проекта можно подключить к одному и тому же репозиторию под разными именами (например, amvera-test и amvera-prod). 

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

git push amvera-test master

А когда все проверили - раскатить изменения на основной:

git push amvera-prod master

Но это конечно только для самых любимых pet-проектов :)

P.S. Если решите развернуть бота по этому туториалу, и по ходу дела возникнут вопросы - welcome в комменты или в личку.

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


  1. kirillkosolapov
    09.04.2024 12:15
    +3

    А почему PostgreSQL, а не SQLite? Кажется, с SQLite не нужно создавать отдельного проекта - будет дешевле


    1. dikirilov Автор
      09.04.2024 12:15
      +3

      Отличный вопрос!

      Не скажу, что сильно долго думал на эту тему, но сходу вижу следующие причины:

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

      • Чтобы не терять состояния можно пробовать настроить сохранение в файлы - и даже положить их в persistenceMount в Amvera, но в контексте pet-проекта не хотелось дополнительно с этим заморачиваться

      • Лично для меня PostgreSQL куда более знакомый и понятный инструмент