Again Mikrotik and again Telegram…

Hello, friends!

Two years ago I wrote an article dedicated to the development of RouterOS.

As part of that project, we controlled Mikrotik devices via a Telegram bot. A lot of experience and code were gained, in the form of libraries in the Mikrotik Script language, for working with the Telegram API, handler functions, and all sorts of forms.

Then came the understanding that Mikrotik in conjunction with Telegram is powerful and there is an option to inexpensively present the service. An idea was needed…

The first thought was to create a cloud storage in Telegram. But in my opinion, this has already been done somewhere and is not too difficult, and I wanted to squeeze the maximum out of the hardware and Telegram API. My friend suggested a great idea for implementation and even agreed to finance this event.

The gist of it is short

Let's imagine a service that allows you to specify a point on an interactive map, attach a comment, video, photo or voice message to it, and then publish it in one of two sections.

Points in sections are displayed as a list with brief information about their contents, creation time and distance to the user.

Any user of the service can find this point if they are within a radius of up to 5 km from it.

When you select it, a form opens where you can build a route, view all the content attached to it, and, if desired, add your own.

In the comments section, there is an option to Disprove or Approve a point if the user is within a radius of 300 meters from it.

Every point has a lifetime. For the first section, it is 1 hour. For the second 24 hoursAfter a timeout, they are permanently deleted.

It is possible to create a point with current coordinates with one click.

Picture
d

d

A detailed description can be found on the channel or in the certificate itself bot.

All this had to be implemented. only by means RouterOS And Telegram APIwithout using third-party services.

Keeping in mind that ROS supports multithreading, was purchased Mikrotik CCR-1036which has on board 36 processor cores and 4 gig of RAM. Housed in its own data center, it acts as backend-A.

And the work began…

Here I want to talk about the tasks that had to be solved in the process. Along the way, I will share the code of the libraries and some functions.

And the tasks were set interesting, let's take a closer look at them.

According to the terms, we cannot use third-party systems. On the one hand, this simplifies the entire scheme, and on the other, it complicates the development. But we are not afraid, we have experience – the son of difficult mistakes.

Main, What It is necessary to implement within the framework of this task, this is the calculation of the distance between two geographic coordinates.

The distance must be in a straight line, since we will be searching within a certain radius.

About coordinates

Telegram carefully sends them to us when you enable the option in the bot settings Inline Location Data.

They are requested every time the user clicks a button that calls Inline menu.

If there are permissions and geolocation is enabled, then the bot will get inline_query with section location as latitude=41.101235 and longitude=35.975326.

It would seem that this is not difficult, there are formulas. But in ROS we can only operate with integers. Here we had to remember mathematics, show a little ingenuity, and also write several auxiliary functions to “assemble the formula” with their help.

First, we need the exponentiation and square root functions.

First It is implemented simply.

Code of the first
#---------------------------------------------------tePow--------------------------------------------------------------
#   Function calculates the power of a number
#   Params for this function:

#   1.  fInputValue 			  	-   a number raised to a power
#   1.  fDegree  			    	-   degree of number

#---------------------------------------------------tePow--------------------------------------------------------------

:global tePow
:if (!any $tePow) do={ :global tePow do={

:local fInputValue $1
:local fDegree $2

:local result []
:local resultValue 1
:local operatingTime [:time {

  :if ($fInputValue = 0 || $fInputValue = 1) do={ :return $fInputValue }

  :for i from=1 to=$fDegree do={
    :set resultValue ($resultValue * $fInputValue)
  }
  :set result $resultValue

  }]
  :return $result
  }
}

Second a little more complicated. It is used binary searchalso known as the bisection method. This gives the required accuracy for integers.

Code two
#---------------------------------------------------teSqrt--------------------------------------------------------------
#   Function returns the root of an integer
#   Params for this function:

#   1.  fInputValue 					-   the number from which to extract the root

#---------------------------------------------------teSqrt--------------------------------------------------------------

:global teSqrt
:if (!any $teSqrt) do={ :global teSqrt do={
  :local root []
  :local iteration 0
  :local operatingTime [:time {

    :if ($fInputValue = 0 || $fInputValue = 1) do={ :return $fInputValue }

    :local lowerBound 1
    :local upperBound $fInputValue
    :set root ($lowerBound + (($upperBound - $lowerBound) / 2))

    :while ($root > ($fInputValue / $root) || ($root + 1) <= ($fInputValue / ($root + 1))) do={

      :if ($root > ($fInputValue / $root)) do={
        :set upperBound $root
        } else={
          :set lowerBound $root;
        }
        :set root ($lowerBound + (($upperBound - $lowerBound) / 2))
        :set iteration ($iteration + 1)
      }
  }]
  :return $root
  }
}

