I am currently stuck with an implementation of a function that has to poll the Telegram API periodically and wait for a response from this. I guess there is some async / await magic that will help me to solve this problem in an elegant way. Maybe someone can jump in an give me a hint
Other suggestions for improvement are also very welcome
defmodule TelegramHelper do
@moduledoc """
Documentation for TelegramHelper.
"""
import Nadia
@chat_id 123456
@spec get_sms_verification_code() :: any()
def get_sms_verification_code do
# telegram api will return some old messages from time to time
# so we have to check if there are old message and get the update_id to poll later only for newer messages
update_id = 0
{:ok, result} = get_updates
if length(result) > 0 do
update_id = List.last(result).update_id + 1
end
send_message(@chat_id, "Please send the Auth Code")
get_reply(update_id)
end
defp get_reply(update_id) do
Process.sleep(1000)
{:ok, result} = get_updates(%{:update_id => update_id})
if length(result) == 0 do
# this should stop after a timeout of e.g. 5 Minutes
# and is obviously the wrong way of doing it
get_reply(update_id)
end
# this is only for reference on what i want to return if i get back a new message from telegram
List.first(result).message.text
end
end
I am using this in some browser automation where i will forward a two factor authentication code from sms to telegram to the login process handling⦠so i will end up with something like that:
Fill in email + password ā click button to login ā click button to send the sms ā wait for sms code
Where āwait for sms codeā will most likely be something similar to what I am doing now with the only difference that the waiting loop will be aborted by the webhook and not by my polling logic
Additionally I need some kind of webapp to handle the webhook (including access from the outside) which is not necessary if i use the polling method
Ah, I seem to have misunderstood the question then ā¦
Iāve never used telegramās polling api, but I guess the same principles as with webhooks would apply ā in general, I abstract the polling / webhooks into a separate process which acts as a ātelegram message producerā and allows other processes to subscribe to it, I then have another processes for it as consumers which in their turn output āactionsā for a ātelegram broadcasterā process ⦠(The last one acting as a queue because telegram doesnāt allow sending more than 30 messages per second).
In your case however, it seems that this is not necessary. So maybe you can have a single process that would keep a map of auth codes as its state and match against them for every incoming message:
defmodule Auther do
@moduledoc """
Polls for new messages from telegram and compares them with auth codes
"""
use GenServer
defstruct :last_update_id, :auth_codes
@doc false
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: opts[:name])
end
@doc false
def init(opts) do
send(self(), :poll)
last_update_id = opts[:update_id] || 0
{:ok, %__MODULE__{last_update_id: last_update_id, auth_codes: %{}}}
end
@doc false
def handle_info(:poll, state) do
case get_updates(last_update_id) do
[] ->
Process.send_after(self(), :poll, 5000)
{:noreply, state}
new_messages ->
{:noreply, handle_new_messages(state, new_messages)}
end
end
defp handle_new_messages(state, new_messages) do
[%{id: last_update_id} | _rest] = :lists.reverse(new_messages)
auth_codes = # iterate over messages, get the ones that provide new auth codes or what have you
%{state | last_update_id: last_update_id, auth_codes: auth_codes}
end
defp get_updates(last_update_id) do
# gets updates since the last_update_id
end
end
The map could look like %{chat_id => auth_code}, so when you receive a message for chat_id, you compare the contents of that message with auth_code and then do something about it ā¦
I believe you should be returning a tuple from handle_info/2:
def handle_info(:poll, state) do
case get_updates(last_update_id) do
[] ->
Process.send_after(self(), :poll, 5000)
{:noreply, state}
new_messages ->
state = handle_new_messages(state, new_messages)
{:noreply, state}
end
end
Nice, that looks like it goes into the right direction, thank you very much for the help
Quick additional Info: that programm is only used by me with a single chat id so I only need the last/one auth code and not a map of codes⦠but I can adapt that to my needsā¦
Would you mind showing me an example on how I would integrate that Auther Module into my procedural browser automation code? E.g. if i have this pseudo code for my browser automation:
fill_email_password("mail", "password")
click_auth_button
# code to wait for telegram code which also handles timeout
# on timeout: log error and exit
fill_auth_code(code_from_telegram)
This can be achieved with Task.await, for example, where youād call the Auther module to add a chat id to its internal state to wait an auth code for.
And exactly thats the point where I am stuck. I couldnāt wrap my head around that. How would this look? Even after reading the Docs about Task i have no clue on how to build a looping async function
I donāt think you need a looping async functions ⦠Iād do something like
# in Auther
def wait_for_chat(server , chat_id, timeout \\ 5000) do
GenServer.call(server, {:wait_for_chat, chat_id}, timeout)
end
@doc false
def handle_call({:wait_for_chat, chat_id}, from, state) do
# note that `Map.put` would override any previous "subscription" (from)
# if this is a problem, store `from`s in a list
# auth_codes = %{chat_id => [from1, from2, from3, ...]}
{:noreply, %{state | auth_codes: Map.put(state.auth_codes, chat_id, from)}}
end
defp handle_new_messages(state, new_messages) do
# go over new messages and check if there are any auth codes from
# the chat ids in state.auth_codes, if there are, reply to
# the client process in the corresponding to that chat_id's `from`
auth_codes = Enum.reduce(new_messages, state.auth_codes, fn %{"chat" => %{"id" => chat_id}, "text" => message}, auth_codes ->
with true <- auth_code?(message),
{from, auth_codes} when not is_nil(from) <- Map.pop(auth_codes, chat_id) do
GenServer.reply(from, message)
auth_codes
else
_ -> auth_codes
end
end)
%{state | auth_codes: auth_codes}
end
Then in your browser automation program you can do
fill_email_password("mail", "password")
click_auth_button
auth_code = Auther.wait_for_chat(auther_pid, chat_id, :infinity) # or just a large number for timeout
fill_auth_code(code_from_telegram)
state.auth_codes should probably be renamed to state.awaiting_auth_codes or awaiting_chats_auth_code something like that. It was named incorrectly the first time since I misunderstood your problem again, it seems.