Телеграм-Бот Для Микротика С Webhook И Парсером Json

Как вы думаете, можно ли, используя только скрипт Микротика, написать интерактивного Telegram-бота, который будет полностью работать в среде роутера с поддержкой Webhook, входящих событий из Telegram API? Предисловие: Я долго откладывал написание этой статьи, но недавние события вокруг мессенджера Telegram подтолкнули меня с новой энергией взяться за задуманное.

Я считаю, что Telegram, помимо всего прочего, является удобной нишевой платформой для информационно-разговорных сервисов, и он должен существовать и хорошо конкурировать с другими системами без искусственных ограничений.

Пусть эта статья станет скромным вкладом в поддержку Telegram. Прежде чем ответить на заданный вопрос, необходимо понять, какой минимум требуется от платформы бота для работы Webhook. Вот что: наличие WEB-сервера с SSL, действующего SSL-сертификата или самозаверяющего сертификата, загруженного в API Telegram, URL-адрес WEB-сервера для обработки Webhook. И если к роутеру можно предоставить доступ из интернета (реальный IP, доменное имя), то у Микротика проблемы с WEB-сервером (на SSL уже даже времени нет), пользовательского сервера просто нет. Но эту проблему можно обойти; решение будет предложено ниже.

Telegram-бот для Микротика — это лишь «верхушка айсберга».

В его основе лежит полноценный (насколько это возможно) парсер JSON, написанный мной на скриптовом языке Mikrotik. В общем, чтобы написать среднестатистического бота, не обязательно делать полный парсинг JSON; можно вполне обойтись поиском и копированием в строках, но я выбрал другой путь.

Далее я расскажу о парсере и некоторых приемах программирования скриптов Микротика, которые я изучил, работая над ним.



Парсер строк JSON на языке Mikrotik

Признаюсь, создание парсера JSON на скриптовом языке Mikrotik было для меня спортом.

Мне было интересно, можно ли это вообще сделать, учитывая ограничения языка сценариев Mikrotik. Но чем дальше я погружался в код, тем яснее видел путь к конечной цели.

Чуть ранее я шлифовал найденный в Интернете аналогичный парсер на VBScript для нужд одной SCADA-системы, поэтому взял за основу логику той реализации VBScript, переработал ее с учетом конструкций языка Микротик и отформатировал код в виде библиотеки функций.

Попутно я обнаружил несколько интересных особенностей языка сценариев, которыми с удовольствием поделюсь ниже.

Несколько слов об ограничениях.

Первое: длина строки в переменных Микротика 4096 байт, с этим ничего не поделаешь, все, что длиннее, просто не присваивается переменной.

Второе: Микротик ничего не знает о действительных числах, поэтому парсер сохраняет float как строковую переменную, типы bool, int, string нормально разбираются во внутреннее представление.



Использование парсера JSON



Телеграм-бот для Микротика с Webhook и парсером JSON

Функции представлены файлом библиотеки JParseFunctions, который «расширяет» код функции в глобальные переменные.

Эту библиотеку можно вызывать в скриптах сколько угодно раз без существенной потери производительности; для каждой функции производится проверка на «расширение» ее по глобальным переменным во избежание дублирования действий.

При редактировании файла библиотеки необходимо удалить глобальные переменные — код функции, чтобы они «пересоздались» с учетом обновлений.

Код библиотеки JParseFunctions: JParseFunctions

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

# -------------------------------- JParseFunctions --------------------------------------------------- # ------------------------------- fJParsePrint ---------------------------------------------------------------- :global fJParsePrint :if (!any $fJParsePrint) do={ :global fJParsePrint do={ :global JParseOut :local TempPath :global fJParsePrint :if ([:len $1] = 0) do={ :set $1 "\$JParseOut" :set $2 $JParseOut } :foreach k,v in=$2 do={ :if ([:typeof $k] = "str") do={ :set k "\"$k\"" } :set TempPath ($1. "->" .

