Здравствуйте Друзья!

Два года назад мною была написана статья, посвящённая разработке в RouterOS.

В рамках того проекта мы управляли устройствами Микротик через Телеграм-бота. Было получено много опыта и много кода, в виде библиотек на языке Mikrotik Script, для работы с API Телеги, функций обработчиков, и всевозможных форм.

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

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

Суть коротко

Давайте представим сервис, который позволяет указать точку на интерактивной карте, прикрепить к ней комментарий, видео, фото или голосовое сообщение, затем опубликовать её в одном из двух разделов.

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

Любой пользователь сервиса может найти эту точку, если находится в радиусе до 5 км. от неё.

При выборе открывается форма, где можно построить маршрут, посмотреть весь контент, который к ней прикреплен, а так же, при желании, добавить свой.

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

У любой точки есть время жизни. Для первого раздела - это 1 час. Для второго 24 часа. После таймаута они безвозвратно удаляются.

Есть возможность создания точки с текущими координатами одним нажатием.

Картинка
d
d

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

Всё это необходимо было реализовать только средствами RouterOS и API Телеграм, без использования сторонних сервисов.

Помня о том, что ROS поддерживает многопоточную обработку, был куплен Mikrotik CCR-1036, у которого на борту 36 процессорных ядер и 4 гига оперативной памяти. Размещен в собственном ЦОД, выступает в роли backend-а.

И началась работа...

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

А задачи стояли интересные, давайте подробнее их разберем.

По условиям мы не можем использовать сторонние системы. Это с одной стороны, упрощает всю схему, а с другой усложняет разработку. Но нам не страшно, у нас есть опыт - сын ошибок трудных.

Главное, что необходимо реализовать в рамках данной задачи, это расчёт расстояния между двумя географическими координатами.

Расстояние нужно по прямой, так как искать мы будем в определенном радиусе.

Про координаты

Их нам заботливо присылает Телеграм, при включении в настройках бота опции Inline Location Data.

Они запрашиваются каждый раз, когда пользователь нажимает кнопку, вызывающую Inline меню.

Если разрешения есть и геолокация включена, то боту прилетит inline_query с разделом location в виде latitude=41.101235 и longitude=35.975326.

Казалось бы это не сложно, формулы есть. Но в ROS мы можем оперировать только целыми числами. Вот здесь пришлось вспомнить математику, проявить немного изобретательности, а так же написать несколько вспомогательных функций, чтобы с их помощью "собрать формулу".

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

Первая реализуется просто.

Код первой
#---------------------------------------------------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
  }
}

Вторая немногим сложнее. Используется двоичный поиск, он же метод половинного деления. Это даёт нужную точность для целых чисел.

Код второй
#---------------------------------------------------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
  }
}

Третья вычисляет длину широты, в зависимости от её номера.

И четвертая с их помощью, рассчитывает расстояние между двумя координатами.

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

Ещё нам понадобятся некоторые константы.

Длина одного градуса меридиана, которая составляет около 111,3 км. Избавляемся от дробей, последовательно умножая значение на 10.

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

Например Пи превратится в 31 415 926.

Средний арифметический радиус Земли в 6 371 009 метров.

Главное потом не забыть вернуть всё обратно до нужной точности, путем того же деления на 10.

У этого фокуса есть минусы. Приходится оперировать большими числами и может случиться так, что значение не поместится в 64bit, выделенные для типа num. Об этом надо помнить.

В итоге получаем точность расчета расстояния между двумя географическими координатами +-5 метров, в радиусе до 200 км, чего для нашей задачи более чем достаточно.

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

Например так выглядит массив для точек.

Массив
Напоминалка

Мы помним, что в Микротик: "В одном массиве отлично живут и индексированные и именованные элементы. Первые доступны по индексу, вторые по ключу. Так каждый массив может содержать служебную информацию о себе."

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

: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="--";
                        ...
                      };
                    };
                  };
               })

Тут надо пояснить. Бот работает на территории ограниченной 5-179 долготой с запада на восток и 40-80 широтой с юга на север. Это вся Россия, часть Юго-Восточной Азии и почти вся Европа.

Карта условно разбита на квадраты по 5 градусов. Каждый квадрат - это отдельный массив. При создании точки, сначала определяется в какой квадрат она попадёт и если в памяти ещё нет нужного массива, то он создается с именем состоящим из префикса и координат первых двух разрядов широты-долготы квадрата. Например - dbase4535.

Основные ключи массива формируются динамически на основании координат, чтобы потом при поиске идти сразу в нужное место.

Функция поиска по массивам с точками учитывает соседние квадраты, если они попадают в радиус. Организовано удаление старых записей по тайм-ауту.

И конечно же на полную используем много ядер устройства.

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

Напомню, что для этого используется команда :execute. Код для неё, в большинстве случаев, формируется динамически в процессе исполнения модуля.

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

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

В прошлой статье я так же рассказывал, что в массиве можно хранить готовые куски кода или целы функции. Причем, если запускать их командой :execute, то в этом коде можем использовать весь контекст Environment, т.е. глобальные переменные там можно не объявлять, если они ранее были созданы.

Собранный из элементов такого массива код, командой :execute сразу отправляется на исполнение, минуя стадию развёртывания в Environment или чтения с диска.

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

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

Поясню в чем суть.
:local test ":global testArray [:toarray \"\"]; :for key from=0 to=150000 do={:set (\$testArray->\$key) [:tostr (\"str=\".\$key)]}"; 
:execute script=$test

Этот код создает массив testArray из 150 000 и одной записи. Элементом является строка "str=$key", где $key - это номер строки.

Уже во время его создания начинается интересное.

В ROS 6, он становится невидимым на вкладке Environment. Но из консоли доступен и с ним можно работать.

Любым элементом такого массива, как мы знаем, может быть код. И если он запускается командой :execute, то на вкладке Jobs его выполнение не сразу вызывает подозрение, потому что строка без имени скрипта и похожа на запущенную консоль.

Только в строке состояния у консоли указано - L (login), а у кода - C (command).

В ROS 7 немного по-другому. На вкладке Environment красными буквами ERROR: action failed. Выполняется так же без имени.

Ещё одна фишка ROS - файл загруженный по ftp и содержащий в имени  *.auto.rsc, будет автоматически исполнен.

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

Но давайте вернемся к задаче. Все основные компоненты готовы и теперь нам нужно донести функционал до пользователя через интерфейс.

В прошлый раз мы использовали обычные inlineButton-ы, которые расположены под картинкой, а для списков использовали группы, ссылки на которые "цеплялись" к кнопке. В этом месте нарушался принцип одного окна.

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

Они несут в себе: медиа, ссылки, координаты или просто текст, в виде InlineQueryResult...

Каждый пункт такого меню может содержать InputMessageContent. Это то, что улетит в чат, при выборе данного пункта. Там пять типов, но нас пока интересует только InputTextMessageContent. Внутри просто текст, но в него мы "зашиваем" команды для бота.

Если в InputMessageContent ничего не указывать, то в чат прилетит сам контент.

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

Зато можно построить маршрут. Сам Телеграм этого делать не умеет, но передает координаты пользователя и точки в программу для карт по-умолчанию на вашем устройстве. Это неплохо расширяет функционал.

Ложка дегтя все же есть. Сообщение с типом Location имеет live_period в интервале от 60 до 86400 секунд. В это время его можно редактировать, т.е. изменять клавиатуру или координаты, после уже не получится. Сейчас вроде появилось значение 0x7FFFFFFF, чтобы редактировать можно было бесконечно, но на тот момент его еще не было. Бот запущен в декабре 2022 года.

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

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

Теперь ближе к коду...

В основе всё тот же mikRobot, вот его код. Главный модуль не сильно отличается от опубликованного.

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..."}}
}

В allowed_updates добавляем inline_query. Теперь бот будет принимать такие сообщения. Они содержат в себе, помимо прочего, поле query с типом String. В это поле попадет текст, который мы формируем на этапе создания кнопки.

В button-e за это отвечает поле switch_inline_query_current_chat с типом String.

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

При нажатии на такую кнопку, зашитая в нее команда, будет вставлена в поле ввода, сразу после имени бота.

Шаблон команды: calledFunctionName,commandName,commandValue,commandTag

Пример: @xgeoBot teInlineGeoPoint,currentscrin,photo

