Action required to keep your Telegram bots alive due to update of Terms of use

Quite a few people here use Telegram to stay aware of events in their network, and few others plan on starting to do that in near future. On July 3rd, Telegram has emphasized that all bots must provide information about their privacy policy upon request, otherwise they will face suspension or termination. To make it even more scary, they haven’t indicated any deadline to make the bots compliant to this requirement.

The requirement is not much of an issue for those who have already been using Telegram bots for interactive tasks, but it may be a challenge for those like me who do not use bots to handle incoming messages at all, the more so if they send the Telegram messages from Mikrotik devices running RouterOS 6.

In my case, multiple devices in each domain of interest use a single common bot account dedicated for that domain to notify the relevant audience about events in that domain, and in some of those domains, any individual device may be offline for extended periods of time. Given these constraints, I needed a solution that would be online “as always as possible” and provide the required responses to the /privacy command for multiple bots. Therefore, the solution below may be an overkill for those who only send messages from a single device using a single bot account. However, I suppose that anyone who has managed to set up a Mikrotik router to send Telegram notifications is skilled enough to downsize the solution if needed.

Even though the solution has to handle multiple bot accounts, I have decided to implement it directly on a RouterOS device as it would be an overkill to spawn a dedicated Linux server for it. It had to be RouterOS 7, mainly because the deserialization of json messages is not available as a script command in RouterOS 6, so it would have to be implemented manually.

Security aspects: the authentication keys to Telegram bot accounts are sensitive information, so I’ve chosen to store them as passwords in the Mikrotik configuration and, more important, to verify the server certificate when sending them to Telegram servers. To make the latter possible, one has to download and import the certificate of the Go Daddy Class 2 Certification Authority, which is the root of the chain of trust of Telegram’s server certificate, from the Go Daddy certificate repository. To overcome the chicken-and-egg problem, one should download that certificate using a browser on a computer and copy it to the router.

/certificate/import file=gdroot-g2.crt name="GoDaddyClass2CA-for-Telegram"
/ppp/profile/add name=telegram
/ppp/secret
add name=bot1234567890 password=AAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa profile=telegram comment="name of my first bot"
add name=bot2345678901 password=BBbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb profile=telegram comment="name of my second bot"

“Green” aspects: to reduce the number of https requests per second while keeping the bot reactive, I use the “long polling” mode where the server responds immediately if a message is waiting in the queue but keeps the session open for a while before sending an empty response as long as the queue is empty. That’s another point where ROS 7 gives slightly better results, as ROS 6 does not support the idle-timeout parameter of tool fetch and closes the connection much sooner than after the 50 seconds as supported by Telegram.

Multi-threading: I need the device to listen at multiple accounts simultaneously, and I prefer to rely on RouterOS itself when it comes to ensuring a continuous operation of user scripts and recovery from eventual errors. So I am using a “worker” script, which processes up to one incoming message on a single account and terminates rather than running in an infinite loop, and an “orchestrator” script scheduled to run every second that spawns an asynchronous instance of the worker script whenever no such instance is running for a given account. You cannot hand over a parameter (here, the ID of the bot) to a script called directly from the script repository, so the worker instance has to be converted into a global function at boot time as well as after any modification.

First, the worker script (named TelegramResponseWorker for the example):

:global updOffset
:local botId $1
:local bot [/ppp secret find where name=$botId]
:if ([:typeof ($bot->0)]="id") do={
  /ip firewall address-list add list=$botId address=0.0.0.0 timeout=60s
  :local botPwd [/ppp secret get ($bot->0) password]
  :local myUrl "https://api.telegram.org/$botId:$botPwd/getUpdates\?limit=1&timeout=55"
  :local offset ($updOffset->$botId)
  :if ([:typeof $offset]="num") do={
    :set myUrl "$myUrl&offset=$offset"
  }
  :local tgIn {"ok"=no}
  :do command={
    :set tgIn [:deserialize from=json ([/tool fetch url=$myUrl output=user check-certificate=yes-without-crl idle-timeout=1m as-value]->"data")]
  } on-error={
    :nothing
  }
  :do command={
    /ip firewall address-list remove [find list=$botId]
  } on-error={
    :nothing
  }
  :if ($tgIn->"ok") do={
    :local update ($tgIn->"result"->0)
    :if ([:typeof $update]="array") do={
      :local updateId [:tonum ($update->"update_id")]
      :local acknowledge yes
      :local chatId ($update->"message"->"chat"->"id")
      :if (($update->"message"->"text")~"^/privacy\$") do={
        :local privManifest ( \
          "__Privacy+policy+statement%3A__%0A*This+bot+does+not+store+any+information+it+receives*,+and+only+uses+it+to+properly+route+the+response\\.%0A" . \
          "_Actually,+the+only+reason+why+it+processes+incoming+messages+at+all+is+that+it+has+to+respond+to+this+/privacy+command" .\
          "+in+order+to+stay+compliant+with+the+Telegram+terms+of+use\\._" \
        )
        :do command={
          /tool fetch url="https://api.telegram.org/$botId:$botPwd/sendMessage\?chat_id=$chatId&text=$privManifest&parse_mode=MarkdownV2" \
                      output=none check-certificate=yes-without-crl
        } on-error={
          :set acknowledge no
        }
      }
      :if $acknowledge do={
        :set ($updOffset->$botId) ($updateId+1)
      }
    } else={
      :set ($updOffset->$botId)
    }
  }
  /ip firewall address-list remove [find list=$botId]
}

Next, the much simpler orchestrator script, named TelegramResponderOrchestrator:

:global updOffset
:if ([:typeof $updOffset] != "array") do={:set updOffset [:toarray ""]}
:global trw
:if ([:typeof $trw] = "code") do={
  :foreach bot in=[/ppp secret find where profile=telegram !disabled] do={
    :local botId [/ppp secret get $bot name]
    :if ([:len [/ip firewall address-list find list=$botId]] = 0) do={
      :local script "{:global trw ; \$trw $botId}"
      :execute script=$script
    }
  }
}

To create the global function from the worker script, use

:global trw [:parse [system/script/get TelegramResponderWorker source]]

And, finally, add the schedulers:

/system/scheduler
add name=init on-event=":global trw [:parse [system/script/get TelegramResponderWorker source]]" start-time=startup
add interval=1s name=TelegramResponderOrchestrator on-event=TelegramResponderOrchestrator start-time=startup

I did not think deeply about the coordination between two devices providing this service, which would be necessary to avoid sending duplicate responses, yet.

Lastly, any technical comments are welcome. To express your excitement about the very need to provide the responses to the /privacy command, please use other means than posts on this technical forum.