The third calculates the length of the latitude, depending on its number.

And the fourth one with their help, calculates the distance between two coordinates.

Unfortunately, I can't publish the code of the latter, because I'm bound by NDA. But the formulas are all known, you can find them on the Internet and turn them into code. Checked.

We also some constants will be needed.

The length of one degree of the meridian, which is about 111.3 km. We get rid of fractions by successively multiplying the value by 10.

This trick must be done with all values ​​if they are not integers or if the bit depth needs to be increased.

For example Pi will turn into 31 415 926.

The arithmetic mean radius of the Earth in 6 371 009 meters.

The main thing is not to forget to return everything back to the required accuracy, by dividing by the same 10.

This trick has its downsides. You have to operate with large numbers and it may happen that the value does not fit in 64bitallocated to the type num. This must be remembered.

As a result, we obtain the accuracy of calculating the distance between two geographic coordinates +-5 meterswithin a radius of up to 200 km, which is more than enough for our task.

For temporary storage and data processing multidimensional are used associative arrays. The required structures are assembled from them.

For example, this is what an array for points looks like.

Array
Reminder

We remember that in Mikrotik: “Both indexed and named elements live well in one array. The former are accessible by index, the latter by key. Thus, each array can contain service information about itself..”

The array's name is written at index zero. During the execution process, you can find out who exactly we are working with at the moment.

:set dbase4535 ({"dbase4535";

                  "4531"={
                    "copsMobile"={
                      "45.200235,31.928862"={
                        pointid="W3K1ruohwV3omXmpopsikdmp95IRoNnPUH2bEy5FPlCKa1t1HHodqBx";
                        title="--";
                        description="------------";
                        createduserid=1111130017;
                        lati="45.100235";
                        longi="31.928862";
                        video={
                          "2756w6d12:54:05.033177800"="BAACAgIAAxkBAAOAY0GUbkBpjYYhxx05BTOAJZlTECAAAqcdAALg2AlKCcjm480gBTMqBA";
                          "2756w6d12:54:15.624125900"="BAACAgIAAxkBAAOBY0GU7wu2Uqvlt7xsHTCv_6W8SqUAAqsdAALg2AlKQwHC1JSywQ8qBA";
                        };
                        photo={
                          "2756w6d12:52:40.116593100"="AgACAgIAAxkBAAICqGNfscL4_oRe0d3SKSMeRoVoiGW1AAKkvjEbBRoAAUsHpAG4FwYxNwEAAwIAA3gAAyoE";
                          "2756w6d12:52:55.667629500"="AgACAgIAAxkBAAICqWNfseXjErj9i8AcuX7pWjQ_WqPPAAKlvjEbBRoAAUsAAcGTzEIxjDIBAAMCAAN5AAMqBA";
                        };
                        voice={
                          "2756w6d12:53:36.481467600"="AwACAgIAAxkBAAIBRGNaYnh-1sqm0xc24ZQrFv-sE80RAAKSHgACEmzRSiitnhuSUi2NKgQ";
                          "2756w6d12:53:54.017525900"="AwACAgIAAxkBAAIBRWNaYptt9RJO4blNUljDGwZSOwcYAAKUHgACEmzRSr5nyzxqYciJKgQ";
                        };
                        notes={
                          "2756w6d12:56:02.007671800"={confirmed=true;userid="1111130017";};
                          "2756w6d12:50:26.628204300"={confirmed=true;userid="3213430070";};
                        };
                        createtime=2755w4d12:16:41.504179900;
                        lastupdate=2755w4d12:17:21.308175300;
                      };
                      "45.099710,31.930477"={
                        pointid="W3K1ruohwV3omXa4Xdfv5IRoNnPUH2bEy5FPlCKa1t1HHodqBx";
                        title="--";
                        ...
                      };
                    };
                  };
               })

Here we need to clarify. The bot operates in a limited area. 5-179 longitude from west to east and 40-80 latitude from south to north. This is all of Russia, part of Southeast Asia and almost all of Europe.

The map is conditionally divided into squares 5 degrees. Each square is a separate array. When creating a point, it is first determined which square it will fall into and if the required array is not yet in memory, it is created with a name consisting of a prefix and the coordinates of the first two digits of the latitude-longitude of the square. For example – dbase4535.