$k) :if ([:typeof $v] = "array") do={ :if ([:len $v] > 0) do={ $fJParsePrint $TempPath $v } else={ :put "$TempPath = [] ($[:typeof $v])" } } else={ :put "$TempPath = $v ($[:typeof $v])" } } }} # ------------------------------- fJParsePrintVar ---------------------------------------------------------------- :global fJParsePrintVar :if (!any $fJParsePrintVar) do={ :global fJParsePrintVar do={ :global JParseOut :local TempPath :global fJParsePrintVar :local fJParsePrintRet "" :if ([:len $1] = 0) do={ :set $1 "\$JParseOut" :set $2 $JParseOut } :foreach k,v in=$2 do={ :if ([:typeof $k] = "str") do={ :set k "\"$k\"" } :set TempPath ($1. "->" .

$k) :if ($fJParsePrintRet != "") do={ :set fJParsePrintRet ($fJParsePrintRet .

"\r\n") } :if ([:typeof $v] = "array") do={ :if ([:len $v] > 0) do={ :set fJParsePrintRet ($fJParsePrintRet .

[$fJParsePrintVar $TempPath $v]) } else={ :set fJParsePrintRet ($fJParsePrintRet .

"$TempPath = [] ($[:typeof $v])") } } else={ :set fJParsePrintRet ($fJParsePrintRet .

"$TempPath = $v ($[:typeof $v])") } } :return $fJParsePrintRet }} # ------------------------------- fJSkipWhitespace ---------------------------------------------------------------- :global fJSkipWhitespace :if (!any $fJSkipWhitespace) do={ :global fJSkipWhitespace do={ :global Jpos :global JSONIn :global Jdebug :while ($Jpos < [:len $JSONIn] and ([:pick $JSONIn $Jpos] ~ "[ \r\n\t]")) do={ :set Jpos ($Jpos + 1) } :if ($Jdebug) do={:put "fJSkipWhitespace: Jpos=$Jpos Char=$[:pick $JSONIn $Jpos]"} }} # -------------------------------- fJParse --------------------------------------------------------------- :global fJParse :if (!any $fJParse) do={ :global fJParse do={ :global Jpos :global JSONIn :global Jdebug :global fJSkipWhitespace :local Char :if (!$1) do={ :set Jpos 0 } $fJSkipWhitespace :set Char [:pick $JSONIn $Jpos] :if ($Jdebug) do={:put "fJParse: Jpos=$Jpos Char=$Char"} :if ($Char="{") do={ :set Jpos ($Jpos + 1) :global fJParseObject :return [$fJParseObject] } else={ :if ($Char="[") do={ :set Jpos ($Jpos + 1) :global fJParseArray :return [$fJParseArray] } else={ :if ($Char="\"") do={ :set Jpos ($Jpos + 1) :global fJParseString :return [$fJParseString] } else={ # :if ([:pick $JSONIn $Jpos ($Jpos+2)]~"^-\?[0-9]") do={ :if ($Char~"[eE0-9.+-]") do={ :global fJParseNumber :return [$fJParseNumber] } else={ :if ($Char="n" and [:pick $JSONIn $Jpos ($Jpos+4)]="null") do={ :set Jpos ($Jpos + 4) :return [] } else={ :if ($Char="t" and [:pick $JSONIn $Jpos ($Jpos+4)]="true") do={ :set Jpos ($Jpos + 4) :return true } else={ :if ($Char="f" and [:pick $JSONIn $Jpos ($Jpos+5)]="false") do={ :set Jpos ($Jpos + 5) :return false } else={ :put "Err.Raise 8732. No JSON object could be fJParseed" :set Jpos ($Jpos + 1) :return [] } } } } } } } }} #-------------------------------- fJParseString --------------------------------------------------------------- :global fJParseString :if (!any $fJParseString) do={ :global fJParseString do={ :global Jpos :global JSONIn :global Jdebug :global fUnicodeToUTF8 :local Char :local StartIdx :local Char2 :local TempString "" :local UTFCode :local Unicode :set StartIdx $Jpos :set Char [:pick $JSONIn $Jpos] :if ($Jdebug) do={:put "fJParseString: Jpos=$Jpos Char=$Char"} :while ($Jpos < [:len $JSONIn] and $Char != "\"") do={ :if ($Char="\\") do={ :set Char2 [:pick $JSONIn ($Jpos + 1)] :if ($Char2 = "u") do={ :set UTFCode [:tonum "0x$[:pick $JSONIn ($Jpos+2) ($Jpos+6)]"] :if ($UTFCode>=0xD800 and $UTFCode<=0xDFFF) do={ # Surrogate pair :set Unicode (($UTFCode & 0x3FF) << 10) :set UTFCode [:tonum "0x$[:pick $JSONIn ($Jpos+8) ($Jpos+12)]"] :set Unicode ($Unicode | ($UTFCode & 0x3FF) | 0x10000) :set TempString ($TempString .

[:pick $JSONIn $StartIdx $Jpos] .

[$fUnicodeToUTF8 $Unicode]) :set Jpos ($Jpos + 12) } else= { # Basic Multilingual Plane (BMP) :set Unicode $UTFCode :set TempString ($TempString .

[:pick $JSONIn $StartIdx $Jpos] .

[$fUnicodeToUTF8 $Unicode]) :set Jpos ($Jpos + 6) } :set StartIdx $Jpos :if ($Jdebug) do={:put "fJParseString Unicode: $Unicode"} } else={ :if ($Char2 ~ "[\\bfnrt\"]") do={ :if ($Jdebug) do={:put "fJParseString escape: Char+Char2 $Char$Char2"} :set TempString ($TempString .

[:pick $JSONIn $StartIdx $Jpos] .

[[:parse "(\"\\$Char2\")"]]) :set Jpos ($Jpos + 2) :set StartIdx $Jpos } else={ :if ($Char2 = "/") do={ :if ($Jdebug) do={:put "fJParseString /: Char+Char2 $Char$Char2"} :set TempString ($TempString .

[:pick $JSONIn $StartIdx $Jpos] .

"/") :set Jpos ($Jpos + 2) :set StartIdx $Jpos } else={ :put "Err.Raise 8732. Invalid escape" :set Jpos ($Jpos + 2) } } } } else={ :set Jpos ($Jpos + 1) } :set Char [:pick $JSONIn $Jpos] } :set TempString ($TempString .

[:pick $JSONIn $StartIdx $Jpos]) :set Jpos ($Jpos + 1) :if ($Jdebug) do={:put "fJParseString: $TempString"} :return $TempString }} #-------------------------------- fJParseNumber --------------------------------------------------------------- :global fJParseNumber :if (!any $fJParseNumber) do={ :global fJParseNumber do={ :global Jpos :local StartIdx :global JSONIn :global Jdebug :local NumberString :local Number :set StartIdx $Jpos :set Jpos ($Jpos + 1) :while ($Jpos < [:len $JSONIn] and [:pick $JSONIn $Jpos]~"[eE0-9.+-]") do={ :set Jpos ($Jpos + 1) } :set NumberString [:pick $JSONIn $StartIdx $Jpos] :set Number [:tonum $NumberString] :if ([:typeof $Number] = "num") do={ :if ($Jdebug) do={:put "fJParseNumber: StartIdx=$StartIdx Jpos=$Jpos $Number ($[:typeof $Number])"} :return $Number } else={ :if ($Jdebug) do={:put "fJParseNumber: StartIdx=$StartIdx Jpos=$Jpos $NumberString ($[:typeof $NumberString])"} :return $NumberString } }} #-------------------------------- fJParseArray --------------------------------------------------------------- :global fJParseArray :if (!any $fJParseArray) do={ :global fJParseArray do={ :global Jpos :global JSONIn :global Jdebug :global fJParse :global fJSkipWhitespace :local Value :local ParseArrayRet [:toarray ""] $fJSkipWhitespace :while ($Jpos < [:len $JSONIn] and [:pick $JSONIn $Jpos]!= "]") do={ :set Value [$fJParse true] :set ($ParseArrayRet->([:len $ParseArrayRet])) $Value :if ($Jdebug) do={:put "fJParseArray: Value="; :put $Value} $fJSkipWhitespace :if ([:pick $JSONIn $Jpos] = ",") do={ :set Jpos ($Jpos + 1) $fJSkipWhitespace } } :set Jpos ($Jpos + 1) # :if ($Jdebug) do={:put "ParseArrayRet: "; :put $ParseArrayRet} :return $ParseArrayRet }} # -------------------------------- fJParseObject --------------------------------------------------------------- :global fJParseObject :if (!any $fJParseObject) do={ :global fJParseObject do={ :global Jpos :global JSONIn :global Jdebug :global fJSkipWhitespace :global fJParseString :global fJParse # Syntax :local ParseObjectRet ({}) don't work in recursive call, use [:toarray ""] for empty array!!! :local ParseObjectRet [:toarray ""] :local Key :local Value :local ExitDo false $fJSkipWhitespace :while ($Jpos < [:len $JSONIn] and [:pick $JSONIn $Jpos]!="}" and !$ExitDo) do={ :if ([:pick $JSONIn $Jpos]!="\"") do={ :put "Err.Raise 8732. Expecting property name" :set ExitDo true } else={ :set Jpos ($Jpos + 1) :set Key [$fJParseString] $fJSkipWhitespace :if ([:pick $JSONIn $Jpos] != ":") do={ :put "Err.Raise 8732. Expecting : delimiter" :set ExitDo true } else={ :set Jpos ($Jpos + 1) :set Value [$fJParse true] :set ($ParseObjectRet->$Key) $Value :if ($Jdebug) do={:put "fJParseObject: Key=$Key Value="; :put $Value} $fJSkipWhitespace :if ([:pick $JSONIn $Jpos]=",") do={ :set Jpos ($Jpos + 1) $fJSkipWhitespace } } } } :set Jpos ($Jpos + 1) # :if ($Jdebug) do={:put "ParseObjectRet: "; :put $ParseObjectRet} :return $ParseObjectRet }} # ------------------- fByteToEscapeChar ---------------------- :global fByteToEscapeChar :if (!any $fByteToEscapeChar) do={ :global fByteToEscapeChar do={ # :set $1 [:tonum $1] :return [[:parse "(\"\\$[:pick "0123456789ABCDEF" (($1 >> 4) & 0xF)]$[:pick "0123456789ABCDEF" ($1 & 0xF)]\")"]] }} # ------------------- fUnicodeToUTF8---------------------- :global fUnicodeToUTF8 :if (!any $fUnicodeToUTF8) do={ :global fUnicodeToUTF8 do={ :global fByteToEscapeChar # :local Ubytes [:tonum $1] :local Nbyte :local EscapeStr "" :if ($1 < 0x80) do={ :set EscapeStr [$fByteToEscapeChar $1] } else={ :if ($1 < 0x800) do={ :set Nbyte 2 } else={ :if ($1 < 0x10000) do={ :set Nbyte 3 } else={ :if ($1 < 0x20000) do={ :set Nbyte 4 } else={ :if ($1 < 0x4000000) do={ :set Nbyte 5 } else={ :if ($1 < 0x80000000) do={ :set Nbyte 6 } } } } } :for i from=2 to=$Nbyte do={ :set EscapeStr ([$fByteToEscapeChar ($1 & 0x3F | 0x80)] .

$EscapeStr) :set $1 ($1 >> 6) } :set EscapeStr ([$fByteToEscapeChar (((0xFF00 >> $Nbyte) & 0xFF) | $1)] .

$EscapeStr) } :return $EscapeStr }} # ------------------- End JParseFunctions----------------------

Давайте рассмотрим, как работает парсер, на примере фрагмента кода Telegram-бота.

Давайте выполним следующие команды шаг за шагом.

Запрос статуса функции getWebhookInfo Telegram API, которая возвращает строку JSON в файл j.txt:

:do {/tool fetch url=" https://api.telegram.org/bot$TToken/getWebhookInfo " dst-path=j.txt} on-error={:put "getWebhookInfo error"};



[admin@MikroTik] > :put [/file get j.txt contents]; {"ok":true,"result":{"url":" https://*****:8443","has_custom_certificate":false,"pending_update_count":0,"last_error_date":1524565055,"last_error_message":"Connection timed out","max_connections":4 0}}

Загрузка строки JSON во входную переменную:

:set JSONIn [/file get j.txt contents]

Выполнение функции синтаксического анализатора $fJParse и сохранение результата в переменную $JParseOut.

:set JParseOut [$fJParse];

В $JParseOut вы можете найти ассоциативный массив, который представляет собой сопоставление исходной строки JSON с массивами и типами данных Mikrotik. Содержание здесь не привожу, оно приведено ниже.

