В этой статье будет рассмотрен способ работы с Telegram через Powershell. Основная задача, которая будет решаться в статье, получение списка заблокированных пользователей и их разблокировка. Обычно, когда такие ситуации случаются, я редко нахожусь за компьютером и мне приходится делать подключение через телефон и RDP - это не удобно. С помощью описанного методы разблокировать пользователя можно будет в 2 клика.
Логика, описанная в статье, применима и для других похожих сервисов. Например я планировал написать тесты для Exchange если пользователь сообщает, что у него проблемы с ящиком.
Для чего нужен API
У многих приложение (веб и локальных) есть понятие API, которое описывает процесс взаимодействия с программой и упрощает его. Например вам нужно узнать количество новых сообщений на своей почте и для этого вы открываете веб страницу. Веб страница весит 7Мб, а такую процедуру вы проделываете 100 раз в минуту. Этот способ плох и для вас и для сервера. Будет лучше, если вам будет отправлено только количество новых сообщений. Одна из таких проблем и решается с помощью API. У нас будет возвращена информация типа:
{response: {messages: 100, new: 10}}
Формат выше называется JSON и это самый популярный способ общения с веб-API. Он похож на хэш-таблицы Powershell и для работы с ним есть отдельный командлет.
Умение разбираться и взаимодействовать с разными API - это весомый навык разработчиков и ,особенно, для системных администраторов.
Регистрация бота Telegram
Для ботов в Telegram есть свой публичный API и что бы им воспользоваться - вы должны пройти регистрацию. Это делается в несколько шагов.
Регистрация вашего персонального бота выполняется через специальный канал BotFather. Найти его вы можете через поиск (у него будет галочка):
В этом боте мы должны выполнить несколько команд. Выведем окно взаимодействия с ботом:
/start
Команда '/start' поможет в случаях, если вы забудете названия ботов или захотите изменить какие-то данные.
Что бы создать бота пишем следующую команду:
/newbot
В Telegram нет ограничений на создания ботов и, если, вы совершите ошибку вы всегда сможете удалить бота и создать нового.
После этого нам будет предложено ввести название для бота. Это название, которое легче всего будет понять вам. Я назову его:
- 'fixmypc_ad_test'
После названия для бота нужно будет придумать название для канала, который он сможет обслуживать. Главным правилом названия - оно должно заканчиваться на 'bot'. Учтите, что вашего бота можно будет найти через поиск и лучше не использовать идентифицирующие вас имена. Я буду использовать:
- 'fixmypc_ad_test_bot'
В выделенном фрагменте, под номером 4, ссылка на ваш канал. Под номером 5 - ваш персональный токен. Токен - это как пароль, если кто-то получит к нему доступ сможет управлять ботом. Этот токен нужно будет отправлять при каждом запросе Telegram.
Нажмите на ссылку во фрагменте 4 и нажмите кнопку /start что бы у вас было хотя бы 1 сообщение.
Взаимодействие с API Telegram
Большая часть взаимодействия с API ботов (есть и другие) сводится к обращению по 1 url. Этот url выглядит следующим образом и состоит из частей:
https://api.telegram.org/bot<token>/<METHOD_NAME>
Где:
- <token> - это поле 'пароля' от вашего бота. Мы его получили раннее;
- <METHOD_NAME> - это команда, которую мы отправляем к Telegram и получаем какой-то ответ.
Названия методов и способ обращения к ним заранее определен и описан в документации. Документация по работе с ботами описана по ссылке.
В рамках всей статьи у меня определены 2 переменных, которые составят часть Url для обращения к Telegram. В примерах вы можете не увидеть эти переменны, но они могут использоваться:
$token = "12345:sadasdasdasd"
$url = "https://api.telegram.org/bot$token/"
Invoke-RestMethod
Как уже писалось выше - Telegram использует формат JSON. Такой формат не поддерживается в базовой реализации Powershell, но он содержит команду Invoke-RestMethod для преобразования JSON в hash-table.
В следующем примере мы соединим наш Url и токеном с методом getMe, который вернет базовую информацию о боте:
$result = Invoke-RestMethod -Uri $($url+'getMe')
$result
Скриншот выше демонстрирует, что мы получили вложенные хэш таблицы типа @{key1=@{key2=@{key='val';}}}. Мы можем 'распаковать' эти массивы:
$result.result
$result.result.username
Получение сообщений
После регистрации бота вы должны были нажать на кнопку /start, т.е. у вас должно быть хотя бы одно сообщение. Что бы получить его воспользуемся методом getUpdates:
$result = Invoke-RestMethod -Uri $($url+'getUpdates')
$result
Выведем информацию, которая находится внутри массивов:
$result.result
# выводим информацию о каждом сообщении
$result.result.message
# выводим текст из каждого сообщения
$result.result.message.text
Обратите внимание, что у каждого пользователя бота свой собственный чат/канал с ним. Вы, как пользователь, не можете видеть сообщения другого пользователя, а он ваши. Бот же видит оба ваших чата и идентифицирует вас по идентификатору 'chat.id'. В дальнейшем мы будем использовать этот идентификатор что бы понять от кого мы получили сообщение и кому будем отправлять ответ:
$result.result.message.chat.id
У меня 2 'chat_id' так как я отправлял 2 сообщения. Для ответа нужно только 1 - и так его можно получить:
$result.result.message.chat.id[-1]
Отправка сообщений
Методы для получения данных от API могут нуждаться в дополнительных, обязательных, параметрах. Эти параметры описаны в документации. Для отправки сообщений используется метод 'sendMessage' и в документации для него описаны следующие обязательные параметры (в колонке Required):
В предыдущем примере мы уже получили chat.id и должны его передать для отправки сообщения. Текст сообщения вы можете изменить на свое усмотрение. Все параметры и их значения помещаются в хэш таблицу, а затем в -Body команды Invoke-RestMethod :
$form = @{
chat_id = $result.result.message.chat.id[-1];
text = 'Пишу бота для AD';
}
$result = Invoke-RestMethod -Uri $($url+'sendMessage') -Body $form
$result
Получаем заблокированных пользователей AD
Свойство, которое отображает заблокированных пользователей AD, называется LockedOut. С помощью следующей команды мы получим всех таких пользователей:
Get-ADUser -Filter * -Properties LockedOut | where LockedOut -eq True
Что бы такой пользователь смог зайти в систему он должен быть разблокирован и ему должен быть установлен новый пароль. В этой статье будет рассмотрен одинаковый пароль во всех случаях - 'Zx12345':
$SID = (Get-ADUser -Filter * -Properties LockedOut | where LockedOut -eq True).SID
$new_password = 'Zx12345'
Unlock-AdAccount -Identity $SID
Set-ADAccountPassword -Identity $SID -Reset -NewPassword (ConvertTo-SecureString -AsPlainText $new_password -Force)
Одинаковый пароль нужен т.к. его легче всего будет запомнить пользователям. Если для вас это не подходит то, опираясь на предыдущую статью по отправке эл.писем, вы можете сделать функционал по генерированию новых паролей и отправкой их начальникам.
Как отправлять письма используя Powershell Send-MailMessage
Связываем Telegram и AD
Все сообщения, которые будут посланы пользователями боту, можно разделить на обычный текст и команды. Команды - это то что мы должны обработать. Удобнее всего сделать команды через Telegram кнопки или ссылки. Их удобство в том, что мы можем не печатать текст, а просто нажать на эту команду. В этой статье будут рассмотрены только ссылки. Они начинаются со знака '/' и не могут содержать символы пробелов:
Таких типов команд должно быть 2 - на получение списка пользователей и разблокировку. Как видно на примере выше - мы не можем использовать в качестве команды имена пользователей т.к. они содержат пробелы. Мы можем использовать UPN, но по нему не всегда понятно что это за пользователь (например aa@domain.local). Кроме этого команды, которые устанавливают пароль и снимают блок (Unlock-AdAccount и Set-ADAccountPassword), работают только с SID/GUID(самые удобные способы). Эти данные тоже не очень хорошо выводить т.к. они являются идентификаторами безопасности. Я предлагаю решить эту проблему создав следующий вид команд и сопутствующего текста:
# Ожидаем получить список заблокированных пользователей
/get_user
# После выполнения предыдущей команды
# будет выводиться текст с информацией и командами
Unlock Anna Petrovna /0
Unlock Елена Патрушева /1
Unlock Игорь Пелевен /2
Каждый идентификатор типа /0, /1, /2 будет соответствовать индексу значения в массиве (массив содержащий пользователей). Создадим функции, которые позволят сделать это.
Формирование списка пользователей
Нам нужно создать функцию, которая получила бы заблокированных пользователей и создала бы из них массив вида:
$users = @(
@{name='Anna Petrovna'; SID='1234'},
@{name='Елена Патрушева'; SID='4567'},
)
В примере выше, Anna Petrovna, будет соответствовать индексу 0, а Патрушева - 1. Нужно так же учитывать, что заблокированных пользователей может не быть. Создадим функцию получения пользователей:
function Get-LockedUsers(){
[CmdletBinding()]
# Пустой массив. Сюда будут помещены пользователи
$locked_users = New-Object System.Collections.Generic.List[System.Object]
# Получаем заблокированных пользователей
$users = Get-ADUser -Filter * -Properties LockedOut | where LockedOut -eq True
# Если заблокированных пользователей нет, то
# прекращаем работу функции и возвращаем 0
if ($users.Length -eq 0){
return 0
}
# Добавляем в новый список пользователей только имя и SID
foreach ($user in $users){
$name = $user.Name
$sid = $user.SID
$locked_users.Add(@(@{name=$name; SID=$sid}))
}
# Возвращаем готовый список
return $locked_users
}
С помощью следующего скрипта мы получим заблокированных пользователей использую созданный командлет и создадим текст для отправки в Telegram (который мы спланировали раннее):
$users = Get-LockedUsers
# текст для отправки
$full_text = ''
foreach ($user in $users){
$name = $user.Name
# Получаем индекс элемента в массиве
$index = $users.indexOf($user)
# формируем сообщение для конкретного пользователя с новой строкой
# и добавляем все к полному тексту
$full_text += "Unlock user $name /$index`n"
}
Отправка списка пользователей
Создадим функцию с помощью которой мы будем отправлять сообщения. Как уже и описывалось раньше для отправки сообщений нужно указать текст и chat_id. Эти данные мы тоже будем передавать через функцию. В примере ниже не видно переменных $token и $url, подразумевается что вы уже объявляли их раннее:
$result = Invoke-RestMethod -Uri $($url+'getUpdates')
$ChatID = $result.result.message.chat.id[-1]
function Send-TelegramMessage($Message, $ChatId){
[CmdletBinding()]
# Создаем объект для отправки в Telegram
$form = @{
chat_id = $ChatId;
text = $Message;
}
$result = Invoke-RestMethod -Uri $($url+'sendMessage') -Body $form
return $result
}
# Блок формирующий текст для отправки в Telegram
$users = Get-LockedUsers
$full_text = ''
foreach ($user in $users){
$name = $user.Name
$index = $users.indexOf($user)
$full_text += "Unlock user $name /$index`n"
}
# Передаем в функцию идентификатор чата и текст
Send-TelegramMessage -Message $full_text -ChatId $ChatID
В выделенном фрагменте видно, что у нас получилось реализовать вывод сообщений в планируемом формате.
Реализация команд
Теперь нам нужно создать команды, которые описывались ранее:
- Команду на получение списка заблокированных пользователей - /get_user;
- Команду на разблокировку пользователей - они у нас уже выводятся в виде /1, /2 и т.д.
Что бы суметь получить команды мы должны создать вечный цикл. Этот вечный цикл, раз в 2 секунды, будет выполнять запрос к API telegram и получать последнее сообщение. Выполнять запросы чаще чем раз в 2 секунды не рекомендуется, т.к. у Telegram может быть на это ограничение. Получить последнее сообщение из чата можно так:
$result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
$result.result.message.message_id
У пользователей есть идентификаторы чатов (chat_id), а у чатов есть идентификаторы сообщений (message_id). В примере выше мы вернули последнее сообщение из всех чатов (с помощью offset='-1'). Последнее сообщение не значит "новое" и нам нужно научится их различать. Один из способов сделать это, при запуске программы, запомнить последнее сообщение написанное в чате. Каждые 2 секунды мы будем повторять запрос и сравнивать идентификаторы сообщений (message_id). Если они отличаются, то значит пришло новое сообщение и мы заменяем старый message_id на новый и выполняем еще какую-то логику:
$token = "12345:sadasdasdasd"
$url = "https://api.telegram.org/bot$token/"
# Получаем последнее сообщение
$result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
# Присваиваем идентификатор последнего сообщения переменной
$MessageId = $result.result.message.message_id
# Идентификатор чата для отправки конкретному пользователю
$ChatId = $result.result.message.chat.id
function Get-TelegramMessage(){
[CmdletBinding()]
# Делаем запрос на новые сообщения
$result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
# Сравниваем идентификаторы сообщений
# идентификатор нового должен быть больше старого
if ($result.result.message.message_id -gt $MessageId){
# возвращаем сообщение, если оно новое
return $result.result.message
}
}
# вечный цикл с таймаутов в 2 секунды
while ($True){
# каждые 2 секунды ищем новое сообщение
$response = Get-TelegramMessage
if ($response){
# если мы получили новое сообщение
# то его идентификатор становится последним
$MessageId = $response.message_id
# выводим текст последнего сообщения
$response.text
# Еще какая-то логика (получение пользователей или разблокировка)
}
sleep 2
}
Теперь нам нужно понять начинается ли сообщение на '/' (т.е. является командой). Если это команда - мы определяем какая именно (разблокировать или получить). Ниже уже готовый скрипт, в котором объедены предыдущие части. Добавлены моменты, которые могли бы вызвать недопонимание:
- Если нет заблокированных пользователей (массив пустой) - будет отправлено сообщение "Нет заблокированных пользователей";
- Если пользователь успешно разблокирован (не было критичных ошибок) - "Пользователь $($user.Name) разблокирован".
$token = "12345"
$url = "https://api.telegram.org/bot$token/"
$result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
$MessageId = $result.result.message.message_id
$ChatId = $result.result.message.chat.id
$new_password = 'Zx12345'
# список последних полученных пользователей
# он будет заполнен при первом запросе /get_user
$users = @()
function Get-TelegramMessage(){
[CmdletBinding()]
$result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
if ($result.result.message.message_id -gt $MessageId){
return $result.result.message
}
}
function Get-LockedUsers(){
[CmdletBinding()]
$locked_users = New-Object System.Collections.Generic.List[System.Object]
$users = Get-ADUser -Filter * -Properties LockedOut | where LockedOut -eq True
if ($users.Length -eq 0){
return 0
}
foreach ($user in $users){
$name = $user.Name
$sid = $user.SID
$locked_users.Add(@(@{name=$name; SID=$sid}))
}
return $locked_users
}
function Send-TelegramMessage($Message, $ChatId){
[CmdletBinding()]
$form = @{
chat_id = $ChatId;
text = $Message;
}
$result = Invoke-RestMethod -Uri $($url+'sendMessage') -Body $form
return $result
}
# вечный цикл с таймаутов в 2 секунды
while ($True){
$response = Get-TelegramMessage
if ($response){
$MessageId = $response.message_id
# Начинаем определять какой тип сообщений
# Это может быть обычный текст или команда
# которая начинается на /
if ($response.text -match '/'){
# убираем знак / из команды и пытаемся понять
# нужно ли вернуть список пользователей
# или нужно разблокировать кого-то
$text = $response.text -replace '/',''
if ("get_user" -eq $text){
$users = Get-LockedUsers
# если у нас нет заблокированных пользователей
# то $users будет равен 0
if ($users -ne 0){
# Формируем текст для отправки в Telegram
$full_text = ''
foreach ($user in $users){
$name = $user.Name
$index = $users.indexOf($user)
$full_text += "Unlock user $name /$index`n"
}
Send-TelegramMessage -Message $full_text -ChatId $ChatID
}
else {
Send-TelegramMessage -Message 'Нет заблокированных пользователей' -ChatId $ChatID
}
}
# если это не команда получение пользователей
# но строка может быть преобразована в число
elseif ($text -match "^\d+$"){
# получаем нужного пользователя по индексу
$user = $users[$text]
# если значение user существует - значит индекс верный
# если этого индекса нет - какая-то ошибка или нас
# пытаются обмануть
if ($user){
# Разблокируем пользователя
$SID = $user.SID
Unlock-AdAccount -Identity $SID
Set-ADAccountPassword -Identity $SID -Reset -NewPassword (ConvertTo-SecureString -AsPlainText $new_password -Force)
# отправляем сообщение о разблокировке
Send-TelegramMessage -Message "Пользователь $($user.Name) разблокирован" -ChatId $ChatID
}
}
}
}
sleep 2
}
Безопасность и возможные проблемы
Работая с ботом вы должны понимать, что он публичный и его может найти и использовать каждый. Есть несколько ограничения доступа. Самый популярный способ - задать явно chat_id и исполнять команды только от него. Так как это персональный идентификатор, который присваивается Telegram каждому пользователю, его будет тяжело подделать. Вообще, вместе с сообщением, возвращается несколько идентификаторов, каждый из которых вы можете использовать:
$result = Invoke-RestMethod -Uri $($url+'getUpdates')
$result.result.message
Чтобы работать с определенным ChatID - нужно будет исправить скрипт в двух местах: в переменной $MessageID и в командлете Get-TelegramMessage:
# Задаем определенный ChatID
$ChatId = '1234'
function Get-TelegramMessage(){
[CmdletBinding()]
$result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
# дополнительно сравниваем $ChatID
if (($result.result.message.message_id -gt $MessageId) -and ($result.result.message.chat.id -eq $ChatId)){
return $result.result.message
}
}
Учтите, что скрипт читает сообщение раз в 2 секунды и если вы отправите в этот промежуток 2 сообщения - будет прочитано только последнее.
Второй способ - это создание приватного канала, в который будет добавлен этот бот. Это другой API и он не рассматривается в этой статье.
Скрипт изначально не делался для работы более чем с 1 пользователем. Это связано с переменной chatid, которая идентифицирует только одного пользователя. Если вам нужно реализовать приватный доступ более чем 1 пользователю вы должны реализовать следующее:
- Объявить массив с ChatID пользователей, которые могут работать через бот;
- При проверке последнего сообщения, в Get-TelegramMessage, добавить проверку на ChatID;
- Если проверка успешная, командлет должен возвращать не только текст сообщения, но и ChatID. Этот ChatID и станет основным для отправки ответа.
У меня нет возможности проверить работу для 2 пользователей, но исправления в этих местах должны сработать:
# Задаем chatid для несколько пользователей
# которые могут выполнять команды
$AllowedChatId = @('12345', '45678')
# Эта строка уже не нужна
# $ChatId = '1234'
function Get-TelegramMessage(){
[CmdletBinding()]
$result = Invoke-RestMethod -Uri ($url+'getUpdates') -Body @{offset='-1';}
# Проверяем что текущий $ChatID в списке разрешенных
if (($result.result.message.message_id -gt $MessageId) -and ($result.result.message.chat.id -in $AllowedChatId)){
return $result.result.message, $result.result.message.chat.id
}
}
# остальной код
while ($True){
# Так было раньше
# $response = Get-TelegramMessage
$answer = Get-TelegramMessage
$response = $answer[0]
$ChatId = $answer[1]
# остальной код
При этом проблема с 2-я сообщениями за 2 секунды останется.
Этот скрипт может дорабатываться и его последнюю версию вы можете посмотреть на моем github.
...
Подписывайтесь на наш Telegram канал
Теги: #powershell #ad