The main keys of the array are formed dynamically based on the coordinates, so that later when searching, you can go straight to the right place.

The search function for arrays with points takes into account neighboring squares if they fall within the radius. Old records are deleted by timeout.

And of course, we make full use of the device's many cores.

Each message is launched for processing in a separate process, which, in turn, can spawn its own processes.

Let me remind you that the command used for this is :execute. The code for it, in most cases, is generated dynamically during the execution of the module.

This is mainly needed to use the value of a variable as the name of another variable or as an array key.

Example
:if ($lfPathStr = true) do={ :execute ":set (\$$dbaseName->\"$dbaseLatiLongi\"->\"$lfTagName\"->\"$currenScrintLati,$currenScrintlongi\") $currentArray" }

In the previous article I also told that you can store ready-made pieces of code or entire functions in an array. Moreover, if you run them with the command :execute, then in this code we can use the whole context Environment, i.e. global variables do not need to be declared there if they were previously created.

The code assembled from the elements of such an array, by the command :execute is immediately sent for execution, bypassing the deployment stage Environment or reading from disk.

IN Environment now only arrays with code and necessary variables may remain, which are much less than the functions themselves. But this is already a story about the architecture, which will change significantly compared to the current one. There will probably be a separate article about this.

Along the way, an interesting bug was discovered in Winboxwhich, in my opinion, can be considered dangerous.

Let me explain what the point is.
:local test ":global testArray [:toarray \"\"]; :for key from=0 to=150000 do={:set (\$testArray->\$key) [:tostr (\"str=\".\$key)]}"; 
:execute script=$test

This code creates an array testArray from 150,000 and one record. The element is the string “str=$key”, where $key is the row number.

Already during its creation, something interesting begins.

IN ROS 6it becomes invisible on the tab Environment. But it is available from the console. and you can work with it.

Any element of such an array, as we know, can be a code. And if it is launched by the command :execute, That on the tab Jobs his The execution does not immediately raise suspicion because the line has no script name and looks like a running console.

Only the console status bar says – L (login), and the code has – C (command).

IN ROS 7 a little differently. On the tab Environment in red letters ERROR: action failed. It is also performed without a name.

Another ROS feature is the file downloaded via ftp and containing in the name *.auto.rsc, will be executed automatically.

All of this together can lead to dire consequences, so always check third-party code before running it.

But let's get back to the task. All the main components are ready and now we need to convey the functionality to the user through the interface.

Last time we used regular ones inlineButton-ы, which are located under the picture, and for the lists they used groupslinks to which were “clung” to the button. In this place, the principle of one window was violated.

InlineMenu radically change the situation. With their help, you can display various content in the form of lists that smoothly appear from the bottom when you click on the button.

They carry: media, links, coordinates or just text, in the form of InlineQueryResult

Each item of such a menu can contain InputMessageContent. This is what will fly into the chat when you select this item. There are five types, but for now we are only interested in InputTextMessageContent. There is just text inside, but we “sew” commands for the bot into it.

If in InputMessageContent Nothing If you don't specify it, the content itself will be sent to the chat.

Instead of a picture, there is now a map. It is interactive, can display a point with arbitrary coordinates and the user's current location. The point is broadcast by the bot, according to the coordinates that we give it. Unfortunately, it is impossible to display several points on the map at the same time.

But you can build a route. Telegram itself can't do this, but it transmits the user's coordinates and points to the default map program on your device. This expands the functionality quite well.

There is still a fly in the ointment. A message with the type Location It has live_period in the range from 60 before 86400 seconds. At this time, it can be edited, i.e. change the keyboard or coordinates, after that it will not work. Now it seems that the value has appeared 0x7FFFFFFFFso that you could edit endlessly, but at that time it did not exist yet. The bot was launched in December 2022.

As a result, we got a rather functional interface. All actions occur within one message, which we edit depending on the command. The main thing is to know its number.

When live_period message has expired, it is programmatically deleted and a Start button press is emulated to display a new one. The message can be deleted programmatically if it is not older than 48 hours. Automatically clearing the chat with the bot once a day ultimately solves the problem.

Now closer to the code…

Basically it's still the same mikRobot, Here its code. The main module is not much different from the published one.

mainBotQuery
:global dbaseBotSettings
:local botID ($dbaseBotSettings->"botID")

:global teCallbackResponse
:global teMessageResponse
:global teViabotResponse
:global teDebugCheck
:global teInlineResponse

