From e97e846ae92548dc2d58c0cc423a26b59bad39e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Mon, 17 Apr 2023 15:59:11 +0200 Subject: [PATCH 1/6] Propotype of WebSocket notification connection --- lib/jellyfish/exception.ex | 14 +++++++++ lib/jellyfish/notifier.ex | 59 ++++++++++++++++++++++++++++++++++++++ mix.exs | 1 + mix.lock | 1 + 4 files changed, 75 insertions(+) create mode 100644 lib/jellyfish/notifier.ex diff --git a/lib/jellyfish/exception.ex b/lib/jellyfish/exception.ex index 1df6363..f7edb09 100644 --- a/lib/jellyfish/exception.ex +++ b/lib/jellyfish/exception.ex @@ -14,4 +14,18 @@ defmodule Jellyfish.Exception do %__MODULE__{message: msg} end end + + defmodule NotificationStructureError do + defexception [:message] + + @impl true + def exception(_opts) do + msg = """ + Received notification with unexpected structure. + Make sure you are using correct combination of Jellyfish and SDK versions. + """ + + %__MODULE__{message: msg} + end + end end diff --git a/lib/jellyfish/notifier.ex b/lib/jellyfish/notifier.ex new file mode 100644 index 0000000..e0596cd --- /dev/null +++ b/lib/jellyfish/notifier.ex @@ -0,0 +1,59 @@ +defmodule Jellyfish.Notifier do + @moduledoc """ + TODO + """ + + use WebSockex + + alias Jellyfish.Exception + + def start_link(pid, url) do + token = Application.fetch_env!(:jellyfish_server_sdk, :token) + start_link(pid, url, token) + end + + def start_link(pid, url, token) do + state = %{receiver_pid: pid} + + auth_msg = + %{type: "controlMessage", data: %{type: "authRequest", token: token}} + |> Jason.encode!() + + with {:ok, pid} <- WebSockex.start("ws://#{url}/socket/server/websocket", __MODULE__, state), + :ok <- WebSockex.send_frame(pid, {:text, auth_msg}) do + Process.link(pid) + {:ok, pid} + else + {:error, _reason} = error -> + Process.exit(pid, :normal) + error + end + end + + def handle_frame({:text, msg}, state) do + with {:ok, decoded_msg} <- Jason.decode(msg), + %{"type" => "controlMessage", "data" => data} <- decoded_msg |> IO.inspect(), + %{"type" => type, "id" => id} <- data, + {:ok, decoded_type} <- decode_type(type) do + send(state.receiver_pid, {decoded_type, id}) + else + _other -> raise Exception.NotificationStructureError + end + + {:ok, state} + end + + defp decode_type(type) do + decoded_type = + case type do + "authenticated" -> :authenticated + "peerConnected" -> :peer_connected + "peerDisconnected" -> :peer_disconected + "roomCrashed" -> :room_crashed + "componentCrashed" -> :component_crashed + _other -> nil + end + + if is_nil(decoded_type), do: {:error, :invalid_type}, else: {:ok, decoded_type} + end +end diff --git a/mix.exs b/mix.exs index 1a538b1..7bb2d34 100644 --- a/mix.exs +++ b/mix.exs @@ -51,6 +51,7 @@ defmodule Membrane.Template.Mixfile do {:tesla, "~> 1.5"}, {:mint, "~> 1.0"}, {:jason, "~> 1.4"}, + {:websockex, "~> 0.4.3"}, # Docs, credo, test coverage, dialyzer {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index a8302ba..c46837c 100644 --- a/mix.lock +++ b/mix.lock @@ -24,4 +24,5 @@ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "tesla": {:hex, :tesla, "1.5.1", "f2ba04f5e6ace0f1954f1fb4375f55809a5f2ff491c18ccb09fbc98370d4280b", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2815d4f6550973d1ed65692d545d079174f6a1f8cb4775f6eb606cbc0666a9de"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, } From 147d9109c62c6fe1c584141fb0727bb60f617ce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Tue, 18 Apr 2023 16:53:46 +0200 Subject: [PATCH 2/6] Extend the implementation of `Jellyfish.Notifier` --- lib/jellyfish/notifier.ex | 155 +++++++++++++++++++++++++++++++++----- 1 file changed, 138 insertions(+), 17 deletions(-) diff --git a/lib/jellyfish/notifier.ex b/lib/jellyfish/notifier.ex index e0596cd..04d95b1 100644 --- a/lib/jellyfish/notifier.ex +++ b/lib/jellyfish/notifier.ex @@ -1,41 +1,130 @@ defmodule Jellyfish.Notifier do @moduledoc """ - TODO + This module defines a process responsible for establishing + WebSocket connection and receiving notifications form Jellyfish server. + + ``` + iex> {:ok, pid} = Jellyfish.Notifier.start("ws://address-of-jellyfish-server.com", "your-token") + {:ok, #PID<0.301.0>} + + # here add a room and a peer using functions from `Jellyfish.Room` module + # when the peer established signalling connection, you should receive notification + + iex> flush() + {:jellyfish_notification, + %{ + id: "5110be31-a252-42af-b833-047edaade500", + room_id: "fd3d1512-3d4d-4e6a-9697-7b132aa0adf6", + type: :peer_connected + }} + :ok + ``` """ use WebSockex alias Jellyfish.Exception - def start_link(pid, url) do - token = Application.fetch_env!(:jellyfish_server_sdk, :token) - start_link(pid, url, token) + @auth_timeout 2000 + + @doc """ + Starts the Notifier process and connects to Jellyfish. + + Acts like `start/1` but links to the calling process. + + See `start/1` for more information. + """ + @spec start_link(String.t()) :: {:ok, pid()} | {:error, term()} + def start_link(address) do + token = Application.fetch_env!(:jellyfish_server_sdk, :server_api_token) + start_link(address, token) end - def start_link(pid, url, token) do - state = %{receiver_pid: pid} + @doc """ + Starts the Notifier process and connects to Jellyfish. + + Acts like `start/2` but links to the calling process. + + See `start/2` for more information. + """ + @spec start_link(String.t(), String.t()) :: {:ok, pid()} | {:error, term()} + def start_link(address, token) do + case start(address, token) do + {:ok, pid} -> + Process.link(pid) + {:ok, pid} + + {:error, _reason} = error -> + error + end + end + + @doc """ + Starts the Notifier process and connects to Jellyfish. + + Received notifications are send to the calling process in + a form of `{:jellyfish_notification, msg}`. + + Uses token set in `config.exs`. To explicitly pass the token, see `start/2`. + ``` + # in config.exs + config :jellyfish_server_sdk, server_api_token: "your-jellyfish-token" + + {:ok, pid} = Jellyfish.Notifier.start("ws://address-of-your-server.com") + ``` + """ + @spec start(String.t()) :: {:ok, pid()} | {:error, term()} + def start(address) do + token = Application.fetch_env!(:jellyfish_server_sdk, :server_api_token) + start(address, token) + end + + @doc """ + Starts the Notifier process and connects to Jellyfish. + + Received notifications are send to the calling process in + a form of `{:jellyfish_notification, msg}`. + + ## Parameters + + * `address` - WebSocket url or IP address of the Jellyfish instance + * `token` - token used for authorizing HTTP requests and WebSocket connection. + It's the same token as the one configured in Jellyfish. + """ + @spec start(String.t(), String.t()) :: {:ok, pid()} | {:error, term()} + def start(address, token) do + state = %{receiver_pid: self()} auth_msg = %{type: "controlMessage", data: %{type: "authRequest", token: token}} |> Jason.encode!() - with {:ok, pid} <- WebSockex.start("ws://#{url}/socket/server/websocket", __MODULE__, state), + with {:ok, pid} <- WebSockex.start("#{address}/socket/server/websocket", __MODULE__, state), :ok <- WebSockex.send_frame(pid, {:text, auth_msg}) do - Process.link(pid) - {:ok, pid} + receive do + {:jellyfish_notification, %{type: :authenticated}} -> + {:ok, pid} + + {:jellyfish_notification, %{type: :invalid_token}} -> + Process.exit(pid, :normal) + {:error, :invalid_token} + after + @auth_timeout -> + Process.exit(pid, :normal) + {:error, :authentication_timeout} + end else {:error, _reason} = error -> - Process.exit(pid, :normal) error end end + @impl true def handle_frame({:text, msg}, state) do with {:ok, decoded_msg} <- Jason.decode(msg), - %{"type" => "controlMessage", "data" => data} <- decoded_msg |> IO.inspect(), - %{"type" => type, "id" => id} <- data, - {:ok, decoded_type} <- decode_type(type) do - send(state.receiver_pid, {decoded_type, id}) + %{"type" => "controlMessage", "data" => data} <- decoded_msg, + {:ok, notification} <- decode_notification(data) do + send(state.receiver_pid, {:jellyfish_notification, notification}) else _other -> raise Exception.NotificationStructureError end @@ -43,17 +132,49 @@ defmodule Jellyfish.Notifier do {:ok, state} end - defp decode_type(type) do + @impl true + def terminate({:remote, 1000, "invalid token"}, state) do + send(state.receiver_pid, {:jellyfish_notification, %{type: :invalid_token}}) + end + + defp decode_notification(%{"type" => type, "roomId" => room_id, "id" => id}) do decoded_type = case type do "authenticated" -> :authenticated "peerConnected" -> :peer_connected "peerDisconnected" -> :peer_disconected - "roomCrashed" -> :room_crashed "componentCrashed" -> :component_crashed _other -> nil end - if is_nil(decoded_type), do: {:error, :invalid_type}, else: {:ok, decoded_type} + if is_nil(decoded_type) do + {:error, :invalid_type} + else + {:ok, %{type: decoded_type, room_id: room_id, id: id}} + end end + + defp decode_notification(%{"type" => type, "room_id" => id}) do + decoded_type = + case type do + "roomCrashed" -> :room_crashed + _other -> nil + end + + if is_nil(decoded_type), + do: {:error, :invalid_type}, + else: {:ok, %{type: decoded_type, room_id: id}} + end + + defp decode_notification(%{"type" => type}) do + decoded_type = + case type do + "authenticated" -> :authenticated + _other -> nil + end + + if is_nil(decoded_type), do: {:error, :invalid_type}, else: {:ok, %{type: decoded_type}} + end + + defp decode_notification(_other), do: {:error, :invalid_type} end From 824b2b3e6aafd69dad3532bf3b8428959d178857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Wed, 19 Apr 2023 16:56:20 +0200 Subject: [PATCH 3/6] Change api for client creation and starting Notifier process --- README.md | 12 +++++- lib/jellyfish/client.ex | 53 +++++++++++------------ lib/jellyfish/notifier.ex | 89 +++++++++++++++++---------------------- lib/jellyfish/utils.ex | 21 +++++++++ 4 files changed, 95 insertions(+), 80 deletions(-) create mode 100644 lib/jellyfish/utils.ex diff --git a/README.md b/README.md index 67ed514..d3ee9cd 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Elixir server SDK for [Jellyfish](https://github.com/jellyfish-dev/jellyfish). Currently it allows for: - making API calls to Jellyfish server (QoL wrapper for HTTP requests) +- listening to Jellyfish server notifications via WebSocket ## Installation @@ -24,9 +25,14 @@ end ## Usage -Make API calls to Jellyfish (authentication required, for more information see [Jellyfish docs](https://jellyfish-dev.github.io/jellyfish-docs/getting_started/authentication)): +Make API calls to Jellyfish (authentication required, for more information see [Jellyfish docs](https://jellyfish-dev.github.io/jellyfish-docs/getting_started/authentication)) +and receive server notifications: ```elixir +# start process responsible for receiving notifications +{:ok, pid} = Jellyfish.Notifier.start("ws://address-of-your-server.com", "your-jellyfish-token") + +# create HTTP client instance client = Jellyfish.Client.new("http://address-of-your-server.com", "your-jellyfish-token") # Create room @@ -38,6 +44,10 @@ room_id # Add peer {:ok, %Jellyfish.Peer{id: peer_id}, peer_token} = Jellyfish.Room.add_peer(client, room_id, "webrtc") +receive do + {:jellyfish_notification, %{type: :peer_connected} = notification} -> # handle the notification +end + # Delete peer :ok = Jellyfish.Room.delete_peer(client, room_id, peer_id) ``` diff --git a/lib/jellyfish/client.ex b/lib/jellyfish/client.ex index cd0d5ae..00d859e 100644 --- a/lib/jellyfish/client.ex +++ b/lib/jellyfish/client.ex @@ -17,6 +17,8 @@ defmodule Jellyfish.Client do For the list of supported Tesla adapters refer to [Tesla docs](https://hexdocs.pm/tesla/readme.html#adapters). """ + alias Jellyfish.Utils + @enforce_keys [ :http_client ] @@ -29,44 +31,37 @@ defmodule Jellyfish.Client do @doc """ Creates a new instance of `t:Jellyfish.Client.t/0`. - ## Parameters + ## Options - * `address` - url or IP address of the Jellyfish server instance - * `server_api_token` - token used for authorizing HTTP requests. It's the same + * `:server_address` - url or IP address of the Jellyfish server instance. + * `:server_api_token` - token used for authorizing HTTP requests. It's the same token as the one configured in Jellyfish. - """ - @spec new(String.t(), String.t()) :: t() - def new(address, server_api_token), do: build_client(address, server_api_token) - - @doc """ - Creates a new instance of `t:Jellyfish.Client.t/0`. - Uses token set in `config.exs`. To explicitly pass the token, see `new/2`. + When an option is not explicily passed, value set in `config.exs` is used: ``` # in config.exs - config :jellyfish_server_sdk, server_api_token: "your-jellyfish-token" - - client = Jellyfish.Client.new("http://address-of-your-server.com") + config :jellyfish_server_sdk, + server_address: "http://you-jellyfish-server-address.com", + server_api_token: "your-jellyfish-token", ``` - - See `new/2` for description of parameters. """ - @spec new(String.t()) :: t() - def new(address) do - server_api_token = Application.fetch_env!(:jellyfish_server_sdk, :server_api_token) - build_client(address, server_api_token) - end + @spec new(server_address: String.t(), server_api_token: String.t()) :: + {:ok, t()} | {:error, term()} + def new(opts) do + with {:ok, {address, api_token}} <- Utils.get_options_or_defaults(opts) do + adapter = Application.get_env(:jellyfish_server_sdk, :tesla_adapter, Tesla.Adapter.Mint) - defp build_client(address, server_api_token) do - middleware = [ - {Tesla.Middleware.BaseUrl, address}, - {Tesla.Middleware.BearerAuth, token: server_api_token}, - Tesla.Middleware.JSON - ] + middleware = [ + {Tesla.Middleware.BaseUrl, address}, + {Tesla.Middleware.BearerAuth, token: api_token}, + Tesla.Middleware.JSON + ] - adapter = Application.get_env(:jellyfish_server_sdk, :tesla_adapter, Tesla.Adapter.Mint) - http_client = Tesla.client(middleware, adapter) + http_client = Tesla.client(middleware, adapter) - %__MODULE__{http_client: http_client} + {:ok, %__MODULE__{http_client: http_client}} + else + {:error, :missing_url_protocol_prefix} = error -> error + end end end diff --git a/lib/jellyfish/notifier.ex b/lib/jellyfish/notifier.ex index 04d95b1..e15fb15 100644 --- a/lib/jellyfish/notifier.ex +++ b/lib/jellyfish/notifier.ex @@ -4,11 +4,11 @@ defmodule Jellyfish.Notifier do WebSocket connection and receiving notifications form Jellyfish server. ``` - iex> {:ok, pid} = Jellyfish.Notifier.start("ws://address-of-jellyfish-server.com", "your-token") + iex> {:ok, pid} = Jellyfish.Notifier.start("ws://address-of-jellyfish-server.com", "your-jellyfish-token") {:ok, #PID<0.301.0>} # here add a room and a peer using functions from `Jellyfish.Room` module - # when the peer established signalling connection, you should receive notification + # you should receive a notification after the peer established connection iex> flush() {:jellyfish_notification, @@ -23,7 +23,7 @@ defmodule Jellyfish.Notifier do use WebSockex - alias Jellyfish.Exception + alias Jellyfish.{Exception, Utils} @auth_timeout 2000 @@ -34,22 +34,10 @@ defmodule Jellyfish.Notifier do See `start/1` for more information. """ - @spec start_link(String.t()) :: {:ok, pid()} | {:error, term()} - def start_link(address) do - token = Application.fetch_env!(:jellyfish_server_sdk, :server_api_token) - start_link(address, token) - end - - @doc """ - Starts the Notifier process and connects to Jellyfish. - - Acts like `start/2` but links to the calling process. - - See `start/2` for more information. - """ - @spec start_link(String.t(), String.t()) :: {:ok, pid()} | {:error, term()} - def start_link(address, token) do - case start(address, token) do + @spec start_link(server_address: String.t(), server_api_token: String.t()) :: + {:ok, pid()} | {:error, term()} + def start_link(opts) do + case start(opts) do {:ok, pid} -> Process.link(pid) {:ok, pid} @@ -65,41 +53,30 @@ defmodule Jellyfish.Notifier do Received notifications are send to the calling process in a form of `{:jellyfish_notification, msg}`. - Uses token set in `config.exs`. To explicitly pass the token, see `start/2`. + ## Options + + * `:server_address` - url or IP address of the Jellyfish server instance. + * `:server_api_token` - token used for authorizing HTTP requests. It's the same + token as the one configured in Jellyfish. + + When an option is not explicily passed, value set in `config.exs` is used: ``` # in config.exs - config :jellyfish_server_sdk, server_api_token: "your-jellyfish-token" - - {:ok, pid} = Jellyfish.Notifier.start("ws://address-of-your-server.com") + config :jellyfish_server_sdk, + server_address: "http://you-jellyfish-server-address.com", + server_api_token: "your-jellyfish-token", ``` """ - @spec start(String.t()) :: {:ok, pid()} | {:error, term()} - def start(address) do - token = Application.fetch_env!(:jellyfish_server_sdk, :server_api_token) - start(address, token) - end - - @doc """ - Starts the Notifier process and connects to Jellyfish. - - Received notifications are send to the calling process in - a form of `{:jellyfish_notification, msg}`. - - ## Parameters - - * `address` - WebSocket url or IP address of the Jellyfish instance - * `token` - token used for authorizing HTTP requests and WebSocket connection. - It's the same token as the one configured in Jellyfish. - """ - @spec start(String.t(), String.t()) :: {:ok, pid()} | {:error, term()} - def start(address, token) do + @spec start(server_address: String.t(), server_api_token: String.t()) :: + {:ok, pid()} | {:error, term()} + def start(opts) do state = %{receiver_pid: self()} - auth_msg = - %{type: "controlMessage", data: %{type: "authRequest", token: token}} - |> Jason.encode!() - - with {:ok, pid} <- WebSockex.start("#{address}/socket/server/websocket", __MODULE__, state), + with {:ok, {address, api_token}} <- Utils.get_options_or_defaults(opts), + address <- convert_url_prefix(address), + {:ok, pid} <- WebSockex.start("#{address}/socket/server/websocket", __MODULE__, state), + auth_msg <- + Jason.encode!(%{type: "controlMessage", data: %{type: "authRequest", token: api_token}}), :ok <- WebSockex.send_frame(pid, {:text, auth_msg}) do receive do {:jellyfish_notification, %{type: :authenticated}} -> @@ -114,8 +91,7 @@ defmodule Jellyfish.Notifier do {:error, :authentication_timeout} end else - {:error, _reason} = error -> - error + {:error, _reason} = error -> error end end @@ -177,4 +153,17 @@ defmodule Jellyfish.Notifier do end defp decode_notification(_other), do: {:error, :invalid_type} + + defp convert_url_prefix(url) do + # assumes that url starts with valid prefix, like "http://" + [prefix, address] = String.split(url, ":", parts: 2) + + new_prefix = + case prefix do + "http" -> "ws" + "https" -> "wss" + end + + "#{new_prefix}:#{address}" + end end diff --git a/lib/jellyfish/utils.ex b/lib/jellyfish/utils.ex new file mode 100644 index 0000000..19fd2a9 --- /dev/null +++ b/lib/jellyfish/utils.ex @@ -0,0 +1,21 @@ +defmodule Jellyfish.Utils do + @moduledoc false + + @valid_prefixes ["http://", "https://"] + + @spec get_options_or_defaults(server_address: String.t(), server_api_token: String.t()) :: + {:ok, {String.t(), String.t()}} | {:error, term()} + def get_options_or_defaults(opts) do + server_address = + opts[:server_address] || Application.fetch_env!(:jellyfish_server_sdk, :server_address) + + server_api_token = + opts[:server_api_token] || Application.fetch_env!(:jellyfish_server_sdk, :server_api_token) + + if String.starts_with?(server_address, @valid_prefixes) do + {:ok, {server_address, server_api_token}} + else + {:error, :invalid_url_protocol_prefix} + end + end +end From 4305d853f6dd5aae6dc15134d145cc30bdcc47dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Thu, 20 Apr 2023 14:30:28 +0200 Subject: [PATCH 4/6] Improvements to api and docs --- README.md | 4 +- lib/jellyfish/client.ex | 52 ++++++++++++++----------- lib/jellyfish/component.ex | 4 +- lib/jellyfish/exception.ex | 10 ++--- lib/jellyfish/notifier.ex | 64 +++++++++++------------------- lib/jellyfish/peer.ex | 4 +- lib/jellyfish/room.ex | 16 ++++---- lib/jellyfish/utils.ex | 20 ++++++---- test/jellyfish/client_test.exs | 71 ++++++++++++++++++++++++++++++---- test/jellyfish/room_test.exs | 46 +++++++++++----------- 10 files changed, 171 insertions(+), 120 deletions(-) diff --git a/README.md b/README.md index d3ee9cd..9d64f03 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ and receive server notifications: ```elixir # start process responsible for receiving notifications -{:ok, pid} = Jellyfish.Notifier.start("ws://address-of-your-server.com", "your-jellyfish-token") +{:ok, pid} = Jellyfish.Notifier.start(server_address: "ws://address-of-your-server.com", server_api_key: "your-jellyfish-token") # create HTTP client instance -client = Jellyfish.Client.new("http://address-of-your-server.com", "your-jellyfish-token") +client = Jellyfish.Client.new(server_address: "http://address-of-your-server.com", server_api_key: "your-jellyfish-token") # Create room {:ok, %Jellyfish.Room{id: room_id}} = Jellyfish.Room.create(client, max_peers: 10) diff --git a/lib/jellyfish/client.ex b/lib/jellyfish/client.ex index 00d859e..d633fb6 100644 --- a/lib/jellyfish/client.ex +++ b/lib/jellyfish/client.ex @@ -28,14 +28,13 @@ defmodule Jellyfish.Client do http_client: Tesla.Client.t() } - @doc """ - Creates a new instance of `t:Jellyfish.Client.t/0`. - - ## Options + @typedoc """ + Options needed to open connection to Jellyfish server. - * `:server_address` - url or IP address of the Jellyfish server instance. - * `:server_api_token` - token used for authorizing HTTP requests. It's the same - token as the one configured in Jellyfish. + * `:server_address` - url or IP address of the Jellyfish server instance. + * `:server_api_token` - token used for authorizing HTTP requests and WebSocket connection. + It's the same token as the one configured in Jellyfish. + * `:secure?` - if true, use HTTPS and WSS instead of HTTP and WS, false by default. When an option is not explicily passed, value set in `config.exs` is used: ``` @@ -43,25 +42,34 @@ defmodule Jellyfish.Client do config :jellyfish_server_sdk, server_address: "http://you-jellyfish-server-address.com", server_api_token: "your-jellyfish-token", + secure?: true ``` """ - @spec new(server_address: String.t(), server_api_token: String.t()) :: - {:ok, t()} | {:error, term()} - def new(opts) do - with {:ok, {address, api_token}} <- Utils.get_options_or_defaults(opts) do - adapter = Application.get_env(:jellyfish_server_sdk, :tesla_adapter, Tesla.Adapter.Mint) + @type connection_options :: [ + server_address: String.t(), + server_api_token: String.t(), + secure?: boolean() + ] - middleware = [ - {Tesla.Middleware.BaseUrl, address}, - {Tesla.Middleware.BearerAuth, token: api_token}, - Tesla.Middleware.JSON - ] + @doc """ + Creates a new instance of `t:Jellyfish.Client.t/0`. + + For information about options, see `t:connection_options/0`. + """ + @spec new(connection_options()) :: t() + def new(opts \\ []) do + {address, api_token, secure?} = Utils.get_options_or_defaults(opts) + address = if secure?, do: "https://#{address}", else: "http://#{address}" + adapter = Application.get_env(:jellyfish_server_sdk, :tesla_adapter, Tesla.Adapter.Mint) + + middleware = [ + {Tesla.Middleware.BaseUrl, address}, + {Tesla.Middleware.BearerAuth, token: api_token}, + Tesla.Middleware.JSON + ] - http_client = Tesla.client(middleware, adapter) + http_client = Tesla.client(middleware, adapter) - {:ok, %__MODULE__{http_client: http_client}} - else - {:error, :missing_url_protocol_prefix} = error -> error - end + %__MODULE__{http_client: http_client} end end diff --git a/lib/jellyfish/component.ex b/lib/jellyfish/component.ex index ed1d1bf..dca75b9 100644 --- a/lib/jellyfish/component.ex +++ b/lib/jellyfish/component.ex @@ -6,7 +6,7 @@ defmodule Jellyfish.Component do For more information refer to [Jellyfish documentation](https://www.membrane.stream) """ - alias Jellyfish.Exception.ResponseStructureError + alias Jellyfish.Exception.StructureError @enforce_keys [ :id, @@ -54,7 +54,7 @@ defmodule Jellyfish.Component do } _other -> - raise ResponseStructureError + raise StructureError end end end diff --git a/lib/jellyfish/exception.ex b/lib/jellyfish/exception.ex index f7edb09..b3506e8 100644 --- a/lib/jellyfish/exception.ex +++ b/lib/jellyfish/exception.ex @@ -1,13 +1,13 @@ defmodule Jellyfish.Exception do @moduledoc false - defmodule ResponseStructureError do + defmodule StructureError do defexception [:message] @impl true def exception(_opts) do msg = """ - Received server response with unexpected structure. + Received server response or notification with unexpected structure. Make sure you are using correct combination of Jellyfish and SDK versions. """ @@ -15,14 +15,14 @@ defmodule Jellyfish.Exception do end end - defmodule NotificationStructureError do + defmodule ProtocolPrefixError do defexception [:message] @impl true def exception(_opts) do msg = """ - Received notification with unexpected structure. - Make sure you are using correct combination of Jellyfish and SDK versions. + Passed address starts with protocol prefix, like "http://" or "https://", which is undesired. + To use SSL, pass `secure?: true` option. """ %__MODULE__{message: msg} diff --git a/lib/jellyfish/notifier.ex b/lib/jellyfish/notifier.ex index e15fb15..c9a4d2c 100644 --- a/lib/jellyfish/notifier.ex +++ b/lib/jellyfish/notifier.ex @@ -1,10 +1,10 @@ defmodule Jellyfish.Notifier do @moduledoc """ - This module defines a process responsible for establishing + Module defining a process responsible for establishing WebSocket connection and receiving notifications form Jellyfish server. ``` - iex> {:ok, pid} = Jellyfish.Notifier.start("ws://address-of-jellyfish-server.com", "your-jellyfish-token") + iex> {:ok, pid} = Jellyfish.Notifier.start(server_address: "address-of-jellyfish-server.com", server_api_token: "your-jellyfish-token") {:ok, #PID<0.301.0>} # here add a room and a peer using functions from `Jellyfish.Room` module @@ -23,7 +23,8 @@ defmodule Jellyfish.Notifier do use WebSockex - alias Jellyfish.{Exception, Utils} + alias Jellyfish.{Client, Utils} + alias Jellyfish.Exception.StructureError @auth_timeout 2000 @@ -34,9 +35,8 @@ defmodule Jellyfish.Notifier do See `start/1` for more information. """ - @spec start_link(server_address: String.t(), server_api_token: String.t()) :: - {:ok, pid()} | {:error, term()} - def start_link(opts) do + @spec start_link(Client.connection_options()) :: {:ok, pid()} | {:error, term()} + def start_link(opts \\ []) do case start(opts) do {:ok, pid} -> Process.link(pid) @@ -53,30 +53,19 @@ defmodule Jellyfish.Notifier do Received notifications are send to the calling process in a form of `{:jellyfish_notification, msg}`. - ## Options - - * `:server_address` - url or IP address of the Jellyfish server instance. - * `:server_api_token` - token used for authorizing HTTP requests. It's the same - token as the one configured in Jellyfish. - - When an option is not explicily passed, value set in `config.exs` is used: - ``` - # in config.exs - config :jellyfish_server_sdk, - server_address: "http://you-jellyfish-server-address.com", - server_api_token: "your-jellyfish-token", - ``` + For information about options, see `t:Jellyfish.Client.connection_options/0`. """ - @spec start(server_address: String.t(), server_api_token: String.t()) :: - {:ok, pid()} | {:error, term()} - def start(opts) do + @spec start(Client.connection_options()) :: {:ok, pid()} | {:error, term()} + def start(opts \\ []) do + {address, api_token, secure?} = Utils.get_options_or_defaults(opts) + address = if secure?, do: "wss://#{address}", else: "ws://#{address}" state = %{receiver_pid: self()} - with {:ok, {address, api_token}} <- Utils.get_options_or_defaults(opts), - address <- convert_url_prefix(address), - {:ok, pid} <- WebSockex.start("#{address}/socket/server/websocket", __MODULE__, state), - auth_msg <- - Jason.encode!(%{type: "controlMessage", data: %{type: "authRequest", token: api_token}}), + auth_msg = + %{type: "controlMessage", data: %{type: "authRequest", token: api_token}} + |> Jason.encode!() + + with {:ok, pid} <- WebSockex.start("#{address}/socket/server/websocket", __MODULE__, state), :ok <- WebSockex.send_frame(pid, {:text, auth_msg}) do receive do {:jellyfish_notification, %{type: :authenticated}} -> @@ -102,12 +91,18 @@ defmodule Jellyfish.Notifier do {:ok, notification} <- decode_notification(data) do send(state.receiver_pid, {:jellyfish_notification, notification}) else - _other -> raise Exception.NotificationStructureError + _other -> raise StructureError end {:ok, state} end + @impl true + def handle_cast(_msg, state) do + # ignore incoming messages + {:ok, state} + end + @impl true def terminate({:remote, 1000, "invalid token"}, state) do send(state.receiver_pid, {:jellyfish_notification, %{type: :invalid_token}}) @@ -153,17 +148,4 @@ defmodule Jellyfish.Notifier do end defp decode_notification(_other), do: {:error, :invalid_type} - - defp convert_url_prefix(url) do - # assumes that url starts with valid prefix, like "http://" - [prefix, address] = String.split(url, ":", parts: 2) - - new_prefix = - case prefix do - "http" -> "ws" - "https" -> "wss" - end - - "#{new_prefix}:#{address}" - end end diff --git a/lib/jellyfish/peer.ex b/lib/jellyfish/peer.ex index 6d22021..9ab9ded 100644 --- a/lib/jellyfish/peer.ex +++ b/lib/jellyfish/peer.ex @@ -7,7 +7,7 @@ defmodule Jellyfish.Peer do For more information refer to [Jellyfish documentation](https://www.membrane.stream) """ - alias Jellyfish.Exception.ResponseStructureError + alias Jellyfish.Exception.StructureError @enforce_keys [ :id, @@ -49,7 +49,7 @@ defmodule Jellyfish.Peer do } _other -> - raise ResponseStructureError + raise StructureError end end end diff --git a/lib/jellyfish/room.ex b/lib/jellyfish/room.ex index 437375a..0fb77cb 100644 --- a/lib/jellyfish/room.ex +++ b/lib/jellyfish/room.ex @@ -25,7 +25,7 @@ defmodule Jellyfish.Room do alias Tesla.Env alias Jellyfish.{Client, Component, Peer} - alias Jellyfish.Exception.ResponseStructureError + alias Jellyfish.Exception.StructureError @enforce_keys [ :id, @@ -72,7 +72,7 @@ defmodule Jellyfish.Room do result <- Enum.map(data, &from_json/1) do {:ok, result} else - :error -> raise ResponseStructureError + :error -> raise StructureError error -> handle_response_error(error) end end @@ -88,7 +88,7 @@ defmodule Jellyfish.Room do result <- from_json(data) do {:ok, result} else - :error -> raise ResponseStructureError + :error -> raise StructureError error -> handle_response_error(error) end end @@ -108,7 +108,7 @@ defmodule Jellyfish.Room do result <- from_json(data) do {:ok, result} else - :error -> raise ResponseStructureError + :error -> raise StructureError error -> handle_response_error(error) end end @@ -140,7 +140,7 @@ defmodule Jellyfish.Room do result <- Peer.from_json(peer) do {:ok, result, token} else - :error -> raise ResponseStructureError + :error -> raise StructureError error -> handle_response_error(error) end end @@ -178,7 +178,7 @@ defmodule Jellyfish.Room do result <- Component.from_json(data) do {:ok, result} else - :error -> raise ResponseStructureError + :error -> raise StructureError error -> handle_response_error(error) end end @@ -215,13 +215,13 @@ defmodule Jellyfish.Room do } _other -> - raise ResponseStructureError + raise StructureError end end defp handle_response_error({:ok, %Env{body: %{"errors" => error}}}), do: {:error, "Request failed: #{error}"} - defp handle_response_error({:ok, %Env{body: _body}}), do: raise(ResponseStructureError) + defp handle_response_error({:ok, %Env{body: _body}}), do: raise(StructureError) defp handle_response_error({:error, reason}), do: {:error, reason} end diff --git a/lib/jellyfish/utils.ex b/lib/jellyfish/utils.ex index 19fd2a9..68ed711 100644 --- a/lib/jellyfish/utils.ex +++ b/lib/jellyfish/utils.ex @@ -1,10 +1,13 @@ defmodule Jellyfish.Utils do @moduledoc false - @valid_prefixes ["http://", "https://"] + alias Jellyfish.Client + alias Jellyfish.Exception.ProtocolPrefixError - @spec get_options_or_defaults(server_address: String.t(), server_api_token: String.t()) :: - {:ok, {String.t(), String.t()}} | {:error, term()} + @protocl_prefixes ["http://", "https://", "ws://", "wss://"] + + @spec get_options_or_defaults(Client.connection_options()) :: + {String.t(), String.t(), boolean()} def get_options_or_defaults(opts) do server_address = opts[:server_address] || Application.fetch_env!(:jellyfish_server_sdk, :server_address) @@ -12,10 +15,11 @@ defmodule Jellyfish.Utils do server_api_token = opts[:server_api_token] || Application.fetch_env!(:jellyfish_server_sdk, :server_api_token) - if String.starts_with?(server_address, @valid_prefixes) do - {:ok, {server_address, server_api_token}} - else - {:error, :invalid_url_protocol_prefix} - end + secure? = + Keyword.get(opts, :secure?, Application.get_env(:jellyfish_server_sdk, :secure?, false)) + + if String.starts_with?(server_address, @protocl_prefixes), do: raise(ProtocolPrefixError) + + {server_address, server_api_token, secure?} end end diff --git a/test/jellyfish/client_test.exs b/test/jellyfish/client_test.exs index a9f2f26..2d8dd92 100644 --- a/test/jellyfish/client_test.exs +++ b/test/jellyfish/client_test.exs @@ -3,23 +3,80 @@ defmodule Jellyfish.ClientTest do alias Jellyfish.Client - @url "https://somemockurl.com" + @server_address "valid-address.com" + @server_api_token "valid-token" - describe "sdk" do - test "creates client struct" do - server_api_token = "mock_token" - client = Client.new(@url, server_api_token) + describe "creates client struct" do + test "with connection options passed explictly" do + address_with_prefix = "http://#{@server_address}" + + client = + Client.new( + server_address: @server_address, + server_api_token: @server_api_token, + secure?: false + ) assert %Client{ http_client: %Tesla.Client{ adapter: {Tesla.Adapter.Mint, :call, [[]]}, pre: [ - {Tesla.Middleware.BaseUrl, :call, [@url]}, - {Tesla.Middleware.BearerAuth, :call, [[token: ^server_api_token]]}, + {Tesla.Middleware.BaseUrl, :call, [^address_with_prefix]}, + {Tesla.Middleware.BearerAuth, :call, [[token: @server_api_token]]}, {Tesla.Middleware.JSON, :call, [[]]} ] } } = client end + + test "with connection options from config" do + :ok = + Application.put_all_env([ + { + :jellyfish_server_sdk, + [ + {:server_address, @server_address}, + {:server_api_token, @server_api_token}, + {:secure?, true} + ] + } + ]) + + addres_with_prefix = "https://#{@server_address}" + client = Client.new() + + assert %Client{ + http_client: %Tesla.Client{ + adapter: {Tesla.Adapter.Mint, :call, [[]]}, + pre: [ + {Tesla.Middleware.BaseUrl, :call, [^addres_with_prefix]}, + {Tesla.Middleware.BearerAuth, :call, [[token: @server_api_token]]}, + {Tesla.Middleware.JSON, :call, [[]]} + ] + } + } = client + + Application.delete_env(:jellyfish_server_sdk, :server_address) + Application.delete_env(:jellyfish_server_sdk, :server_api_token) + Application.delete_env(:jellyfish_server_sdk, :secure?) + end + + test "when address contains protocol prefix" do + address_with_prefix = "http://#{@server_address}" + + assert_raise( + Jellyfish.Exception.ProtocolPrefixError, + fn -> + Client.new(server_address: address_with_prefix, server_api_token: @server_api_token) + end + ) + end + + test "when options are not passed and config is not set" do + assert_raise( + ArgumentError, + fn -> Client.new() end + ) + end end end diff --git a/test/jellyfish/room_test.exs b/test/jellyfish/room_test.exs index 8d2c54b..772a767 100644 --- a/test/jellyfish/room_test.exs +++ b/test/jellyfish/room_test.exs @@ -7,8 +7,8 @@ defmodule Jellyfish.RoomTest do @server_api_token "testtoken" - @url "http://mockurl.com" - @invalid_url "http://invalid-url.com" + @url "mockurl.com" + @invalid_url "invalid-url.com" @component_id "mock_component_id" @component_type "hls" @@ -43,7 +43,7 @@ defmodule Jellyfish.RoomTest do end end) - %{client: Client.new(@url, @server_api_token)} + %{client: Client.new(server_address: @url, server_api_token: @server_api_token)} end describe "auth" do @@ -52,7 +52,7 @@ defmodule Jellyfish.RoomTest do mock(fn %{ method: :post, - url: "#{@url}/room", + url: "http://#{@url}/room", body: ^valid_body } = env -> case Tesla.get_header(env, "authorization") do @@ -71,7 +71,7 @@ defmodule Jellyfish.RoomTest do end test "invalid token" do - client = Client.new(@url, "invalid" <> @server_api_token) + client = Client.new(server_address: @url, server_api_token: "invalid" <> @server_api_token) assert {:error, _reason} = Room.create(client, max_peers: @max_peers) end end @@ -84,14 +84,14 @@ defmodule Jellyfish.RoomTest do mock(fn %{ method: :post, - url: "#{@url}/room", + url: "http://#{@url}/room", body: ^valid_body } -> json(%{"data" => build_room_json(true)}, status: 201) %{ method: :post, - url: "#{@url}/room", + url: "http://#{@url}/room", body: ^invalid_body } -> json(%{"errors" => @error_message}, status: 400) @@ -114,13 +114,13 @@ defmodule Jellyfish.RoomTest do mock(fn %{ method: :delete, - url: "#{@url}/room/#{@room_id}" + url: "http://#{@url}/room/#{@room_id}" } -> text("", status: 204) %{ method: :delete, - url: "#{@url}/room/#{@invalid_room_id}" + url: "http://#{@url}/room/#{@invalid_room_id}" } -> json(%{"errors" => @error_message}, status: 404) end) @@ -140,13 +140,13 @@ defmodule Jellyfish.RoomTest do mock(fn %{ method: :get, - url: "#{@url}/room" + url: "http://#{@url}/room" } -> json(%{"data" => [build_room_json(false)]}, status: 200) %{ method: :get, - url: "#{@invalid_url}/room" + url: "http://#{@invalid_url}/room" } -> %Tesla.Env{status: 404, body: nil} end) @@ -159,7 +159,7 @@ defmodule Jellyfish.RoomTest do test "when request is invalid" do middleware = [ - {Tesla.Middleware.BaseUrl, @invalid_url}, + {Tesla.Middleware.BaseUrl, "http://#{@invalid_url}"}, Tesla.Middleware.JSON ] @@ -167,7 +167,7 @@ defmodule Jellyfish.RoomTest do http_client = Tesla.client(middleware, adapter) invalid_client = %Client{http_client: http_client} - assert_raise Jellyfish.Exception.ResponseStructureError, fn -> + assert_raise Jellyfish.Exception.StructureError, fn -> Room.get_all(invalid_client) end end @@ -178,13 +178,13 @@ defmodule Jellyfish.RoomTest do mock(fn %{ method: :get, - url: "#{@url}/room/#{@room_id}" + url: "http://#{@url}/room/#{@room_id}" } -> json(%{"data" => build_room_json(false)}, status: 200) %{ method: :get, - url: "#{@url}/room/#{@invalid_room_id}" + url: "http://#{@url}/room/#{@invalid_room_id}" } -> json(%{"errors" => @error_message}, status: 404) end) @@ -208,14 +208,14 @@ defmodule Jellyfish.RoomTest do mock(fn %{ method: :post, - url: "#{@url}/room/#{@room_id}/component", + url: "http://#{@url}/room/#{@room_id}/component", body: ^valid_body } -> json(%{"data" => build_component_json()}, status: 201) %{ method: :post, - url: "#{@url}/room/#{@room_id}/component", + url: "http://#{@url}/room/#{@room_id}/component", body: ^invalid_body } -> json(%{"errors" => @error_message}, status: 400) @@ -238,13 +238,13 @@ defmodule Jellyfish.RoomTest do mock(fn %{ method: :delete, - url: "#{@url}/room/#{@room_id}/component/#{@component_id}" + url: "http://#{@url}/room/#{@room_id}/component/#{@component_id}" } -> text("", status: 204) %{ method: :delete, - url: "#{@url}/room/#{@room_id}/component/#{@invalid_component_id}" + url: "http://#{@url}/room/#{@room_id}/component/#{@invalid_component_id}" } -> json(%{"errors" => @error_message}, status: 404) end) @@ -268,14 +268,14 @@ defmodule Jellyfish.RoomTest do mock(fn %{ method: :post, - url: "#{@url}/room/#{@room_id}/peer", + url: "http://#{@url}/room/#{@room_id}/peer", body: ^valid_body } -> json(%{"data" => build_peer_json()}, status: 201) %{ method: :post, - url: "#{@url}/room/#{@room_id}/peer", + url: "http://#{@url}/room/#{@room_id}/peer", body: ^invalid_body } -> json(%{"errors" => @error_message}, status: 400) @@ -298,13 +298,13 @@ defmodule Jellyfish.RoomTest do mock(fn %{ method: :delete, - url: "#{@url}/room/#{@room_id}/peer/#{@peer_id}" + url: "http://#{@url}/room/#{@room_id}/peer/#{@peer_id}" } -> text("", status: 204) %{ method: :delete, - url: "#{@url}/room/#{@room_id}/peer/#{@invalid_peer_id}" + url: "http://#{@url}/room/#{@room_id}/peer/#{@invalid_peer_id}" } -> json(%{"errors" => @error_message}, status: 404) end) From ce5446c88d84795c5e4e774e412a93ec682be743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Fri, 21 Apr 2023 11:45:07 +0200 Subject: [PATCH 5/6] Apply some of the requested changes --- README.md | 2 +- lib/jellyfish/client.ex | 4 +- lib/jellyfish/exception.ex | 2 +- lib/jellyfish/notifier.ex | 93 ++++++++++++++++++++------------------ lib/jellyfish/utils.ex | 4 +- 5 files changed, 54 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 9d64f03..6c6d129 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ and receive server notifications: ```elixir # start process responsible for receiving notifications -{:ok, pid} = Jellyfish.Notifier.start(server_address: "ws://address-of-your-server.com", server_api_key: "your-jellyfish-token") +{:ok, _pid} = Jellyfish.Notifier.start(server_address: "ws://address-of-your-server.com", server_api_key: "your-jellyfish-token") # create HTTP client instance client = Jellyfish.Client.new(server_address: "http://address-of-your-server.com", server_api_key: "your-jellyfish-token") diff --git a/lib/jellyfish/client.ex b/lib/jellyfish/client.ex index d633fb6..6cc42d7 100644 --- a/lib/jellyfish/client.ex +++ b/lib/jellyfish/client.ex @@ -34,13 +34,13 @@ defmodule Jellyfish.Client do * `:server_address` - url or IP address of the Jellyfish server instance. * `:server_api_token` - token used for authorizing HTTP requests and WebSocket connection. It's the same token as the one configured in Jellyfish. - * `:secure?` - if true, use HTTPS and WSS instead of HTTP and WS, false by default. + * `:secure?` - if `true`, use HTTPS and WSS instead of HTTP and WS, `false` by default. When an option is not explicily passed, value set in `config.exs` is used: ``` # in config.exs config :jellyfish_server_sdk, - server_address: "http://you-jellyfish-server-address.com", + server_address: "you-jellyfish-server-address.com", server_api_token: "your-jellyfish-token", secure?: true ``` diff --git a/lib/jellyfish/exception.ex b/lib/jellyfish/exception.ex index b3506e8..d9c6c54 100644 --- a/lib/jellyfish/exception.ex +++ b/lib/jellyfish/exception.ex @@ -22,7 +22,7 @@ defmodule Jellyfish.Exception do def exception(_opts) do msg = """ Passed address starts with protocol prefix, like "http://" or "https://", which is undesired. - To use SSL, pass `secure?: true` option. + To use SSL, set `secure?: true` option in `config.exs` or pass this option to called function. """ %__MODULE__{message: msg} diff --git a/lib/jellyfish/notifier.ex b/lib/jellyfish/notifier.ex index c9a4d2c..d1619d9 100644 --- a/lib/jellyfish/notifier.ex +++ b/lib/jellyfish/notifier.ex @@ -11,12 +11,9 @@ defmodule Jellyfish.Notifier do # you should receive a notification after the peer established connection iex> flush() - {:jellyfish_notification, - %{ - id: "5110be31-a252-42af-b833-047edaade500", - room_id: "fd3d1512-3d4d-4e6a-9697-7b132aa0adf6", - type: :peer_connected - }} + {:jellyfish, + {:peer_connected, "21604fbe-8ac8-44e6-8474-98b5f50f1863", + "ae07f94e-0887-44c3-81d5-bfa9eac96252"}} :ok ``` """ @@ -37,51 +34,22 @@ defmodule Jellyfish.Notifier do """ @spec start_link(Client.connection_options()) :: {:ok, pid()} | {:error, term()} def start_link(opts \\ []) do - case start(opts) do - {:ok, pid} -> - Process.link(pid) - {:ok, pid} - - {:error, _reason} = error -> - error - end + connect(:start_link, opts) end @doc """ Starts the Notifier process and connects to Jellyfish. Received notifications are send to the calling process in - a form of `{:jellyfish_notification, msg}`. + a form of `{:jellyfish, msg}`, where `msg` is + `type` or `{type, room_id}` or `{type, room_id, (peer/component)_id}`. + Refer to [Jellyfish docs](https://jellyfish-dev.github.io/jellyfish-docs/) to learn more about server notifications. For information about options, see `t:Jellyfish.Client.connection_options/0`. """ @spec start(Client.connection_options()) :: {:ok, pid()} | {:error, term()} def start(opts \\ []) do - {address, api_token, secure?} = Utils.get_options_or_defaults(opts) - address = if secure?, do: "wss://#{address}", else: "ws://#{address}" - state = %{receiver_pid: self()} - - auth_msg = - %{type: "controlMessage", data: %{type: "authRequest", token: api_token}} - |> Jason.encode!() - - with {:ok, pid} <- WebSockex.start("#{address}/socket/server/websocket", __MODULE__, state), - :ok <- WebSockex.send_frame(pid, {:text, auth_msg}) do - receive do - {:jellyfish_notification, %{type: :authenticated}} -> - {:ok, pid} - - {:jellyfish_notification, %{type: :invalid_token}} -> - Process.exit(pid, :normal) - {:error, :invalid_token} - after - @auth_timeout -> - Process.exit(pid, :normal) - {:error, :authentication_timeout} - end - else - {:error, _reason} = error -> error - end + connect(:start, opts) end @impl true @@ -89,7 +57,9 @@ defmodule Jellyfish.Notifier do with {:ok, decoded_msg} <- Jason.decode(msg), %{"type" => "controlMessage", "data" => data} <- decoded_msg, {:ok, notification} <- decode_notification(data) do - send(state.receiver_pid, {:jellyfish_notification, notification}) + # notification will be either {type, room_id, peer_id/component_id}, + # {type, room_id} or just type + send(state.receiver_pid, {:jellyfish, notification}) else _other -> raise StructureError end @@ -105,7 +75,40 @@ defmodule Jellyfish.Notifier do @impl true def terminate({:remote, 1000, "invalid token"}, state) do - send(state.receiver_pid, {:jellyfish_notification, %{type: :invalid_token}}) + send(state.receiver_pid, {:jellyfish, :invalid_token}) + end + + @impl true + def terminate(_reason, state) do + send(state.receiver_pid, {:jellyfish, :disconnected}) + end + + defp connect(fun, opts) do + {address, api_token, secure?} = Utils.get_options_or_defaults(opts) + address = if secure?, do: "wss://#{address}", else: "ws://#{address}" + state = %{receiver_pid: self()} + + auth_msg = + %{type: "controlMessage", data: %{type: "authRequest", token: api_token}} + |> Jason.encode!() + + with {:ok, pid} <- + apply(WebSockex, fun, ["#{address}/socket/server/websocket", __MODULE__, state]), + :ok <- WebSockex.send_frame(pid, {:text, auth_msg}) do + receive do + {:jellyfish, :authenticated} -> + {:ok, pid} + + {:jellyfish, :invalid_token} -> + {:error, :invalid_token} + after + @auth_timeout -> + Process.exit(pid, :normal) + {:error, :authentication_timeout} + end + else + {:error, _reason} = error -> error + end end defp decode_notification(%{"type" => type, "roomId" => room_id, "id" => id}) do @@ -121,7 +124,7 @@ defmodule Jellyfish.Notifier do if is_nil(decoded_type) do {:error, :invalid_type} else - {:ok, %{type: decoded_type, room_id: room_id, id: id}} + {:ok, {decoded_type, room_id, id}} end end @@ -134,7 +137,7 @@ defmodule Jellyfish.Notifier do if is_nil(decoded_type), do: {:error, :invalid_type}, - else: {:ok, %{type: decoded_type, room_id: id}} + else: {:ok, {decoded_type, id}} end defp decode_notification(%{"type" => type}) do @@ -144,7 +147,7 @@ defmodule Jellyfish.Notifier do _other -> nil end - if is_nil(decoded_type), do: {:error, :invalid_type}, else: {:ok, %{type: decoded_type}} + if is_nil(decoded_type), do: {:error, :invalid_type}, else: {:ok, decoded_type} end defp decode_notification(_other), do: {:error, :invalid_type} diff --git a/lib/jellyfish/utils.ex b/lib/jellyfish/utils.ex index 68ed711..742fe40 100644 --- a/lib/jellyfish/utils.ex +++ b/lib/jellyfish/utils.ex @@ -4,7 +4,7 @@ defmodule Jellyfish.Utils do alias Jellyfish.Client alias Jellyfish.Exception.ProtocolPrefixError - @protocl_prefixes ["http://", "https://", "ws://", "wss://"] + @protocol_prefixes ["http://", "https://", "ws://", "wss://"] @spec get_options_or_defaults(Client.connection_options()) :: {String.t(), String.t(), boolean()} @@ -18,7 +18,7 @@ defmodule Jellyfish.Utils do secure? = Keyword.get(opts, :secure?, Application.get_env(:jellyfish_server_sdk, :secure?, false)) - if String.starts_with?(server_address, @protocl_prefixes), do: raise(ProtocolPrefixError) + if String.starts_with?(server_address, @protocol_prefixes), do: raise(ProtocolPrefixError) {server_address, server_api_token, secure?} end From 37f00d8b343fd1340aaa824a267ce32f27858c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Fri, 21 Apr 2023 12:00:23 +0200 Subject: [PATCH 6/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c6d129..4ecf4a4 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ room_id {:ok, %Jellyfish.Peer{id: peer_id}, peer_token} = Jellyfish.Room.add_peer(client, room_id, "webrtc") receive do - {:jellyfish_notification, %{type: :peer_connected} = notification} -> # handle the notification + {:jellyfish, {:peer_connected, ^room_id, ^peer_id}} -> # handle the notification end # Delete peer