По мотивам статьи Телеграмм-бот для системного администратора (статья не моя, я только прочитал) захотел поделиться опытом создания 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("", "", "", ""))"}
}
Emoji:
в командлете Get-Random используются emoji, в код в статье у меня их встроить не получилось, но PS понимает их нативно
Часть 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
Собираем все воедино, немного поменяв блок switch
# 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>*********************************</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("", "", "", ""))"}
}
У меня всё.