Хотел отложить написание второй части трилогии в долгий ящик, но судя по просмотрам первого эпизода - тема создания Телеграм-ботов все еще актуальна на Хабр. Во второй части сфокусируемся на разработке бизнес-логики бота. В нашем проекте, для взаимодействия с Telegram, будем использовать библиотеку Aiogram. Для Python написано достаточное количество библиотек для работы с ТГ, но Aiogram, наверное, самая популярная. Советую прочитать руководство по работе с Aiogram от Groosha - для меня это была основная теоретическая база. Кроме непосредственной работы с функционалом библиотеки, советую обратить внимание на раздел "Роутеры. Структура" - я буду следовать этой логике при создании бота.  Мы будем работать в асинхронном режиме. Это достаточно сложная тема для неподготовленного читателя и не могу сказать, что я освоил все аспекты работы Python в асинхронном режиме. Для поверхностного погружения рекомендую почитать серию статей от @vlakirна Хабр. Не буду описывать процесс регистрации бота в ТГ - это достаточно простая процедура, чтобы тратить на нее время тут. В интернете очень много материала на эту тему. Будем считать, что у вас на руках уже есть Token вашего бота от BotFather. Также, в ТГ необходимо создать публичный канал и добавить бота в администраторы этого канала. Вопрос открытия комментариев к постам - на ваше усмотрение.  Бот должен стать промежуточным этапом между публикацией новости в канал. По расписанию или запросу скрипт должен пробегать по RSS-ленте сайта и проверять ее на наличие новых, ранее не опубликованных новостей. В случае, если появилось что-то новое, направлять сообщение, состоящее из заголовка, краткого содержания и ссылки на новость в ТГ-канал администратору с двумя inline-кнопками - "Удалить" и "Отправить в ChatGPT". В случае нажатия на кнопку "ChatGPT" скрипт направляет текст новости (после работы процедуры parse, созданной в Первой части материала) на сервер ChatGPT с заданным Prompt и удаляет исходную новость из ТГ-канала. Ответ от "ChatGPT" возвращается новым сообщением в бот с единственной кнопкой - "Отправить в канал", после нажатия на которую сообщение улетает в канал и удаляется из бота.  Мне хотелось на  этапе "Отправить в канал" реализовать возможность редактирования материала после ChatGPT, но к сожалению, это невозможно на уровне логики работы Telegram — администратор не может править сообщения от бота. Текст сообщения можно будет отредактировать уже в канале стандартным инструментарием работы с сообщениями Telegram. Конечно, это не очень удобно, но если делать быстро и канал небольшой — вполне нормально:) Возможно вы сможете найти более изящное решение этой проблемы.  Для реализации подобного функционала понадобится, как минимум, где-то хранить информацию о ранее опубликованных новостях. Можно пойти по пути@DimaFromMaiс его агрегатором новостей и хранить информацию в оперативной памяти. Но нужно понимать, что при любом сбое/перезагрузке сервера вы теряете всю историю и заново получаете всю ленту новостей, включая ранее опубликованное. Для нашего примера я решил чуть-чуть усложнить задачу и хранить все в базе данных. Выбор пал на SQLite, поскольку это наиболее простая и легковесная база для небольших проектов. В первой части мы написали парсер RSS-ленты Мотор'a. При запуске скрипта он может пройтись по списку всех новостей и вывести на печать заголовок, ссылку на новость и другую информацию из ленты. Сосредоточимся на ссылках на новости - нам нужно  определить уникальный ключ для хранения в нашей базе данных, который можно получить из ссылок, при этом не сохраняя всю ссылку как ключ (это очень жестоко по отношению к базе): Результат будет выглядеть примерно так: К сожалению, Мотор не присваивает цифровой код своим новостям, но очевидно, что в качестве уникального ключа можно использовать все, что идет после "news/"  и до расширения ".html". Чтобы вычленить из ссылки ключ воспользуемся регулярными выражениями и библиотекой re (она уже идет вместе с Python и не требует установки, достаточно ее испортировать - import re): Создадим новую процедуру parselink(link) в файле parse.py, которая будет вырезать из полученной строки (ссылки) нужный нам ключ: Понимаю, что конструкция "r'/news/([^/]+)\.htm$'" выглядит жутко для неподготовленного пользователя.  Если попробовать объяснить просто, то логика в следующем: "/news/" и "\.htm$" означают начало и конец нашего "вхождения". Символ "\" служит для экранирования точки, т.к. она является спецсимволом, а символ "$" говорит о конце строки; ([^/]+) - внутри группирующих скобок "()" то, что мы должны проверить.    "[^/]+" - любой символ кроме "/", а "+" обозначает одно или более повторений. Маленький лайфхак - очень удобно формировать шаблоны к re  при помощи ChatGPT:)) Достаточно сформулировать запрос в стиле "Как при помощи регулярных выражений в Python  из строки  "а" получить подстроку "б"".  Часть ссылок в ленте RSS на Мотор может вести не только на новостную ленту (с префиксом "/news/"), но и на другие страницы сайта, где другая разметка и структура (например - 'https://motor.ru/selector/nemolodo-zeleno-elektricheskie-restomody.htm'). Поскольку нам нужно будет парсить текст страницы в будущем, сфокусируемся только на новостных страницах, для чего используем конструкцию try...except - при попытке обработать ссылку не содержащую "/news"  re выдает ошибку, которую мы и отлавливаем. Доработаем bot.py следующим образом: При запуске получаем либо ключ новости, либо None - если ссылка ведет на иную, чем новостная, часть сайте. Все подготовительные процедуры, предшествующие созданию базы данных выполнены. Приступим. Для работы с БД будем использовать библиотеку aiosqlite ($pip install aiosqlite). Принципы работы с SQL в Python можно почитать тут,  а работу с aiosqlite можно посмотреть во второй части материала от @vlakirна Хабр. Я сразу приведу код с комментариями в спойлере, который необходимо поместить в файл  sql.py в корне проекта:  Для  нашего проекта нам достаточно создать 4 процедуры: async def create_table() - для создания БД в корневом каталоге. Причем благодаря "IF NOT EXISTS" мы проверяем, есть ли файл с БД, и если его нет - создаем базу и таблицу motor с полями id, news_id, title, link, status, date; async def save_to_db(news_id, title, link) - для создания записи по полям  news_id, title, link; async def update_db(news_id, status, date) - для обновления записи - добавления данных в поля status, date по полю news_id; async def select_for_db(news_id, column) - для создания SELECT'ов к базе. Поскольку библиотека aiosqlite асинхронная, то весь модуль работы с базой данных у нас уже написан в async. Допилим файл bot.py следующим образом: Переделываем все под асинхронную работу, вместо библиотеки requests используем класс AsyncClient из httpx ($ pip install httpx). Внутри цикла добавляем два ветвления: первое проверяет, что ссылка ведет именно на новость (результат работу процедуры parselink); второе ветвление проверяет через select_for_db, что запись ранее в базу не попадала. Если запись новая - она добавляется в базу через save_to_db. Далее здесь будет код отправки сообщения администратору ТГ-бота. В случае, если запись уже есть в базе (старая новость) - переходим к следующему шагу цикла через оператор continue. Вот теперь самое интересное, ради  чего все это делалось - разработка бота, подключение aiogram и вот это вот все.  Последние приготовления: Установить aiogram в окружение venv: pip install aiogram Определить номер id администратора/-ов канала. Это ваш ID в Телеграм. Получить его можно при помощи бота getmyid_bot, послав ему команду /start. Ваш ID будет "Your user ID"; Определить Id-канала. Тут чуть сложнее. Необходимо зайти в ваш Телеграм через web-версию мессенджера. Нажать на ваш канал (не бот!) и в адресной строке браузера скопировать номер после # с "-". Например, если в адресной строке браузера вы увидите что-то подобное: "....telegram.org/a/#-1001984511001", то id-канала будет "-1001984511001".  Создаем в корне проекта файл variables.py  и добавляем туда наши константы: Хранить переменные с чувствительными данными в коде не самая лучшая идея. Есть очень хорошее решение с использованием библиотеки pydantic и хранением данных в  .env файле. Прочитать подробный гайд можно в том же мануале по  Aiogram от Groosha - параграф "Файлы конфигурации".  В очередной раз грубо доработаем рашпилем  файл bot.py:  Переименовали процедуру main() в feed_reader(), перенесли процедуру создания БД в новую main(), скормили BOT_TOKEN aiogram. Напишем процедуру отправки сообщений в бота в файл bot.py. Пока без inline-кнопок, их добавим на следующем шаге: А вместо print (f"Новость ID {news_id} добавлена в БД") в процедуре feed_reader() сделаем отправку сообщений в бота в виде заголовка статьи и ссылки на нее: Если все сделали правильно, то после запуска скрипта в бот придут первые сообщения. По сути мы уже сделали собственный простой feed-reader для Телеграм. Теперь добавим этой штуке стероидов. Начнем с кнопок. У нас будет 2 группы inline-кнопок: первая группа (first_collection) - две кнопки для отправки в Chat-GPT и/или удаления сообщения, вторая группа (second_collection) - одна кнопка для отправки сообщения в новостной канал.  Следуя логике из мануала по aiogram от Groosha cоздадим отдельную папку "entrails" в корне проекта  и в ней файл keyboards.py со следующим кодом: После этого добавим в процедуру  send_message_to_bot в файле bot.py нашу первую коллекцию кнопок: Чтобы было удобно отлаживать наш скрипт и каждый раз не удалять базу данных и не смотреть на 80+ сообщений в канале, советую поставить какой-нибудь редактор для  SQLite баз и перед каждым запускам удалять пару строчек из базы для "отладочного" прогона. Например, я использую DB Browser for SQLite - бесплатный, есть почти на все платформы. Ура, у нас уже есть модные inline-кнопки. Все как у взрослых:)) Одна проблема, нажимать на них бесполезно - скрипт пока не умеет с ними работать.  Попробуем исправить эту оплошность. Нам нужно написать обработчики событий, который будет отлавливать нажатия на кнопки и совершать нужные действия.  Задача сводится к тому, чтобы отслеживать callback_data (см. код кнопок выше) от кнопки. Создадим в папке entrails файл handlers.py.  Начнем с обработки события по нажатию кнопки Delete. Чтобы остаться в разумных пределах статьи Хабра и не допиливать этот обработчик несколько раз, сразу добавим функцию, которая позволит по Id-статьи добавлять в уже имеющуюся в базе-данных запись информацию об удалении новости и дате и времени этого действия. Мы создавали поля status и date в таблице motor  и процедуру update_db - настало их время. В поле статус будем ставить статус "Del" или "Can't del - more 48H", в случае если мы захотим удалить сообщение по истечение 48 часов с момента отправки (Это особенность работы Telegram - сообщения можно удалить только в течение 48 часов после отправки). В поле date - добавляем текущую дату и время в системе при помощи библиотеки datetime. Обратите внимание на то, как мы вытаскиваем из сообщения ссылку из которой уже при помощи знакомой процедуры parselink() тянем news_id. У объекта message есть свои entities по коллекции которых мы проходим циклом до появления объекта с типом text_link. Если же смотреть на принцип работы обработки нажатия кнопки, то через декоратор роутера мы отслеживаем коллбэки, и если появляется событие "gpt_button_delete" запускаем процедуру  delete_message(). Внимательным читателям гайда по Aiogram  должно быть все понятно.  Теперь у нас полностью рабочая кнопка Delete - сообщение удаляется, а в базу данных заносится статус удаления и время этого удаления. Если вы еще не забыли, то в первой коллекции кнопок у нас есть кнопка - "Send to Chat-GPT".  Для нашего примера будем использовать ChatGPT и их python-библиотеку openai, которую необходимо установить в окружение проекта стандартным способом через утилиту pip.  Понимаю, что в России сейчас есть определенные трудности с доступом, регистрацией и оплатой сервисов OpenAI. В сети достаточно примеров того, как решить эту проблему. Будем считать, что вы удачно зарегестрировались, внесли оплату и получили токен. В примере я буду использовать модель "gpt-4" - на мой взгляд, результат ее работы лучше, чем у "gpt-3.5 turbo". Но стоит каждый такой запрос дороже и работает она существенно медленнее. Формирование вопроса (Prompt) к Chat-GPT отдельная и сложная тема, можно сказать - целое искусство. Советую почитать этот материал. В любом случае, вам советую запастись терпением и бюджетом на эксперименты - тут придется экспериментировать какое-то время. Допустим, наш Prompt будет выглядеть следующим образом (позже вставим в код процедуры): Как администратор новостного канала напиши короткое изложение новости (не больше 100 слов) Создадим в корне проекта файл gptai.py и добавим в него код: Все достаточно просто - процедура get_GPT() получает текст статьи и дальше скармливает его openai - направляя запрос на две роли: пользователя с текстом статьи и "системы" с нашим prompt'ом. Процедура в итоге возвращает ответ ChatGPT. Осталось написать новый хендлер, отлавливающий нажатие кнопки Send to Chat-GPT и отправляющий текст в get_GPT(). В файл handlers.py дописываем:  Я снабдил код под спойлером подробными комментариями, по этому не буду повторяться. Сообщение формируется из ранее занесенных в базу title, link и ответа Chat-GPT. Внимательный читатель заметит, что в callback.message.answer мы добавили disable_web_page_preview=Tre  чтобы отключить предпросмотр страницы по ссылке, а также вторую коллекцию с одной единственной кнопкой - Send to Channel.  Обратите внимание, что модель gpt-4 работает очень медленно. После нажатия на кнопку Send to Chat-GPT придется подождать секунд 30-40 прежде, чем вам придет обратное сообщение в бот. Крайне не советую жать на кнопки сразу в нескольких сообщениях, не дожидаясь ответа - это может вызвать сбой. Напишем для кнопки Send to Channel еще один маленький хендлер, который будет отправлять копию сообщения (уже без кнопок) в новостной канал: В итоге сообщение в новостном канале должно выглядеть примерно так: Считаю, что для второй части нашей трилогии материала достаточно.  Мы разобрали следующие темы: Коснулись темы регулярных выражений Научились основам работы с базами-данных Написали 95%  бота  Подключились к ChatGPT В заключительной, третьей части, мы научимся запускать процедуру feed_reader() по расписанию и напишем еще один хандлер в handlers.py, который будет вызывать feed_reader() по команде. Кроме этого, соберем Docker Image бота и попробуем его запустить на удаленном сервере.