├── config ├── dev.exs ├── test.exs └── config.exs ├── test ├── test_helper.exs ├── resources │ ├── 1.txt │ ├── 2.txt │ ├── 4.txt │ └── 3.txt └── packet_test.exs ├── .gitignore ├── .travis.yml ├── lib ├── elixir_mod_event.ex └── elixir_mod_event │ ├── content.ex │ ├── header.ex │ ├── packet.ex │ ├── erlang.ex │ └── connection.ex ├── mix.exs ├── mix.lock ├── LICENSE └── README.md /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /test/resources/1.txt: -------------------------------------------------------------------------------- 1 | Content-Type: auth/request 2 | 3 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | import_config "#{Mix.env}.exs" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cover 2 | doc 3 | /_build 4 | /deps 5 | erl_crash.dump 6 | *.ez 7 | -------------------------------------------------------------------------------- /test/resources/2.txt: -------------------------------------------------------------------------------- 1 | Content-Type: command/reply 2 | Reply-Text: +OK accepted 3 | 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.0.0 4 | - 1.0.1 5 | - 1.0.2 6 | - 1.0.3 7 | - 1.0.4 8 | - 1.0.5 9 | - 1.1.0 10 | - 1.1.1 11 | - 1.2.0 12 | - 1.3.0 13 | - 1.4.0 14 | otp_release: 15 | - 18.0 16 | -------------------------------------------------------------------------------- /lib/elixir_mod_event.ex: -------------------------------------------------------------------------------- 1 | defmodule FSModEvent do 2 | @moduledoc """ 3 | Main module. 4 | 5 | Copyright 2015 Marcelo Gornstein 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | """ 18 | 19 | end 20 | -------------------------------------------------------------------------------- /test/resources/4.txt: -------------------------------------------------------------------------------- 1 | Content-Length: 654 2 | Content-Type: text/event-plain 3 | 4 | Command: sendevent%20myeventito 5 | content-length: 57 6 | hdr1: val1 7 | Event-UUID: ee38d831-c80a-4520-8249-7424fa353408 8 | Event-Name: CUSTOM 9 | Core-UUID: a95e64ec-df1f-48f4-acbf-34b4c359d747 10 | FreeSWITCH-Hostname: faulty.local 11 | FreeSWITCH-Switchname: faulty.local 12 | FreeSWITCH-IPv4: 192.168.1.102 13 | FreeSWITCH-IPv6: %3A%3A1 14 | Event-Date-Local: 2015-07-04%2020%3A12%3A06 15 | Event-Date-GMT: Sat,%2004%20Jul%202015%2023%3A12%3A06%20GMT 16 | Event-Date-Timestamp: 1436051526425813 17 | Event-Calling-File: mod_event_socket.c 18 | Event-Calling-Function: parse_command 19 | Event-Calling-Line-Number: 2209 20 | Event-Sequence: 4205 21 | Content-Length: 57 22 | 23 | this is a custom payload this is a custom payload oh yeah -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule FSModEvent.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :elixir_mod_event, 7 | name: "elixir_mod_event", 8 | version: "0.0.10", 9 | description: description(), 10 | package: package(), 11 | source_url: "https://github.com/marcelog/elixir_mod_event", 12 | elixir: "~> 1.0", 13 | build_embedded: Mix.env == :prod, 14 | start_permanent: Mix.env == :prod, 15 | deps: deps() 16 | ] 17 | end 18 | 19 | def application do 20 | [ 21 | applications: [:logger] 22 | ] 23 | end 24 | 25 | defp description do 26 | """ 27 | Elixir client for FreeSWITCH mod_event_socket. 28 | 29 | Find the user guide in the github repo at: https://github.com/marcelog/elixir_mod_event. 30 | """ 31 | end 32 | 33 | defp package do 34 | [ 35 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 36 | maintainers: ["Marcelo Gornstein"], 37 | licenses: ["Apache 2.0"], 38 | links: %{ 39 | "GitHub" => "https://github.com/marcelog/elixir_mod_event" 40 | } 41 | ] 42 | end 43 | 44 | defp deps do 45 | [ 46 | {:earmark, "~> 1.0.3", only: :dev}, 47 | {:ex_doc, "~> 0.14.5", only: :dev}, 48 | {:coverex, "~> 1.4.12", only: :test}, 49 | {:uuid, "~> 1.1.6"} 50 | ] 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /test/resources/3.txt: -------------------------------------------------------------------------------- 1 | Content-Length: 555 2 | Content-Type: text/event-plain 3 | 4 | Event-Name: RE_SCHEDULE 5 | Core-UUID: a95e64ec-df1f-48f4-acbf-34b4c359d747 6 | FreeSWITCH-Hostname: faulty.local 7 | FreeSWITCH-Switchname: faulty.local 8 | FreeSWITCH-IPv4: 192.168.1.102 9 | FreeSWITCH-IPv6: %3A%3A1 10 | Event-Date-Local: 2015-07-04%2015%3A18%3A14 11 | Event-Date-GMT: Sat,%2004%20Jul%202015%2018%3A18%3A14%20GMT 12 | Event-Date-Timestamp: 1436033894106095 13 | Event-Calling-File: switch_scheduler.c 14 | Event-Calling-Function: switch_scheduler_execute 15 | Event-Calling-Line-Number: 71 16 | Event-Sequence: 1996 17 | Task-ID: 2 18 | Task-Desc: heartbeat 19 | Task-Group: core 20 | Task-Runtime: 1436033914 21 | 22 | Content-Length: 554 23 | Content-Type: text/event-plain 24 | 25 | Event-Name: RE_SCHEDULE 26 | Core-UUID: a95e64ec-df1f-48f4-acbf-34b4c359d747 27 | FreeSWITCH-Hostname: faulty.local 28 | FreeSWITCH-Switchname: faulty.local 29 | FreeSWITCH-IPv4: 192.168.1.102 30 | FreeSWITCH-IPv6: %3A%3A1 31 | Event-Date-Local: 2015-07-04%2015%3A18%3A14 32 | Event-Date-GMT: Sat,%2004%20Jul%202015%2018%3A18%3A14%20GMT 33 | Event-Date-Timestamp: 1436033894106095 34 | Event-Calling-File: switch_scheduler.c 35 | Event-Calling-Function: switch_scheduler_execute 36 | Event-Calling-Line-Number: 71 37 | Event-Sequence: 1997 38 | Task-ID: 3 39 | Task-Desc: check_ip 40 | Task-Group: core 41 | Task-Runtime: 1436033954 42 | 43 | -------------------------------------------------------------------------------- /lib/elixir_mod_event/content.ex: -------------------------------------------------------------------------------- 1 | defmodule FSModEvent.Content do 2 | @moduledoc """ 3 | Parses the given payload. 4 | 5 | Copyright 2015 Marcelo Gornstein 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | """ 18 | alias FSModEvent.Header, as: Header 19 | 20 | @doc """ 21 | Will parse and return a payload according to the content type given. 22 | """ 23 | @spec parse(String.t, char_list) :: term 24 | def parse("text/event-plain", data) do 25 | event_plain data, %{} 26 | end 27 | 28 | def parse(_, data), do: {data, nil} 29 | 30 | defp event_plain(data, acc) do 31 | case Header.parse data do 32 | {key, value, rest} -> 33 | acc = Map.put acc, key, URI.decode_www_form(value) 34 | case rest do 35 | [?\n|rest] -> {acc, rest} 36 | _ -> event_plain rest, acc 37 | end 38 | _error -> nil 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /lib/elixir_mod_event/header.ex: -------------------------------------------------------------------------------- 1 | defmodule FSModEvent.Header do 2 | @moduledoc """ 3 | Header parsing functions. 4 | 5 | Copyright 2015 Marcelo Gornstein 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | """ 18 | 19 | @doc """ 20 | Given a line terminated in \n tries to parse a header in the form: 21 | Key: Value\n 22 | """ 23 | @spec parse(char_list) :: {String.t, String.t, char_list} | :error 24 | def parse(char_list) do 25 | char_list |> parse_key |> parse_value |> normalize 26 | end 27 | 28 | defp parse_key(char_list) do 29 | parse_key char_list, [] 30 | end 31 | 32 | defp parse_key([c, ?:, 32|rest], acc) do 33 | {Enum.reverse([c|acc]), rest} 34 | end 35 | 36 | defp parse_key([c|rest], acc) do 37 | parse_key rest, [c|acc] 38 | end 39 | 40 | defp parse_key(_, _), do: :error 41 | 42 | defp parse_value({key, rest}) do 43 | parse_value {key, rest}, [] 44 | end 45 | 46 | defp parse_value(error), do: error 47 | 48 | defp parse_value({key, [?\n|rest]}, acc) do 49 | {key, Enum.reverse(acc), rest} 50 | end 51 | 52 | defp parse_value({key, [c|rest]}, acc) do 53 | parse_value {key, rest}, [c|acc] 54 | end 55 | 56 | defp parse_value(_, _), do: :error 57 | 58 | defp normalize({key, value, rest}) do 59 | {String.downcase(to_string(key)), to_string(value), rest} 60 | end 61 | 62 | defp normalize(error), do: error 63 | end 64 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []}, 2 | "coverex": {:hex, :coverex, "1.4.12", "b18d737734edeac578a854cfaa5aa48c5d05e649c77ce02efb7f723a316b6a3b", [:mix], [{:hackney, "~> 1.5", [hex: :hackney, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, 3 | "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, 4 | "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, 5 | "hackney": {:hex, :hackney, "1.6.5", "8c025ee397ac94a184b0743c73b33b96465e85f90a02e210e86df6cbafaa5065", [:rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, 6 | "httpoison": {:hex, :httpoison, "0.8.3", "b675a3fdc839a0b8d7a285c6b3747d6d596ae70b6ccb762233a990d7289ccae4", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, 7 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 8 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 9 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 10 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, 11 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}, 12 | "uuid": {:hex, :uuid, "1.1.6", "4927232f244e69c6e255643014c2d639dad5b8313dc2a6976ee1c3724e6ca60d", [:mix], []}} 13 | -------------------------------------------------------------------------------- /lib/elixir_mod_event/packet.ex: -------------------------------------------------------------------------------- 1 | defmodule FSModEvent.Packet do 2 | @moduledoc """ 3 | Parses FreeSWITCH packets. 4 | 5 | Copyright 2015 Marcelo Gornstein 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | """ 18 | alias FSModEvent.Header, as: Header 19 | alias FSModEvent.Content, as: Content 20 | defstruct type: nil, 21 | success: false, 22 | headers_complete: false, 23 | payload_complete: false, 24 | complete: false, 25 | parse_error: false, 26 | headers: %{}, 27 | length: 0, 28 | rest: nil, 29 | job_id: nil, 30 | custom_payload: nil, 31 | payload: nil 32 | 33 | @type t :: %FSModEvent.Packet{} 34 | 35 | @doc """ 36 | true if the given packet is a response to an issued command. 37 | """ 38 | @spec is_response?(FSModEvent.Packet.t) :: boolean 39 | def is_response?(pkt) do 40 | pkt.type === "api/response" or 41 | pkt.type === "command/reply" 42 | end 43 | 44 | @doc """ 45 | Parses all packets found in the given input buffer. Returns a tuple with the 46 | buffer leftovers and the packets parsed. 47 | """ 48 | @spec parse( 49 | char_list, [FSModEvent.Packet.t] 50 | ) :: {char_list, [FSModEvent.Packet.t]} 51 | def parse(char_list, acc \\ []) do 52 | pkt = parse_real char_list 53 | if pkt.parse_error or not pkt.complete do 54 | {char_list, Enum.reverse acc} 55 | else 56 | parse pkt.rest, [%FSModEvent.Packet{pkt | rest: nil} | acc] 57 | end 58 | end 59 | 60 | defp parse_real(data) do 61 | %FSModEvent.Packet{ 62 | rest: data, 63 | } |> headers |> payload |> normalize 64 | end 65 | 66 | defp headers(pkt = %FSModEvent.Packet{parse_error: false}) do 67 | case Header.parse pkt.rest do 68 | {key, value, rest} -> 69 | pkt = %FSModEvent.Packet{pkt | 70 | headers: Map.put(pkt.headers, key, value) 71 | } 72 | case rest do 73 | [?\n|rest] -> 74 | %FSModEvent.Packet{pkt | 75 | headers_complete: true, 76 | rest: rest 77 | } 78 | _ -> headers %FSModEvent.Packet{pkt | rest: rest} 79 | end 80 | _error -> %FSModEvent.Packet{pkt | parse_error: true} 81 | end 82 | end 83 | 84 | defp headers(pkt) do 85 | %FSModEvent.Packet{pkt | parse_error: true} 86 | end 87 | 88 | defp payload(pkt = %FSModEvent.Packet{ 89 | parse_error: false, 90 | headers: headers, 91 | rest: rest 92 | }) do 93 | l = case headers["content-length"] do 94 | nil -> 0 95 | l -> 96 | {l, ""} = Integer.parse l 97 | l 98 | end 99 | {p, rest} = Enum.split rest, l 100 | if length(p) === l do 101 | ctype = headers["content-type"] 102 | {p, custom_payload} = case Content.parse ctype, p do 103 | nil -> {"", ""} 104 | r -> r 105 | end 106 | %FSModEvent.Packet{pkt | 107 | payload_complete: true, 108 | length: l, 109 | payload: p, 110 | custom_payload: custom_payload, 111 | rest: rest 112 | } 113 | else 114 | %FSModEvent.Packet{pkt | parse_error: true} 115 | end 116 | end 117 | 118 | defp payload(pkt), do: pkt 119 | 120 | defp normalize(pkt = %FSModEvent.Packet{parse_error: false}) do 121 | complete = pkt.headers_complete and pkt.payload_complete 122 | job_id = case pkt.headers["reply-text"] do 123 | nil -> if(is_map(pkt.payload) and not is_nil pkt.payload["job-uuid"]) do 124 | pkt.payload["job-uuid"] 125 | else 126 | nil 127 | end 128 | job_id -> case Regex.run ~r/\+OK Job-UUID: (.*)$/, job_id do 129 | nil -> nil 130 | [_, job_id] -> job_id 131 | end 132 | end 133 | success = case pkt.headers["reply-text"] do 134 | nil -> false 135 | <<"+OK", _rest :: binary>> -> true 136 | _ -> false 137 | end 138 | ctype = pkt.headers["content-type"] 139 | %FSModEvent.Packet{pkt | 140 | complete: complete, 141 | success: success, 142 | job_id: job_id, 143 | type: ctype 144 | } 145 | end 146 | 147 | defp normalize(pkt), do: pkt 148 | end 149 | -------------------------------------------------------------------------------- /test/packet_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FSModEvent.Test.Packet do 2 | use ExUnit.Case, async: true 3 | alias FSModEvent.Packet, as: Packet 4 | require Logger 5 | 6 | test "can parse auth/request" do 7 | {'', [p]} = Packet.parse read!("1.txt") 8 | assert p.type === "auth/request" 9 | refute p.success 10 | assert p.headers_complete 11 | assert p.payload_complete 12 | assert p.complete 13 | refute p.parse_error 14 | assert p.headers === %{"content-type" => "auth/request"} 15 | assert p.length === 0 16 | assert is_nil p.rest 17 | assert is_nil p.job_id 18 | assert p.payload === '' 19 | end 20 | 21 | test "can parse command/reply" do 22 | {'', [p]} = Packet.parse read!("2.txt") 23 | assert p.type === "command/reply" 24 | assert p.success 25 | assert p.headers_complete 26 | assert p.payload_complete 27 | assert p.complete 28 | refute p.parse_error 29 | assert p.headers === %{ 30 | "reply-text" => "+OK accepted", 31 | "content-type" => "command/reply" 32 | } 33 | assert p.length === 0 34 | assert is_nil p.rest 35 | assert is_nil p.job_id 36 | assert p.payload === '' 37 | end 38 | 39 | test "can parse multiple" do 40 | {'', pkts} = Packet.parse read!("3.txt") 41 | assert length(pkts) === 2 42 | [p1, p2] = pkts 43 | assert p1.type === "text/event-plain" 44 | refute p1.success 45 | assert p1.headers_complete 46 | assert p1.payload_complete 47 | assert p1.complete 48 | refute p1.parse_error 49 | assert p1.headers === %{ 50 | "content-type" => "text/event-plain", 51 | "content-length" => "555" 52 | } 53 | assert p1.length === 555 54 | assert is_nil p1.rest 55 | assert is_nil p1.job_id 56 | assert p1.payload === %{ 57 | "task-runtime" => "1436033914", 58 | "task-group" => "core", 59 | "task-desc" => "heartbeat", 60 | "task-id" => "2", 61 | "event-sequence" => "1996", 62 | "event-calling-line-number" => "71", 63 | "event-calling-function" => "switch_scheduler_execute", 64 | "event-calling-file" => "switch_scheduler.c", 65 | "event-date-timestamp" => "1436033894106095", 66 | "event-date-gmt" => "Sat, 04 Jul 2015 18:18:14 GMT", 67 | "event-date-local" => "2015-07-04 15:18:14", 68 | "freeswitch-ipv6" => "::1", 69 | "freeswitch-ipv4" => "192.168.1.102", 70 | "freeswitch-switchname" => "faulty.local", 71 | "freeswitch-hostname" => "faulty.local", 72 | "core-uuid" => "a95e64ec-df1f-48f4-acbf-34b4c359d747", 73 | "event-name" => "RE_SCHEDULE" 74 | } 75 | 76 | assert p2.type === "text/event-plain" 77 | refute p2.success 78 | assert p2.headers_complete 79 | assert p2.payload_complete 80 | assert p2.complete 81 | refute p2.parse_error 82 | assert p2.headers === %{ 83 | "content-type" => "text/event-plain", 84 | "content-length" => "554" 85 | } 86 | assert p2.length === 554 87 | assert is_nil p2.rest 88 | assert is_nil p2.job_id 89 | assert p2.payload === %{ 90 | "task-runtime" => "1436033954", 91 | "task-group" => "core", 92 | "task-desc" => "check_ip", 93 | "task-id" => "3", 94 | "event-sequence" => "1997", 95 | "event-calling-line-number" => "71", 96 | "event-calling-function" => "switch_scheduler_execute", 97 | "event-calling-file" => "switch_scheduler.c", 98 | "event-date-timestamp" => "1436033894106095", 99 | "event-date-gmt" => "Sat, 04 Jul 2015 18:18:14 GMT", 100 | "event-date-local" => "2015-07-04 15:18:14", 101 | "freeswitch-ipv6" => "::1", 102 | "freeswitch-ipv4" => "192.168.1.102", 103 | "freeswitch-switchname" => "faulty.local", 104 | "freeswitch-hostname" => "faulty.local", 105 | "core-uuid" => "a95e64ec-df1f-48f4-acbf-34b4c359d747", 106 | "event-name" => "RE_SCHEDULE" 107 | } 108 | end 109 | 110 | test "can parse custom data" do 111 | {'', [p]} = Packet.parse read!("4.txt") 112 | assert p.type === "text/event-plain" 113 | refute p.success 114 | assert p.headers_complete 115 | assert p.payload_complete 116 | assert p.complete 117 | refute p.parse_error 118 | assert p.headers === %{ 119 | "content-type" => "text/event-plain", 120 | "content-length" => "654" 121 | } 122 | assert p.length === 654 123 | assert is_nil p.rest 124 | assert is_nil p.job_id 125 | assert p.payload === %{ 126 | "content-length" => "57", 127 | "event-sequence" => "4205", 128 | "event-calling-line-number" => "2209", 129 | "event-calling-function" => "parse_command", 130 | "event-calling-file" => "mod_event_socket.c", 131 | "event-date-timestamp" => "1436051526425813", 132 | "event-date-gmt" => "Sat, 04 Jul 2015 23:12:06 GMT", 133 | "event-date-local" => "2015-07-04 20:12:06", 134 | "freeswitch-ipv6" => "::1", 135 | "freeswitch-ipv4" => "192.168.1.102", 136 | "freeswitch-switchname" => "faulty.local", 137 | "freeswitch-hostname" => "faulty.local", 138 | "core-uuid" => "a95e64ec-df1f-48f4-acbf-34b4c359d747", 139 | "event-name" => "CUSTOM", 140 | "event-uuid" => "ee38d831-c80a-4520-8249-7424fa353408", 141 | "hdr1" => "val1", 142 | "content-length" => "57", 143 | "command" => "sendevent myeventito", 144 | } 145 | assert p.custom_payload === 'this is a custom payload this is a custom payload oh yeah' 146 | end 147 | 148 | defp read!(file) do 149 | to_char_list File.read!("test/resources/#{file}") 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | -------------------------------------------------------------------------------- /lib/elixir_mod_event/erlang.ex: -------------------------------------------------------------------------------- 1 | defmodule FSModEvent.Erlang do 2 | @moduledoc """ 3 | Interface to mod_erlang_event. 4 | 5 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event 6 | 7 | Copyright 2015 Marcelo Gornstein 8 | 9 | Licensed under the Apache License, Version 2.0 (the "License"); 10 | you may not use this file except in compliance with the License. 11 | You may obtain a copy of the License at 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | """ 20 | defmodule Error do 21 | defexception message: "default message" 22 | end 23 | 24 | require Logger 25 | @timeout 5000 26 | 27 | @doc """ 28 | Runs an API command in foreground. 29 | 30 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-api 31 | """ 32 | @spec api(node, String.t, String.t) :: String.t | no_return 33 | def api(node, command, args \\ "") do 34 | run node, :api, {:api, String.to_atom(command), args} 35 | end 36 | 37 | @doc """ 38 | Runs an API command in background. Returns a job id. The caller process will 39 | receive a message with a tuple like this {:fs_job_result, job_id, status, result} 40 | 41 | Where: 42 | 43 | job_id :: String.t 44 | status :: :ok | :error 45 | result :: :timeout | String.t 46 | 47 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-bgapi 48 | """ 49 | @spec bgapi(node, String.t, String.t) :: String.t | no_return 50 | def bgapi(node, command, args \\ "", timeout \\ @timeout) do 51 | caller = self() 52 | spawn fn -> 53 | job_id = run node, :bgapi, {:bgapi, String.to_atom(command), args} 54 | send caller, {:fs_job_id, job_id} 55 | receive do 56 | {status, ^job_id, x} -> 57 | status = if status === :bgok do 58 | :ok 59 | else 60 | :error 61 | end 62 | send caller, {:fs_job_result, job_id, status, x} 63 | after timeout -> 64 | send caller, {:fs_job_result, job_id, :error, :timeout} 65 | end 66 | end 67 | receive do 68 | {:fs_job_id, job_id} -> job_id 69 | end 70 | end 71 | 72 | @doc """ 73 | Registers the caller process as a log handler. Will receive all logs as 74 | messages. 75 | 76 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-register_log_handler 77 | """ 78 | @spec register_log_handler(node) :: :ok | no_return 79 | def register_log_handler(node) do 80 | run node, :foo, :register_log_handler 81 | end 82 | 83 | @doc """ 84 | Subscribe to an event. 85 | 86 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-event 87 | """ 88 | @spec event(node, String.t, String.t) :: :ok | no_return 89 | def event(node, event, value \\ nil) do 90 | if is_nil value do 91 | run node, :foo, {:event, String.to_atom(event)} 92 | else 93 | run node, :foo, {:event, String.to_atom(event), String.to_atom(value)} 94 | end 95 | end 96 | 97 | @doc """ 98 | Unsubscribes from an event. 99 | 100 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-nixevent 101 | """ 102 | @spec nixevent(node, String.t, String.t) :: :ok | no_return 103 | def nixevent(node, event, value \\ nil) do 104 | if is_nil value do 105 | run node, :foo, {:nixevent, String.to_atom(event)} 106 | else 107 | run node, :foo, {:nixevent, String.to_atom(event), String.to_atom(value)} 108 | end 109 | end 110 | 111 | @doc """ 112 | Registers the caller process as an event handler. Will receive all events as 113 | messages. 114 | 115 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-register_event_handler 116 | """ 117 | @spec register_event_handler(node) :: :ok | no_return 118 | def register_event_handler(node) do 119 | run node, :foo, :register_event_handler 120 | end 121 | 122 | @doc """ 123 | Changes the log level. 124 | 125 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-set_log_level 126 | """ 127 | @spec set_log_level(node, String.t) :: :ok | no_return 128 | def set_log_level(node, level) do 129 | run node, :foo, {:set_log_level, String.to_atom(level)} 130 | end 131 | 132 | @doc """ 133 | Disables logging. 134 | 135 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-nolog 136 | """ 137 | @spec nolog(node) :: :ok | no_return 138 | def nolog(node) do 139 | run node, :nolog 140 | end 141 | 142 | @doc """ 143 | Closes the connection. 144 | 145 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-exit 146 | """ 147 | @spec exit(node) :: :ok | no_return 148 | def exit(node) do 149 | run node, :exit 150 | end 151 | 152 | @doc """ 153 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-sendevent 154 | """ 155 | @spec sendevent(node, String.t, [{String.t, String.t}]) :: :ok | no_return 156 | def sendevent(node, event, headers) do 157 | run node, :sendevent, {:sendevent, event, headers} 158 | end 159 | 160 | @doc """ 161 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-sendmsg 162 | """ 163 | @spec sendmsg_exec( 164 | node, String.t, String.t, String.t, Integer.t 165 | ) :: :ok | no_return 166 | def sendmsg_exec(name, uuid, command, args \\ "", loops \\ 1) do 167 | sendmsg name, uuid, 'execute', [ 168 | {'execute-app-name', to_char_list(command)}, 169 | {'execute-app-arg', to_char_list(args)}, 170 | {'loops', to_char_list(loops)} 171 | ] 172 | end 173 | 174 | @doc """ 175 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-sendmsg 176 | """ 177 | @spec sendmsg_hangup(node, String.t, Integer.t) :: :ok | no_return 178 | def sendmsg_hangup(name, uuid, cause \\ 16) do 179 | sendmsg name, uuid, 'hangup', [{'hangup-cause', to_char_list(cause)}] 180 | end 181 | 182 | @doc """ 183 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-sendmsg 184 | """ 185 | @spec sendmsg_unicast( 186 | node, String.t, String.t, String.t, 187 | String.t, Integer.t, String.t, Integer.t 188 | ) :: FSModEvent.Packet.t 189 | def sendmsg_unicast( 190 | name, uuid, transport \\ "tcp", flags \\ "native", 191 | local_ip \\ "127.0.0.1", local_port \\ 8025, 192 | remote_ip \\ "127.0.0.1", remote_port \\ 8026 193 | ) do 194 | sendmsg name, uuid, 'unicast', [ 195 | {'local-ip', to_char_list(local_ip)}, 196 | {'local-port', to_char_list(local_port)}, 197 | {'remote-ip', to_char_list(remote_ip)}, 198 | {'remote-port', to_char_list(remote_port)}, 199 | {'transport', to_char_list(transport)}, 200 | {'flags', to_char_list(flags)} 201 | ] 202 | end 203 | 204 | @doc """ 205 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-sendmsg 206 | """ 207 | @spec sendmsg_nomedia(node, String.t, String.t) :: FSModEvent.Packet.t 208 | def sendmsg_nomedia(node, uuid, info \\ "") do 209 | sendmsg node, uuid, :nomedia, [{'nomedia-uuid', to_char_list(info)}] 210 | end 211 | 212 | @doc """ 213 | Makes FreeSWITCH send all the events related to the given call uuid to the 214 | given regitered process name (or the caller process, if none is given). 215 | 216 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-handlecall 217 | """ 218 | @spec handlecall(node, String.t, atom) :: :ok | no_return 219 | def handlecall(node, uuid, process \\ nil) do 220 | if is_nil process do 221 | run node, :handlecall, {:handlecall, uuid} 222 | else 223 | run node, :handlecall, {:handlecall, uuid, process} 224 | end 225 | end 226 | 227 | @doc """ 228 | Binds the caller process as a configuration provider for the given 229 | configuration section. The sections are the same as for mod_xml_curl, see: 230 | https://freeswitch.org/confluence/display/FREESWITCH/mod_xml_curl. 231 | 232 | You will receive messages of the type: 233 | 234 | {fetch,
, , , , , } 235 | 236 | Where FetchID is the ID you received in the request and XMLString is the XML 237 | reply you want to send. FetchID and XML can be binaries or strings. 238 | 239 | To tell the switch to take some action, send back a reply of the format: 240 | {fetch_reply, , } 241 | 242 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-XMLsearchbindings 243 | """ 244 | @spec config_bind(node, String.t) :: :ok | no_return 245 | def config_bind(node, type) do 246 | run node, :bind, {:bind, String.to_atom(type)} 247 | end 248 | 249 | @doc """ 250 | Sends an XML in response to a configuration message (see config_bind). The 251 | XML should be the same as the one supported by mod_xml_curl, see: 252 | https://freeswitch.org/confluence/display/FREESWITCH/mod_xml_curl. 253 | 254 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-XMLsearchbindings 255 | """ 256 | @spec config_reply(node, String.t, String.t) :: :ok | no_return 257 | def config_reply(node, fetch_id, xml) do 258 | run node, :send, {:fetch_reply, fetch_id, xml} 259 | end 260 | 261 | @doc """ 262 | Returns the fake pid of the "erlang process" running in the freeswitch erlang 263 | node. 264 | 265 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-getpid 266 | """ 267 | @spec pid(atom) :: pid | no_return 268 | def pid(node) do 269 | run node, :getpid 270 | end 271 | 272 | @doc """ 273 | Disable all events. 274 | 275 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-noevents 276 | """ 277 | @spec noevents(atom) :: pid | no_return 278 | def noevents(node) do 279 | run node, :noevents 280 | end 281 | 282 | defp sendmsg(node, uuid, command, headers) do 283 | headers = [{'call-command', command}|headers] 284 | run node, :sendmsg, {:sendmsg, to_char_list(uuid), headers} 285 | end 286 | 287 | defp run(node, command, payload \\ nil, timeout \\ @timeout) do 288 | payload = if is_nil payload do 289 | command 290 | else 291 | payload 292 | end 293 | send {command, node}, payload 294 | receive do 295 | {:ok, x} -> x 296 | :ok -> :ok 297 | {:error, x} -> raise FSModEvent.Erlang.Error, message: "#{inspect x}" 298 | :error -> raise FSModEvent.Erlang.Error, message: "unknown error" 299 | after timeout -> 300 | raise FSModEvent.Erlang.Error, message: "timeout" 301 | end 302 | end 303 | end 304 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/marcelog/elixir_mod_event.svg)](https://travis-ci.org/marcelog/elixir_mod_event) 2 | 3 | # elixir_mod_event 4 | Elixir client for the [FreeSWITCH mod_event_socket](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket). 5 | 6 | It also supports the [mod_erlang_event](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event). 7 | 8 | ---- 9 | 10 | # Using it with Mix 11 | 12 | To use it in your Mix projects, first add it as a dependency: 13 | 14 | ```elixir 15 | def deps do 16 | [{:elixir_mod_event, "~> 0.0.6"}] 17 | end 18 | ``` 19 | Then run mix deps.get to install it. 20 | 21 | ---- 22 | 23 | # Documentation 24 | 25 | Feel free to take a look at the [documentation](http://hexdocs.pm/elixir_mod_event/) 26 | served by hex.pm or the source itself to find more. 27 | 28 | ---- 29 | 30 | # Inbound Mode (TCP connection) 31 | 32 | ## Starting a TCP connection 33 | To connect to FreeSWITCH just start a [Connection](https://github.com/marcelog/elixir_mod_event/blob/master/lib/elixir_mod_event/connection.ex), 34 | which is just a [GenServer](http://elixir-lang.org/docs/v1.0/elixir/GenServer.html) that you 35 | can plug into your own supervisor tree. 36 | ```elixir 37 | > alias FSModEvent.Connection, as: C 38 | > C.start :connection_name, fs_host, fs_port, fs_password 39 | {:ok, #PID<0.158.0>} 40 | ``` 41 | 42 | You can also start and link the connection: 43 | ```elixir 44 | > C.start :connection_name, fs_host, fs_port, fs_password 45 | {:ok, #PID<0.159.0>} 46 | ``` 47 | 48 | ## Results 49 | When executing a command (either in foreground or background) or receiving events, 50 | the result will be a [Packet](https://github.com/marcelog/elixir_mod_event/blob/master/lib/elixir_mod_event/packet.ex), 51 | with a structure with some fields of interest: 52 | 53 | * **success**: Boolean. When executing foreground commands will be true if the command 54 | was executed successfuly. 55 | * **type**: String. The type of the packet (e.g: "text/event-plain", "command/reply", etc). 56 | * **payload**: Map or String. Depends on the type of the packet. 57 | * **length**: Payload length, useful when the payload is a string. 58 | * **job_id**: String. May contain a job id, related to a response or an event. 59 | * **headers**: Map. Packet headers. 60 | * **custom_payload**: String. May contain additional payload, depends on the packet and command sent/received. 61 | 62 | ### Receiving events 63 | 64 | To receive events register processes with a filter function like this: 65 | ```elixir 66 | > C.start_listening :fs1 67 | ``` 68 | 69 | The default filter function will let all events pass through to the process, but 70 | you can specify a custom filter function: 71 | ```elixir 72 | > C.start_listening :fs1, fn(pkt) -> pkt.payload["event-name"] === "HEARTBEAT" end 73 | ``` 74 | 75 | To unregister the listener process: 76 | ```elixir 77 | > C.stop_listening :fs1 78 | ``` 79 | 80 | **NOTE**: The caller process will be monitored and auto-unregister when the registered process dies. 81 | 82 | ## Examples 83 | 84 | ### [api](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-api) 85 | Sends a [command](https://freeswitch.org/confluence/display/FREESWITCH/mod_commands). 86 | 87 | ```elixir 88 | > C.api :fs1, "host_lookup", "google.com" 89 | %FSModEvent.Packet{ 90 | payload: '173.194.42.78', 91 | ... 92 | } 93 | ``` 94 | 95 | ### [bgapi](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-bgapi) 96 | Like `api` but runs the command without blocking the process. The calling process will 97 | receive a message with the result of the command. Be sure to subscribe to the 98 | [BACKGROUND_JOB](https://freeswitch.org/confluence/display/FREESWITCH/Event+List#EventList-Otherevents) event. 99 | 100 | ```elixir 101 | > C.event :fs1, "BACKGROUND_JOB" 102 | > C.bgapi :fs1, "md5", "some_data" 103 | "b857e1dd-e4de-424e-9ff6-8e05e9a076d9" 104 | > flush 105 | {:fs_job_result, "b857e1dd-e4de-424e-9ff6-8e05e9a076d9", %FSModEvent.Packet{ 106 | custom_payload: '0d9247cbce34aba4aca8d5c887a0f0a4', 107 | ... 108 | }} 109 | ``` 110 | 111 | ### [linger](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-linger) 112 | ```elixir 113 | > C.linger :fs1 114 | ``` 115 | 116 | ### [nolinger](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-nolinger) 117 | ```elixir 118 | > C.nolinger :fs1 119 | ``` 120 | 121 | ### [event](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-event) 122 | ```elixir 123 | > C.event :fs1, "all" 124 | > C.event :fs1, "CUSTOM", "conference::maintenance" 125 | ``` 126 | 127 | ### [myevents](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-SpecialCase-'myevents') 128 | ```elixir 129 | > C.myevents :fs1, "e96b78d8-1dc2-4634-84c4-58366f1a92b1" 130 | ``` 131 | 132 | ### [divert_events](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-divert_events) 133 | ```elixir 134 | > C.enable_divert_events :fs1 135 | > C.disable_divert_events :fs1 136 | ``` 137 | 138 | ### [filter](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-filter) 139 | ```elixir 140 | > C.filter :fs1, "Event-Name", "CHANNEL_EXECUTE" 141 | ``` 142 | 143 | ### [filter_delete](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-filterdelete) 144 | ```elixir 145 | > C.filter_delete :fs1, "Event-Name", "CHANNEL_EXECUTE" 146 | ``` 147 | 148 | ### [sendevent](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-sendevent) 149 | ```elixir 150 | > C.sendevent :fs1, "custom_event", [{"header1", "value1"}], "custom payload" 151 | ``` 152 | 153 | ### [sendmsg](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-sendmsg) 154 | ```elixir 155 | > C.sendmsg_exec :fs1, "e96b78d8-1dc2-4634-84c4-58366f1a92b1", "uuid_answer", "e96b78d8-1dc2-4634-84c4-58366f1a92b1" 156 | > C.sendmsg_hangup :fs1, "e96b78d8-1dc2-4634-84c4-58366f1a92b1", 16 157 | > C.sendmsg_unicast :fs1, "e96b78d8-1dc2-4634-84c4-58366f1a92b1", "tcp", "native", "127.0.0.1", 8025, "127.0.0.1", 8026 158 | > C.sendmsg_nomedia :fs1, "e96b78d8-1dc2-4634-84c4-58366f1a92b1", "info" 159 | ``` 160 | 161 | ### [exit](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-exit) 162 | ```elixir 163 | > C.exit :fs1 164 | ``` 165 | 166 | ### [log](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-log) 167 | ```elixir 168 | > C.log :fs1, "debug" 169 | ``` 170 | 171 | ### [nolog](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-nolog) 172 | ```elixir 173 | > C.nolog :fs1 174 | ``` 175 | 176 | ### [nixevent](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-nixevent) 177 | ```elixir 178 | > C.nixevent :fs1, "all" 179 | ``` 180 | 181 | ### [noevents](https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-noevents) 182 | ```elixir 183 | > C.noevents :fs1 184 | ``` 185 | 186 | ---- 187 | 188 | # Inbound Mode (Erlang node connection) 189 | 190 | To "talk" to the FreeSWITCH erlang node, use the [Erlang](https://github.com/marcelog/elixir_mod_event/blob/master/lib/elixir_mod_event/erlang.ex) module: 191 | 192 | ```elixir 193 | > alias FSModEvent.Erlang, as: E 194 | > node = :"freeswitch@host.local" 195 | ``` 196 | 197 | ### [api](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-api) 198 | Sends a [command](https://freeswitch.org/confluence/display/FREESWITCH/mod_commands). 199 | 200 | ```elixir 201 | > E.api node, "host_lookup", "google.com" 202 | "173.194.42.82" 203 | ``` 204 | 205 | ### [bgapi](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-bgapi) 206 | Like `api` but runs the command without blocking the process. The caller process will 207 | receive a message with a tuple like this: 208 | 209 | ```elixir 210 | {:fs_job_result, job_id, status, result} 211 | ``` 212 | 213 | Where: 214 | 215 | ```elixir 216 | job_id :: String.t 217 | 218 | status :: :ok | :error 219 | 220 | result :: :timeout | String.t 221 | ``` 222 | 223 | ```elixir 224 | > E.bgapi node, "md5", "some_data" 225 | "4a41cfc1-d9b7-4966-95d0-5de5ec690a07" 226 | 227 | > flush 228 | {:fs_job_result, "4a41cfc1-d9b7-4966-95d0-5de5ec690a07", :ok, 229 | "0d9247cbce34aba4aca8d5c887a0f0a4"} 230 | ``` 231 | 232 | ### [register_event_handler](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-register_event_handler) 233 | ```elixir 234 | > E.register_event_handler node 235 | ``` 236 | 237 | ### [event](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-event) 238 | ```elixir 239 | > E.event node, "all" 240 | > E.event node, "CUSTOM", "conference::maintenance" 241 | ``` 242 | 243 | ### [nixevent](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-nixevent) 244 | ```elixir 245 | > E.nixevent node, "all" 246 | ``` 247 | 248 | ### [noevents](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-noevents) 249 | ```elixir 250 | > E.noevents node 251 | ``` 252 | 253 | ### [register_log_handler](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-register_log_handler) 254 | ```elixir 255 | > E.register_log_handler node 256 | ``` 257 | 258 | ### [set_log_level](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-set_log_level) 259 | ```elixir 260 | > E.set_log_level node, "debug" 261 | ``` 262 | 263 | ### [nolog](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-nolog) 264 | ```elixir 265 | > E.nolog node 266 | ``` 267 | 268 | ### [exit](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-exit) 269 | ```elixir 270 | > E.exit node 271 | ``` 272 | 273 | ### [sendmsg](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-sendmsg) 274 | ```elixir 275 | > E.sendmsg_exec node, "e96b78d8-1dc2-4634-84c4-58366f1a92b1", "uuid_answer", "e96b78d8-1dc2-4634-84c4-58366f1a92b1" 276 | > E.sendmsg_hangup node, "e96b78d8-1dc2-4634-84c4-58366f1a92b1", 16 277 | > E.sendmsg_unicast node, "e96b78d8-1dc2-4634-84c4-58366f1a92b1", "tcp", "native", "127.0.0.1", 8025, "127.0.0.1", 8026 278 | > E.sendmsg_nomedia node, "e96b78d8-1dc2-4634-84c4-58366f1a92b1", "info" 279 | ``` 280 | 281 | ### [pid](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-getpid) 282 | ```elixir 283 | > E.pid node 284 | ``` 285 | 286 | ### [handlecall](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-handlecall) 287 | ```elixir 288 | > E.handlecall node, "e96b78d8-1dc2-4634-84c4-58366f1a92b1" 289 | > E.handlecall node, "e96b78d8-1dc2-4634-84c4-58366f1a92b1", :my_call_handler 290 | ``` 291 | 292 | ### Configuration hooks 293 | You can also configure FreeSWITCH by sending and receiving regular erlang messages by 294 | binding to the needed configuration sections. See [XML Search Bindings](https://freeswitch.org/confluence/display/FREESWITCH/mod_erlang_event#mod_erlang_event-XMLsearchbindings). 295 | 296 | The format and sections correspond to the ones supported by [mod_xml_curl](https://freeswitch.org/confluence/display/FREESWITCH/mod_xml_curl). 297 | 298 | ```elixir 299 | # Bind to the "directory" section 300 | > E.config_bind node, "directory" 301 | 302 | # Sample XML text 303 | > xml = " receive do 307 | {:fetch, :directory, "domain", "name", domain_name, uuid, headers} -> 308 | E.config_reply node, uuid, xml 309 | after 10 -> :ok 310 | end 311 | ``` 312 | 313 | ---- 314 | 315 | # License 316 | The source code is released under Apache 2 License. 317 | 318 | Check [LICENSE](https://github.com/marcelog/elixir_mod_event/blob/master/LICENSE) file for more information. 319 | 320 | -------------------------------------------------------------------------------- /lib/elixir_mod_event/connection.ex: -------------------------------------------------------------------------------- 1 | defmodule FSModEvent.Connection do 2 | @moduledoc """ 3 | Connection process. A GenServer that you can plug into your own supervisor 4 | tree. 5 | 6 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket 7 | 8 | Copyright 2015 Marcelo Gornstein 9 | 10 | Licensed under the Apache License, Version 2.0 (the "License"); 11 | you may not use this file except in compliance with the License. 12 | You may obtain a copy of the License at 13 | http://www.apache.org/licenses/LICENSE-2.0 14 | 15 | Unless required by applicable law or agreed to in writing, software 16 | distributed under the License is distributed on an "AS IS" BASIS, 17 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | See the License for the specific language governing permissions and 19 | limitations under the License. 20 | """ 21 | alias FSModEvent.Packet, as: Packet 22 | use GenServer 23 | require Logger 24 | defstruct name: nil, 25 | host: nil, 26 | port: nil, 27 | password: nil, 28 | socket: nil, 29 | buffer: '', 30 | state: nil, 31 | sender: nil, 32 | jobs: %{}, 33 | listeners: %{} 34 | 35 | @typep t :: %FSModEvent.Connection{} 36 | 37 | @doc """ 38 | Registers the caller process as a receiver for all the events for which the 39 | filter_fun returns true. 40 | """ 41 | @spec start_listening(GenServer.server, fun) :: :ok 42 | def start_listening(name, filter_fun \\ fn(_) -> true end) do 43 | GenServer.cast name, {:start_listening, self(), filter_fun} 44 | end 45 | 46 | @doc """ 47 | Unregisters the caller process as a listener. 48 | """ 49 | @spec stop_listening(GenServer.server) :: :ok 50 | def stop_listening(name) do 51 | GenServer.cast name, {:stop_listening, self()} 52 | end 53 | 54 | @doc """ 55 | Starts a connection to FreeSWITCH. 56 | """ 57 | @spec start( 58 | atom, String.t, Integer.t, String.t 59 | ) :: GenServer.on_start 60 | def start(name, host, port, password) do 61 | options = [ 62 | host: host, 63 | port: port, 64 | password: password, 65 | name: name 66 | ] 67 | GenServer.start __MODULE__, options, name: name 68 | end 69 | 70 | @doc """ 71 | Starts and links a connection to FreeSWITCH. 72 | """ 73 | @spec start_link( 74 | atom, String.t, Integer.t, String.t 75 | ) :: GenServer.on_start 76 | def start_link(name, host, port, password) do 77 | options = [ 78 | host: host, 79 | port: port, 80 | password: password, 81 | name: name 82 | ] 83 | GenServer.start_link __MODULE__, options, name: name 84 | end 85 | 86 | @doc """ 87 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-api 88 | 89 | For a list of available commands see: https://freeswitch.org/confluence/display/FREESWITCH/mod_commands 90 | """ 91 | @spec api(GenServer.server, String.t, String.t) :: FSModEvent.Packet.t 92 | def api(name, command, args \\ "") do 93 | block_send name, "api #{command} #{args}" 94 | end 95 | 96 | @doc """ 97 | Executes an API command in background. Returns a Job ID. The calling process 98 | will receive a message like {:fs_job_result, job_id, packet} with the result. 99 | 100 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-bgapi 101 | """ 102 | @spec bgapi(GenServer.server, String.t, String.t) :: String.t 103 | def bgapi(name, command, args \\ "") do 104 | GenServer.call name, {:bgapi, self(), command, args} 105 | end 106 | 107 | @doc """ 108 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-linger 109 | """ 110 | @spec linger(GenServer.server) :: FSModEvent.Packet.t 111 | def linger(name) do 112 | block_send name, "linger" 113 | end 114 | 115 | @doc """ 116 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-nolinger 117 | """ 118 | @spec nolinger(GenServer.server) :: FSModEvent.Packet.t 119 | def nolinger(name) do 120 | block_send name, "nolinger" 121 | end 122 | 123 | @doc """ 124 | This will always prepend your list with "plain". 125 | 126 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-event 127 | """ 128 | @spec event(GenServer.server, String.t) :: FSModEvent.Packet.t 129 | def event(name, events) do 130 | block_send name, "event plain #{events}" 131 | end 132 | 133 | @doc """ 134 | This will always prepend your list with "plain". 135 | 136 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-SpecialCase-'myevents' 137 | """ 138 | @spec myevents(GenServer.server, String.t) :: FSModEvent.Packet.t 139 | def myevents(name, uuid) do 140 | block_send name, "myevents plain #{uuid}" 141 | end 142 | 143 | @doc """ 144 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-divert_events 145 | """ 146 | @spec enable_divert_events(GenServer.server) :: FSModEvent.Packet.t 147 | def enable_divert_events(name) do 148 | block_send name, "divert_events on" 149 | end 150 | 151 | @doc """ 152 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-divert_events 153 | """ 154 | @spec disable_divert_events(GenServer.server) :: FSModEvent.Packet.t 155 | def disable_divert_events(name) do 156 | block_send name, "divert_events off" 157 | end 158 | 159 | @doc """ 160 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-filter 161 | """ 162 | @spec filter(GenServer.server, String.t, String.t) :: FSModEvent.Packet.t 163 | def filter(name, key, value \\ "") do 164 | block_send name, "filter #{key} #{value}" 165 | end 166 | 167 | @doc """ 168 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-filterdelete 169 | """ 170 | @spec filter_delete( 171 | GenServer.server, String.t, String.t 172 | ) :: FSModEvent.Packet.t 173 | def filter_delete(name, key, value \\ "") do 174 | block_send name, "filter delete #{key} #{value}" 175 | end 176 | 177 | @doc """ 178 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-sendevent 179 | """ 180 | @spec sendevent( 181 | GenServer.server, String.t, [{String.t, String.t}], String.t 182 | ) :: FSModEvent.Packet.t 183 | def sendevent(name, event, headers \\ [], body \\ "") do 184 | length = String.length body 185 | headers = [{"content-length", to_string(length)}|headers] 186 | headers = for {k, v} <- headers, do: "#{k}: #{v}" 187 | lines = Enum.join ["sendevent #{event}"|headers], "\n" 188 | payload = if length === 0 do 189 | "#{lines}" 190 | else 191 | "#{lines}\n\n#{body}" 192 | end 193 | block_send name, payload 194 | end 195 | 196 | @doc """ 197 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-sendmsg 198 | """ 199 | @spec sendmsg_exec( 200 | GenServer.server, String.t, String.t, String.t, Integer.t, String.t 201 | ) :: FSModEvent.Packet.t 202 | def sendmsg_exec(name, uuid, command, args \\ "", loops \\ 1, body \\ "") do 203 | sendmsg name, uuid, "execute", [ 204 | {"execute-app-name", command}, 205 | {"execute-app-arg", args}, 206 | {"loops", to_string(loops)} 207 | ], body 208 | end 209 | 210 | @doc """ 211 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-sendmsg 212 | """ 213 | @spec sendmsg_hangup( 214 | GenServer.server, String.t, Integer.t 215 | ) :: FSModEvent.Packet.t 216 | def sendmsg_hangup(name, uuid, cause \\ 16) do 217 | sendmsg name, uuid, "hangup", [{"hangup-cause", to_string(cause)}] 218 | end 219 | 220 | @doc """ 221 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-sendmsg 222 | """ 223 | @spec sendmsg_unicast( 224 | GenServer.server, String.t, String.t, String.t, 225 | String.t, Integer.t, String.t, Integer.t 226 | ) :: FSModEvent.Packet.t 227 | def sendmsg_unicast( 228 | name, uuid, transport \\ "tcp", flags \\ "native", 229 | local_ip \\ "127.0.0.1", local_port \\ 8025, 230 | remote_ip \\ "127.0.0.1", remote_port \\ 8026 231 | ) do 232 | sendmsg name, uuid, "unicast", [ 233 | {"local-ip", local_ip}, 234 | {"local-port", to_string(local_port)}, 235 | {"remote-ip", remote_ip}, 236 | {"remote-port", to_string(remote_port)}, 237 | {"transport", transport}, 238 | {"flags", flags} 239 | ] 240 | end 241 | 242 | @doc """ 243 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-sendmsg 244 | """ 245 | @spec sendmsg_nomedia( 246 | GenServer.server, String.t, String.t 247 | ) :: FSModEvent.Packet.t 248 | def sendmsg_nomedia(name, uuid, info \\ "") do 249 | sendmsg name, uuid, "nomedia", [{"nomedia-uuid", info}] 250 | end 251 | 252 | @doc """ 253 | https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-exit 254 | """ 255 | @spec exit(GenServer.server) :: FSModEvent.Packet.t 256 | def exit(name) do 257 | block_send name, "exit" 258 | end 259 | 260 | @doc """ 261 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-log 262 | """ 263 | @spec log(GenServer.server, String.t) :: FSModEvent.Packet.t 264 | def log(name, level) do 265 | block_send name, "log #{level}" 266 | end 267 | 268 | @doc """ 269 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-nolog 270 | """ 271 | @spec nolog(GenServer.server) :: FSModEvent.Packet.t 272 | def nolog(name) do 273 | block_send name, "nolog" 274 | end 275 | 276 | @doc """ 277 | Suppress the specified type of event. 278 | 279 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-nixevent 280 | """ 281 | @spec nixevent(GenServer.server, String.t) :: FSModEvent.Packet.t 282 | def nixevent(name, events) do 283 | block_send name, "nixevent #{events}" 284 | end 285 | 286 | @doc """ 287 | See: https://freeswitch.org/confluence/display/FREESWITCH/mod_event_socket#mod_event_socket-noevents 288 | """ 289 | @spec noevents(GenServer.server) :: FSModEvent.Packet.t 290 | def noevents(name) do 291 | block_send name, "noevents" 292 | end 293 | 294 | @spec init([term]) :: {:ok, FSModEvent.Connection.t} | no_return 295 | def init(options) do 296 | Logger.info "Starting FS connection" 297 | {:ok, socket} = :gen_tcp.connect( 298 | to_char_list(options[:host]), options[:port], [ 299 | packet: 0, active: :once, mode: :list 300 | ] 301 | ) 302 | {:ok, %FSModEvent.Connection{ 303 | name: options[:name], 304 | host: options[:host], 305 | port: options[:port], 306 | password: options[:password], 307 | socket: socket, 308 | buffer: '', 309 | sender: nil, 310 | state: :connecting, 311 | jobs: %{} 312 | }} 313 | end 314 | 315 | @spec handle_call( 316 | term, term, FSModEvent.Connection.t 317 | ) :: {:noreply, FSModEvent.Connection.t} | 318 | {:reply, term, FSModEvent.Connection.t} 319 | def handle_call({:bgapi, caller, command, args}, _from, state) do 320 | id = UUID.uuid4 321 | cmd_send state.socket, "bgapi #{command} #{args}\nJob-UUID: #{id}" 322 | jobs = Map.put state.jobs, id, caller 323 | {:reply, id, %FSModEvent.Connection{state | jobs: jobs}} 324 | end 325 | 326 | def handle_call({:send, command}, from, state) do 327 | cmd_send state.socket, command 328 | {:noreply, %FSModEvent.Connection{state | sender: from}} 329 | end 330 | 331 | def handle_call(call, _from, state) do 332 | Logger.warn "Unknown call: #{inspect call}" 333 | {:reply, :unknown_call, state} 334 | end 335 | 336 | @spec handle_cast( 337 | term, FSModEvent.Connection.t 338 | ) :: {:noreply, FSModEvent.Connection.t} 339 | def handle_cast({:start_listening, caller, filter_fun}, state) do 340 | key = Base.encode64 :erlang.term_to_binary(caller) 341 | listeners = Map.put state.listeners, key, %{pid: caller, filter: filter_fun} 342 | Process.monitor caller 343 | {:noreply, %FSModEvent.Connection{state | listeners: listeners}} 344 | end 345 | 346 | def handle_cast({:stop_listening, caller}, state) do 347 | key = Base.encode64 :erlang.term_to_binary(caller) 348 | listeners = Map.delete state.listeners, key 349 | {:noreply, %FSModEvent.Connection{state | listeners: listeners}} 350 | end 351 | 352 | def handle_cast(cast, state) do 353 | Logger.warn "Unknown cast: #{inspect cast}" 354 | {:noreply, state} 355 | end 356 | 357 | @spec handle_info( 358 | term, FSModEvent.Connection.t 359 | ) :: {:noreply, FSModEvent.Connection.t} 360 | def handle_info({:DOWN, _, _, pid, _}, state) do 361 | handle_cast {:stop_listening, pid}, state 362 | end 363 | 364 | def handle_info({:tcp, socket, data}, state) do 365 | :inet.setopts(socket, active: :once) 366 | 367 | buffer = state.buffer ++ data 368 | {rest, ps} = Packet.parse buffer 369 | state = Enum.reduce ps, state, &process/2 370 | {:noreply, %FSModEvent.Connection{state | buffer: rest}} 371 | end 372 | 373 | def handle_info({:tcp_closed, _}, state) do 374 | Logger.info "Connection closed" 375 | {:stop, :normal, state} 376 | end 377 | 378 | def handle_info(message, state) do 379 | Logger.warn "Unknown message: #{inspect message}" 380 | {:noreply, state} 381 | end 382 | 383 | @spec terminate(term, FSModEvent.Connection.t) :: :ok 384 | def terminate(reason, _state) do 385 | Logger.info "Terminating with #{inspect reason}" 386 | :ok 387 | end 388 | 389 | @spec code_change( 390 | term, FSModEvent.Connection.t, term 391 | ) :: {:ok, FSModEvent.Connection.t} 392 | def code_change(_old_vsn, state, _extra) do 393 | {:ok, state} 394 | end 395 | 396 | defp process( 397 | %Packet{type: "auth/request"}, 398 | state = %FSModEvent.Connection{state: :connecting} 399 | ) do 400 | auth state.socket, state.password 401 | state 402 | end 403 | 404 | defp process( 405 | pkt = %Packet{type: "command/reply"}, 406 | state = %FSModEvent.Connection{state: :connecting} 407 | ) do 408 | if pkt.success do 409 | %FSModEvent.Connection{state | state: :connected} 410 | else 411 | raise "Could not login to FS: #{inspect pkt}" 412 | end 413 | end 414 | 415 | defp process(p, %FSModEvent.Connection{state: :connecting}) do 416 | raise "Unexpected packet while authenticating: #{inspect p}" 417 | end 418 | 419 | defp process(pkt, state) do 420 | new_state = cond do 421 | # Command immediate response 422 | Packet.is_response?(pkt) -> 423 | if not is_nil state.sender do 424 | GenServer.reply state.sender, pkt 425 | end 426 | state 427 | # Background job response 428 | not is_nil pkt.job_id -> 429 | if not is_nil state.jobs[pkt.job_id] do 430 | send state.jobs[pkt.job_id], {:fs_job_result, pkt.job_id, pkt} 431 | jobs = Map.delete(state.jobs, pkt.job_id) 432 | %FSModEvent.Connection{state | jobs: jobs} 433 | else 434 | state 435 | end 436 | # Regular event 437 | true -> 438 | Enum.each state.listeners, fn({_, v}) -> 439 | if v.filter.(pkt) do 440 | send v.pid, {:fs_event, pkt} 441 | end 442 | end 443 | state 444 | end 445 | %FSModEvent.Connection{new_state | sender: nil} 446 | end 447 | 448 | defp auth(socket, password) do 449 | cmd_send socket, "auth #{password}" 450 | end 451 | 452 | defp sendmsg(name, uuid, command, headers, body \\ "") do 453 | length = String.length body 454 | headers = if length > 0 do 455 | [ 456 | {"content-length", to_string(length)}, 457 | {"content-type", "text/plain"} 458 | |headers 459 | ] 460 | else 461 | headers 462 | end 463 | headers = [{"call-command", command}|headers] 464 | headers = for {k, v} <- headers, do: "#{k}: #{v}" 465 | lines = Enum.join ["sendmsg #{uuid}"|headers], "\n" 466 | payload = if length === 0 do 467 | "#{lines}" 468 | else 469 | "#{lines}\n\n#{body}" 470 | end 471 | block_send name, payload 472 | end 473 | 474 | defp block_send(name, command) do 475 | GenServer.call name, {:send, command} 476 | end 477 | 478 | defp cmd_send(socket, command) do 479 | c = "#{command}\n\n" 480 | Logger.debug "Sending #{c}" 481 | :ok = :gen_tcp.send socket, c 482 | end 483 | end 484 | --------------------------------------------------------------------------------