I’d want to give Elixir a try, creating a Telegram bot. At this point, I’m lost at how I should organize my command handlers. I’d want to use pattern matching, e.g.
defmodule MyBot.CommandHandler do
..
def handle_command(:some_command, args), do: ...
def handle_command(:other_command, args), do: ...
end
But I want to avoid bloating up this file as I continue to create new commands. What would be a good approach to that problem?
You could try to use protocols for your commands, where each command is represented as a struct and args are struct fields.
defprotocol MyBot.Command do
def exec(command)
end
defmodule MyBot.Commands.SomeCommand do
defstruct [...]
defimpl MyBot.Command do
def exec(command), do: ...
end
end
defmodule MyBot.Commands.OtherCommand do
defstruct [...]
defimpl MyBot.Command do
def exec(command), do: ...
end
end
I interact with Telegram Bot API via Webhooks and use a Cowboy Plug server.
Here’s an excerpt from my Plug router file:
post "/api/updates/:telegram_token" do
Logger.debug("Received Update: #{inspect(conn.params, pretty: true)}")
resp = Handlers.handle(conn.params)
conn
|> put_resp_content_type("application/json")
|> send_resp(200, Jason.encode!(resp))
end
match _ do
send_resp(conn, 404, "Not Found")
end
Then inside Handlers module I’m trying to logically dispatch Updates:
def handle(%{"callback_query" => _} = update) do
CallbackHandler.handle(update)
end
def handle(%{"pre_checkout_query" => _} = update) do
CheckoutHandler.handle(update)
end
def handle(%{"message" => %{"successful_payment" => _}} = update) do
CheckoutHandler.handle(update)
end
def handle(%{"message" => %{"contact" => _}} = update) do
ContactHandler.handle(update)
end
def handle(%{"message" => %{"reply_to_message" => _}} = update) do
ReplyHandler.handle(update)
end
def handle(%{"message" => _} = update) do
MessageHandler.handle(update)
end
def handle(%{"edited_message" => _} = update) do
EditedMessageHandler.handle(update)
end
def handle(update) do
Logger.debug("Unhandled Update: #{inspect(update, pretty: true)}")
%{}
end
Then inside MessageHandler I’m pattern matching on the type of the command and delegating the work to the corresponding business logic module:
@username :my_awesome_bot
def handle(%{"message" => %{"text" => text}} = update) when text in ["/help", "/help@#{@username}"] do
Command.help(update)
end
def handle(%{"message" => %{"text" => text}} = update) when text in ["/ping", "/ping@#{@username}"] do
Command.ping(update)
end
That’s the best code organisation pattern I could come up with. Please let me know if you have additional questions.
defmodule MyBot.MessageHandler do
def handle(%{"message" => %{"text" => text}} = update) do
# some function to convert `/help` into `:help`, but skip undefined commands so I don't overflow the VM with atoms
{:ok, command_atom} = extract_atom(text)
CommandHandler.handle(command_atom, ...)
end
end
defmodule MyBot.CommandHandler do
def handle(:help, ...), do: MyBot.Commands.Help.handle(...)
def handle(:something_else, ...), do: MyBot.Commands.SomethingElse.handle(...)
end
Or maybe I could construct an atom referencing a module (e.g., /help -> MyBot.Commands.Help) and call handle on it directly, thus removing CommandHandler. Or maybe make the handlers implement a protocol and pass them to a module that works with the implementations. I assume I could do that because module names are atoms.
If I had lots of similar commands I would’ve probably generated handlers at compile time using metaprogramming:
def MessageHandler do
@username :my_awesome_bot
@commands [:ping, :pong, :foo, :bar]
for command <- @commands do
private_chat_command = "/#{command}"
group_chat_command = "#{private_chat_command}@#{@username}"
def handle(%{"message" => %{"text" => text}} = update)
when text in [unquote(private_chat_command), unquote(group_chat_command)] do
apply(CommandModule, unquote(command), [update])
end
end
end
However, I think it’s not worth it unless you’re going to handle significant amount of commands.
I’m new to Elixir, so I’m a bit scared of metaprogramming, but I think I’ll go with that once I feel more confident with the language. Thanks a lot! For now, I’m going to def a bunch of pattern-matched handlers and call the corresponding module inside them.