Вы можете установить глобальную переменную $Jdebug (true), тогда в ручном режиме при вызове функции в консоли роутера вы сможете получить дополнительный вывод для нужд отладки.



Многомерные ассоциативные массивы

Язык Микротик поддерживает вложенные (многомерные) ассоциативные массивы.

Вот пример вывода глобальной переменной $JParseOut, в которую записывается результат работы парсера:

[admin@MikroTik] > :put $JParseOut ok=true;result=has_custom_certificate=false;max_connections=40;pending_update_count=0;url= https://*****.

ru:8443



[admin@MikroTik] > :put ($JParseOut->"result") has_custom_certificate=false;max_connections=40;pending_update_count=0;url= https://*****:8443



[admin@MikroTik] > :put ($JParseOut->"result"->"max_connections") 40

Видно, что ключ «результат» также содержит в качестве значения ассоциативный массив, к элементам которого можно добраться с помощью цепочки «-> ».

При этом важно, чтобы все элементы имели свой тип данных (число, строка, логическое значение, массив):

[admin@MikroTik] > :put [:typeof ($JParseOut->"result")] array



[admin@MikroTik] > :put [:typeof ($JParseOut->"result"->"max_connections")] num



[admin@MikroTik] > :put [:typeof ($JParseOut->"result"->"url")] str

Именно эксперименты с такой многоуровневой конструкцией натолкнули на идею создания парсера JSON. Формат JSON хорошо транслируется во внутреннее представление языка сценариев Mikrotik.

Функции, рекурсивный вызов

Для многих не секрет, что определить свои функции можно на форуме сайта.

www.mikrotik.com Вы можете найти множество примеров таких структур.

Мой парсер также построен на функциях, вложенных и рекурсивных вызовах.

Да, рекурсивные вызовы функций поддерживаются! В качестве примера приведу функцию $fJParsePrint из набора парсера, которая печатает в читаемом виде содержимое ассоциативного массива $JParseOut (точнее, в виде путей, которые можно копировать и использовать в своих скриптах для доступа к массиву).

элементы) и результат его работы:

:global fJParsePrint :if (!any $fJParsePrint) do={ :global fJParsePrint do={ :global JParseOut :local TempPath :global fJParsePrint :if ([:len $1] = 0) do={ :set $1 "\$JParseOut" :set $2 $JParseOut } :foreach k,v in=$2 do={ :if ([:typeof $k] = "str") do={ :set k "\"$k\"" } :set TempPath ($1. "->" .

$k) :if ([:typeof $v] = "array") do={ :if ([:len $v] > 0) do={ $fJParsePrint $TempPath $v } else={ :put "$TempPath = [] ($[:typeof $v])" } } else={ :put "$TempPath = $v ($[:typeof $v])" } } }}



[admin@MikroTik] > $fJParsePrint $JParseOut->"ok" = true (bool) $JParseOut->"result"->"has_custom_certificate" = false (bool) $JParseOut->"result"->"last_error_date" = 1524483204 (num) $JParseOut->"result"->"last_error_message" = Connection timed out (str) $JParseOut->"result"->"max_connections" = 40 (num) $JParseOut->"result"->"pending_update_count" = 0 (num) $JParseOut->"result"->"url" = https://*****.

ru:8443 (str)

В коде функции вы можете увидеть рекурсивный вызов, который передает текущий уровень вложенности и элемент подмассива внутрь функции, таким образом обходя все дерево массива в переменной $JParseOut.

$fJParsePrint $TempPath $v

Ради интереса можно вызвать эту функцию с параметрами из консоли, указать начальный путь вывода, например, «home», а переменную массива вручную:

[admin@MikroTik] > $fJParsePrint "home" $JParseOut home->"ok" = true (bool) home->"result"->"has_custom_certificate" = false (bool) home->"result"->"last_error_date" = 1524483204 (num) home->"result"->"last_error_message" = Connection timed out (str) home->"result"->"max_connections" = 40 (num) home->"result"->"pending_update_count" = 0 (num) home->"result"->"url" = https://*****.

ru:8443 (str)

Функция написана для обработки вызова с параметрами и без, т.е.

используется переменное количество параметров.

Традиционно перед вызовом нужно объявить (точнее объявить) глобальные переменные и функции внутри блока, в данном случае в теле функции.

Обратите внимание, что есть объявление «:global fJParsePrint», т.е.

объявлена сама функция, ничего удивительного, это нужно для рекурсивного вызова.



Парсинг строки кода на лету и ее выполнение

Давайте посмотрим на функцию $fByteToEscapeChar:

:global fByteToEscapeChar :if (!any $fByteToEscapeChar) do={ :global fByteToEscapeChar do={ # :set $1 [:tonum $1] :return [[:parse "(\"\\$[:pick "0123456789ABCDEF" (($1 >> 4) & 0xF)]$[:pick "0123456789ABCDEF" ($1 & 0xF)]\")"]] }}

Эта функция преобразует параметр $1 (номер байта) в строковый символ, т. е.

преобразует код ASCII в символы.

Например, есть код 0x2B, который соответствует символу «+».

Вы можете установить символ как код, используя escape-код «\NN», где NN — это код ASCII, но только в строке:

[admin@MikroTik] > :put "\2B" +

Но если исходный код представлен числом (байтом), то получение символа – непростая задача, так как для этого нет готовой встроенной функции.

Здесь на помощь приходит еще одна встроенная функция парсинга, позволяющая собрать строку — выражение, escape-последовательность на основе исходного числа, например, «(\2B)».

Выражение типа:

:put [:parse "(\"\\$[:pick "0123456789ABCDEF" ((0x2B >> 4) & 0xF)]$[:pick "0123456789ABCDEF" (0x2B & 0xF)]\")"] (<%% + )

— собирает строку кода, которую необходимо выполнить, чтобы получить на выходе строковый символ.

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

], поэтому итоговое выражение приобретает довольно замысловатый вид, обрамленный двойными квадратными скобками [[.

]], после чего получаем ожидаемый символ:

[admin@MikroTik] > :put [[:parse "(\"\\$[:pick "0123456789ABCDEF" ((0x2B >> 4) & 0xF)]$[:pick "0123456789ABCDEF" (0x2B & 0xF)]\")"]] +



Telegram-бот на основе парсера JSON



Опросный бот

Теперь, когда мы можем легко получить доступ к содержимому ответов JSON из API Telegram, давайте напишем первую версию бота, который работает в режиме опроса, т. е.

периодического запроса к API Telegram. Он будет отвечать на некоторые команды, например, uptime — запрос времени работы роутера, ip — запрос всех IP-адресов DHCP-клиента, parse — вывод содержимого переменной $JParseOut, т.е.

разобранный JSON-ответ на последний запрос.

Когда вы вводите любые другие команды или символы, бот просто ответит эхом.

Этот бот представляет собой одиночный скрипт, который периодически вызывается из планировщика, например раз в минуту, и считывает API-функцию телеграммы getUpdates, после разбора ответа делает выбор действия if-else с помощью $v-> "message"- > «текстовая» переменная.

Также хочу обратить внимание на вызов функции text=$[$fJParsePrintVar] из набора функций парсера, которая возвращает содержимое $JParseOut в читаемом виде.

Полный код бота представлен ниже.

Плюс: поскольку обмен инициирует скрипт, то он будет работать через NAT без настроек.

Недостатки такой реализации: скорость ответа Микротика на запрос определяется частотой вызова скрипта; при каждом вызове выполняется запрос getUpdates, парсящий, в общем, полный цикл запроса-анализа, который нагружает процессор; каждый вызов приводит к записи файла j.txt, это плохо для раздела на флешке, но неплохо для RAM-диска.

Код скрипта опросного бота: Telegram PollingBot

/system script run JParseFunctions :global TToken "12312312:32131231231" :global TChatId "43242342423" :global Toffset :if ([:typeof $Toffset] != "num") do={:set Toffset 0} /tool fetch url=" https://api.telegram.org/bot$TToken/getUpdates\Эchat_id=$TChatId&offset=$Toffset " dst-path=j.txt #:delay 2 :global JSONIn [/file get j.txt contents] :global fJParse :global fJParsePrintVar :global Jdebug false :global JParseOut [$fJParse] :local Results ($JParseOut->"result") :if ([:len $Results]>0) do={ :foreach k,v in=$Results do={ :if (any ($v->"message"->"text")) do={ :if ($v->"message"->"text" ~ "uptime") do={ /tool fetch url=" https://api.telegram.org/bot$TToken/sendmessage\Эchat_id=$TChatId " http-method=post http-data="text=$[/system resource get uptime]" keep-result=no } else={ :if ($v->"message"->"text" ~ "ip") do={ /tool fetch url=" https://api.telegram.org/bot$TToken/sendmessage\Эchat_id=$TChatId " http-method=post http-data="text=$[/ip dhcp-client print as-value]" keep-result=no } else={ :if ($v->"message"->"text" ~ "parse") do={ /tool fetch url=" https://api.telegram.org/bot$TToken/sendmessage\Эchat_id=$TChatId " http-method=post http-data="text=$[$fJParsePrintVar]" keep-result=no } else={ /tool fetch url=" https://api.telegram.org/bot$TToken/sendmessage\Эchat_id=$TChatId " http-method=post http-data="text=$($v->"message"->"text")" keep-result=no } } } } :set $Toffset ($v->"update_id" + 1) } } else={ :set $Toffset 0 }



Вебхук-бот

Чтобы избавиться от этих недостатков, создадим вторую версию скрипта, который будет обрабатывать Webhook, т.е.

когда Telegram API сам «попадает» на роутер по заданному адресу с целью отправки новых сообщений.

Микротик, конечно, не умеет создавать внутри себя собственный веб-сервер, необходимый для полноценной работы уведомлений Webhook из Telegram API. Но можно хитро обойти эту проблему.

Для этого нужно мониторить некий несуществующий TCP-сокет, в который будет «тыкаться» Webhook, делается это с помощью правила Mangle (или Firewall).

API Telegram позволяет работать с Webhook (функция API setWebhook), указывает доменное имя роутера и TCP-порт, SSL-сертификат здесь никакой роли не играет, т.е.

не нужен! Изменяя значение счетчика пакетов правила Mangle, можно понять, что Webhook (или что-то еще ;) «долбится» в несуществующий TCP-порт; лишнее можно отрезать фильтром src-address=149.154.167.192/26).

К сожалению, правило Mangle не может напрямую вызвать пользовательский скрипт (такого действия нет), но можно запросить счетчик пакетов из скрипта.

Скрипт также выполняется по расписанию, но с минимальным интервалом в 1 секунду.

В состоянии ожидания выполняется только проверка изменения значения счетчика пакетов.

После обнаружения нового входящего пакета в Telegram API отправляется запрос на отключение Webhook, а сообщения читаются и обрабатываются, как и в первой версии скрипта (опрос), затем Webhook снова включается и возвращается в состояние ожидания.

Основные шаги проиллюстрированы на схеме работы скрипта.



Телеграм-бот для Микротика с Webhook и парсером JSON

Как уже говорилось, скрипт запускается часто, и для предотвращения дублирования экземпляров вызываемого скрипта в начале скрипта делается защита от дублирования; Там должно быть указано название этого скрипта.



:if ([:len [/system script job find script=TelegramWebhookBot]] <= 1) do={.

}

Код скрипта бота Webhook: TelegramWebhookBot

:if ([:len [/system script job find script=TelegramWebhookBot]] <= 1) do={ #:while (true) do={ :global TelegramWebhookPackets :local TWebhookURL " https://www.yourdomain " :local TWebhookPort "8443" # Create Telegram webhook mangle action :if ([:len [/ip firewall mangle find dst-port=$TWebhookPort]] = 0) do={ /ip firewall mangle add action=accept chain=prerouting connection-state=new dst-port=$TWebhookPort protocol=tcp src-address=149.154.167.192/26 comment="Telegram" } :if ([/ip firewall mangle get [find dst-port=$TWebhookPort] packets] != $TelegramWebhookPackets) do={ /system script run JParseFunctions :local TToken "123123123:123123123123123" :local TChatId "3213123123123" :global TelegramOffset :global fJParse :global fJParsePrintVar :global Jdebug false :global JSONIn :global JParseOut :if ([:typeof $TelegramOffset] != "num") do={:set TelegramOffset 0} :put "getWebhookInfo" :do {/tool fetch url=" https://api.telegram.org/bot$TToken/getWebhookInfo " dst-path=j.txt} on-error={:put "getWebhookInfo error"} :set JSONIn [/file get j.txt contents] :set JParseOut [$fJParse] :put $JParseOut :if ($JParseOut->"result"->"pending_update_count" > 0) do={ :put "pending_update_count > 0" :do {/tool fetch url=" https://api.telegram.org/bot$TToken/deleteWebhook " http-method=get keep-result=no} on-error={:put "deleteWebhook error"} :put "getUpdates" :do {/tool fetch url=" https://api.telegram.org/bot$TToken/getUpdates\Эchat_id=$TChatId&offset=$TelegramOffset " dst-path=j.txt} on-error={:put "getUpdates error"} :set JSONIn [/file get j.txt contents] :set JParseOut [$fJParse] :put $JParseOut :if ([:len ($JParseOut->"result")] > 0) do={ :foreach k,v in=($JParseOut->"result") do={ :if (any ($v->"message"->"text")) do={ :if ($v->"message"->"text" ~ "uptime") do={ :do {/tool fetch url=" https://api.telegram.org/bot$TToken/sendmessage\Эchat_id=$TChatId " http-method=post http-data="text=$[/system resource get uptime]" keep-result=no} on-error={:put "sendmessage error"} } else={ :if ($v->"message"->"text" ~ "ip") do={ :do {/tool fetch url=" https://api.telegram.org/bot$TToken/sendmessage\Эchat_id=$TChatId " http-method=post http-data="text=$[/ip dhcp-client print as-value]" keep-result=no} on-error={:put "sendmessage error"} } else={ :if ($v->"message"->"text" ~ "parse") do={ :do {/tool fetch url=" https://api.telegram.org/bot$TToken/sendmessage\Эchat_id=$TChatId " http-method=post http-data="text=$[$fJParsePrintVar]" keep-result=no} on-error={:put "sendmessage error"} } else={ :if ($v->"message"->"text" ~ "add") do={ :local addIP [:toip [:pick ($v->"message"->"text") 4 [:len ($v->"message"->"text")]]] :if ([:typeof $addIP] = "ip") do={ :do {/ip firewall address-list add address=$addIP list=ExtAccessIPList timeout=10m comment="temp"} on-error={:put "ip in list error"} } :local Str1 "" :foreach item in=[/ip firewall address-list print as-value where list=ExtAccessIPList and dynamic] do={:set Str1 ($Str1 .

"$($item->"address") $($item->"timeout") $($item->"comment")\r\n")} :do {/tool fetch url=" https://api.telegram.org/bot$TToken/sendmessage\Эchat_id=$TChatId " http-method=post http-data="text=$Str1" keep-result=no} on-error={:put "sendmessage error"} } else={ :put ($v->"message"->"text") :do {/tool fetch url=" https://api.telegram.org/bot$TToken/sendmessage\Эchat_id=$TChatId " http-method=post http-data="text=$($v->"message"->"text")" keep-result=no} on-error={:put "sendmessage error"} } } } } } :set $TelegramOffset ($v->"update_id" + 1) } } else={ # :set $TelegramOffset 0 } :put "getUpdates" :do {/tool fetch url=" https://api.telegram.org/bot$TToken/getUpdates\Эchat_id=$TChatId&offset=$TelegramOffset " keep-result=no} on-error={:put "getUpdates error"} :put "setWebhook" :do {/tool fetch url=" https://api.telegram.org/bot$TToken/setWebhook\Эurl=$TWebhookURL:$TWebhookPort " keep-result=no} on-error={:put "setWebhook error"} } else={ :if ($JParseOut->"result"->"url"="") do={ :put "setWebhook" :do {/tool fetch url=" https://api.telegram.org/bot$TToken/setWebhook\Эurl=$TWebhookURL:$TWebhookPort " keep-result=no} on

Теги: #Сетевые технологии #Алгоритмы #ИТ-инфраструктура #Администрирование серверов #Микротик Telegram бот скрипт json

Вместе с данным постом часто просматривают: