Telegram bot for Mikrotik with Webhook and JSON parser

    Do you think it is possible, using only the Mikrotik script, to write an interactive Telegram bot that will work entirely in a router environment with Webhook support, incoming events from the Telegram API?

    Preface: I
    put off writing this article for a long time, but recent events around the Telegram messenger spurred me with new energy to take up my plan. I believe that Telegram, among other things, is a convenient niche platform for informational interactive services, and it should exist and compete well with other systems without artificial restrictions. Let this article be a modest contribution to supporting Telegram.


    Before answering the asked question, you need to understand what is minimally required from the bot platform for Webhook to work. And here's what: the presence of a WEB server with SSL, a valid SSL certificate or a self-signed certificate loaded into the Telegram API, the URL of the WEB server for processing Webhook. And if access from the Internet (real IP, domain name) to the router can be ensured, then Mikrotik has a problem with the WEB server (there is no longer even SSL), there is simply no user server. But this problem can be circumvented, a solution will be proposed below.

    The Telegram bot for Mikrotik is just the "tip of the iceberg." It is based on the full-fledged (as far as possible) JSON parser written in the Mikrotik script language. In general, to write an average bot, it is not necessary to do a full analysis of JSON, you can completely manage to search and copy in the lines, but I chose a different path. Next, I will talk about the parser and some programming techniques of the Mikrotik script, mastered while working on it.

    Parser JSON strings in the Mikrotik language


    I admit, creating a JSON parser in the Mikrotik script language was a sport for me. It was interesting whether this could be done at all, given the limitations of the Mikrotik scripting language. But the farther into the code, the more clearly the paths to the final goal were seen. Earlier, I brought to mind a similar VBScript parser, found on the open spaces, for the needs of one SCADA system, so I took the logic of the VBScript implementation as a basis, reworked it taking into account the constructions of the Mikrotik language and designed the code in the form of a library of functions. Along the way, I discovered several interesting features of the scripting language, which I will gladly share below. A few words about restrictions. First: the string length in Mikrotik variables is 4096 bytes, there's nothing to be done, all that is simply not assigned to a variable anymore. The second:

    Using JSON parser




    Functions are represented by the JParseFunctions library file, which “expands” the function code into global variables. This library can be called in scripts as many times as desired, without much loss of performance; for each function, it is checked for its “expansion” in global variables to avoid duplication of actions. When editing a library file, you need to delete the global variables - the function code so that they are "recreated" with the updates.

    JParseFunctions library code:

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

    Consider the work of the parser using the example of a piece of Telegram bot code. Let's execute the following commands step by step.

    A request for the status of the getWebhookInfo Telegram API function, which returns a JSON string to the j.txt file:

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

    Loading a JSON string into an input variable:

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

    Executing the parser function $ fJParse and uploading the result to the variable $ JParseOut

    :set JParseOut [$fJParse];

    In $ JParseOut, you can find an associative array, which is a mapping of the original JSON string to Mikrotik arrays and data types. I do not provide the content here, it is given below.

    You can set the global variable $ Jdebug (true), then in manual mode, when you call the function in the console of the router, you can get additional output for debugging needs.

    Multidimensional Associative Arrays


    Mikrotik supports nested (multidimensional) associative arrays.
    Here is an example of the output of the global variable $ JParseOut, into which the result of the parser is written:

    [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

    It can be seen that the key "result" also contains an associative array as a value, the elements of which can be reached using the "->" chain. Moreover, it is important that all elements have their own data type (number, string, boolean, array):

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

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

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

    It was experiments with this multi-level construction that prompted the idea of ​​creating a JSON parser. The JSON format translates well into such an internal representation of the Mikrotik scripting language.

    Functions, recursive call


    For many, it is no secret that you can define your own functions; on the forum site www.mikrotik.com you can find many examples of such designs. My parser is also built on functions, nested and recursive calls. Yes, recursive function calls are supported!

    As an example, I’ll give the $ fJParsePrint function from the parser set that prints in a readable form the contents of the associative array $ JParseOut (or rather, in the form of paths that you can copy and use in your scripts to access the elements of the array) and the result of its work:

    :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)

    You can see a recursive call in the function code that passes the current attachment level and subarray element into the function, thus traversing the entire array tree in the $ JParseOut variable.

    $fJParsePrint $TempPath $v

    For interest, you can call this function with parameters from the console, specify the initial output path, for example, “home”, and the array variable manually:

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

    The function is written to handle a call with and without parameters, i.e. a variable number of parameters is used. Traditionally, before calling you need to declare (or rather declare) global variables and functions inside the block, in this case, in the body of the function. Note that there is an ad ": global fJParsePrint", i.e. the function itself is declared, no wonder it is needed for a recursive call.

    Parsing a string with the code "on the fly" and its execution


    Let's look at the $ fByteToEscapeChar function:

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

    This function converts the parameter $ 1 (byte number) to a string character, i.e. Converts an ASCII code to a character. For example, there is a code 0x2B, which corresponds to the symbol "+". You can specify a character code using the escape "\ NN", where NN is the ASCII code, but only on the line:

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

    But if the source code is represented by a number (byte), then obtaining a character is not a simple task, since there is no ready-made built-in function for this. Here another built-in parse function comes to the rescue, which allows you to collect a string - an expression that controls the sequence based on the original number, for example, "(\ 2B)".

    Expression of the form:

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

    - collects a line of code that must be executed to get a string character in the output. The second execution of the code received after parse is done using the same square brackets [...], so the final expression takes on a rather intricate look, framing with double square brackets [[...]], after which we get the expected character:

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

    Telegram bot based on JSON parser


    Polling bot


    Now that we can easily access the content of JSON responses from the Telegram API, we will write the first version of the bot working in polling mode, i.e. Telegram API periodic request. It will respond to some commands, for example, uptime - requesting router operating time, ip - requesting all DHCP Client IP addresses, parse - displaying the contents of the variable $ JParseOut, i.e. parsed JSON response to the last request. When you enter any other commands or characters, the bot will simply echo.

    This bot is one script that is called periodically from the scheduler, for example, once a minute and reads the getUpdates telegram API function. After parsing the response, it makes if-else chooses the action for the variable $ v -> "message" -> "text". I also want to pay attention to the call to the function "text = $ [$ fJParsePrintVar]" from the set of parser functions, which returns the contents of $ JParseOut in a readable form. The full bot code is presented below.

    From the pros: since the script initiates the exchange, it will work through NAT without settings.
    The disadvantages of this implementation: the speed of Mikrotik's response to a request is determined by the frequency of the script call, with each call the getUpdates request is executed, parsing, in general, a complete request-analysis cycle, which loads the processor; each call leads to writing a j.txt file, for a partition on a flash disk it is bad, for a RAM disk it is not scary.

    Polling bot script code:

    TelegramPollingBot
    /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 bot


    To get rid of these minuses, we will create the second version of the script that will handle Webhook, i.e. when the Telegram API itself "slams" at the specified address into the router in order to send new messages.

    Mikrotik, of course, does not know how to make a custom Web server inside itself, which is required for the full operation of Webhook notifications from the Telegram API. But you can cunningly get around this problem. To do this, you need to monitor a certain non-existent TCP socket into which the Webhook will "crash", this is done using the Mangle (or Firewall) rule. The Telegram API includes work with Webhook (setWebhook API function), the domain name of the router and the TCP port are indicated, the SSL certificate does not play any role here, i.e. not needed! By changing the value of the packet counter of the Mangle rule, you can understand that a Webhook is "chopped" into a non-existent TCP port (or something else;), the excess can be cut off with the src-address = 149.154.167.192 / 26 filter). Unfortunately, the Mangle rule cannot directly call a user script (there is no such action), but you can poll the packet counter from the script. The script also runs on schedule, but with a minimum interval of 1 second. In the standby state, only the change in the packet counter value is checked. After detecting a new incoming packet, a request is sent to the Telegram API to disable Webhook, and messages are read and processed as in the first version of the script (polling), then Webhook is turned on again with a return to the standby state. The main steps are illustrated in the diagram of the script. and reading and processing messages is done as in the first version of the script (polling), then Webhook is turned on again with a return to the standby state. The main steps are illustrated in the diagram of the script. and reading and processing messages is done as in the first version of the script (polling), then Webhook is turned on again with a return to the standby state. The main steps are illustrated in the diagram of the script.



    As already mentioned, the script is run often, and in order to avoid duplication of instances of the called script, protection against duplication is made at the beginning of the script, the name of this script should be indicated there.

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

    Webhook bot script code:

    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-error={:put "setWebhook error"}
          }
        }
      :set TelegramWebhookPackets [/ip firewall mangle get [find dst-port=$TWebhookPort] packets]
      :put "--------------------------------------------------"
      }
    }
    


    The add command was added to this bot script, which adds an IP address to the resolving list of ExtAccessIPList addresses for 10 minutes.

    An example of a request and response in Telegram. The last line is the temporary address already added to the IP list: It remains to indicate the minuses and advantages of this approach. Cons: Webhook needs access to the IP and the specified TCP port of the router from the Internet, in fact a real IP address, preferably bound to a domain. As for the availability of a domain name, I’m not sure whether you need to “smoke” the Telegram API, perhaps it does not allow you to make a Webhook on an IP server. It works with a dynamic real IP address and a dynamic DNS service.

    >add 1.1.1.1
    >> 90.0.0.97 h*******
    100.0.0.157 6*******
    90.0.0.2 i*******.ru
    100.0ю0.66 b*******.ru
    1.1.1.1 00:10:00 temp




    Pros: the main part of the script actually sleeps all the time, waiting for incoming packets to the surrogate socket. If you call the script often (I have it once per second), then the Webhooks are executed very quickly, as in normal Telegram bots.

    Also, the source code can be found here .

    And a little video:



    Also popular now: