Telegram-бот для управления инфраструктурой

от автора

image

По мотивам статьи Телеграмм-бот для системного администратора (статья не моя, я только прочитал) захотел поделиться опытом создания Telegram-бота на PowerShell для управления серверами приложений. Будет текст, код и немножко картинок. Конструктивная критика приветствуется ( главное чтобы не звучало «зачем на PowerShell? Надо было на perl» ).

Думаю что статья больше подойдет «новичкам» в PowerShell, но и опытные администраторы могут что-то полезное здесь увидеть.

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

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

  • Простота добавления/изменения задач в Telegram-бот
  • Многозадачность или параллелизация
  • «Понятный» интерфейс управления
  • Хоть какая-то безопасность


В какой то момент было решено выносить конфиг в отдельный файл – в нашем случае xml (тут кто-то может сказать, что давайте всё в json, но мы сделали в xml и были довольны)
Начнем с начала:

Часть 1: простой телеграм-бот

Ищем папку-бота (не каталог) – BotFather (@BotFather) в Telegram

BotFather

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

После этого BotFather выдаст токен, который мы и будем использовать:

image

Дальше можно загрузить для бота картинку, поставить 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 {}

Нот бэд. Напишем что-нибудь боту:

Hello

И прочитаем:

# 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 понимает их нативно
Get-Random

Часть 2: нужны кнопки

В телеграм боте есть опция задания списка команд (открывается вот по этому значку Icon )
Первоначально мы так и сделали – был набор команд, в качестве параметров передавали туда имена серверов или сервисов. Потом решили, что нужно двигаться дальше в сторону 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 

Получаем кнопки:

Buttons

Делаем функцию по отправке кнопок, на вход будем подавать массив кнопок

# 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)...

При нажатии на кнопку никакой текст не возвращается, соответственно, нужно модифицировать функцию. Нажимаем на кнопку

PushTheButton

И запускаем кусок кода для проверки содержимого $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 на строку, иначе они отображаются не полностью:

KeyBoard

Будем делать по 3 кнопки в линию (максимально).

Схематично массив keyboard должен выглядеть так:

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     }  

Проверяем:

Keyboard_Telegram

Итоговый скрипт

[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 } 

Output

Теперь прикрутим «немного безопасности»

Добавляем в 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *