Packages

Elixir library for building Telegram bots with macro-based API

Current section

Files

Jump to
telegram_ex lib command.ex
Raw

lib/command.ex

defmodule TelegramEx.Command do
@moduledoc """
Helpers for defining and registering Telegram bot commands.
`defcommand/3` defines a regular `handle_message/2` clause for messages that
start with a Telegram command and stores metadata for `setMyCommands`.
`register_all/2` is called by `TelegramEx.Server` on startup. It reads command
metadata from the bot module and its routers, then sends the list to Telegram
using `setMyCommands`.
See [Commands](commands.md) for examples and binding rules.
## Fields
- `:command` - The command name (string)
- `:description` - Command description used by Telegram clients
- `:message` - The original message struct (TelegramEx.Types.Message.t())
"""
alias TelegramEx.API
@typedoc """
Command struct type.
Contains all fields related to a Telegram command.
"""
@type t :: %__MODULE__{
command: String.t(),
description: String.t() | nil,
message: TelegramEx.Types.Message.t()
}
defstruct [
:command,
:description,
:message
]
@doc """
Registers all commands defined in a bot module and its routers.
"""
@spec register_all(String.t(), module()) :: :ok | {:error, term()}
def register_all(token, module) do
commands =
module
|> command_modules()
|> Enum.flat_map(&module_commands/1)
|> Enum.uniq_by(& &1.command)
API.set_my_commands(commands, token)
end
@doc """
Defines a command handler and adds the command to the module metadata.
The command name should be passed without the leading slash. `:description`
is required because Telegram requires it for `setMyCommands`.
See [Commands](commands.md) for full examples.
"""
defmacro defcommand(command_name, opts, do: block) do
bind = Keyword.get(opts, :bind, [])
message_var = Macro.var(:message, nil)
ctx_var = Macro.var(:ctx, nil)
args_var = Macro.var(:args, nil)
command_var = Macro.var(:command, nil)
description = Keyword.fetch!(opts, :description)
vars = %{message: message_var, ctx: ctx_var, args: args_var, command: command_var}
quote do
@commands %{command: unquote(command_name), description: unquote(description)}
def handle_message(
%{text: "/#{unquote(command_name)}" <> _rest} = unquote(message_var),
unquote(ctx_var)
) do
unquote(args_assignment(bind, vars))
unquote(command_assignment(bind, vars, command_name, description))
unquote_splicing(bindings(bind, vars))
unquote(block)
end
end
end
defp args_assignment(bind, %{args: args_var, message: message_var}) do
if :args in bind do
quote do
unquote(args_var) = String.split(unquote(message_var).text, " ") |> Enum.drop(1)
end
end
end
defp command_assignment(
bind,
%{command: command_var, message: message_var},
command_name,
description
) do
if :command in bind do
quote do
unquote(command_var) = %TelegramEx.Command{
command: unquote(command_name),
description: unquote(description),
message: unquote(message_var)
}
end
end
end
defp bindings(bind, vars) do
Enum.map(bind, &binding(&1, vars))
end
defp binding(name, vars) do
var = Macro.var(name, nil)
source = binding_source(name, vars)
quote do
var!(unquote(var)) = unquote(source)
end
end
defp binding_source(:message, %{message: message_var}), do: message_var
defp binding_source(:ctx, %{ctx: ctx_var}), do: ctx_var
defp binding_source(:args, %{args: args_var}), do: args_var
defp binding_source(:command, %{command: command_var}), do: command_var
defp binding_source(name, _vars) do
raise ArgumentError, "Unsupported bind variable: #{name}."
end
defp command_modules(module) do
routers =
if function_exported?(module, :__routers__, 0),
do: module.__routers__(),
else: []
[module | routers]
end
defp module_commands(module) do
if function_exported?(module, :__commands__, 0),
do: module.__commands__(),
else: []
end
end