:global fJParse
:global Jtoffset
:global JSONIn []
:global JParseOut []
:global Jdebug false

:while (true) do={
	:local fDBGmainBot [$teDebugCheck fDebugVariableName="fDBGmainBot"]
		:do {
			:if ([:typeof $Jtoffset] != "num") do={:set Jtoffset [:tonum $Jtoffset]}
			:local tgUrl "https://api.telegram.org/$botID/getUpdates\?&allowed_updates=[%22chosen_inline_result%22,%22inline_query%22,%22message%22,%22callback_query%22]&offset=$Jtoffset&timeout=5"
			:set JSONIn [/tool fetch ascii=yes url=$tgUrl as-value output=user]
			:if ([:len [:tostr ($JSONIn->"data")]] != 0) do={
				:if ($fDBGmainBot) do={:put "1 mainBot - Jtoffset $Jtoffset"; :log info "1 mainBot - Jtoffset $Jtoffset"}
				:set JSONIn ($JSONIn->"data")
				:set JParseOut [$fJParse]
				:if ([:len ($JParseOut->"result")] != 0) do={
					:set JParseOut ($JParseOut->"result")
				} else={error message="no data..." }
			}

			:if ($fDBGmainBot) do={:log info "teBot result loaded"}

			:foreach k,v in=$JParseOut do={
				:set $Jtoffset ($v->"update_id" + 1)

				:if (any ($v->"callback_query")) do={
					:local queryID ($v->"callback_query"->"id")
					:local userName ($v->"callback_query"->"from"->"username")
					:local userChatID [:tostr ($v->"callback_query"->"from"->"id")]

					:if ([$teCallbackResponse fCallback=$v] = true) do={
					:set $Jtoffset ($v->"update_id" + 1)
					} else={
						:local execText "[\$teDbAlerts fQueryID=$queryID fChatID=\"$userChatID\" fUserName=\"$userName\" fAlertType=\"callbackNotInstalled\"]";
						:execute script=$execText
						:set $Jtoffset ($v->"update_id" + 1)
					}
					:set $Jtoffset ($v->"update_id" + 1)
				}

				:if (any ($v->"message")) do={
					:local messageText ($v->"message"->"text")

					:if ($fDBGmainBot) do={:put "teBot - messageText $messageText"; :log info "teBot - messageText $messageText"}

					:if (any ($v->"message"->"via_bot")) do={

						:if (any ($v->"message"->"venue")) do={
							:if ([$teViabotResponse fMessage=$v] = true) do={
								:set $Jtoffset ($v->"update_id" + 1)
							}
						}

						:if (any ($v->"message"->"text")) do={
							:if ([$teViabotResponse fMessage=$v] = true) do={
								:set $Jtoffset ($v->"update_id" + 1)
							}
						}

					} else={

							:if ([$teMessageResponse fMessage=$v] = true) do={
								:set $Jtoffset ($v->"update_id" + 1)
							}

					}
					:set $Jtoffset ($v->"update_id" + 1)
				}

				:if (any ($v->"inline_query")) do={
					:local queryText [:tostr ($v->"inline_query"->"query")]

					:if ($fDBGmainBot) do={:put "teBot - inline_query $queryText"; :log info "teBot - inline_query $queryText"}
					:if ([:len $queryText] != 0) do={
						:if ([$teInlineResponse fInline=$v] = true) do={
							:if ($fDBGmainBot) do={:log warning "teBot = teInlineResponse runing"}
						}
					}
					:set $Jtoffset ($v->"update_id" + 1)
					:if ($fDBGmainBot) do={:log warning "teBot Jtoffset = $Jtoffset"}
				}
			}
		} on-error={ :if ($fDBGmainBot) do={:log info "no data..."}}
}

IN allowed_updates we add inline_query. Now the bot will accept such messages. They contain, among other things, the field query with type String. IN This field will contain the text that we form at the stage of creating the button.

IN button-e field is responsible for this switch_inline_query_current_chat with type String.

Example
:local pictPhoto ($dbasePictures->"icons"->"pictPhoto")
:set switchCurrentChatValue "$switchCurrent,$inlineCommand,photo"
:local buttonPhoto [$teBuildButton fPictButton=$pictPhoto fTextButton=$photoCount fSwitchCurrentChat=$switchCurrentChatValue]

When you click on such a button, the command embedded in it will be inserted into the input field, immediately after the bot name.

Command template: calledFunctionName,commandName,commandValue,commandTag

Example: @xgeoBot teInlineGeoPoint,currentscreen,photo

In this case calledFunctionName = teInlineGeoPoint. Control will be transferred to her.

Next come the parameters: commandName = currentscreen; commandValue = photo

This command will be processed by the dispatcher function. teInlineResponse. It will generate code to launch the called module and send it for execution with the command :execute.

teInlineResponse Code
#---------------------------------------------------teInlineResponse --------------------------------------------------------------

#	fInline		-		current inline query from Telegram

#   if the global variable fDBGteInlineResponse=true, then a debug event will be logged

#---------------------------------------------------teInlineResponse--------------------------------------------------------------

:global teInlineResponse
:if (!any $teInlineResponse) do={ :global teInlineResponse do={

	:global teDebugCheck
	:local fDBGteInlineResponse [$teDebugCheck fDebugVariableName="fDBGteInlineResponse"]

	:global dbaseGeoUsers

	:local queryID ($fInline->"inline_query"->"id")
	:local userChatID [:tostr ($fInline->"inline_query"->"from"->"id")]

	:if ($fDBGteInlineResponse = true) do={:log info "teInlineResponse userChatID = $userChatID"}

	:if (any ($fInline->"inline_query"->"location")) do={
		:local inlineLatitude ($fInline->"inline_query"->"location"->"latitude")
		:local inlineLongitude ($fInline->"inline_query"->"location"->"longitude")
	} else={
		:local execText "[\$teDbAlerts fChatID=\"$userChatID\" fQueryID=\"$queryID\" fAlertType=\"locationError\"]";
		:execute script=$execText
		:return false
	}

	:local queryChatType [:tostr ($fInline->"inline_query"->"chat_type")]
	:local queryLatitude ($fInline->"inline_query"->"location"->"latitude")
	:local queryLongitude ($fInline->"inline_query"->"location"->"longitude")
	:local queryOffset ($fInline->"inline_query"->"offset")
	:if ([:len $queryOffset] = 0) do={ :set queryOffset 0 }

	:set ($dbaseGeoUsers->$userChatID->"lastseegeo"->"lati") $queryLatitude
	:set ($dbaseGeoUsers->$userChatID->"lastseegeo"->"longi") [:tostr $queryLongitude]
	:set ($dbaseGeoUsers->$userChatID->"lastseegeo"->"lastime") [:timestamp]

	:if ($fDBGteInlineResponse = true) do={:log info "teInlineResponse queryLatitude = $queryLatitude"}
	:if ($fDBGteInlineResponse = true) do={:log info "teInlineResponse queryLongitude = $queryLongitude"}

	:local messageID ($dbaseGeoUsers->$userChatID->"rootmessageid")

	:local query [:toarray ""]
	:set query [:toarray ($fInline->"inline_query"->"query")]
	:if ($fDBGteInlineResponse = true) do={:log info "teInlineResponse query = $query"}

	:local calledFunctionName ($query->0)
	:local commandName ($query->1)
	:local commandValue ($query->2)
	:local commandTag ($query->3)

	:if ([:len $commandTag] = 0) do={
		:set commandTag ""
	} else={ :set commandTag "commandTag=$commandTag" }

	:if ($fDBGteInlineResponse = true) do={:log info "teInlineResponse calledFunctionName = $calledFunctionName"}
	:if ([:len [system script find name=$calledFunctionName]] = 0) do={
		:return false
	}

	:local result []
	:if ([:len $commandValue] = 0) do={
			:set result "[\$$calledFunctionName queryID=$queryID offset=$queryOffset userChatID=$userChatID latitude=$queryLatitude longitude=$queryLongitude messageID=$messageID commandName=$commandName $commandTag]"
	} else={
		:set result "[\$$calledFunctionName queryID=$queryID offset=$queryOffset userChatID=$userChatID latitude=$queryLatitude longitude=$queryLongitude messageID=$messageID commandName=$commandName commandValue=$commandValue $commandTag]"
	}

	:execute script=$result
#	:if ($fDBGteInlineResponse = true) do={:log info "teInlineResponse result = $result"}

	:set $result true
	:return $result
 }
}

When you select an element, a message flies into the chat, the same one InputTextMessageContent. The message seems to be from you, but through a bot. It will differ in the presence of message array via_botin which lie InputMessageContent. Module teViabotResponse processes these commands and runs the required code for execution. It has the same structure as other handlers.

