-
-
Save voughtdq/1799160e125593bb4f19160d07416165 to your computer and use it in GitHub Desktop.
| defmodule Kazoo do | |
| require Logger | |
| # original from https://github.com/2600hz/kazoo/blob/4.3.142/applications/ecallmgr/src/mod_kazoo.erl | |
| @timeout 3000 | |
| @type freeswitch_node :: atom | |
| @type freeswitch_section_binding :: | |
| :config | :directory | :dialplan | :languages | :chatplan | :channels | |
| @type freeswitch_event_binding :: atom | |
| @type freeswitch_api_ok :: {:ok, binary()} | :ok | |
| @type freeswitch_api_error :: {:error, :timeout | :exception | :badarg | binary()} | |
| @type freeswitch_api_return :: freeswitch_api_ok() | freeswitch_api_error() | |
| @doc "Returns the current mod_kazoo version running on FreeSWITCH." | |
| @spec version(node :: freeswitch_node) :: freeswitch_api_return() | |
| def version(node), do: call(node, :version) | |
| @doc "Instructs FreeSWITCH to stop sending events." | |
| @spec no_events(node :: freeswitch_node) :: freeswitch_api_return() | |
| def no_events(node), do: call(node, :noevents) | |
| @doc "Closes the connection to the FreeSWITCH node." | |
| @spec close(node :: freeswitch_node) :: :ok | |
| def close(node), do: call(node, :exit) | |
| @doc "Returns the PID for the FreeSWITCH node." | |
| @spec get_pid(node :: freeswitch_node) :: {:ok, pid} | |
| def get_pid(node), do: call(node, :getpid) | |
| @doc """ | |
| Instructs the FreeSWITCH node to begin requesting configuration from | |
| the calling process. | |
| ## Examples | |
| iex> bind(:"[email protected]", :dialplan) | |
| """ | |
| @spec bind(node :: freeswitch_node, binding :: freeswitch_section_binding) :: | |
| freeswitch_api_return() | |
| def bind(node, binding), do: call(node, {:bind, binding}) | |
| @doc """ | |
| Reply to a configuration request sent from a FreeSWITCH node. | |
| """ | |
| @spec fetch_reply( | |
| node :: freeswitch_node, | |
| fetch_id :: String.t(), | |
| section :: String.t(), | |
| reply :: String.t() | |
| ) :: freeswitch_api_return() | |
| def fetch_reply(node, fetch_id, section, reply), | |
| do: cast(node, {:fetch_reply, section, fetch_id, reply}) | |
| @doc """ | |
| Instructs FreeSWITCH to start sending events. Returns a tuple with | |
| the IP and port that we can listen on to consume the events. | |
| """ | |
| @spec subscribe_events(node :: freeswitch_node, bindings :: [freeswitch_event_binding]) :: | |
| {:ok, :inet.ip(), :inet.port()} | |
| def subscribe_events(node, bindings), do: call(node, {:event, List.wrap(bindings)}) | |
| @doc """ | |
| Call an API on FreeSWITCH. | |
| """ | |
| @spec api(node :: atom, cmd :: atom, args :: String.t()) :: freeswitch_api_return() | |
| def api(node, cmd, args \\ ""), do: call(node, {:api, cmd, args}) | |
| @doc """ | |
| Calls an API command in a background thread, returning the job id. The caller is responsible for receiving the message | |
| """ | |
| def async_bgapi(node, cmd, args \\ "") do | |
| call(node, {:bgapi, cmd, args}) | |
| end | |
| @doc """ | |
| Calls an API command in a background thread. | |
| """ | |
| @spec bgapi(node :: atom, cmd :: atom, args :: String.t()) :: freeswitch_api_return() | |
| def bgapi(node, cmd, args \\ "") do | |
| parent = self() | |
| spawn(fn -> | |
| try do | |
| call(node, {:bgapi, cmd, args}) | |
| catch | |
| e, r -> | |
| _ = | |
| Logger.info(fn -> | |
| formatted = Exception.format(e, r) | |
| {"exec bgapi #{to_string(cmd)} #{args} failed with #{formatted}", [node: node]} | |
| end) | |
| send(parent, {:api, {:error, :exception}}) | |
| else | |
| {:ok, job_id} = job_ok -> | |
| send(parent, {:api, job_ok}) | |
| receive do | |
| {:bgok, ^job_id, _} = bg_ok -> send(parent, bg_ok) | |
| {:bgerror, ^job_id, _} = bg_error -> send(parent, bg_error) | |
| after | |
| @timeout -> send(parent, {:bgerror, job_id, :timeout}) | |
| end | |
| {:error, reason} -> | |
| send(parent, {:api, {:error, reason}}) | |
| :timeout -> | |
| send(parent, {:api, {:error, :timeout}}) | |
| end | |
| end) | |
| receive do | |
| {:api, result} -> result | |
| end | |
| end | |
| def bgapi4(node, cmd, args, fun, params) do | |
| parent = self() | |
| spawn(fn -> | |
| case call(node, {:bgapi4, cmd, args}) do | |
| {:ok, "-ERR " <> reason} -> | |
| send(parent, {:api, internal_fs_error(reason)}) | |
| {:ok, job_id} = job_ok -> | |
| send(parent, {:api, job_ok}) | |
| receive do | |
| {:bgok, ^job_id, reply} when is_function(fun, 3) -> | |
| send(parent, fun.(:ok, reply, [job_id | params])) | |
| {:bgerror, ^job_id, reply} when is_function(fun, 3) -> | |
| send(parent, fun.(:error, reply, [job_id | params])) | |
| {:bgok, ^job_id, reply, data} when is_function(fun, 4) -> | |
| fun.(:ok, reply, data, [job_id | params]) | |
| {:bgerror, ^job_id, reply, data} when is_function(fun, 4) -> | |
| fun.(:error, reply, data, [job_id | params]) | |
| end | |
| {:error, _} = err -> | |
| send(parent, {:api, err}) | |
| :timeout -> | |
| send(parent, {:api, {:error, :timeout}}) | |
| end | |
| end) | |
| receive do | |
| {:api, result} -> result | |
| end | |
| end | |
| def json_api(node, cmd, args \\ nil, uuid \\ nil) do | |
| json = | |
| Jason.encode!(%{ | |
| "command" => cmd, | |
| "data" => args, | |
| "uuid" => uuid | |
| }) | |
| case call(node, {:json_api, json}) do | |
| {:ok, response} when is_binary(response) -> | |
| {:ok, Jason.decode!(response)} | |
| {:error, response} when is_binary(response) -> | |
| {:error, Jason.decode!(response)} | |
| other -> | |
| other | |
| end | |
| end | |
| @doc false | |
| def call(node, args, timeout \\ @timeout) do | |
| GenServer.call({:mod_kazoo, node}, args, timeout) | |
| catch | |
| :exit, {{:nodedown, node}, _} -> | |
| {:error, {:nodedown, node}} | |
| error, reason -> | |
| Logger.warning(fn -> | |
| { | |
| "Attempt to call #{inspect(args)} failed:\n" <> Exception.format(error, reason), | |
| node: node | |
| } | |
| end) | |
| {:error, :exception} | |
| end | |
| def cast(node, args), do: GenServer.cast({:mod_kazoo, node}, args) | |
| def internal_fs_error(reason) do | |
| error = String.replace(reason, "\n", "") | |
| {:error, error} | |
| end | |
| end |
No problem, hope it helps!
any(?)
I am going to assume yes even though I have not personally tested every BEAM language 😄
Yes! You could even experiment with it if you wanted to try Elixir, like maybe in a different FreeSWITCH context! I'm fuzzy on the details because it's been a while, but I think Kazoo does bind(:freeswitch_node, :dialplan), which causes you to start receiving {:fetch, _} messages (I thought _ was a tuple, but maybe it has changed). You respond to those messages using fetch_reply/4.
The basic "entrypoint" (eliding all the other stuff that happens before getting to the dialplan) for calls in Kazoo is something like this:
<context name="kazoo">
<extension name="entrypoint">
<condition>
<action application="park"/>
</condition>
</extension>
</context>This can be the thing you give with fetch_reply/4 (last argument named reply) or maybe you don't need to bind to the dialplan at all and could just subscribe_events(node, [:HEARTBEAT, :CHANNEL_CREATE])? I am imagining you could transfer the parked call to whatever controlling application you want.
I realized this gist is incomplete, missing some sendcmd stuff.
Thanks again! I've been meaning to dive into mod_kazoo for a while now, but it's undocumented status discouraged, so I appreciate giving me an entry point:)
Thank you for this! Especially for the reminder that one can use any(?) BEAM-based language to interact with C-nodes; I keep forgetting this..:)