defmodule AMI.Client do
@moduledoc """
Client module to connect Asterisk server and login AMI.
Module fits to be used in a supervisor tree.
When connection is lost in case of network problems it will
keep trying to re-connect again every 3 seconds.
## Example using with supervisor
```
children = [
%{
id: :pbx01,
start: {
AMI.Client,
:start_link,
[{'pbx01.myphones.com', 5038, "admin", "secret4", MyAMImodule}]
}
},
%{
id: :pbx02,
start: {
AMI.Client,
:start_link,
[{'127.0.0.1', 5038, "admin", "secret9", MyAMImodule}]
}
},
]
Supervisor.start_link(children, strategy: :one_for_one)
```
"""
require Logger
use GenServer
@timeout 3000
@table_name :amiex_socks_table
@doc """
Starts a AMI client linked to the current process.
Can be userd to start the Client in a supervisor tree.
## Options
* `host` - remote Asterisk host or IP to connect
* `port` - remote AMI port
* `user` - AMI username as string
* `passwd` - AMI password as string
* `module` - elixir module that uses AMI and which will receive all incoming events.
See exmple in AMI module
## Example
```
AMI.Client.start_link({'localhost', 5447, "admin", "5ecR37", MyModule})
```
"""
def start_link({host, port, user, passwd, module}) do
case :ets.whereis(@table_name) do
:undefined -> :ets.new(@table_name, [:named_table, :public])
_ -> nil
end
GenServer.start_link(
__MODULE__,
[host, port, user, passwd, module],
name: pname(host, port)
)
end
@doc """
Send AMI Action packet via established connection by client
identified by addr
"""
@spec send(action :: AMI.Action.t(), addr :: String.t()) :: :ok
def send(action, addr) do
Logger.debug("Sending action to #{addr}")
GenServer.cast(pname(addr), {:send, action, addr})
end
@doc """
Send AMI Action to all established connections via clients
"""
@spec broadcast(action :: AMI.Action.t()) :: :ok
def broadcast(action) do
broadcast(:ets.tab2list(@table_name), action)
end
@impl true
def init([host, port, user, pass, module]) do
Process.send(self(), :connect, [])
{:ok, %{host: host, port: port, user: user, secret: pass, module: module, packet: ""}}
end
@impl true
def handle_cast({:send, action, addr}, state) do
Logger.debug("Cast action to #{addr}")
case :ets.lookup(@table_name, addr) do
[{^addr, sock}] ->
sendto(sock, AMI.Action.to_string(action))
[] ->
Logger.error("Failed to find socket for '#{addr}' to send action")
end
{:noreply, state}
end
@impl true
def handle_info({:tcp, _pid, 'Asterisk Call Manager/' ++ _}, state) do
Logger.info("Received AMI prompt line")
Process.send(self(), :login, [])
{:noreply, state}
end
@impl true
def handle_info(
{:tcp, _pid, msg},
%{packet: packet, host: host, port: port, module: mod} = state
) do
packet =
case msg do
'\r\n' ->
parse_msg(packet, mod, to_key(host, port))
line ->
packet <> to_string(line)
end
{:noreply, %{state | packet: packet}}
end
@impl true
def handle_info({:tcp_closed, _pid}, state) do
Logger.warn("Connection closed")
{:noreply, connect(state)}
end
@impl true
def handle_info(:connect, %{host: host, port: port} = state) do
Logger.info("Connecting to #{host}:#{port}")
{:noreply, connect(state)}
end
@impl true
def handle_info(:login, %{sock: sock, user: user, secret: pwd} = state) do
Logger.info("Login to AMI server with user '#{user}'")
sendto(sock, AMI.Action.login(user, pwd))
{:noreply, state}
end
defp connect(%{host: host, port: port} = state) do
case tcp_connect(host, port) do
{:ok, sock} ->
:ets.insert(@table_name, {to_key(host, port), sock})
Map.put(state, :sock, sock)
{:error, reason} ->
Logger.error("Failed to connect #{host}:#{port} - '#{reason}'")
Process.send_after(self(), :connect, @timeout)
Logger.warn("Will retry in #{@timeout}ms...")
state
end
end
defp tcp_connect(host, port) do
opts = [active: true, send_timeout: @timeout, packet: :line]
case :gen_tcp.connect(host, port, opts) do
{:ok, sock} ->
Logger.info("Connected to #{host}:#{port}")
{:ok, sock}
{:error, err} ->
{:error, err}
end
end
defp parse_msg(packet, mod, key) do
case AMI.Packet.parse(packet) do
{:ok, event} -> mod.handle_message(event, key)
{:error, reason} -> Logger.warn("Failed to parse packet: #{reason}")
end
# return empty line in both cases to reset packet
""
end
defp sendto(sock, action) do
case :gen_tcp.send(sock, action) do
:ok -> Logger.debug("Successfully send AMI action")
{:error, reason} -> Logger.error("Failed to send AMI packet: #{reason}")
end
end
defp broadcast([{addr, _} | tail], action) do
Logger.debug("Broadcast to #{addr}")
__MODULE__.send(action, addr)
broadcast(tail, action)
end
defp broadcast([], _action) do
Logger.debug("Broadcast done")
:ok
end
defp to_key(host, port), do: "#{host}:#{port}"
defp pname(host, port), do: {:global, {__MODULE__, to_key(host, port)}}
defp pname(addr), do: {:global, {__MODULE__, addr}}
end