To complete the picture, I will show how it is formed inlineMenu using the example of selecting a default tag in the bot settings form.

Example
			:if ($commandName = "defaultTag") do={

				:local currentTag ($dbaseGeoUsers->$userChatID->"settings"->"defaultTag")
				:local currentTagEmty ($dbasePictures->"teGeoSettings"->"searchRadiusEmpty")
				:local tagChecked ($dbasePictures->"teGeoSettings"->"searchRadiusChecked")

				:local inlineID [:rndstr from="qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789-" length=7]
				:local messageContent [$teInputTextMessageContent fMessageText="skip,settings,defaultTag"]
				:local tagInfoPictures ($dbasePictures->"inlinePictures"->$currentTag)
				:local infoTitle ($dbaseStr->$userLang->"teGeoSettings"->"inlineTagInfoTitle")
				:local infoDescription ($dbaseStr->$userLang->"teGeoSettings"->"inlineTagInfoDescription")
				:local commandInfo [$teInlineQueryResultArticle fThumbUrl=$tagInfoPictures fThumbWidth=5 fThumbHeight=5 fInputMessageContent=$messageContent fArticleUrl="" fHideUrl=true fInlineQueryID=$inlineID fTitle=$infoTitle fDescription=$infoDescription]

				:local inlineCommands $commandInfo

				:local messageContentText "settings,defaultTag,Police"
				:set messageContent [$teInputTextMessageContent fMessageText=$messageContentText]
				:local searchInfoPictures $currentTagEmty
				:if ($currentTag = "Police") do={ :set searchInfoPictures $tagChecked }
				:set inlineID [:rndstr from="qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789-" length=7]
				:set infoTitle ($dbaseStr->$userLang->"teGeoSettings"->"inlineTagPoliceTitle")
				:set infoDescription ($dbaseStr->$userLang->"teGeoSettings"->"inlineTagDescription")
				:set $currentItem [$teInlineQueryResultArticle fThumbUrl=$searchInfoPictures fThumbWidth=5 fThumbHeight=5 fInputMessageContent=$messageContent fArticleUrl="" fHideUrl=true fInlineQueryID=$inlineID fTitle=$infoTitle fDescription=$infoDescription]
				:local separator ""; :if ([:len $inlineCommands] != 0) do={ :set separator "," }
				:set inlineCommands ($inlineCommands . $separator . $currentItem)

				:set messageContentText "settings,defaultTag,Incidents"
				:set messageContent [$teInputTextMessageContent fMessageText=$messageContentText]
				:set searchInfoPictures $currentTagEmty
				:if ($currentTag = "Incidents") do={ :set searchInfoPictures $tagChecked }
				:set inlineID [:rndstr from="qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM0123456789-" length=7]
				:set infoTitle ($dbaseStr->$userLang->"teGeoSettings"->"inlineTagIncidentsTitle")
				:set infoDescription ($dbaseStr->$userLang->"teGeoSettings"->"inlineTagDescription")
				:set $currentItem [$teInlineQueryResultArticle fThumbUrl=$searchInfoPictures fThumbWidth=5 fThumbHeight=5 fInputMessageContent=$messageContent fArticleUrl="" fHideUrl=true fInlineQueryID=$inlineID fTitle=$infoTitle fDescription=$infoDescription]
				:set separator ""; :if ([:len $inlineCommands] != 0) do={ :set separator "," }
				:set inlineCommands ($inlineCommands . $separator . $currentItem)

				:local resultQuery [$teBuilQueryResult fResults=$inlineCommands]
				:if ([$teAnswerInlineQuery fInlineQueryId=$queryID fResults=$resultQuery fCacheTime=0 fIsPersonal=true] = 0) do={
					:return false
				}
				:return true
			}

If you noticed, all the text and “pictures” that are assigned to variables are in separate arrays. In this way, we put them in parameters and this allows us to implement, for example, switching languages ​​on the fly.

Working in iPhone

I don't know if it's a pity or not, but all this beauty works crookedly on the iPhone. You can't expand the picture in the inline menu to full screen, the voice doesn't play, and neither does the video. All this is a long-standing “fuss” between Apple and Telegram. But on Android it's beautiful.

I guess I'll finish here.

To sum it up – constructor mikrobot has been replenished with libraries for building inlineMenu, messaging functions teSendLocation, teSendVenue, teEditLiveLocation And a pair of mathematical functions. The code is published Here. Enjoy in good health.

Thank you for reading to the end. I hope it was interesting.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *