Проект — электрический помощник для редакции RUVDS

xirf68rnxwxjeplbc3tjokwkilw.png


Казалось бы, для чего редакции может понадобиться telegram-бот? Мы смогли вполне точно ответить на это, когда число наших авторов начало превышать разумные возможности редакторов. В новых экономических реалиях ценна каждая рабочая минута, потому встала задача убрать часть работы, связанной с повторяющимися вопросами, которые можно было бы свалить на бота, для улучшения комфорта взаимодействия всех сторон и, конечно, экономии бесценного времени. А учитывая, что большая часть общения происходит именно внутри телеграма, то и было принято решение завести себе там электрического помощника. Как говорится: телеграм-бот — это не только 40 строчек кода, но ещё и очень полезный выхлоп.

▍ Без чёткого ТЗ — результат …


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

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

Был и второй фактор: редакция всё чаще отвечала на вопросы — ответы на которые можно почти полностью типизировать, а соответственно и запихать в какой-то автоответ. Когда JohurN в очередной раз пожаловалась на выполнение обезьяньей работы (не в обиду авторам), стало понятно, что необходимо свести все запросы в единый инструмент, который бы облегчил взаимодействие коллектива авторов с нашими редакторами, и я предложил ей создать телеграм-бота. Это и время сэкономит, и работа станет эффективней, и авторы смогут получать ответы на часть вопросов много быстрее, так как все обращения будут попадать либо в ред. чат, минуя каждого отдельного сотрудника (и тогда ему ответит любой свободный сотрудник ред. команды), либо человек получит ответ сразу от бота.

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

Результатом мысленного штурма было рождено следующее техническое задание:

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

Внешний вид бота должен иметь следующие кнопки:

  • Прислать статью.
  • Запросить рабочие условия.
  • Подписаться на канал.
  • Задать вопрос.
  • Получить файл с информацией по вёрстке.
  • Узнать тематики, которые мы публикуем в блоге.

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

Таким образом, в ручном режиме можно:

  • Прислать статью. Это пункт нужен нам на случай, когда редактор, который общается с автором, заболел или ушёл в отпуск. Тогда можно спокойно послать статью сразу в бота и знать, что она уйдёт в редакционный канал и не пропадёт на неопределённый срок.
  • Задать вопрос — это кнопка для новичков, которые нажав на неё, имеют возможность задать интересующий вопрос или попросить связаться с редактором (ну и для интровертов, конечно).
  • Запросить рабочие условия. Этот пункт весьма деликатен, так как есть какая-то общая часть условий, которая неизменна, а есть другая часть, насчёт которой всегда можно договориться с каждым автором отдельно.


Если подытожить, мы взяли, собрали весь самый часто используемый функционал взаимодействия с авторами и превратили его в кнопочки в боте. Плюс решили, что стоит разбавить унылость формализма общения с роботом и насыпали весёлых стикеров и вот он — технический прогресс, шагающий по планете, и киберпанк по всей красе:)

▍ Внешний вид получившегося бота:

vf0nxqwqnzzjxyunsichypeghga.png

vxroqmsmzzn6afj1a_okku0vtf4.jpeg

Кнопка в редакционном канале с оповещениями

▍ Пути решения задачи, выбор и работа с бот-API


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

https://api.telegram.org/bot ::токен бота:: /sendMessage


У телеграма существует два API: одно полное Telegram API, которое вызывается через протокол MTProto, второе — Bot API, реализующее только функции нужные ботам, и доступное через простые POST- и GET-методы по обычному HTTP протоколу. Оба варианта прекрасно документированы, правда самостоятельно лезть в MTProto без нужды смысла нет, так как есть готовые решения. Данный бот был написан на Python, а, следовательно, логичным выбором в качестве сервисной библиотеки стал популярный AIOGram.

Файл проект включает в себя четыре секции:

mjil1_dfik1gtyoa4eha6e89iq8.png

  • @settings — Макросы для сборки итогового.py файла
  • Таски — Трекер задач
  • CODE telebot.py — Код проекта
  • OUTPUT — Раздел, в который записывается итоговый файл


Раздел макросов содержит:

m4ncfrgqrxamjpw1xunjbl35qow.png

  • FORMAT — автоформатирование редактируемого участка кода с помощью программы Black.
  • RUN NODE — выполнение редактируемого участка кода.
  • MAKE_PY — сборка содержимого в разделе CODE telebot.py в итоговый файл.
  • RUN — вызов MAKE_PY и выполнение итогового файла.


Раздел кода состоит из следующих подразделов:

5kqtlhydfzlxpn-pf6nb1lsqkku.png

  • HEADER — заголовок файла telebot.py
  • IMPORTS — секция импорта
  • CLASSES — пользовательские классы
  • EVENTMACHINE — машина событий


Определены следующие пользовательские классы:

gfvga2y7jhzrbscapq3qtfykciy.png

  • Stringdict(dict) — словарь, возвращающий пустую строку при обращении к несуществующему ключу. Используется для хранения настроек во время выполнения.
  • Bot_Commander — центральный класс, управляющий всем.
  • Buttons_Factory — производитель кнопок, встраивающихся в сообщения.


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

  1. Зафиксировать где-то токен нового бота.
  2. Установить описание, которое будет показываться новым пользователям.
  3. Зарегистрировать команду start (и другие команды при наличии), чтобы пользователь мог использовать стандартное меню команд.


Приведённый ниже код является примером минимально функционального бота. Инициализируем клиента с помощью токена бота, регистрируем обработчики команд и сообщений, что-то делаем в ответ на действия пользователя:

from aiogram import Bot, types
from aiogram.dispatcher import Dispatcher
from aiogram.utils import executor

class Bot_Commander:
	def __init__(self):
		self.bot = Bot(token='54645656:DFLDF-4GJDFG034OLVBJMB0540OERKGB')
		self.dispatcher = Dispatcher(bot=self.bot, loop=asyncio.get_event_loop())
		# Подключаем обработчик команды /start
		self.dispatcher.register_message_handler(self.on_msg_start, commands=['start'])
		# Подключаем обработчик простых сообщений, определение content_types позволяет реагировать на файлы
		self.dispatcher.register_message_handler(self.on_msg, content_types=types.ContentType.all())
		# Подключаем обработчик нажатий на кнопки
		self.dispatcher.register_callback_query_handler(self.on_callback)

	def start(self):
		executor.start_polling(self.dispatcher)

	async def on_msg_start(self, message: types.Message):
		print(f'Команда start в чате {message.chat.id} от {message.from_user.first_name} @{message.from_user.username}')
		self.show_start_menu(message.from_user.id)

	async def on_msg(self, message: types.Message):
		print(f'сообщение {message.text}')
		await self.bot.send_message(message.from_user.id, 'Спасибо, ваше мнение очень ценно!')

	async def on_callback(self, callback_query: types.CallbackQuery):
		await self.bot.answer_callback_query(callback_query.id)
		user_choice = int(callback_query.data)
		event_machine.react(user_choice, callback_query.message)


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

У меня возникла ровно одна настоящая сложность: по нажатию на одну из кнопок меню необходимо было показывать информацию, содержащуюся в определённых сообщениях в приватном канале.

Во-первых, Bot Api не содержит функций для чтения сообщений из каналов. Поэтому пришлось использовать дополнительную библиотеку Pyrogram, умеющую обращаться к полному Telegram Api.

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

Telegram Api позволяет вызывать некоторые методы от лица бота без полноценного логина. В официальной документации под такими методами написано: Bots can use this method. Среди них есть метод для чтения сообщений по идентификатору getMessages.

Таким образом, с помощью тупого перебора можно найти нужное сообщение и сохранить его для дальнейшего показа пользователю. Есть, правда, тут небольшая тонкость, обращение к Telegram Api в любом варианте требует указания идентифицирующих пользователя api_id и api_hash, но для методов, доступных ботам, можно использовать известную пару api_id + api_hash от какой-нибудь библиотек или клиента, что можно нагуглить. При этом не будет возникать требование от сервера подтвердить телефон.

Платой за отсутствие сессии внутри бота становится сложноватенькая конструкция:

nuxn9wtmy2zh7fe1txlwy8emeae.png

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

Полный алгоритм выглядит так:

  1. Пользователю отдаётся кэшированный текст, который периодически обновляется.
  2. Метод get_jobs вызывается при старте бота и потом раз в сутки с помощью Apscheduler.
  3. В код записан последний известный идентификатор нужного сообщения, используется как начальное значение. Обновляется при уведомлениях о новых сообщениях в канале и при работе get_jobs.
  4. Относительно известного значения перебираются возможные идентификаторы новых сообщений. Если произошло десять неудач подряд, запоминаем идентификатор последнего успешного ответа.
  5. Зная идентификатор последнего сообщения, начинаем читать всё подряд в обратном порядке, пока не встретится шаблон нужного сообщения.


Следует понимать, что такой брутфорс, естественно, вызывает подозрения со стороны телеграма, и в ответ на запросы часто приходит таймаут на 10 секунд, который Pyrogram, к счастью, нормально обрабатывает, поэтому для промышленного чтения множества сообщений с каналов эта лазейка не подходит.

Теперь про раздел EVENTMACHINE. Что такое машина событий? Это логическая конструкция, назначение которой совпадает с назначением машины состояний. Машина состояний долгое время мне казалась изящной идеей, но практика показала, что человеческие возможности модифицировать её быстро теряются при хоть сколько-нибудь серьёзных задачах. Она красиво выглядит там, где уместнее было бы использовать конструкции типа Switch. Большие машины состояний имеют смысл только при программной генерации. В результате усталости от борьбы с энтропией появилась идея машины событий, в основе которой лежит банальный принцип «стимул-реакция». Похоже на систему прерываний: eсть перечень идентификаторов возможных сигналов, с каждым из которых сопоставлена соответствующая реакция. Управление высокой сложностью достигается независимостью, уникальностью и удобной группировкой сигналов. Конкретно в данном проекте сигналы организованы иерархически, при этом в качестве дополнительной защиты от сайд-эффектов проверка сигналов ограничена текущим уровнем дерева. Машина событий создаётся в одном экземпляре, а пользователей много, поэтому для каждого пользователя запоминается пройденный по дереву путь.

v8-0grlbywo7dzz2gd86ir407jc.png

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

event_machine.react(event_id, user_message)


  • event_id — нужный номер события
  • user_message — сообщение пользователя в чате с ботом


Код из дерева собирается в единый словарь (dict) в виде выражений lambda. С одной стороны, это накладывает ограничения на доступные к использованию конструкции, с другой стороны, при правильной организации кода машина событий должна исключительно вызывать методы.

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

▍ Где и как был развёрнут бот


Для этого, чтобы электронный помощник мог работать, его необходимо разместить на VPS в интернете, да так, чтобы сервер был доступен 24/7.

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

zv0nmxyvkmhk8ddbc1f_aqdciew.png

Пользователей будет мало, поэтому подойдёт самая простая конфигурация на Debiane 9. Оплачиваем и немного ждём, пока разворачивается образ на новой системе:

gpghgmngqnlahtjns1jbkevakmo.png

Запускаем сервер кнопочкой ON и подключаемся любимым SSH клиентом. Первым делом проводим минимальную настройку безопасности:

  1. Создаём новый админский аккаунт.
  2. Записываем длинный и сложный пароль от нового аккаунта.
  3. Подключаемся с нового аккаунта.
  4. Делаем невозможным удалённый логин по SSH от имени root.
  5. Ставим ufw для ленивой настройки файервола.
  6. ufw allow OpenSSH
  7. ufw enable


Детали всех манипуляций были ранее описаны в статье «Магия ssh».

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

Далее копируем на сервер файл с исходником бота. Ставим conda для безболезненной установки питона (мы ведь ленивые). Ставим сторонние библиотеки aiogram, apscheduler, pyrogram. Запускаем бота фоновым процессом без привязки к конкретному терминалу:

nohup python telebot.py &n


Хоба, и бот готов к дальнейшей эксплуатации.

▍ Заключение


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

Жить стало интереснее, жить стало веселее. Добро пожаловать в дивный новый мир с электрическим помощником. Интересно, снятся ли ему сны?

hq2htmoqfyuw34i6v6pa2k_nidy.jpegElectric Sheep (с англ. — «Электрические овцы») — проект распределенных вычислений, позволяющий генерировать, загружать и просматривать фрактальные заставки.

ПыСы Уважаемые авторы, а чтобы вы хотели добавить в этого бота?

sz7jpfj8i1pa6ocj-eia09dev4q.png

© Habrahabr.ru