Открыть Электронные книги
Категории
Открыть Аудиокниги
Категории
Открыть Журналы
Категории
Открыть Документы
Категории
Из песочницы
Реклама
По мотивам статьи Телеграмм-бот для системного администратора (статья не моя, я только прочитал) захотел поделиться
опытом создания Telegram-бота на PowerShell для управления серверами приложений. Будет текст, код и немножко картинок.
Конструктивная критика приветствуется ( главное чтобы не звучало «зачем на PowerShell? Надо было на perl» ).
Думаю что статья больше подойдет «новичкам» в PowerShell, но и опытные администраторы могут что-то полезное здесь
увидеть.
Саму статью старался построить по частям – от простого к сложному. Возможно, встретится плагиат, будьте бдительны!
Итак, у нас есть необходимость осуществлять управление сервисами или приложениями на нескольких серверах
(останавливать, запускать), перезагружать сервера, смотреть логи и еще какую-то информацию при необходимости. Всё это
хочется делать (на самом деле нет), находясь в метро, в магазине или даже лёжа на диване, без VPN и ноутбуков. Из
требований (которые были написаны, конечно, на коленке).
В какой то момент было решено выносить конфиг в отдельный файл – в нашем случае xml (тут кто-то может сказать, что
давайте всё в json, но мы сделали в xml и были довольны)
Начнем с начала:
Часть 1: простой телеграм-бот
Пишем /newbot
Далее, нужно придумать имя боту (в моем случае я назвал Haaaabr специально для статьи) и username, который должен
заканчиваться на «bot» (Haaaabr_bot)
Для справки нам понадобятся описания вызовов API Telegram Bot API
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
# Token
$token = "***********************"
# Telegram URLs
$URL_get = "https://api.telegram.org/bot$token/getUpdates"
$URL_set = "https://api.telegram.org/bot$token/sendMessage"
ok result
-- ------
True {}
Нот бэд. Напишем что-нибудь боту:
И прочитаем:
# Token
$token = "***********************"
# Telegram URLs
$URL_get = "https://api.telegram.org/bot$token/getUpdates"
$URL_set = "https://api.telegram.org/bot$token/sendMessage"
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"
$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
Делаем
# 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
$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
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
# 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
}
}
Start-Sleep -s $timeout
}
Теперь, вызвав
Показать код
# 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
# 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
}
Emoji:
в командлете Get-Random используются emoji, в код в статье у меня их встроить не получилось, но PS понимает их нативно
В телеграм боте есть опция задания списка команд (открывается вот по этому значку )
Первоначально мы так и сделали – был набор команд, в качестве параметров передавали туда имена серверов или сервисов.
Потом решили, что нужно двигаться дальше в сторону User Friendly интерфейсов и подключили функционал кнопок.
# Token
$token = "***********************"
# Telegram URLs
$URL_get = "https://api.telegram.org/bot$token/getUpdates"
$URL_set = "https://api.telegram.org/bot$token/sendMessage"
$ht = @{
parse_mode = "Markdown"
reply_markup = $keyboard
chat_id = ******** # здесь нужно указать свой Telegram ID
text = "Test Text"
}
Получаем 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"
$ht = @{
parse_mode = "Markdown"
reply_markup = $keyboard
chat_id = ********
text = "Test Text"
}
Получаем кнопки:
# Token
$token = "***********************"
# Telegram URLs
$URL_get = "https://api.telegram.org/bot$token/getUpdates"
$URL_set = "https://api.telegram.org/bot$token/sendMessage"
# 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
# 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
}
}
Start-Sleep -s $timeout
}
Теперь на «привет» бот будет отправлять нам пару кнопок. Осталось понять, какую кнопку нажал пользователь. В текущей ps-
функции getUpdates есть проверка на
if($text)...
При нажатии на кнопку никакой текст не возвращается, соответственно, нужно модифицировать функцию. Нажимаем на кнопку
# 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
# 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, если было нажатие на кнопку. На этапе тестов
словили ошибку при вызове:
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»}
$return.callback_data = “Project1_CD”
Итоговый скрипт
# 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
}
# 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
}
<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) и для каждой задачи указываем скрипт или команду.
Проверяем:
foreach($task in $xmlConfig.config.tasks.task)
{
$task.name # имя кнопки
$task.script # скрипт
}
# 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
}
# 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
}
}
# если было нажатие на кнопку
elseif($return.callback_data)
{
sendMessage $URL_set $($return.chat_id) $($return.callback_data)
}
Start-Sleep -s $timeout
}
Теперь, если написать «привет» — бот вернет список кнопок, который соответствует задачам, описанным в xml-файлы. В
callback_data будет команда или скрипт.
Если делать косметические изменения – то желательно, чтобы кнопок было 3-4 на строку, иначе они отображаются не
полностью:
Line[1-3] — это массивы (из кнопок), которые хранят в себе массивы кнопок (это важно)
Keyboard – массив из Line’ов.
Проверяем:
Итоговый скрипт
# 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
}
$keyboard = @{}
# Тут необходимо использовать ArrayList, т.к внутри него мы будем хранить объекты - другие массивы
$lines = 3
$ht = @{
parse_mode = "Markdown"
reply_markup = $keyboard
chat_id = $chat_id
text = $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
}
}
# если было нажатие на кнопку
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
}
Для многозадачности будем использовать механизм 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
#запускаем Job
Start-Job -ScriptBlock $script_block -ArgumentList $script -Name $job_name | Out-Null
А это ниже
$job | Remove-Job
На просторах интернета находим информацию, что длина сообщения не может превышать 4096 символов. Оукей…
$output.Length
$text = $null
foreach($string in $output)
{
$text = "$text`n$string"
}
sendMessage $URL_set $job.Name $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
}
# 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
}
}
# если было нажатие на кнопку
elseif($return.callback_data)
{
$script = $($return.callback_data)
$job_name = $($return.chat_id)
write-host "$script $job_name"
#запускаем Job
Start-Job -ScriptBlock $script_block -ArgumentList $script -Name $job_name | Out-Null
}
$text = $null
foreach($string in $output)
{
$text = "$text`n$string"
}
# отправляем результат тому, кто вызвал job
sendMessage $URL_set $job.Name $text
$job | Remove-Job
Добавляем в xml конфиг новую строку, назовем ее users и укажем там chat_id тех, кому можно общаться с ботом:
<system>
<token>*********************************</token>
<timeout desc="bot check timeout in seconds">1</timeout>
<users>111111111, 222222222</users>
</system>
$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
}
# Пустой массив
$buttons = @()
foreach($task in $xmlConfig.config.tasks.task)
{
$i++
$button = @{ "text" = $task.name; callback_data = $task.script}
$buttons += $button
}
}
# если было нажатие на кнопку
elseif($return.callback_data)
{
$script = $($return.callback_data)
$job_name = $($return.chat_id)
write-host "$script $job_name"
#запускаем Job
Start-Job -ScriptBlock $script_block -ArgumentList $script -Name $job_name | Out-Null
}
$text = $null
foreach($string in $output)
{
$text = "$text`n$string"
}
# отправляем результат тому, кто вызвал job
sendMessage $URL_set $job.Name $text
$job | Remove-Job
}
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} -Arg
umentList $service
write-output $output.Value
В этом случае, учетная запись, из-под которой будет работать скрипт телеграм-бота должна иметь соответствующие
привилегии на удаленной машине.
Также, я не затронул функционал логировая, но здесь, думаю, все просто, по желанию каждый сам может решить что он хочет
логировать, а что нет.
Наверняка у кого-то будет проблема с отправкой сообщения > 4096 символов, но это решаемо Substring и циклом отправки.
И напоследок – удаленное управление из любой точки мира (почти из любой) это хорошо, но всегда есть риск, что что-то
пойдет не так (управление ботом вдруг может получить кто-то нехороший). На этот случай мы просто добавили Exit из скрипта
по определенному слову
# Пустой массив
$buttons = @()
foreach($task in $xmlConfig.config.tasks.task)
{
$i++
$button = @{ "text" = $task.name; callback_data = $task.script}
$buttons += $button
}
У меня всё.
@Andrey_O
Пользователь
ПОХОЖИЕ ПУБЛИКАЦИИ
ЗАКАЗЫ
Комментарии 30
chat id может измениться если изменить администратора группы, да и вообще, изменение любых настроек чата ведет вроде как к
изменению id, но состав пользователей вроде можно менять. У нас короче точно несколько раз id менялся
Глупость говоришь.
У пользователей айди всегда один. Он выглядит примерно так: 334463248.
Когда ты создаешь беседу (или группу, если тебе так удобнее называть), у беседы будет айди примерно такого же вида, только
со знаком минус.
Если ты зайдешь в настройки этой группы и сделаешь её публичной, применишь настройки, то эта группа превратится в так
называемую супер-группу. У супергрупп айди всегда такого вида: -1001492481741.
С этого момента айди группы у тебя не поменяется, что бы ты с ней не делал (кроме удаления этой группы, очевидно).
Также, когда ты в первый раз устанавливаешь приватность группы на значение «Публичная» и применяешь настройки, телеграм
присылает 2 сообщения (если это CallbackApi) или ты «вытягиваешь» это сообщение (если это LongPoolApi). Одно из этих
сообщений (или оба, точно не помню) сообщение содержит поля «migrate_from_chat_id» и «migrate_to_chat_id»
Подробнее можешь прочитать в официальной документации: core.telegram.org/bots/api#message
А long-polling можно реализовать в Powershell? Дергать API каждую секунду не самый изящный вариант.
long polling это же и есть вариант, когда бот будет дергать сервер делеграмма N раз в секунду, для получения данных по
инициативе сервера надо реализовывать webhook
Вы ошибаетесь. Long-polling это установка соединения с API и ожидание команд с сервера. Соединение при этом не
разрывается и команды бот получает мгновенно, а не через N секунд.
Если сделать GET к методу GetUpdates с параметром offset больше, чем последнее полученное сообщение, то GET повиснет в
ожидании данных с сервера, пока не наступит таймаут.
centralhardware 13 января 2020 в 22:00 0
Да ладно. Сервера телеграмма будут держать открытым соединения с тысячами серверов управляющими ботами?
Они же держат соединения с тысячами десктопных клиентов, у которых нет push. Как функционируют тысячи ботов, у
которых нет внешнего ip и сертификата на webhook, но которые отвечают мгновенно, не через секунду?
Посмотрите документацию core.telegram.org/bots/api#getupdates
Они там прямым текстом пишут, что лучше использовать таймаут побольше. Видимо, потому что накладные расходы на
обслуживание нового соединения куда больше, чем на поддержание открытого.
Я не вижу в описании что коннект может остаться открытым. Я сейчас как раз пишу ВК бота. Там я получаю через long
pool обновления и соединение закрывается. Бот периодически (где то раз в секунду) стучится на их сервера за новыми
обновлениями.
Да, после получения обновлений соединение закрывается и нужно устанавливать новое. Смысл в том, что при
ожидании обновлений соединение остается открытым. Вы делаете GET запрос к серверу, а он вам не сразу отвечает,
а только когда будут обновления, до этого соединение висит открытым. Так понятнее?
With long polling, the client requests information from the server exactly as in normal polling, but with the expectation the
server may not respond immediately. If the server has no new information for the client when the poll is received, instead
of sending an empty response, the server holds the request open and waits for response information to become available.
Once it does have new information, the server immediately sends an HTTP/S response to the client, completing the open
HTTP/S Request. Upon receipt of the server response, the client often immediately issues another server request.
Когда вы просто дергаете сервер раз в секунду, это называется short poll, обычно это не рекомендуют делать,
возрастает нагрузка на сервер, плюс обновления приходят не мгновенно, а с задержкой в секунду. Да еще и лишний
трафик по сети гуляет, что критично в случае мобильных клиентов.
Incoming updates are stored on the server until the bot receives them either way, but they will not be kept longer
than 24 hours.
Раз в 24 часа соединение в любом случае прервется, поскольку они удаляют обновления, если их не забрали за
сутки. Я обычно ставлю 5 минут, если после пяти минут ничего не пришло, то соединение закрывается и
открывается новое. Так и трафик небольшой, и нагрузка на API слабая.
Вы спалили token. Зачем использовать для написания бота powershell? Смысл то есть, но не в том чтобы ещё раз писать его на голом
апи, а вот написать библиотеку для работы с телеграмм апи это уже интересно, а потом подключив ее к скрипту можно было бы
доказать всему миру то что бота на powershell/bash могут быть написаны не только ради эксперементов.
Andrey_O 13 января 2020 в 09:44 0
Спасибо, действительно пропустил в одном месте. Смысл написания на PS был как раз в том, чтобы продемонстрировать подход,
который мы использовали (уверен, что кому-то это будет полезно) + показать саму реализацию кода (функции, условия, обработка
— опять таки, для тех, у кого не очень богатый опыт работы с PS)
Но идею с библиотекой возьму на заметку
Просто на мой взгляд в отсутствие удобной библиотеки и отталкивает от написания ботов на данных языках, так как писать на
голом апи это мало кому надо
есть враперы на чем угодно, можно ими пользоваться, но использование голого апи более чем удобно, да и нет там ничего
сложного, городить для этого библиотеку — глупо
Вопрос в желаемой сложности для простого не имеет смысла для чего то более сложного ещё какой.
там апи простейший, я вообще не понимаю зачем там либа нужна, вне зависимости от сложности проекта обращение к
апи читабельно и незамутнено кривыми руками библиотекописателя, если сложно использовать напрямую обращение к
апи то может стоит подумать об уходе из профессии?
Последний бот которого я писал имел 12 диалогов с одним из которых содержал 13 шагов, 17 команд, и 16 callback и
при таком раскладе хочется написать один раз код который делает всю грязную работу и убрать его с глаз долой. Да
можно писать каждый раз кучу шаблонного кода, но делать так это медленное самоубийство.
Правильно я понимаю, что в настоящий момент нет возможности ограничить список chat_id через настройки бота при помощи
BotFather? Как вы оцениваете вероятность нахождения бота и его DDoS? (Не придирки )
Функционал ограничения через BotFather отсутствует. Кейсы с DDoS мы не проверяли, но точно могу сказать, что за время работы
бота было несколько случаев, что ему писали с неизвестных chat_id. В целом, отказ бота возможен по причине утечки памяти
Мне боту о котором знает не больше 20 человек за пол года писали от силы 10 левых пользователей
Так ограничивать через BotFather и не надо, легко же решается созданием списка с разрешенными chat_id, и проверять от кого
пришло сообщение. Если этого chat_id нет в списке, то игнорировать его.
Все верно, но это не сможет помешать кому угодно писать боту (да, он может не отвечать, но сообщения все равно будут
обрабатываться)
Так как бот на bash его возможности могут оказаться меньше чем лимиты телеграмма. Надо тестить.
САМОЕ ЧИТАЕМОЕ
Песочница
Если нашли опечатку в посте, выделите ее и нажмите Ctrl+Enter, чтобы сообщить автору.
© 2006 – 2020 «TM» Настройка языка О сайте Служба поддержки Мобильная версия