В данном случае calledFunctionName = teInlineGeoPoint. Ей будет передано управление.

Дальше идут параметры: commandName = currentscrin; commandValue = photo

Эту команду будет обрабатывать функция диспетчер teInlineResponse. Она сформирует код для запуска вызываемого модуля и отправит его на исполнение командой :execute.

Код teInlineResponse
#---------------------------------------------------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
 }
}

При выборе элемента, в чат улетает сообщение, тот самый InputTextMessageContent. Сообщение вроде как от Вас, но через бота. Отличаться будет наличием в message массива via_bot, в котором лежит информация о боте. Модуль teViabotResponse обрабатывает эти команды и запускает на исполнение нужный код. Он имеет такую же структуру, как и остальные обработчики.

Для полноты картины покажу как формируется inlineMenu на примере выбора тэга по-умолчанию в форме настроек бота.

Пример
			: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
			}

Если заметили, весь текст и "картинки", которые присваиваются переменным, лежат в отдельных массивах. Таким образом мы выносим их в параметры и это позволяет реализовать, например, переключение языков на лету.

Работа в iPhone

Не знаю к сожалению или нет, но вся эта красота криво работает в iPhone. Картинку в инлайн-меню там не развернуть на весь экран, голос не воспроизводится, видео тоже. Всё это многолетняя "возня" между Apple и Telegram. Зато на Android красота.

На этом, наверное, буду заканчивать.

Резюмируя - конструктор mikRobot пополнился библиотеками для построения inlineMenu, функциями работы с сообщениями teSendLocation, teSendVenue, teEditLiveLocation и парой математических функций. Код опубликован здесь. Пользуйтесь кому надо.

Спасибо, что дочитали до конца. Надеюсь было интересно.

Комментарии (8)


  1. evg86
    31.07.2024 05:45
    +1

    Надо будет обязательно прочесть статью, написанную пару лет назад...


  1. ARGOA
    31.07.2024 05:45
    +1

    Только мне одному показалось что «сервис» кто-то разрабатывает для «закладок» и их поиска? Фото, описание, карта, маршрут, срок хранения… Не хватает только процесса оплаты и автоматической выдачи «точки». Скоро обещают в телеге оплату криптой сделать…


    1. BrookXVII Автор
      31.07.2024 05:45

      Бот разрабатывался совсем для других целей. Он публичный и для закладок не пойдет, потому что все кто в радиусе могут увидеть эти точки.


  1. net_racoon
    31.07.2024 05:45
    +1

    Я правильно понял, вместо того чтобы взять нормальный язык программирования и написать своего бота в нем с возможностью масштабирования и без вышеописанных костылей, вы взяли это и усложнили задачу в разы?


    1. BrookXVII Автор
      31.07.2024 05:45

      На других языках уже написана куча ботов. Для них такая же куча библиотек. Плюс для всего этого нужна инфраструктура и вспомогательные системы, это дорого. А тут очень даже бюджетно получилось. Всё в одном. Для небольших сервисов вполне рабочая схема.

      И чем LUA не язык? Я написал под него библиотечные функции Телеги. Для других языков кто-то когда-то тоже их писал. Масштабирование? Да пожалуйста, хоть вертикально, хоть горизонтально. Для второго у ROS есть REST API.

      А на счет усложнил, тут Вы наверное правы. Но усложнил то я для себя. Зато упростил всю схему. Теперь есть библиотеки, которые упростят разработку для остальных.


      1. ret77876
        31.07.2024 05:45
        +1

        А бот по итогу работает на Mikrotik CCR-1036? Просто если под "бюджетно" иметь ввиду стоимость железа и обслуживания, то условный Orange PI и бот на Python или любом другом языке будет намного дешевле.


        1. BrookXVII Автор
          31.07.2024 05:45

          Да, работает на Mikrotik CCR-1036. Но тут скорее спортивный интерес. Хочется проверить какую нагрузку он потянет в виде пользователей. Пока он бота вообще не замечает.

          А так бота можно запустить и на домашнем компе, на виртуалке с двумя ядрами и 256M оперативы. И он будет работать.


        1. BrookXVII Автор
          31.07.2024 05:45

          Вот это видео, как раз на тестовом записывалось...