VerifyBot. Управление Telegram-группами на PowerShell

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

С Active Directory и корпоративными информационными системами этот процесс хорошо автоматизируется через IDM/IGA-системы.

А вот с Telegram всё несколько интереснее. Поэтому готовьтесь, повествование будет длинным.

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

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

Значит нужно связать доступ к корпоративным Telegram-группам с актуальными кадровыми данными и исключить из этого процесса человеческий фактор.

Так появился VerifyBot.

Бот на PowerShell, который проверяет пользователя по номеру телефона в кадровом источнике, генерирует приглашения в корпоративные группы и периодически перепроверяет кадровый статус уже зарегистрированных сотрудников.

В качестве кадрового источника используется 1С:Зарплата и управление персоналом, а для получения информации — расширение api query, позволяющее публиковать HTTP-интерфейсы для выполнения запросов к данным 1С.

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

Дополнительно подготовлена интеграция с Telegram Gateway для подтверждения номера одноразовым кодом. Но этот сервис платный, поэтому в рабочем сценарии мы остановились на штатной передаче номера средствами Telegram и проверке через ЗУП.

Основой проекта стал мой предыдущий PowerBot — шаблон Telegram-бота на PowerShell с многопоточной обработкой событий через Runspaces.

Получилось приложение, которое умеет работать с Telegram Bot API, кадровой системой, состояниями пользователей, очередями сообщений и регламентными заданиями.

И всё это на PowerShell.

Архитектура. Runspaces, очереди и немного параллелизма

Начнём с того, как устроен VerifyBot изнутри.

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

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

Проблема в том, что внешние сервисы отвечают не всегда быстро. Например, пока ЗУП обрабатывает запрос одного пользователя, остальные не должны ждать своей очереди.

Поэтому я использовал Runspaces — механизм PowerShell, позволяющий выполнять несколько задач параллельно в рамках одного процесса.

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

Шесть основных обработчиков

Точкой входа служит pbStart/Create-Runspace.ps1.

Он загружает настройки, создаёт общие структуры данных и запускает основные обработчики:

Обработчик

Назначение

GetUpdate

Получение обновлений Telegram через getUpdates

MessageHandler

Обработка сообщений пользователей и событий групп

CallbackHandler

Обработка нажатий на inline-кнопки

InlineHandler

Обработка inline-запросов, включая выбор групп

SendingHandler

Отправка сообщений и выполнение операций Telegram Bot API

CheckStaff

Регламентная перепроверка сотрудников и отзыв доступа

Для этих задач создаётся общий RunspacePool:

$runspacePool = [runspacefactory]::CreateRunspacePool( 1, 6, $sessionState, $Host)$runspacePool.Open()

Здесь максимальное количество одновременно работающих основных пространств выполнения ограничено шестью, но ничто не мешает изменить это значение при необходимости.

Обработчики сообщений, callback- и inline-запросов, в свою очередь, используют собственные дочерние пулы Runspaces. Их размер можно изменять через настройки приложения.

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

Очереди без отдельного брокера

Для обмена данными между обработчиками используются синхронизированные хеш-таблицы:

$queueUsersHash = [hashtable]::Synchronized(@{})$queueGroupsHash = [hashtable]::Synchronized(@{})$queueMessageHash = [hashtable]::Synchronized(@{})$queueCallbackHash = [hashtable]::Synchronized(@{})$queueInlineHash = [hashtable]::Synchronized(@{})$queueSendingHash = [hashtable]::Synchronized(@{})

queueUsersHash и queueGroupsHash содержат сведения о пользователях и группах.

Остальные таблицы используются как очереди входящих и исходящих операций.

Например, когда Telegram возвращает очередное обновление, GetUpdate помещает сообщение в соответствующую очередь:

if ($PSItem.message) { $tempDate = Get-Tempkey $queueMessageHash[$tempDate] = $PSItem.message continue}

Дальше событие обрабатывается независимо от процесса получения обновлений.

При необходимости отправить ответ обработчик добавляет задание в queueSendingHash:

$queueSendingHash[$tempKey] = @{ action_type = 'send_message' chat_id = $chatID text = $messageText reply_markup = $replyMarkup}

Таким образом, логика обработки пользовательского действия отделена от непосредственного обращения к Telegram API.

Но есть один нюанс.

[hashtable]::Synchronized() обеспечивает потокобезопасность отдельных операций с хеш-таблицей, но не гарантирует целостность данных, когда необходимо выполнить несколько связанных действий.

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

Чтобы избежать такой ситуации, в VerifyBot для некоторых операций используется дополнительная блокировка через System.Threading.Monitor.

Посмотрим на пример из Create-Runspace.ps1:

[System.Threading.Monitor]::Enter($queueUsersHash.SyncRoot)try { $tmpUsersHash = [System.Collections.SortedList] $queueUsersHash $tmpUsersHash | ConvertTo-Json -Depth 10 | Out-File "$appPathpbAssetsqueueUsersHash.json"}finally { [System.Threading.Monitor]::Exit($queueUsersHash.SyncRoot)}

Сначала Monitor.Enter() захватывает блокировку объекта SyncRoot, связанного с хеш-таблицей. Пока блокировка удерживается, другие Runspaces, использующие тот же механизм синхронизации, не смогут изменить таблицу.

Затем создаётся её копия в SortedList, которая преобразуется в JSON и записывается в файл.

После завершения операции Monitor.Exit() освобождает блокировку. Причём используется конструкция try/finally, поэтому освобождение произойдёт даже при возникновении ошибки во время сохранения.

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

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

Централизованная отправка и Token Bucket

В актуальной версии VerifyBot был переработан SendingHandler и добавлено ограничение скорости запросов через алгоритм Token Bucket.

Принцип простой: у обработчика есть набор токенов. Каждый исходящий запрос расходует один токен. Токены постепенно восстанавливаются с заданной скоростью.

Если доступных токенов нет, запрос откладывается.

Например, глобальный ограничитель создаётся так:

$script:bucketGlobal = New-TokenBucket ` -ratePerSecond 30.0 ` -capacity 30.0

Дополнительно предусмотрены ограничители для отдельных чатов и групп.

В текущей реализации используются следующие настройки:

  • глобально — 30 запросов в секунду;

  • для отдельного чата — один запрос в секунду;

  • для группы — до 20 запросов в минуту.

При получении ответа 429 Too Many Requests обработчик учитывает значение retry_after. Для временных ошибок предусмотрены повторные попытки с увеличивающейся задержкой.

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

Отдельно остановлюсь на журналировании.

Ранее при ошибке отправки в queueErrorHash сохранялся полный исходный объект задания — originalMessage. В нём могли находиться текст сообщения, персональные данные или ссылка-приглашение.

После анализа безопасности я отключил сохранение этого поля:

$queueErrorHash[$tempKey] = @{ source = 'sendingHandlerScript' message = $msg # originalMessage = $originalMessage}

Теперь обработчик сохраняет описание ошибки без полного объекта отправки.

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

Где храним состояние

Отдельную СУБД для бота я не использовал.

Сведения о пользователях и группах находятся в памяти, а основной скрипт периодически сохраняет их в JSON:

$tmpUsersHash = [System.Collections.SortedList] $queueUsersHash$tmpUsersHash | ConvertTo-Json -Depth 10 | Out-File "$appPathpbAssetsqueueUsersHash.json"

При запуске данные загружаются обратно.

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

В частности, необходимо ограничить доступ к JSON-файлам, поскольку там хранятся номера телефонов, Telegram ID, идентификаторы кадровых записей и состояния доступа пользователей.

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

Проверяем сотрудника и выдаём приглашение

Теперь самое интересное. Мы уже разобрались, как бот получает события и передаёт их обработчикам. Осталось понять, на каком основании он решает, кому можно попасть в корпоративную группу.

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

Откуда берём данные

Напомню, на стороне ЗУП установлено расширение api query, которое позволяет публиковать HTTP-эндпоинты для получения данных по запросам.

Бот посылает запрос, который ищет только действующего сотрудника по номеру телефона. Проверяются два реквизита — мобильный и рабочий телефон. Если запрос вернул данные, значит сотрудник есть и он работает.

Таким образом, сама проверка кадрового статуса выполняется на стороне 1С. Боту достаточно обработать полученный результат.

Получаем номер телефона

В начальной форме бот предлагает пользователю кнопку «Отправить номер»:

$contactButton = Build-ReplyButton ` -picture "📎 " ` -text "Отправить номер" ` -requestContact $true

Ключевой параметр — requestContact. Он преобразуется в request_contact = true при формировании клавиатуры Telegram.

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

В MessageHandler.ps1 предусмотрена обработка объекта contact, включая проверку отсутствия vcard.

После этого номер сохраняется в состоянии пользователя. Если он изменился по сравнению с ранее сохранённым, бот фиксирует изменение.

Теперь можно обращаться к ЗУП.

Проверка через HTTP API

За взаимодействие с кадровой системой отвечает функция New-CheckStaff, расположенная в pbModules/Utils/Utils.ps1.

Подготовка запроса выглядит так:

$body = @{ id_api_query = "rabotayushchie_sotrudniki" token = $token param = @{ number = $phone }} | ConvertTo-Json -Depth 5 -Compress

id_api_query определяет опубликованный на стороне 1С запрос. Тут для наглядности используется имя rabotayushchie_sotrudniki.

token — дополнительный проверочный токен API, а number — номер телефона пользователя.

Для обращения к сервису используются также учётные данные из хранилища секретов. В моей реализации это PowerShell SecretManagement.

Сам HTTP-запрос выполняется через Invoke-RestMethod.

Результат проверки обрабатывается достаточно просто:

if ($response -and $response.fio) { $queueUsersHash["$userID"].is_staff = $true $queueUsersHash["$userID"].guid = $response[0].guid return $true}else { $queueUsersHash["$userID"].is_staff = $false return $false}

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

При успешной проверке сохраняется идентификатор сотрудника и устанавливается признак is_staff.

При отрицательном ответе пользователь получает сообщение о невозможности прохождения проверки.

Технические ошибки обращения к API обрабатываются отдельно.

Выбираем группу

После успешной проверки пользователю становится доступен список групп, зарегистрированных в VerifyBot.

При выборе группы происходит ещё одно обращение к кадровому API. Оно реализовано в ViaBotHandler.ps1.

Сначала бот получает сохранённый номер пользователя, затем повторно вызывает New-CheckStaff.

Это сделано намеренно.

Между первоначальной проверкой и попыткой получить приглашение может пройти достаточно много времени. Сотрудник мог уволиться, а сохранённый в памяти признак is_staff остался прежним.

Поэтому при выборе группы кадровый статус проверяется заново.

Создаём приглашение

Если сотрудник прошёл повторную проверку, бот открывает экран выбранной группы с кнопкой «Получить приглашение».

Нажатие обрабатывается в CallBackHandler.ps1.

Сначала выполняется unbanChatMember. Это позволяет повторно разрешить вступление пользователю, который ранее был исключён или заблокирован в группе.

Затем создаётся ссылка-приглашение:

$resultInvite = New-ChatInviteLink ` -botID $botID ` -chatID $userGroup ` -memberLimit 1

Здесь важно правильно понимать параметр memberLimit = 1.

Согласно спецификации Telegram Bot API, он ограничивает количество участников, которые могут одновременно находиться в группе после вступления по данной ссылке.

Это не криптографическая привязка ссылки к Telegram ID и не гарантия её однократного использования навсегда. После выхода участника возможность использования ссылки появится снова.

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

В текущей реализации VerifyBot создаёт ссылку с memberLimit = 1 и сохраняет её в состоянии пользователя.

Если Telegram успешно создал приглашение, бот устанавливает статус Verified.

После этого на экране появляется кнопка «Вступить в группу».

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

Таким образом, создание ссылки и фактическое вступление учитываются отдельно.

А если что-то пошло не так?

Если Telegram не смог создать приглашение, пользователь увидит сообщение об ошибке, а администраторы получат уведомление.

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

В результате основная модель выглядит так:

  • штатная передача номера средствами Telegram,

  • проверка сотрудника через ЗУП,

  • приглашение в выбранную группу с ограничением на использование.

Всё это без ежедневного участия администратора при выдаче каждого приглашения.

Но на этом управление доступом не заканчивается.

Управление группами.

Выдать приглашение — это только половина задачи. Нужно ещё контролировать происходящее в самих группах.

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

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

Для начала познакомимся

Прежде чем бот начнёт работать с группой, его туда необходимо добавить.

При этом вручную прописывать идентификаторы всех групп в конфигурации я не хотел.

В MessageHandler.ps1 предусмотрен механизм автоматической регистрации групп, в которые добавляют бота.

Когда Telegram сообщает об изменении статуса самого бота через my_chat_member, VerifyBot обратится к API методом getChatAdministrators и определит владельца группы.

Далее его Telegram ID сравнивается со значением botCreator из настроек:

if ($admin.status -eq "creator") { if ($admin.user.id -ne $botCreator) { $resultLeave = New-LeaveChat ` -botID $botID ` -chatID $param.my_chat_member.chat.id }}

Если владелец группы не совпадает с доверенным пользователем, бот пытается самостоятельно её покинуть.

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

Если владелец соответствует настройкам, бот получает сведения о группе через getChat и сохраняет их в queueGroupsHash.

Эта таблица используется при формировании списка доступных групп.

Что происходит с участниками

Telegram передаёт события изменения членства через chat_member.

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

VerifyBot обрабатывает вступление и выход участников, фиксирует соответствующие события и отправляет уведомления администраторам.

В частности, он различает вступление по приглашению и добавление пользователя без ссылки.

Кроме того, предусмотрен контроль сообщений от неверифицированных участников.

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

if ($userVerifyState -notlike "Verified") { if ($banIfNotVerified) { $userKick = New-KickChatMember ` -botID $botID ` -chatID $groupChatID ` -userID $userID }}

Параметр banIfNotVerified управляет этой функцией.

Если он равен false, автоматическое исключение отключено. Если true, обработчик пытается заблокировать неверифицированного пользователя при обнаружении его текстового сообщения.

Для владельца группы предусмотрено исключение из проверки.

Сам New-KickChatMember использует метод Telegram Bot API banChatMember. Поэтому боту необходимы соответствующие административные права на группу.

Важно, что это реакция на сообщение, а не непрерывная проверка полного списка участников.

Уведомления без информационного шума

Отдельно остановлюсь на уведомлениях администраторам.

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

Поэтому в VerifyBot предусмотрена служебная Telegram-супергруппа с включёнными темами.

Для каждой категории событий задаётся идентификатор соответствующей темы:

Параметр

Категория событий

alertThreadUninvited

Добавление участников без приглашений

alertThreadUsersOut

Выход и исключение участников

alertThreadSystemError

Ошибки работы бота

alertThreadPhoneNotFound

Номер не найден в кадровой базе

alertThreadAdded

Подключение бота к группам

alertThreadBalance

События добавления пользователя в группу

Идентификатор самой служебной группы задаётся параметром alertGroup.

Чтобы отправить уведомление в определённую тему, достаточно указать message_thread_id:

$queueSendingHash[$tempKey] = @{ action_type = 'send_message' chat_id = $alertChatID message_thread_id = $alertUninvited text = $messageText reply_markup = $null}

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

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

Ночная проверка сотрудников

Есть ещё один вопрос, который не решается проверкой при выдаче приглашения: что делать с сотрудником, который уже состоит в корпоративной группе, но впоследствии увольняется?

Ведь приглашение он получил совершенно законно. На тот момент работал в компании, прошёл проверку и вступил в группу.

Но кадровый статус изменился, а Telegram об этом, разумеется, не знает.

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

В планировщике Windows настроено ночное задание, которое запускает pbStartCheckStaff.ps1.

Скрипт открывает конфигурационный JSON и устанавливает:

$botSettings.checkStaffRun = $true

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

Никаких обращений к Telegram в этом скрипте нет. Планировщик только сообщает уже работающему боту, что пора провести сверку.

Основной процесс Create-Runspace.ps1 обнаруживает установленный флаг, сбрасывает его в конфигурации и передаёт команду отдельному Runspace CheckStaff.

Тот ожидает сигнал:

if ($queueCommandsHash['checkStaffRun']) { $queueCommandsHash['checkStaffRun'] = $false # Запуск проверки сотрудников}

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

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

Для этого используется признак staffCleanupPending.

Отзываем доступ

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

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

$kickResult = New-KickChatMember ` -botID $botID ` -chatID $groupKey ` -userID $userkey$kickOk = [bool]($kickResult -and $kickResult.ok)

Дополнительно обрабатываются сохранённые приглашения: для ссылки вызывается revokeChatInviteLink.

В текущем алгоритме активированная ссылка считается использованной, поэтому повторный отзыв для неё пропускается.

Успешность исключения и обработки приглашения учитывается отдельно.

Для этого в состоянии группы используются два признака:

  • staffCleanupKickDone — подтверждено исключение пользователя;

  • staffCleanupRevokeDone — завершена предусмотренная обработка приглашения.

Если необходимые операции выполнены успешно, запись о группе удаляется из состояния пользователя.

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

После обработки всех известных групп бот сбрасывает признак is_staff и при возможности обновляет интерфейс пользователя.

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

Администраторы получают соответствующие уведомления.

А если ЗУП недоступна?

Отдельно предусмотрена ситуация, когда проблема не в сотруднике, а в кадровом API.

Если запрос завершился технической ошибкой, New-CheckStaff возвращает ERROR.

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

Проверка будет повторена при следующем запуске задания.

Это принципиальный момент: отсутствие сотрудника в кадровой базе и невозможность получить ответ от базы — разные события. Последнее не должно автоматически приводить к отзыву доступа.

В результате VerifyBot реализует два механизма: проверку перед выдачей приглашения и последующую регламентную сверку.

При этом ночная проверка работает с зарегистрированными пользователями VerifyBot, а не выполняет независимую инвентаризацию полного состава всех Telegram-групп.

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

Telegram Gateway. Проверяем номер ещё одним способом

При разработке VerifyBot я рассматривал дополнительное подтверждение номера телефона через Telegram Gateway.

Это отдельный сервис Telegram для отправки и проверки одноразовых кодов.

По сути, альтернатива SMS-подтверждению. Только сообщение приходит через Telegram.

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

Однако в рабочем сценарии мы решили обойтись без него из-за платной модели сервиса.

Реализованные модули я сохранил — возможно, они пригодятся в других проектах.

Как работает Telegram Gateway

Первое, что необходимо понимать: Telegram Bot API и Gateway API — разные интерфейсы.

Бот продолжает взаимодействовать с пользователем через обычный Bot API, а Gateway используется для доставки и проверки кодов.

Алгоритм выглядит следующим образом:

  1. Пользователь передаёт номер телефона.

  2. Бот обращается к Gateway и проверяет возможность отправки кода.

  3. Gateway возвращает идентификатор запроса request_id.

  4. Бот запрашивает отправку кода.

  5. Пользователь получает код через Telegram и вводит его боту.

  6. Бот отправляет введённый код в Gateway для проверки.

  7. При успешном подтверждении можно продолжить выдачу приглашения.

Сам бот не обязан генерировать и хранить правильный код. Проверку выполняет Telegram.

Три основных метода API

В проекте реализованы отдельные PowerShell-функции для каждого этапа.

checkSendAbility — проверка возможности доставки:

$resultCheckSend = New-CheckSendAbility ` -phoneNumber $userPhoneNumber ` -gateID $gateID

Здесь $gateID — токен Telegram Gateway, а не токен бота от BotFather.

Успешный ответ содержит request_id, стоимость запроса и сведения о балансе.

sendVerificationMessage — отправка одноразового кода:

$resultSend = Send-VerificationMessage ` -phoneNumber $userPhoneNumber ` -gateID $gateID ` -requestID $resultCheckSend.requestID ` -ttl $codeTTL

В настройках VerifyBot срок действия кода определяется параметром verifiTtl.

После успешной отправки пользователь переводится в состояние Awaiting_Code.

checkVerificationStatus — проверка введённого кода:

$result = Get-VerificationStatus ` -requestID $requestID ` -gateID $gateID ` -code $userCommandif ($result.IsValid) { # Код подтверждён}

В этом случае правильность кода устанавливает сам Gateway.

Telegram возвращает, в частности, статусы code_valid, code_invalid, expired или code_max_attempts_exceeded.

Особенности тарификации

Согласно стандартному тарифу Telegram Gateway, стоимость составляет $0,01 за доставленный код.

Но есть нюанс: вызов checkSendAbility может сопровождаться предварительным списанием стоимости, если сервис подтвердил возможность доставки.

Полученный request_id позволяет затем отправить сообщение без повторной оплаты того же запроса.

Для тестирования собственного номера предусмотрены бесплатные отправки.

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

Для нашей модели допуска штатной передачи номера и проверки через ЗУП оказалось достаточно.

Что осталось в проекте

В каталоге pbModules/GateWay находятся:

  • CheckSendAbility.ps1;

  • SendVerificationMessage.ps1;

  • CheckVerifiStatus.ps1.

В CallBackHandler.ps1 предусмотрена обработка действия getCode, а в пользовательской форме сохранились состояния интерфейса для ввода кода.

Но полный сценарий Gateway в текущей версии не включён: основная форма предлагает получение приглашения напрямую, а ветка обработки введённого кода в MessageHandler.ps1 закомментирована.

Кроме того, перед повторным включением необходимо привести параметры функций к актуальной спецификации Gateway API. Например, HTTP-запрос отправки должен использовать поле request_id, а не внутреннее имя параметра PowerShell requestID.

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

Документация:

  • Telegram Gateway API

  • Руководство по интеграции Gateway

Настройка и запуск VerifyBot

Теперь попробуем всё это запустить.

Сразу оговорюсь: VerifyBot это не универсальный сервис, который достаточно установить и указать токен Telegram.

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

С другой стороны, ничего экзотического здесь нет: PowerShell, Telegram Bot API, HTTP-сервис и несколько конфигурационных файлов.

Что потребуется

Для развёртывания необходимы Windows с PowerShell 5.1 или совместимым PowerShell 7, сетевой доступ к Telegram Bot API, токен бота и доступный HTTP API кадровой системы.

Также нужны учётные данные для кадрового API и каталог для хранения состояния и журналов.

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

Если планируется использовать Gateway, потребуется отдельный токен этого сервиса.

Готовим Telegram

Начинаем с @BotFather.

Создаём бота командой /newbot, получаем токен и сохраняем его в защищённом месте.

Поскольку VerifyBot использует inline-интерфейс выбора групп, включаем Inline Mode через /setinline.

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

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

Потребуются идентификатор группы (chat_id) и идентификаторы соответствующих тем (message_thread_id).

Ещё один параметр — botCreator. В моей модели это Telegram ID доверенного владельца групп. Если у разных групп разные владельцы, соответствующую проверку придётся адаптировать.

Подготавливаем проект

Для публикации исходников я подготовил отдельный репозиторий:

BrookXVII/VRF_Clear

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

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

git clone https://github.com/BrookXVII/VRF_Clear.git VerifyBotSet-Location .VerifyBot

Основные каталоги:

Каталог

Содержимое

pbStart

Запуск, остановка и Runspaces

pbHandlers

Обработчики событий Telegram

pbModules

Функции Telegram API и интеграций

pbForms

Пользовательский интерфейс

pbSettings

Конфигурация

pbAssets

Ресурсы и состояние

pbLogs

Журналы

Главное на этом этапе — правильно задать appPath, абсолютный путь к корню проекта.

Именно относительно него скрипты находят остальные файлы.

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

Настраиваем BotSettings.json

Основной файл — pbSettings/BotSettings.json.

Он содержит параметры Telegram, кадровой системы, интерфейса и обработчиков.

В репозитории также находится PowerShell-файл pbSettings/BotSettings.ps1, который формирует конфигурацию в формате JSON. Его значения токенов и идентификаторов заменены демонстрационными.

Для собственного окружения потребуется указать рабочие значения и изменить путь сохранения JSON в конце скрипта.

При этом рабочий JSON-файл не должен попадать в Git. Соответствующее правило уже предусмотрено в .gitignore.

Ключевые параметры:

Параметр

Назначение

appPath

Путь к корневому каталогу проекта

botID

Токен Telegram-бота

botNum

Числовой идентификатор бота

botCreator

ID доверенного владельца групп

apiUrl

URL кадрового API

checkToken

Токен для кадрового запроса

alertGroup

Группа уведомлений

banIfNotVerified

Автоматическое исключение неверифицированных участников

botRun

Главный флаг работы приложения

checkStaffRun

Сигнал запуска кадровой сверки

Параметр botRun используется для запуска и остановки бота. botRun=$true — бот запустится, botRun=$false — бот остановится.

Обратите внимание на значение botID.

В моей реализации функции формируют URL вида:

https://api.telegram.org/$botID/getUpdates

Поэтому в настройках используется токен с префиксом bot, например bot123456789:....

А botNum — только числовой идентификатор.

Для первого запуска рекомендую установить banIfNotVerified = false, протестировать регистрацию групп и выдачу приглашений и только затем включать автоматическое исключение.

Секреты и кадровый API

В текущей версии пароль для доступа к кадровому API получается так:

$credAPI = Get-Secret ` -Vault domainPdb ` -Name apiquery ` -AsPlainText

Здесь domainPdb — название хранилища, а apiquery — имя секрета.

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

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

Сам кадровый запрос также требует адаптации: опубликованный в ЗУП rabotayushchie_sotrudniki является примером реализации для наглядности.

Его необходимо создать в используемой кадровой системе либо реализовать аналогичный HTTP API.

Первый запуск

До запуска стоит проверить синтаксис PowerShell-скриптов.

Для этого можно использовать встроенный парсер:

$errors = @()Get-ChildItem -Recurse -Filter *.ps1 | ForEach-Object { $tokens = $null $fileErrors = $null [System.Management.Automation.Language.Parser]::ParseFile( $_.FullName, [ref]$tokens, [ref]$fileErrors ) | Out-Null if ($fileErrors) { $errors += $fileErrors } }if ($errors.Count) { $errors | Format-List throw "Обнаружены синтаксические ошибки"}"Синтаксис проверен"

После подготовки конфигурации запускаем приложение:

$settingsPath = Join-Path (Get-Location) ` 'pbSettingsBotSettings.json'.pbStartCreate-Runspace.ps1 ` -settingsPath $settingsPath

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

VerifyBot использует получение обновлений через getUpdates.

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

Затем проверяем события в служебной группе и только после этого — автоматические блокировки и ночную сверку.

Регламентная проверка

Для запуска кадровой сверки используется скрипт:

pbStart/pbStartCheckStaff.ps1

Он выставляет checkStaffRun = true в конфигурации, после чего основное приложение передаёт команду обработчику CheckStaff.

Скрипт удобно запускать ночью через Windows Task Scheduler.

Значение checkStaffSleepTimeSec определяет интервал опроса управляющего сигнала. В текущей конфигурации используется 3600 секунд.

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

Если необходима более оперативная реакция, интервал можно сократить.

При этом необходимо учитывать, что скрипт установки сигнала и основной процесс изменяют один файл настроек. В текущей реализации используется временный файл и System.IO.File.Replace, что снижает риск повреждения JSON при записи, но не является полноценным механизмом координации нескольких одновременно работающих экземпляров приложения.

Для штатной остановки служит pbStart/pbStop.ps1, который изменяет флаг botRun.

В этих скриптах также необходимо заменить абсолютные пути к файлу настроек.

Пример файла настроек

{ "appPath": "D:\Scripts\VerifyBot", "apiUrl": "https://example.invalid/hs/query/query_data/", "botID": "bot123456789:REPLACE_WITH_BOT_TOKEN", "botNum": "123456789", "botCreator": "111111111", "checkToken": "REPLACE_WITH_INTERNAL_API_TOKEN", "gateID": "", "banIfNotVerified": false, "botRun": true, "checkStaffRun": false, "alertGroup": "-1000000000000", "logSendGroup": "", "alertThreadBlocked": 1, "alertThreadUninvited": 2, "alertThreadPhoneNotFound": 3, "alertThreadSystemError": 4, "alertThreadAdded": 5, "alertThreadBalance": 6, "alertThreadUsersOut": 7, "eulaUrl": "https://example.invalid/privacy", "mainLogo": "https://habrastorage.org/webt/ki/2h/fg/ki2hfg4jcygigtulu3mdxbfwgia.jpeg", "groupItemLogo": "https://habrastorage.org/webt/vd/4t/nv/vd4tnvymbckxha5vvsjuf08dtca.png", "groupItemVerified": "https://habrastorage.org/webt/zf/nk/z4/zfnkz4bon0husnteh8a6cmc0t7u.png", "groupItemBlocked": "https://habrastorage.org/webt/vk/d9/w9/vkd9w91xcimfhytxjq2da6agtu8.png", "groupItemWaiting": "https://habrastorage.org/webt/ky/cv/ii/kycvii7ycg2i61uuxkwnyurmc5e.png", "inlineRunspacesMax": 10, "inlineRunspacesSleepTime": 10, "messageRunspacesMax": 10, "messageRunspacesSleepTime": 100, "callbackRunspacesMax": 10, "callbackRunspacesSleepTime": 100, "sendingRunspacesMax": 20, "sendingRunspacesSleepTime": 100, "sendingMaxRetries": 3, "preserveOrderPerChat": true, "getUpdateTimeoutSec": 10, "getUpdateSleepTime": 100, "checkStaffSleepTimeSec": 3600, "verifiTtl": 1800, "blockPeriod": 600, "logging": { "logFile": "pbLogs\queueLogHash.json", "errorFile": "pbLogs\queueErrorHash.json", "debugFile": "pbLogs\queueDebugHash.json", "maxSizeMB": 10, "keepFiles": 10 }}

Задержки обработчиков задаются в миллисекундах, кроме параметров с суффиксом Sec, а также verifiTtl и blockPeriod, для которых используются секунды.

Часть параметров сохранилась от экспериментальных механизмов:

  • sendingRunspacesMax — относится к альтернативным вариантам отправщика;

  • preserveOrderPerChat — в действующем SendingHandler отдельно не используется;

  • sendingMaxRetries — число повторов в текущем отправщике задано непосредственно в коде;

  • logSendGroup — архивный параметр;

  • gateID, verifiTtl, blockPeriod — относятся к дополнительной верификации через Telegram Gateway.

Параметры inlineRunspacesMax, messageRunspacesMax и callbackRunspacesMax управляют дочерними пулами обработчиков.

Что не попадает в Git

Перед подготовкой репозитория к публикации я дополнительно проверил исходный код на наличие конфиденциальных сведений.

Для дальнейшей работы с проектом в .gitignore добавлены правила исключения локальных данных:

/pbSettings/BotSettings.json/pbSettings/BotSettingsTest.ps1/pbAssets/queueUsersHash*.json/pbAssets/queueGroupsHash*.json/pbAssets/*.csv/pbLogs/*!/pbLogs/.gitignore*.pending

Демонстрационный BotSettings.ps1 оставлен в репозитории с заменёнными значениями. При использовании проекта рабочие секреты не следует записывать в этот отслеживаемый Git файл.

Вместо заключения. Что получилось и куда двигаться дальше?

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

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

Причём основную часть работы выполняет PowerShell.

Когда я публиковал PowerBot, мне хотелось показать, что Telegram-бот на PowerShell необязательно должен быть скриптом из трёх команд и бесконечного while ($true).

VerifyBot стал практическим продолжением этой идеи.

А стоило ли писать всё это на PowerShell?

Предвижу закономерный вопрос: почему не Python, Node.js или готовый фреймворк для Telegram-ботов?

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

Итог

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

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

Исходный код проекта — GitHub: BrookXVII/VRF_Clear.

Замечания, вопросы и предложения по архитектуре приветствуются. Особенно если кто-то уже решал похожую задачу другим способом.

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев