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>*********************************</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("", "", "", ""))"}
            }  

У меня всё.