Итак, мы подошли к финальной части ботостроительной трилогии. Наш бот уже умеет смотреть RSS-ленту Мотора, подгружать свежие новости, слать сообщения администратору бота, а также реализована вся логика работы с модерацией сообщений перед отправкой в канал и работой с ChatGPT. Осталась одна проблема - обновление ленты происходит единожды, при запуске скрипта. Исправим эту оплошность.

Для тех кто пропустил: первая и вторая части.

Обновление новостей из бота

Обновление ленты происходит при вызове процедуры feed_reader(). Сейчас она вызывается единожды - при запуске бота из процедуры main(). Логично сделать функционал, который позволит администратору бота подгружать новости по команде. Проще всего это сделать по вводу текстовой команды через "/". Создадим еще один короткий хэндлер в файле handlers.py:

#подгружаем дополнительные модули из aiogram
from aiogram.types import CallbackQuery, Message
from aiogram.filters import Command

#импортируем процедуру feed_reader()
from bot import feed_reader


@router.message (Command(commands=['start', 'load']))
async def load_news(message: Message):
    await feed_reader()

Теперь администратор сможет обновлять новостную ленту по командам /start (включая нажатие на кнопку "start" при очистке истории бота) и /load.

Работа по расписанию

Принудительное обновление новостной ленты - удобно, но хочется чего-то большего. Например, вызывать процедуру feed_reader()по расписанию. Сделать это, как оказалось, очень просто.

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

Установим библиотеку в виртуальное окружение pip install apscheduler.

Доработаем процедуру main() в bot.py:

#импортируем модуль AsyncIOScheduler из apscheduler
from apscheduler.schedulers.asyncio import AsyncIOScheduler

async def main():
    #Не забываем про диспетчера и удаляем webhook на всякий случай
    dp = Dispatcher()
    dp.include_router(handlers.router)
    await bot.delete_webhook(drop_pending_updates=True)

    #создаем БД, есои ее нет
    await create_table()

    #вызываем чтение ленты
    await feed_reader()
    
    #создаем экземпляр класса AsyncIOScheduler
    scheduler = AsyncIOScheduler()
    #создаем задачу в режиме 'cron' которая будет запускать процедуру feed_reader() в 10:30 и 17:30
    scheduler.add_job(feed_reader, 'cron', hour='10,17', minute='30')
    #стартуем работу
    scheduler.start()

    #пуллим бот - чтобы наш скрипт постоянно запрашивал сервер ТГ о состоянии нашего бота
    await dp.start_polling(bot)

Все достаточно просто и реализуется в 3 строки кода. Создаем экземпляр класса AsyncIOScheduler - scheduler, устанавливаем job в режиме 'cron' на запуск процедуры feed_reader() по расписанию. В примере я сделал два запуска в 10:30 и в 17:30. Можно сделать другие интервалы, запускать чаще чем два раза в день - все как вам удобно.

Поздравляю, разработка бота полностью завершена. Весь код проекта можно взять в Git.

А дальше?

Итак, на текущий момент бот полностью разработан и даже как-то работает на компьютере. Но оставлять его "жить" у себя на локальной машине не самая разумная мысль - "домашний" компьютер засыпает, зависает, перезагружается при обновлениях и вообще редко работает 24 часа в сутки. А если ноутбук? Можно, конечно, собрать и сконфигурировать небольшой компьютер и засунуть его куда-то на антресоль гудеть. Но, согласитесь, это очень радикально.

Самое разумное решение арендовать небольшой виртуальный (VDS) сервер и поселить туда нашего бота. В сети вы сможете найти огромное количество предложений, я не ставлю цель кого-то рекламировать и рекомендовать. Могу сказать, что я арендовал под свой проект VDS со следующими характеристиками: Cores 1 / RAM 0.5 Gb / Disk 5 Gb на Debian 11 64 bit. Это было минимальное предложение. Все это в месяц стоит примерно как чашка капуччино.

Ок, робота написали, VDS сервер арендовали. Осталось переселить нашего подопечного жить и работать на ново место.

Запуск бота на сервере

Самое простое решение - скопировать все файлы бота на сервер и потом настроить его запуск в фоновом режиме.

Во-первых необходимо освоить базовые команды работы с Linux. Если раньше вы не работали с командной строкой - будет легкий шок. Почитайте, например, это.

Во-вторых, перед загрузкой файлов на сервер, крайне рекомендую обновить файл requirements.txt  командой pip freeze > requirements.txt из терминала VSCode на вашем компьютере. Необходимо перенести в него все зависимости, включая зависимости библиотек которые мы подключили.

После загрузки всех файлов на сервер необходимо зайти в папку с файлами бота, где должен лежать, в том числе, наш файл requirements.txt и установить все зависимости из него командой pip install -r requirements.txt 

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

Кажется, что это наиболее простой способ. Однако он таит в себе много проблем с отладкой. Например, я столкнулся с тем, что бот просто не запускался - были конфликты с зависимостями. Пробовал переставлять, перегружать, обновлять. В итоге мне эти пляски с бубном в командной строке надоели и я решил освоить Docker.

Docker или дивный мир DevOps

Если очень поверхностно, то Docker это система, которая позволяет разворачивать свои приложения/сервисы внутри замкнутой среды - контейнера. Контейнер представляет собой изолированную среду (по сути свой "виртуальный сервер") с инстансом ОС, обычно какой-то сборки Linux, в который устанавливается нужное окружение и ваша программа. Контейнеры могут взаимодействовать с внешним миром и друг с другом. Развернув и отладив свое приложения в контейнере (или в группе контейнеров), вы можете легко перенести его вместе с контейнером на другую машину где оно будет работать также. Тема очень сложная, интересная. Советую почитать/посмотреть больше материала про контейнеры и Docker, например, тут.

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

Первое, что необходимо сделать, это установить на ваш компьютер приложение Docker c официального сайта под вашу ОС. Также необходимо создать аккаунт и зайти через него в учетную запись в приложении. Уже можно ставить разные готовые образы из Docker-Hub и играть с ними.

После установки приложения переходим в VSCode с проектом и устанавливаем расширение Docker от Microsoft. После его установки создаем в корне нашего проекта файл с именем "Dockerfile" без расширения. Если до этого все было сделано корректно, то VSCode сразу присвоит файлу лого Docker'a. В этом файле мы напишем скрипт, который соберет Image нашего контейнера.

Логика создания Image следующая: выбираем один из стандартных образов из Docker-Hub, доустанавливаем в него нужные нам пакеты, загружаем файлы нашей программы, и говорим, какой файл нужно запускать при запуске контейнера. Все это нужно отразить в файле Dockerfile.

Для нашего проекта я выбрал минималистичный образ alpine. Описание на Docker-Hub его звучит как:

A minimal Docker image based on Alpine Linux with a complete package index and only 5 MB in size!

Прочитав такое, я решил, что смогу запихнуть своего бота в файл до 10 mb и все будет супер легко и быстро перенесено на мой хилый VDS. Но дальше все пошло не по плану. Во-первых, в alpine не идет утилита pip, при помощи который мы привычно устанавливаем библиотеки в python, а во-вторых в ней невозможно установить тайм-зону, что для нашего проекта очень важно, поскольку у нас есть события, которые работают по расписанию. Все это можно исправить "доустановкой" нужного, но Image раздувается до 160+ mb:))

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

#Базовая версия инстанса ОС
FROM alpine:3.18.4

# Установка таймзоны
RUN apk add -U tzdata
ENV TZ=Europe/Moscow
RUN cp /usr/share/zoneinfo/$TZ /etc/localtime

# Создаем внутри образа папку под нашего бота
WORKDIR /motor

# Копируем файл с окружением
COPY requirements.txt .

# Устанавливаем утилиту pip и после запускаем установку окружения в образ
RUN apk add --update py-pip
RUN pip install -r requirements.txt

# Копируем файлы бота
COPY entrails/ entrails/
COPY bot.py .
COPY gptai.py .
COPY parse.py .
COPY sql.py .
COPY variables.py .

#Говорим, какой файл необходимо запустить
CMD ["python", "bot.py"]

Теперь в терминале VSCode необходимо ввести следующую команду (при этом приложение Docker должно быть запущено, а файл Dockerfile сохранен):

docker build -t motor .

Создание образа займет несколько минут. Если все завершилось удачно, то по команде docker images вы увидите созданный Image:

Первый image готов:))
Первый image готов:))

Настало время потестировать то, что мы собрали. Для этого переходим в приложение Docker и заходим в раздел Images. Там уже должен быть наш Image с именем motor. Нажимаем на кнопку Run в группе Actions, подтверждаем еще раз запуск. После этого Docker создает тот-самый контейнер и запускает через Python скрипт bot.py. У вас откроется окно с контейнером и на вкладке Logs вы увидите все, что происходит с приложением - если все нормально, то там должно быть пусто. Можно пользоваться ботом - посылать команды и все такое. Вверху логичные кнопки управления - остановка, запуск.

Будем считать, что все у вас заработало нормально и image можно переносить на сервер.
В терминале VSCode вводим команду docker save motor > motor.tar

После выполнения команды, в корне проекта должен появится файл motor.tar. Его можно перенести на VDS-сервер.

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

После установки Docker на VDS сервер, копируем туда наш motor.tar. В терминале переходим в каталог с файлом и выполняем команду: docker load -i motor.tar

Проверить загрузку образа можно командой docker images

Осталось запустить контейнер из образа в фоновом режиме. Для этого выполняем команду docker run -d motor

Посмотреть, что контейнер создан и запущен можно по команде docker ps -a

Заключение

Целью трилогии было показать, как имея совсем скромные знания в программировании сделать что-то рабочее и действительно полезное, что можно использовать каждый день. Затрагиваются все аспекты жизненного цикла ПО (SDLC) - от идеи до деплоя на сервере, что на мой взгляд, дает целостную картину начинающему, представление о жизненном цикле ПО.

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

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