По мотивам статьи Телеграмм-бот для системного администратора (статья не моя, я только прочитал) захотел поделиться опытом создания Telegram-бота на PowerShell для управления серверами приложений. Будет текст, код и немножко картинок. Конструктивная критика приветствуется ( главное чтобы не звучало «зачем на PowerShell? Надо было на perl» ).
Думаю что статья больше подойдет «новичкам» в PowerShell, но и опытные администраторы могут что-то полезное здесь увидеть.
Саму статью старался построить по частям – от простого к сложному. Возможно, встретится плагиат, будьте бдительны!
Итак, у нас есть необходимость осуществлять управление сервисами или приложениями на нескольких серверах (останавливать, запускать), перезагружать сервера, смотреть логи и еще какую-то информацию при необходимости. Всё это хочется делать (на самом деле нет), находясь в метро, в магазине или даже лёжа на диване, без VPN и ноутбуков. Из требований (которые были написаны, конечно, на коленке).
- Простота добавления/изменения задач в Telegram-бот
- Многозадачность или параллелизация
- «Понятный» интерфейс управления
- Хоть какая-то безопасность
В какой то момент было решено выносить конфиг в отдельный файл – в нашем случае xml (тут кто-то может сказать, что давайте всё в json, но мы сделали в xml и были довольны)
Начнем с начала:
Часть 1: простой телеграм-бот
Ищем папку-бота (не каталог) – BotFather (@BotFather) в Telegram

Пишем /newbot
Далее, нужно придумать имя боту (в моем случае я назвал Haaaabr специально для статьи) и username, который должен заканчиваться на «bot» (Haaaabr_bot)
После этого BotFather выдаст токен, который мы и будем использовать:

Дальше можно загрузить для бота картинку, поставить Description, создать список команд, но мне было лень.
Делаем простого бота, который будет принимать сообщения и отвечать на них.
Я буду писать код PS частями и периодически вставлять full-код для референса.
Для справки нам понадобятся описания вызовов API Telegram Bot API
Нам будет нужно 2 метода:
getUpdates – получение ботом(скриптом) сообщений
sendMessage – отправка сообщений ботом(скриптом) пользователю
Там же, видим, что:
Making requests
All queries to the Telegram Bot API must be served over HTTPS and need to be presented in this form: api.telegram.org/bot<token>/METHOD_NAME
Шаг 1 – прием сообщений
Переменные
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage"
Теперь будем проверять, что отдает вызов $URL_get
Invoke-RestMethod -Uri $URL_get
ok result
-- ------
True {}
Нот бэд. Напишем что-нибудь боту:

И прочитаем:
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" Invoke-RestMethod -Uri $URL_get
ok result }
-- ------
True {@{update_id=635172027; message=}, @{update_id=635172028; message=}
Очевидно, что нам нужен result. Сразу скажу, что нас интересует только последнее сообщение от пользователя, поэтому так:
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" $json = Invoke-RestMethod -Uri $URL_get $data = $json.result | Select-Object -Last 1 $data.update_id $data.message.chat.id $data.message.text $data.message.chat.first_name $data.message.chat.last_name $data.message.chat.type $data.message.chat.username
Теперь нужно сделать confirm, что мы получили сообщение. Делается это все также, через метод getUpdates с параметром offset:
By default, updates starting with the earliest unconfirmed update are returned. An update is considered confirmed as soon as getUpdates is called with an offset higher than its update_id
Делаем
Invoke-RestMethod "$($URL_get)?offset=$($($data.update_id)+1)" -Method Get | Out-Null
И кидаем это все в цикл c таймаутом в 1 секунду:
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" # timeout sec $timeout = 1 while($true) # вечный цикл { $json = Invoke-RestMethod -Uri $URL_get $data = $json.result | Select-Object -Last 1 $data.update_id $data.message.chat.id $data.message.text $data.message.chat.first_name $data.message.chat.last_name $data.message.chat.type $data.message.chat.username Invoke-RestMethod "$($URL_get)?offset=$($($data.update_id)+1)" -Method Get | Out-Null Start-Sleep -s $timeout }
Теперь сделаем из этого функцию чтения сообщений. Т.к. нам нужно возвращать несколько значений из функции – решили использовать HashTable (именованный/ассоциативный массив)
# Token $token = "***********************"# Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" # timeout sec $timeout = 1 function getUpdates($URL) { $json = Invoke-RestMethod -Uri $URL $data = $json.result | Select-Object -Last 1 #$data.update_id $chat_id = $data.message.chat.id $text = $data.message.text $f_name = $data.message.chat.first_name $l_name = $data.message.chat.last_name $type = $data.message.chat.type $username = $data.message.chat.username # проверяем что text есть if($text) { # confirm Invoke-RestMethod "$($URL)?offset=$($($data.update_id)+1)" -Method Get | Out-Null # HashTable $ht = @{} $ht["chat_id"] = $chat_id $ht["text"] = $text $ht["f_name"] = $f_name $ht["l_name"] = $l_name $ht["username"] = $username return $ht } } while($true) # вечный цикл { # вызываем функцию getUpdates $URL_get Start-Sleep -s $timeout }
Шаг 2 – отправка данных
Для отправки сообщения нам нужен метод sendMessage и поля chat_id и text (остальные опционально https://core.telegram.org/bots/api#sendmessage).
Сразу запилим функцию
function sendMessage($URL, $chat_id, $text) { # создаем HashTable, можно объявлять ее и таким способом $ht = @{ text = $text # указан способ разметки Markdown parse_mode = "Markdown" chat_id = $chat_id } # Данные нужно отправлять в формате json $json = $ht | ConvertTo-Json # Делаем через Invoke-RestMethod, но никто не запрещает сделать и через Invoke-WebRequest # Method Post - т.к. отправляем данные, по умолчанию Get Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json }
Теперь, вызвав
sendMessage $URL_set <ваш_телеграм_id> "Тест123"
получим сообщение в телеге.
Шаг 3 – собираем все вместе
Ниже весь код для отправки-получения сообщений
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" # timeout sec $timeout = 1 function getUpdates($URL) { $json = Invoke-RestMethod -Uri $URL $data = $json.result | Select-Object -Last 1 #$data.update_id $chat_id = $data.message.chat.id $text = $data.message.text $f_name = $data.message.chat.first_name $l_name = $data.message.chat.last_name $type = $data.message.chat.type $username = $data.message.chat.username # проверяем что text есть if($text) { # confirm Invoke-RestMethod "$($URL)?offset=$($($data.update_id)+1)" -Method Get | Out-Null # HashTable $ht = @{} $ht["chat_id"] = $chat_id $ht["text"] = $text $ht["f_name"] = $f_name $ht["l_name"] = $l_name $ht["username"] = $username return $ht } } function sendMessage($URL, $chat_id, $text) { # создаем HashTable, можно объявлять ее и таким способом $ht = @{ text = $text # указан способ разметки Markdown parse_mode = "Markdown" chat_id = $chat_id } # Данные нужно отправлять в формате json $json = $ht | ConvertTo-Json # Делаем через Invoke-RestMethod, но никто не запрещает сделать и через Invoke-WebRequest # Method Post - т.к. отправляем данные, по умолчанию Get Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json | Out-Null } while($true) # вечный цикл { $return = getUpdates $URL_get if($return) { # http://apps.timwhitlock.info/emoji/tables/unicode#block-1-emoticons sendMessage $URL_set $return.chat_id (Get-Random("", "", "", "")) } Start-Sleep -s $timeout }
Дальнейшую логику можно строить на основе $return.text и, например, оператора switch:
switch -Wildcard ($return["text"]) { "*привет*" { sendMessage $URL_set $return.chat_id "Привет, $($return["f_name"])" } "*как дела?*" { sendMessage $URL_set $return.chat_id "Хорошо" } default {sendMessage $URL_set $return.chat_id "$(Get-Random("", "", "", ""))"} }
Часть 2: нужны кнопки
В телеграм боте есть опция задания списка команд (открывается вот по этому значку
)
Первоначально мы так и сделали – был набор команд, в качестве параметров передавали туда имена серверов или сервисов. Потом решили, что нужно двигаться дальше в сторону User Friendly интерфейсов и подключили функционал кнопок.
Используется вызвов sendMessage c параметром reply_markup
Для нашего функционала мы использовали тип InlineKeyboardMarkup
https://core.telegram.org/bots/api#inlinekeyboardmarkup .
Из описания следует, что поле inline_keyboard– это массив из массива кнопок
(Array of Array of InlineKeyboardButton )
Пробуем сделать тестовую отправку кнопок
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" # Используем поле callback_data чтобы знать, какую кнопку нажал пользователь $button1 = @{ "text" = "Test1"; callback_data = "Test1_CD"} $button2 = @{ "text" = "Test2"; callback_data = "Test2_CD"} $keyboard = @{"inline_keyboard" = @(,@($button1, $button2))} $ht = @{ parse_mode = "Markdown" reply_markup = $keyboard chat_id = ******** # здесь нужно указать свой Telegram ID text = "Test Text" } $json = $ht | ConvertTo-Json Invoke-RestMethod $URL_set -Method Post -ContentType 'application/json; charset=utf-8' -Body $json
Получаем Error:
Invoke-RestMethod: {«ok»:false,«error_code»:400,«description»:«Bad Request: field \»inline_keyboard\» of the InlineKeyboardMarkup should be an Array of Arrays»}
At line:21 char:1
Проверяем что содержит переменная $json
Вывод:
{ "reply_markup": { "inline_keyboard": [ "System.Collections.Hashtable System.Collections.Hashtable" ] }, "chat_id": **********, "text": "Test Text", "parse_mode": "Markdown" }
Видимо как-то не очень передавать объект HashTable («System.Collections.Hashtable System.Collections.Hashtable») для api телеграма. Немного гугла и итог – при конвертации в Json ставим глубину конвертации
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" # Используем поле callback_data чтобы знать, какую кнопку нажал пользователь $button1 = @{ "text" = "Test1"; callback_data = "Test1_CD"} $button2 = @{ "text" = "Test2"; callback_data = "Test2_CD"} $keyboard = @{"inline_keyboard" = @(,@($button1, $button2))} $ht = @{ parse_mode = "Markdown" reply_markup = $keyboard chat_id = ******** text = "Test Text" } $json = $ht | ConvertTo-Json -Depth 5 Invoke-RestMethod $URL_set -Method Post -ContentType 'application/json; charset=utf-8' -Body $json
Получаем кнопки:

Делаем функцию по отправке кнопок, на вход будем подавать массив кнопок
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" # Используем поле callback_data чтобы знать, какую кнопку нажал пользователь $button1 = @{ "text" = "Test1"; callback_data = "Test1_CD"} $button2 = @{ "text" = "Test2"; callback_data = "Test2_CD"} $buttons = ($button1, $button2) function sendKeyboard($URL, $buttons) { $keyboard = @{"inline_keyboard" = @(,$buttons)} $ht = @{ parse_mode = "Markdown" reply_markup = $keyboard chat_id = ******** text = "Test Text" } $json = $ht | ConvertTo-Json -Depth 5 Invoke-RestMethod $URL_set -Method Post -ContentType 'application/json; charset=utf-8' -Body $json } sendKeyboard $URL_set $buttons
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" # timeout sec $timeout = 1 function getUpdates($URL) { $json = Invoke-RestMethod -Uri $URL $data = $json.result | Select-Object -Last 1 #$data.update_id $chat_id = $data.message.chat.id $text = $data.message.text $f_name = $data.message.chat.first_name $l_name = $data.message.chat.last_name $type = $data.message.chat.type $username = $data.message.chat.username # проверяем что text есть if($text) { # confirm Invoke-RestMethod "$($URL)?offset=$($($data.update_id)+1)" -Method Get | Out-Null # HashTable $ht = @{} $ht["chat_id"] = $chat_id $ht["text"] = $text $ht["f_name"] = $f_name $ht["l_name"] = $l_name $ht["username"] = $username return $ht } } function sendMessage($URL, $chat_id, $text) { # создаем HashTable, можно объявлять ее и таким способом $ht = @{ text = $text # указан способ разметки Markdown parse_mode = "Markdown" chat_id = $chat_id } # Данные нужно отправлять в формате json $json = $ht | ConvertTo-Json # Делаем через Invoke-RestMethod, но никто не запрещает сделать и через Invoke-WebRequest # Method Post - т.к. отправляем данные, по умолчанию Get Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json | Out-Null } function sendKeyboard($URL, $buttons, $chat_id, $text) { $keyboard = @{"inline_keyboard" = @(,$buttons)} $ht = @{ parse_mode = "Markdown" reply_markup = $keyboard chat_id = $chat_id text = $text } $json = $ht | ConvertTo-Json -Depth 5 Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json } while($true) # вечный цикл { $return = getUpdates $URL_get if($return) { # http://apps.timwhitlock.info/emoji/tables/unicode#block-1-emoticons #sendMessage $URL_set $return.chat_id (Get-Random("", "", "", "")) write-host "$($return["chat_id"])" switch -Wildcard ($return["text"]) { "*привет*" { $button1 = @{ "text" = "Project1"; callback_data = "Project1_CD"} $button2 = @{ "text" = "Project2"; callback_data = "Project2_CD"} $buttons = ($button1, $button2) $text = "Available projects:" $chat_id = $return.chat_id sendKeyboard $URL_set $buttons $chat_id $text #sendMessage $URL_set $return.chat_id "Привет, $($return["f_name"])" } "*как дела?*" { sendMessage $URL_set $return.chat_id "Хорошо" } default {sendMessage $URL_set $return.chat_id "$(Get-Random("", "", "", ""))"} } } Start-Sleep -s $timeout }
Теперь на «привет» бот будет отправлять нам пару кнопок. Осталось понять, какую кнопку нажал пользователь. В текущей ps-функции getUpdates есть проверка на
if($text)...
При нажатии на кнопку никакой текст не возвращается, соответственно, нужно модифицировать функцию. Нажимаем на кнопку

И запускаем кусок кода для проверки содержимого $data
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" # timeout sec $timeout = 1 function getUpdates($URL) { $json = Invoke-RestMethod -Uri $URL $data = $json.result | Select-Object -Last 1 $data <# $chat_id = $data.message.chat.id $text = $data.message.text $f_name = $data.message.chat.first_name $l_name = $data.message.chat.last_name $type = $data.message.chat.type $username = $data.message.chat.username # проверяем что text есть if($text) { # confirm Invoke-RestMethod "$($URL)?offset=$($($data.update_id)+1)" -Method Get | Out-Null # HashTable $ht = @{} $ht["chat_id"] = $chat_id $ht["text"] = $text $ht["f_name"] = $f_name $ht["l_name"] = $l_name $ht["username"] = $username return $ht } #> } getUpdates $URL_get
Никакой message больше не прилетает. Вместо него теперь callback_query. Правим функцию
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" # timeout sec $timeout = 1 function getUpdates($URL) { $json = Invoke-RestMethod -Uri $URL $data = $json.result | Select-Object -Last 1 # Нажатие на кнопку if($data.callback_query) { $callback_data = $data.callback_query.data $chat_id = $data.callback_query.from.id $f_name = $data.callback_query.from.first_name $l_name = $data.callback_query.from.last_name $username = $data.callback_query.from.username } # Обычное сообщение elseif($data.message) { $chat_id = $data.message.chat.id $text = $data.message.text $f_name = $data.message.chat.first_name $l_name = $data.message.chat.last_name $type = $data.message.chat.type $username = $data.message.chat.username } $ht = @{} $ht["chat_id"] = $chat_id $ht["text"] = $text $ht["f_name"] = $f_name $ht["l_name"] = $l_name $ht["username"] = $username $ht["callback_data"] = $callback_data # confirm Invoke-RestMethod "$($URL)?offset=$($($data.update_id)+1)" -Method Get | Out-Null return $ht } getUpdates $URL_get
Теперь функция возвращает text, если есть сообщение, или callback_data, если было нажатие на кнопку. На этапе тестов словили ошибку при вызове:
sendMessage $URL_set $($return.chat_id) $($return.callback_data)
Invoke-RestMethod: {«ok»:false,«error_code»:400,«description»:«Bad Request: can’t parse entities: Can’t find end of the entity starting at byte offset 8»}
Так как parse_mode выставлен в Markdown, а отправляемый текст
$return.callback_data = “Project1_CD”
нужно перед отправкой форматировать сообщение, подробнее тут:
https://core.telegram.org/bots/api#formatting-options
или убрать нижнее подчеркивание «_»
# Token $token = "***********************" # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" # timeout sec $timeout = 1 function getUpdates($URL) { $json = Invoke-RestMethod -Uri $URL $data = $json.result | Select-Object -Last 1 # Обнуляем переменные $text = $null $callback_data = $null # Нажатие на кнопку if($data.callback_query) { $callback_data = $data.callback_query.data $chat_id = $data.callback_query.from.id $f_name = $data.callback_query.from.first_name $l_name = $data.callback_query.from.last_name $username = $data.callback_query.from.username } # Обычное сообщение elseif($data.message) { $chat_id = $data.message.chat.id $text = $data.message.text $f_name = $data.message.chat.first_name $l_name = $data.message.chat.last_name $type = $data.message.chat.type $username = $data.message.chat.username } $ht = @{} $ht["chat_id"] = $chat_id $ht["text"] = $text $ht["f_name"] = $f_name $ht["l_name"] = $l_name $ht["username"] = $username $ht["callback_data"] = $callback_data # confirm Invoke-RestMethod "$($URL)?offset=$($($data.update_id)+1)" -Method Get | Out-Null return $ht } function sendMessage($URL, $chat_id, $text) { # создаем HashTable, можно объявлять ее и таким способом $ht = @{ text = $text # указан способ разметки Markdown parse_mode = "Markdown" chat_id = $chat_id } # Данные нужно отправлять в формате json $json = $ht | ConvertTo-Json # Делаем через Invoke-RestMethod, но никто не запрещает сделать и через Invoke-WebRequest # Method Post - т.к. отправляем данные, по умолчанию Get Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json | Out-Null } function sendKeyboard($URL, $buttons, $chat_id, $text) { $keyboard = @{"inline_keyboard" = @(,$buttons)} $ht = @{ parse_mode = "Markdown" reply_markup = $keyboard chat_id = $chat_id text = $text } $json = $ht | ConvertTo-Json -Depth 5 Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json } while($true) # вечный цикл { $return = getUpdates $URL_get #$return # Если обычное сообщение if($return.text) { # http://apps.timwhitlock.info/emoji/tables/unicode#block-1-emoticons #sendMessage $URL_set $return.chat_id (Get-Random("", "", "", "")) write-host "$($return["chat_id"])" switch -Wildcard ($return["text"]) { "*привет*" { $button1 = @{ "text" = "Project1"; callback_data = "Project1CD"} $button2 = @{ "text" = "Project2"; callback_data = "Project2CD"} $buttons = ($button1, $button2) $text = "Available projects:" $chat_id = $return.chat_id sendKeyboard $URL_set $buttons $chat_id $text #sendMessage $URL_set $return.chat_id "Привет, $($return["f_name"])" } "*как дела?*" { sendMessage $URL_set $return.chat_id "Хорошо" } default {sendMessage $URL_set $return.chat_id "$(Get-Random("", "", "", ""))"} } } # если было нажатие на кнопку elseif($return.callback_data) { sendMessage $URL_set $($return.chat_id) $($return.callback_data) } Start-Sleep -s $timeout }
Часть 3: делаем конфиг
Настало время всё вынести в конфиг. Тут все просто – делаем xml:
<config> <system> <token>***********************</token> <timeout desc="bot check timeout in seconds">1</timeout> </system> <tasks> <task name="Перезагрузить все" script="c:\Temp\Habr\reboot_all.ps1"></task> <task name="Статус серверов" script="c:\Temp\Habr\status.ps1"></task> <task name="ipconfig1" script="ipconfig"></task> <task name="ipconfig2" script="ipconfig"></task> <task name="ipconfig3" script="ipconfig"></task> <task name="ipconfig4" script="ipconfig"></task> <task name="ipconfig5" script="ipconfig"></task> </tasks> </config>
Описываем задачи (tasks) и для каждой задачи указываем скрипт или команду.
Проверяем:
[xml]$xmlConfig = Get-Content -Path ("c:\Temp\Habr\telegram_bot.xml") $token = $xmlConfig.config.system.token $timeout = $xmlConfig.config.system.timeout.'#text' foreach($task in $xmlConfig.config.tasks.task) { $task.name # имя кнопки $task.script # скрипт }
[xml]$xmlConfig = Get-Content -Path ("c:\Temp\Habr\telegram_bot.xml") $token = $xmlConfig.config.system.token $timeout = $xmlConfig.config.system.timeout.'#text' # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" function getUpdates($URL) { $json = Invoke-RestMethod -Uri $URL $data = $json.result | Select-Object -Last 1 # Обнуляем переменные $text = $null $callback_data = $null # Нажатие на кнопку if($data.callback_query) { $callback_data = $data.callback_query.data $chat_id = $data.callback_query.from.id $f_name = $data.callback_query.from.first_name $l_name = $data.callback_query.from.last_name $username = $data.callback_query.from.username } # Обычное сообщение elseif($data.message) { $chat_id = $data.message.chat.id $text = $data.message.text $f_name = $data.message.chat.first_name $l_name = $data.message.chat.last_name $type = $data.message.chat.type $username = $data.message.chat.username } $ht = @{} $ht["chat_id"] = $chat_id $ht["text"] = $text $ht["f_name"] = $f_name $ht["l_name"] = $l_name $ht["username"] = $username $ht["callback_data"] = $callback_data # confirm Invoke-RestMethod "$($URL)?offset=$($($data.update_id)+1)" -Method Get | Out-Null return $ht } function sendMessage($URL, $chat_id, $text) { # создаем HashTable, можно объявлять ее и таким способом $ht = @{ text = $text # указан способ разметки Markdown parse_mode = "Markdown" chat_id = $chat_id } # Данные нужно отправлять в формате json $json = $ht | ConvertTo-Json # Делаем через Invoke-RestMethod, но никто не запрещает сделать и через Invoke-WebRequest # Method Post - т.к. отправляем данные, по умолчанию Get Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json | Out-Null } function sendKeyboard($URL, $buttons, $chat_id, $text) { $keyboard = @{"inline_keyboard" = @(,$buttons)} $ht = @{ parse_mode = "Markdown" reply_markup = $keyboard chat_id = $chat_id text = $text } $json = $ht | ConvertTo-Json -Depth 5 Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json } while($true) # вечный цикл { $return = getUpdates $URL_get # Если обычное сообщение if($return.text) { # http://apps.timwhitlock.info/emoji/tables/unicode#block-1-emoticons #sendMessage $URL_set $return.chat_id (Get-Random("", "", "", "")) write-host "$($return["chat_id"])" switch -Wildcard ($return["text"]) { "*привет*" { # Пустой массив $buttons = @() foreach($task in $xmlConfig.config.tasks.task) { $button = @{ "text" = $task.name; callback_data = $task.script} $buttons += $button } $text = "Available tasks:" $chat_id = $return.chat_id sendKeyboard $URL_set $buttons $chat_id $text #sendMessage $URL_set $return.chat_id "Привет, $($return["f_name"])" } "*как дела?*" { sendMessage $URL_set $return.chat_id "Хорошо" } default {sendMessage $URL_set $return.chat_id "$(Get-Random("", "", "", ""))"} } } # если было нажатие на кнопку elseif($return.callback_data) { sendMessage $URL_set $($return.chat_id) $($return.callback_data) } Start-Sleep -s $timeout }
Теперь, если написать «привет» — бот вернет список кнопок, который соответствует задачам, описанным в xml-файлы. В callback_data будет команда или скрипт.
Если делать косметические изменения – то желательно, чтобы кнопок было 3-4 на строку, иначе они отображаются не полностью:

Будем делать по 3 кнопки в линию (максимально).
Схематично массив keyboard должен выглядеть так:

Таким образом:
Button[i] — массив (ассоциативный) вида
$button = @{ "text" = $task.name; callback_data = $task.script}
Line[1-3] — это массивы (из кнопок), которые хранят в себе массивы кнопок (это важно)
Keyboard – массив из Line’ов.
Модифицируем функцию sendKeyboard
function sendKeyboard($URL, $buttons, $chat_id, $text) { $keyboard = @{} # Тут необходимо использовать ArrayList, т.к внутри него мы будем хранить объекты - другие массивы $lines = 3 $buttons_line = New-Object System.Collections.ArrayList for($i=0; $i -lt $buttons.Count; $i++) { # Добавляем кнопки в линию (line). Как только добавили 3 - добавляем line в keyboard $buttons_line.Add($buttons[$i]) | Out-Null # Проверяем счетчик - остаток от деления должен быть 0 if( ($i + 1 )%$lines -eq 0 ) { # добавляем строку кнопок в keyboard $keyboard["inline_keyboard"] += @(,@($buttons_line)) $buttons_line.Clear() } } # добавляем оставшиеся последние кнопки $keyboard["inline_keyboard"] += @(,@($buttons_line)) #$keyboard = @{"inline_keyboard" = @(,$buttons)} $ht = @{ parse_mode = "Markdown" reply_markup = $keyboard chat_id = $chat_id text = $text } $json = $ht | ConvertTo-Json -Depth 5 Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json }
Проверяем:

[xml]$xmlConfig = Get-Content -Path ("c:\Temp\Habr\telegram_bot.xml") $token = $xmlConfig.config.system.token $timeout = $xmlConfig.config.system.timeout.'#text' # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" function getUpdates($URL) { $json = Invoke-RestMethod -Uri $URL $data = $json.result | Select-Object -Last 1 # Обнуляем переменные $text = $null $callback_data = $null # Нажатие на кнопку if($data.callback_query) { $callback_data = $data.callback_query.data $chat_id = $data.callback_query.from.id $f_name = $data.callback_query.from.first_name $l_name = $data.callback_query.from.last_name $username = $data.callback_query.from.username } # Обычное сообщение elseif($data.message) { $chat_id = $data.message.chat.id $text = $data.message.text $f_name = $data.message.chat.first_name $l_name = $data.message.chat.last_name $type = $data.message.chat.type $username = $data.message.chat.username } $ht = @{} $ht["chat_id"] = $chat_id $ht["text"] = $text $ht["f_name"] = $f_name $ht["l_name"] = $l_name $ht["username"] = $username $ht["callback_data"] = $callback_data # confirm Invoke-RestMethod "$($URL)?offset=$($($data.update_id)+1)" -Method Get | Out-Null return $ht } function sendMessage($URL, $chat_id, $text) { # создаем HashTable, можно объявлять ее и таким способом $ht = @{ text = $text # указан способ разметки Markdown parse_mode = "Markdown" chat_id = $chat_id } # Данные нужно отправлять в формате json $json = $ht | ConvertTo-Json # Делаем через Invoke-RestMethod, но никто не запрещает сделать и через Invoke-WebRequest # Method Post - т.к. отправляем данные, по умолчанию Get Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json | Out-Null } function sendKeyboard($URL, $buttons, $chat_id, $text) { $keyboard = @{} # Тут необходимо использовать ArrayList, т.к внутри него мы будем хранить объекты - другие массивы $lines = 3 $buttons_line = New-Object System.Collections.ArrayList for($i=0; $i -lt $buttons.Count; $i++) { # Добавляем кнопки в линию (line). Как только добавили 3 - добавляем line в keyboard $buttons_line.Add($buttons[$i]) | Out-Null # Проверяем счетчик - остаток от деления должен быть 0 if( ($i + 1 )%$lines -eq 0 ) { # добавляем строку кнопок в keyboard $keyboard["inline_keyboard"] += @(,@($buttons_line)) $buttons_line.Clear() } } # добавляем оставшиеся посление кнопки $keyboard["inline_keyboard"] += @(,@($buttons_line)) $ht = @{ parse_mode = "Markdown" reply_markup = $keyboard chat_id = $chat_id text = $text } $json = $ht | ConvertTo-Json -Depth 5 Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json } while($true) # вечный цикл { $return = getUpdates $URL_get #$return.text = "привет" # Если обычное сообщение if($return.text) { # http://apps.timwhitlock.info/emoji/tables/unicode#block-1-emoticons #sendMessage $URL_set $return.chat_id (Get-Random("", "", "", "")) switch -Wildcard ($return["text"]) { "*привет*" { # Пустой массив $buttons = @() foreach($task in $xmlConfig.config.tasks.task) { $i++ $button = @{ "text" = $task.name; callback_data = $task.script} $buttons += $button } $text = "Available tasks:" $chat_id = $return.chat_id sendKeyboard $URL_set $buttons $chat_id $text #sendMessage $URL_set $return.chat_id "Привет, $($return["f_name"])" } "*как дела?*" { sendMessage $URL_set $return.chat_id "Хорошо" } default {sendMessage $URL_set $return.chat_id "$(Get-Random("", "", "", ""))"} } } # если было нажатие на кнопку elseif($return.callback_data) { #sendMessage $URL_set $($return.chat_id) $($return.callback_data) write-host "$($return.chat_id) $($return.callback_data)" } Start-Sleep -s $timeout }
Часть 4: задачность и многозадачность
Настало время по кнопке делать дела.
Для многозадачности будем использовать механизм Job’ов. Проверяем такой кусок кода:
$script = "ipconfig" $script_block = { Param($script) ; Invoke-Expression $script } $job_name = "TestJob" Start-Job -ScriptBlock $script_block -ArgumentList $script -Name $job_name | Out-Null
И через 5 секунд выполняем:
foreach($job in (Get-Job | Where {$_.State -eq "Completed"} )) { $output = Get-Job -ID $job.Id | Receive-Job $output $job | Remove-Job }
$output должен возвращать ipconfig с localhost
Добавляем это в основной скрипт, в блок callback_data
# если было нажатие на кнопку elseif($return.callback_data) { $script = $($return.callback_data) $job_name = $($return.chat_id) $script_block = { Param($script) ; Invoke-Expression $script } #запускаем Job Start-Job -ScriptBlock $script_block -ArgumentList $script -Name $job_name | Out-Null }
А это ниже
# смотрим, какие job'ы уже выполнились foreach($job in (Get-Job | Where {$_.State -eq "Completed"} )) { $output = Get-Job -ID $job.Id | Receive-Job # отправляем результат тому, кто вызвал job sendMessage $URL_set $job.Name $output $job | Remove-Job # и снова шлем клавиатуру $text = "Available tasks:" sendKeyboard $URL_set $buttons $job.Name $text }
Проверяем, ловим error
Invoke-RestMethod: {«ok»:false,«error_code»:400,«description»:«Bad Request: message is too long»}
На просторах интернета находим информацию, что длина сообщения не может превышать 4096 символов. Оукей…
$output.Length
говорит что длина 39
Долго думаем что не так, в результате пробуем такой кусок кода:
$text = $null foreach($string in $output) { $text = "$text`n$string" } sendMessage $URL_set $job.Name $text
[xml]$xmlConfig = Get-Content -Path ("c:\Temp\Habr\telegram_bot.xml") $token = $xmlConfig.config.system.token $timeout = $xmlConfig.config.system.timeout.'#text' # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" function getUpdates($URL) { $json = Invoke-RestMethod -Uri $URL $data = $json.result | Select-Object -Last 1 # Обнуляем переменные $text = $null $callback_data = $null # Нажатие на кнопку if($data.callback_query) { $callback_data = $data.callback_query.data $chat_id = $data.callback_query.from.id $f_name = $data.callback_query.from.first_name $l_name = $data.callback_query.from.last_name $username = $data.callback_query.from.username } # Обычное сообщение elseif($data.message) { $chat_id = $data.message.chat.id $text = $data.message.text $f_name = $data.message.chat.first_name $l_name = $data.message.chat.last_name $type = $data.message.chat.type $username = $data.message.chat.username } $ht = @{} $ht["chat_id"] = $chat_id $ht["text"] = $text $ht["f_name"] = $f_name $ht["l_name"] = $l_name $ht["username"] = $username $ht["callback_data"] = $callback_data # confirm Invoke-RestMethod "$($URL)?offset=$($($data.update_id)+1)" -Method Get | Out-Null return $ht } function sendMessage($URL, $chat_id, $text) { # создаем HashTable, можно объявлять ее и таким способом $ht = @{ text = $text # указан способ разметки Markdown parse_mode = "Markdown" chat_id = $chat_id } # Данные нужно отправлять в формате json $json = $ht | ConvertTo-Json # Делаем через Invoke-RestMethod, но никто не запрещает сделать и через Invoke-WebRequest # Method Post - т.к. отправляем данные, по умолчанию Get Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json | Out-Null } function sendKeyboard($URL, $buttons, $chat_id, $text) { $keyboard = @{} $lines = 3 # Тут необходимо использовать ArrayList, т.к внутри него мы будем хранить объекты - другие массивы $buttons_line = New-Object System.Collections.ArrayList for($i=0; $i -lt $buttons.Count; $i++) { # Добавляем кнопки в линию (line). Как только добавили 3 - добавляем line в keyboard $buttons_line.Add($buttons[$i]) | Out-Null # Проверяем счетчик - остаток от деления должен быть 0 if( ($i + 1 )%$lines -eq 0 ) { # добавляем строку кнопок в keyboard $keyboard["inline_keyboard"] += @(,@($buttons_line)) $buttons_line.Clear() } } # добавляем оставшиеся последние кнопки $keyboard["inline_keyboard"] += @(,@($buttons_line)) $ht = @{ parse_mode = "Markdown" reply_markup = $keyboard chat_id = $chat_id text = $text } $json = $ht | ConvertTo-Json -Depth 5 Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json } while($true) # вечный цикл { $return = getUpdates $URL_get #$return.text = "привет" # Если обычное сообщение if($return.text) { # http://apps.timwhitlock.info/emoji/tables/unicode#block-1-emoticons #sendMessage $URL_set $return.chat_id (Get-Random("", "", "", "")) switch -Wildcard ($return["text"]) { "*привет*" { # Пустой массив $buttons = @() foreach($task in $xmlConfig.config.tasks.task) { $i++ $button = @{ "text" = $task.name; callback_data = $task.script} $buttons += $button } $text = "Available tasks:" $chat_id = $return.chat_id sendKeyboard $URL_set $buttons $chat_id $text #sendMessage $URL_set $return.chat_id "Привет, $($return["f_name"])" } "*как дела?*" { sendMessage $URL_set $return.chat_id "Хорошо" } default {sendMessage $URL_set $return.chat_id "$(Get-Random("", "", "", ""))"} } } # если было нажатие на кнопку elseif($return.callback_data) { $script = $($return.callback_data) $job_name = $($return.chat_id) write-host "$script $job_name" $script_block = { Param($script) ; Invoke-Expression $script } #запускаем Job Start-Job -ScriptBlock $script_block -ArgumentList $script -Name $job_name | Out-Null } # смотрим, какие job'ы уже выполнились foreach($job in (Get-Job | Where {$_.State -eq "Completed"} )) { $output = Get-Job -ID $job.Id | Receive-Job $text = $null foreach($string in $output) { $text = "$text`n$string" } # отправляем результат тому, кто вызвал job sendMessage $URL_set $job.Name $text $job | Remove-Job # и снова шлем клавиатуру $text = "Available tasks:" sendKeyboard $URL_set $buttons $job.Name $text } Start-Sleep -s $timeout }

Теперь прикрутим «немного безопасности»
Добавляем в xml конфиг новую строку, назовем ее users и укажем там chat_id тех, кому можно общаться с ботом:
<system> <token>610373243:AAF7Z0HnruKYsPwtkuupi01XqVOV-PtXgFM</token> <timeout desc="bot check timeout in seconds">1</timeout> <users>111111111, 222222222</users> </system>
В скрипте будем получать массив users
$users = (($xmlConfig.config.system.users).Split(",")).Trim()
И проверять
if($users -contains $return.chat_id) { ... }
[xml]$xmlConfig = Get-Content -Path ("c:\Temp\Habr\telegram_bot.xml") $token = $xmlConfig.config.system.token $timeout = $xmlConfig.config.system.timeout.'#text' $users = (($xmlConfig.config.system.users).Split(",")).Trim() # Telegram URLs $URL_get = "https://api.telegram.org/bot$token/getUpdates" $URL_set = "https://api.telegram.org/bot$token/sendMessage" function getUpdates($URL) { $json = Invoke-RestMethod -Uri $URL $data = $json.result | Select-Object -Last 1 # Обнуляем переменные $text = $null $callback_data = $null # Нажатие на кнопку if($data.callback_query) { $callback_data = $data.callback_query.data $chat_id = $data.callback_query.from.id $f_name = $data.callback_query.from.first_name $l_name = $data.callback_query.from.last_name $username = $data.callback_query.from.username } # Обычное сообщение elseif($data.message) { $chat_id = $data.message.chat.id $text = $data.message.text $f_name = $data.message.chat.first_name $l_name = $data.message.chat.last_name $type = $data.message.chat.type $username = $data.message.chat.username } $ht = @{} $ht["chat_id"] = $chat_id $ht["text"] = $text $ht["f_name"] = $f_name $ht["l_name"] = $l_name $ht["username"] = $username $ht["callback_data"] = $callback_data # confirm Invoke-RestMethod "$($URL)?offset=$($($data.update_id)+1)" -Method Get | Out-Null return $ht } function sendMessage($URL, $chat_id, $text) { # создаем HashTable, можно объявлять ее и таким способом $ht = @{ text = $text # указан способ разметки Markdown parse_mode = "Markdown" chat_id = $chat_id } # Данные нужно отправлять в формате json $json = $ht | ConvertTo-Json # Делаем через Invoke-RestMethod, но никто не запрещает сделать и через Invoke-WebRequest # Method Post - т.к. отправляем данные, по умолчанию Get Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json | Out-Null } function sendKeyboard($URL, $buttons, $chat_id, $text) { $keyboard = @{} $lines = 3 # Тут необходимо использовать ArrayList, т.к внутри него мы будем хранить объекты - другие массивы $buttons_line = New-Object System.Collections.ArrayList for($i=0; $i -lt $buttons.Count; $i++) { # Добавляем кнопки в линию (line). Как только добавили 3 - добавляем line в keyboard $buttons_line.Add($buttons[$i]) | Out-Null # Проверяем счетчик - остаток от деления должен быть 0 if( ($i + 1 )%$lines -eq 0 ) { # добавляем строку кнопок в keyboard $keyboard["inline_keyboard"] += @(,@($buttons_line)) $buttons_line.Clear() } } # добавляем оставшиеся последние кнопки $keyboard["inline_keyboard"] += @(,@($buttons_line)) $ht = @{ parse_mode = "Markdown" reply_markup = $keyboard chat_id = $chat_id text = $text } $json = $ht | ConvertTo-Json -Depth 5 Invoke-RestMethod $URL -Method Post -ContentType 'application/json; charset=utf-8' -Body $json } while($true) # вечный цикл { $return = getUpdates $URL_get if($users -contains $return.chat_id) { # Если обычное сообщение if($return.text) { #write-host $return.chat_id # http://apps.timwhitlock.info/emoji/tables/unicode#block-1-emoticons #sendMessage $URL_set $return.chat_id (Get-Random("", "", "", "")) switch -Wildcard ($return["text"]) { "*привет*" { # Пустой массив $buttons = @() foreach($task in $xmlConfig.config.tasks.task) { $i++ $button = @{ "text" = $task.name; callback_data = $task.script} $buttons += $button } $text = "Available tasks:" $chat_id = $return.chat_id sendKeyboard $URL_set $buttons $chat_id $text #sendMessage $URL_set $return.chat_id "Привет, $($return["f_name"])" } "*как дела?*" { sendMessage $URL_set $return.chat_id "Хорошо" } default {sendMessage $URL_set $return.chat_id "$(Get-Random("", "", "", ""))"} } } # если было нажатие на кнопку elseif($return.callback_data) { $script = $($return.callback_data) $job_name = $($return.chat_id) write-host "$script $job_name" $script_block = { Param($script) ; Invoke-Expression $script } #запускаем Job Start-Job -ScriptBlock $script_block -ArgumentList $script -Name $job_name | Out-Null } # смотрим, какие job'ы уже выполнились foreach($job in (Get-Job | Where {$_.State -eq "Completed"} )) { $output = Get-Job -ID $job.Id | Receive-Job $text = $null foreach($string in $output) { $text = "$text`n$string" } # отправляем результат тому, кто вызвал job sendMessage $URL_set $job.Name $text $job | Remove-Job # и снова шлем клавиатуру $text = "Available tasks:" sendKeyboard $URL_set $buttons $job.Name $text } } else { if($return.text) { sendMessage $URL_set $return.chat_id "Вы кто такие? Я вас не звал!" } } Start-Sleep -s $timeout }
Часть 5: в заключение
Проверяем функционал бота – добавим туда скриптов, которые будут делать что-то полезное
Для операций на удаленных серверах мы используем Invoke-Command с последующим Write-Output
$hostname = "hostname" $service = "MSSQLSERVER" $output = Invoke-Command -ComputerName $hostname -ScriptBlock{param($service); (Get-Service -Name $service).Status} -ArgumentList $service write-output $output.Value
В этом случае, учетная запись, из-под которой будет работать скрипт телеграм-бота должна иметь соответствующие привилегии на удаленной машине.
Также, я не затронул функционал логировая, но здесь, думаю, все просто, по желанию каждый сам может решить что он хочет логировать, а что нет.
Наверняка у кого-то будет проблема с отправкой сообщения > 4096 символов, но это решаемо Substring и циклом отправки.
И напоследок – удаленное управление из любой точки мира (почти из любой) это хорошо, но всегда есть риск, что что-то пойдет не так (управление ботом вдруг может получить кто-то нехороший). На этот случай мы просто добавили Exit из скрипта по определенному слову
switch -Wildcard ($return["text"]) { "*привет*" { # Пустой массив $buttons = @() foreach($task in $xmlConfig.config.tasks.task) { $i++ $button = @{ "text" = $task.name; callback_data = $task.script} $buttons += $button } $text = "Available tasks:" $chat_id = $return.chat_id sendKeyboard $URL_set $buttons $chat_id $text #sendMessage $URL_set $return.chat_id "Привет, $($return["f_name"])" } "*как дела?*" { sendMessage $URL_set $return.chat_id "Хорошо" } "алярма!" {sendMessage $URL_set $return.chat_id "bb" ; Exit} default {sendMessage $URL_set $return.chat_id "$(Get-Random("", "", "", ""))"} }
У меня всё.
ссылка на оригинал статьи https://habr.com/ru/post/483660/
Добавить комментарий