aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mediasync/constants.ex4
-rw-r--r--lib/mediasync/http_errors.ex30
-rw-r--r--lib/mediasync/room.ex67
-rw-r--r--lib/mediasync/room_connection.ex11
-rw-r--r--lib/mediasync/room_id.ex3
-rw-r--r--lib/mediasync/router.ex171
-rw-r--r--lib/mediasync/templates.ex17
-rw-r--r--lib/mediasync/utils.ex45
-rw-r--r--lib/mix/tasks/vendor.ex22
9 files changed, 280 insertions, 90 deletions
diff --git a/lib/mediasync/constants.ex b/lib/mediasync/constants.ex
new file mode 100644
index 0000000..9aabc45
--- /dev/null
+++ b/lib/mediasync/constants.ex
@@ -0,0 +1,4 @@
+defmodule Mediasync.Constants do
+ def query_param_discord_activity_inner, do: "discord_activity_inner"
+ def query_param_instance_id, do: "instance_id"
+end
diff --git a/lib/mediasync/http_errors.ex b/lib/mediasync/http_errors.ex
index 2b11ea6..c38486a 100644
--- a/lib/mediasync/http_errors.ex
+++ b/lib/mediasync/http_errors.ex
@@ -21,12 +21,7 @@ defmodule Mediasync.HTTPErrors do
)
end
- @invalid_video_url Jason.encode!(
- %{
- "error" => "invalid_video_url"
- },
- pretty: true
- )
+ @invalid_video_url Jason.encode!(%{"error" => "invalid_video_url"}, pretty: true)
@spec send_invalid_video_url(Plug.Conn.t()) :: Plug.Conn.t()
@spec send_invalid_video_url(Plug.Conn.t(), []) :: Plug.Conn.t()
@@ -58,6 +53,19 @@ defmodule Mediasync.HTTPErrors do
)
end
+ @forbidden Jason.encode!(%{"error" => "forbidden"}, pretty: true)
+
+ @spec send_forbidden(Plug.Conn.t()) :: Plug.Conn.t()
+ @spec send_forbidden(Plug.Conn.t(), []) :: Plug.Conn.t()
+ def send_forbidden(conn, _opts \\ []) do
+ conn
+ |> put_json_content_type()
+ |> send_resp(
+ 403,
+ @forbidden
+ )
+ end
+
@invalid_csrf_token Jason.encode!(
%{
"error" => "invalid_csrf_token",
@@ -74,6 +82,16 @@ defmodule Mediasync.HTTPErrors do
|> send_resp(400, @invalid_csrf_token)
end
+ @bad_gateway Jason.encode!(%{"error" => "bad_gateway"}, pretty: true)
+
+ @spec send_bad_gateway(Plug.Conn.t()) :: Plug.Conn.t()
+ @spec send_bad_gateway(Plug.Conn.t(), []) :: Plug.Conn.t()
+ def send_bad_gateway(conn, _opts \\ []) do
+ conn
+ |> put_json_content_type()
+ |> send_resp(502, @bad_gateway)
+ end
+
@unknown Jason.encode!(
%{
"error" => "unknown",
diff --git a/lib/mediasync/room.ex b/lib/mediasync/room.ex
index c425441..0367f40 100644
--- a/lib/mediasync/room.ex
+++ b/lib/mediasync/room.ex
@@ -1,10 +1,21 @@
+defmodule Mediasync.Room.VideoInfo do
+ @enforce_keys [:url, :content_type]
+
+ defstruct [:url, :content_type]
+
+ @type t() :: %Mediasync.Room.VideoInfo{
+ url: binary(),
+ content_type: binary()
+ }
+end
+
defmodule Mediasync.Room.State do
- @enforce_keys [:video_url, :host_user_token_hash]
+ @enforce_keys [:video_info, :host_user_token_hash]
@host_disconnected_tries_max 5 * 6
defstruct [
- :video_url,
+ :video_info,
:host_user_token_hash,
:room_id,
host_disconnected_tries: @host_disconnected_tries_max
@@ -15,7 +26,7 @@ defmodule Mediasync.Room.State do
end
@type t() :: %Mediasync.Room.State{
- video_url: binary(),
+ video_info: Mediasync.Room.VideoInfo.t(),
host_user_token_hash: Mediasync.UserToken.hash(),
room_id: Mediasync.RoomID.t(),
host_disconnected_tries: integer()
@@ -24,6 +35,7 @@ end
defmodule Mediasync.Room do
use GenServer
+ require Logger
@spec start_link(Mediasync.Room.State.t()) :: tuple()
def start_link(state = %Mediasync.Room.State{}) do
@@ -41,9 +53,9 @@ defmodule Mediasync.Room do
)
end
- @spec get_video_url(GenServer.server()) :: binary()
- def get_video_url(pid) do
- GenServer.call(pid, :get_video_url)
+ @spec get_video_info(GenServer.server()) :: Mediasync.Room.VideoInfo.t()
+ def get_video_info(pid) do
+ GenServer.call(pid, :get_video_info)
end
@spec host?(GenServer.server(), Mediasync.UserToken.hash()) :: boolean()
@@ -51,6 +63,24 @@ defmodule Mediasync.Room do
GenServer.call(pid, {:host?, user_token_hash})
end
+ @spec host_connected?(GenServer.server()) :: boolean()
+ def host_connected?(pid) do
+ GenServer.call(pid, :host_connected?)
+ end
+
+ defp host_connected_inner(state = %Mediasync.Room.State{}) do
+ case Registry.lookup(
+ Mediasync.RoomConnectionRegistry,
+ {state.room_id, state.host_user_token_hash}
+ ) do
+ [{_pid, _value}] ->
+ true
+
+ [] ->
+ false
+ end
+ end
+
@spec publish_playback_state(GenServer.server(), Mediasync.PlaybackState.t()) :: :ok
def publish_playback_state(pid, playback_state = %Mediasync.PlaybackState{}) do
GenServer.call(pid, {:publish_playback_state, playback_state})
@@ -67,8 +97,8 @@ defmodule Mediasync.Room do
end
@impl true
- def handle_call(:get_video_url, _from, state = %Mediasync.Room.State{}) do
- {:reply, state.video_url, state}
+ def handle_call(:get_video_info, _from, state = %Mediasync.Room.State{}) do
+ {:reply, state.video_info, state}
end
@impl true
@@ -82,6 +112,11 @@ defmodule Mediasync.Room do
end
@impl true
+ def handle_call(:host_connected?, _from, state = %Mediasync.Room.State{}) do
+ {:reply, host_connected_inner(state), state}
+ end
+
+ @impl true
def handle_call(
{:publish_playback_state, playback_state = %Mediasync.PlaybackState{}},
_from,
@@ -97,21 +132,17 @@ defmodule Mediasync.Room do
@impl true
def handle_info(:check_if_active, state) do
state =
- case Registry.lookup(
- Mediasync.RoomConnectionRegistry,
- {state.room_id, state.host_user_token_hash}
- ) do
- [{_pid, _value}] ->
- %{state | host_disconnected_tries: Mediasync.Room.State.host_disconnected_tries_max()}
-
- _ ->
- %{state | host_disconnected_tries: state.host_disconnected_tries - 1}
+ if host_connected_inner(state) do
+ %{state | host_disconnected_tries: Mediasync.Room.State.host_disconnected_tries_max()}
+ else
+ %{state | host_disconnected_tries: state.host_disconnected_tries - 1}
end
Process.send_after(self(), :check_if_active, @inactive_check_wait_milliseconds)
if state.host_disconnected_tries <= 0 do
- {:stop, :no_host, state}
+ Logger.info("Room #{state.room_id} shutting down: no host.")
+ {:stop, {:shutdown, :no_host}, state}
else
{:noreply, state}
end
diff --git a/lib/mediasync/room_connection.ex b/lib/mediasync/room_connection.ex
index ec44b7b..f165dc1 100644
--- a/lib/mediasync/room_connection.ex
+++ b/lib/mediasync/room_connection.ex
@@ -20,7 +20,7 @@ defmodule Mediasync.RoomConnection.State do
end
defmodule Mediasync.RoomConnection do
- import Mediasync.Utils, only: [bool_to_int_repr: 1, int_repr_to_bool!: 1]
+ import Mediasync.Utils, only: [bool_to_int_repr: 1, int_repr_to_bool: 1]
@behaviour WebSock
@@ -66,7 +66,7 @@ defmodule Mediasync.RoomConnection do
) do
if state.host? do
Mediasync.Room.publish_playback_state(state.room_pid, %Mediasync.PlaybackState{
- paused?: int_repr_to_bool!(paused?),
+ paused?: int_repr_to_bool(paused?),
position_milliseconds: position_milliseconds
})
@@ -93,13 +93,14 @@ defmodule Mediasync.RoomConnection do
@impl true
def handle_info(
- {:DOWN, ref, :process, _object, _reason},
+ {:DOWN, ref, :process, _object, reason},
state = %Mediasync.RoomConnection.State{}
) do
room_monitor_ref = state.room_monitor_ref
- case ref do
- ^room_monitor_ref -> {:stop, {:error, :room_exited}, state}
+ case {ref, reason} do
+ {^room_monitor_ref, :shutdown} -> {:stop, :normal, state}
+ {^room_monitor_ref, _} -> {:stop, {:error, :room_exited}, state}
_ -> {:stop, {:error, :unexpected_down_message}, state}
end
end
diff --git a/lib/mediasync/room_id.ex b/lib/mediasync/room_id.ex
index 64a8d23..3579f07 100644
--- a/lib/mediasync/room_id.ex
+++ b/lib/mediasync/room_id.ex
@@ -3,7 +3,6 @@ defmodule Mediasync.RoomID do
@spec generate() :: t()
def generate do
- Application.get_env(:mediasync, :node_id) <>
- "~" <> Base.url_encode64(:crypto.strong_rand_bytes(16), padding: false)
+ Base.url_encode64(:crypto.strong_rand_bytes(16), padding: false)
end
end
diff --git a/lib/mediasync/router.ex b/lib/mediasync/router.ex
index 54ef533..a27bfc9 100644
--- a/lib/mediasync/router.ex
+++ b/lib/mediasync/router.ex
@@ -1,4 +1,5 @@
defmodule Mediasync.Router do
+ import Mediasync.Constants
import Mediasync.Utils
import Mediasync.UserToken
use Plug.Router
@@ -11,15 +12,8 @@ defmodule Mediasync.Router do
plug(:put_secret_key_base)
- plug(Plug.Session,
- store: :cookie,
- key: "_mediasync_session",
- encryption_salt: {Mediasync.Utils, :get_session_encryption_salt, []},
- signing_salt: {Mediasync.Utils, :get_session_signing_salt, []}
- )
-
+ plug(:session_wrapper)
plug(:fetch_session)
-
plug(:ensure_user_token)
plug(:match)
@@ -34,19 +28,48 @@ defmodule Mediasync.Router do
plug(:dispatch)
get "/" do
- conn
- |> put_html_content_type()
- |> send_resp(200, Mediasync.Templates.home())
+ enable_discord_activity? = Application.fetch_env!(:mediasync, :enable_discord_activity?)
+
+ conn =
+ conn
+ |> put_html_content_type()
+
+ send_resp(
+ conn,
+ 200,
+ cond do
+ enable_discord_activity? and Map.get(conn.query_params, query_param_instance_id()) ->
+ Mediasync.Templates.discord_activity()
+
+ enable_discord_activity? and
+ Map.get(conn.query_params, query_param_discord_activity_inner()) ->
+ Mediasync.Templates.home(:discord_activity)
+
+ true ->
+ Mediasync.Templates.home()
+ end
+ )
end
post "/host_room" do
- video_url = conn.body_params["video_url"]
+ param_video_url = conn.body_params["video_url"]
+
+ video_info = %Mediasync.Room.VideoInfo{
+ url: param_video_url,
+ content_type:
+ hd(
+ Req.Response.get_header(
+ Req.head!(param_video_url, receive_timeout: 5000, retry: false),
+ "content-type"
+ )
+ )
+ }
cond do
- byte_size(video_url) > Application.get_env(:mediasync, :max_video_url_size) ->
+ byte_size(video_info.url) > Application.get_env(:mediasync, :max_video_url_size) ->
Mediasync.HTTPErrors.send_video_url_too_large(conn)
- elem(URI.new(video_url), 0) != :ok ->
+ elem(URI.new(video_info.url), 0) != :ok ->
Mediasync.HTTPErrors.send_invalid_video_url(conn)
true ->
@@ -55,21 +78,47 @@ defmodule Mediasync.Router do
Mediasync.RoomSupervisor,
{Mediasync.Room,
%Mediasync.Room.State{
- video_url: video_url,
+ video_info: video_info,
host_user_token_hash: get_user_token_hash!(conn)
}}
)
- redirect(conn, status: 303, location: "/room/#{room_id}")
+ suffix =
+ if Application.fetch_env!(:mediasync, :enable_discord_activity?) and
+ Map.get(conn.query_params, query_param_discord_activity_inner()) do
+ "?#{query_param_discord_activity_inner()}"
+ else
+ ""
+ end
+
+ redirect(conn, status: 303, location: "/room/#{room_id}#{suffix}")
end
end
get "/room/:room_id" do
- case Registry.lookup(Mediasync.RoomRegistry, conn.path_params["room_id"]) do
+ room_id = conn.path_params["room_id"]
+
+ case Registry.lookup(Mediasync.RoomRegistry, room_id) do
[{pid, _value}] ->
+ video_info = Mediasync.Room.get_video_info(pid)
+
+ {video_info, websocket_path, state_url, home_url} =
+ if Application.fetch_env!(:mediasync, :enable_discord_activity?) and
+ Map.get(conn.query_params, query_param_discord_activity_inner()) do
+ {%{video_info | url: "/.proxy/room/#{room_id}/video"},
+ "/.proxy/room/#{room_id}/websocket?#{query_param_discord_activity_inner()}",
+ "/.proxy/room/#{room_id}/state.json?#{query_param_discord_activity_inner()}",
+ "/.proxy/?#{query_param_discord_activity_inner()}"}
+ else
+ {video_info, "/room/#{room_id}/websocket", "/room/#{room_id}/state.json", nil}
+ end
+
conn
|> put_html_content_type()
- |> send_resp(200, Mediasync.Templates.room(Mediasync.Room.get_video_url(pid)))
+ |> send_resp(
+ 200,
+ Mediasync.Templates.room(video_info, websocket_path, state_url, home_url)
+ )
[] ->
Mediasync.HTTPErrors.send_not_found(conn)
@@ -77,22 +126,48 @@ defmodule Mediasync.Router do
end
get "/room/:room_id/websocket" do
- # TODO: verify origin before doing any of this
+ if MapSet.member?(
+ Application.fetch_env!(:mediasync, :websocket_origin),
+ hd(get_req_header(conn, "origin"))
+ ) do
+ user_token_hash = get_user_token_hash!(conn)
+ room_id = conn.path_params["room_id"]
+
+ case Registry.lookup(Mediasync.RoomRegistry, room_id) do
+ [{pid, _value}] ->
+ conn
+ |> WebSockAdapter.upgrade(
+ Mediasync.RoomConnection,
+ %Mediasync.RoomConnection.State{
+ room_pid: pid,
+ room_id: room_id,
+ user_token_hash: user_token_hash
+ },
+ max_frame_size: Application.fetch_env!(:mediasync, :websocket_max_frame_octets)
+ )
+
+ [] ->
+ Mediasync.HTTPErrors.send_not_found(conn)
+ end
+ else
+ Mediasync.HTTPErrors.send_forbidden(conn)
+ end
+ end
- user_token_hash = get_user_token_hash!(conn)
+ get "/room/:room_id/state.json" do
room_id = conn.path_params["room_id"]
case Registry.lookup(Mediasync.RoomRegistry, room_id) do
[{pid, _value}] ->
conn
- |> WebSockAdapter.upgrade(
- Mediasync.RoomConnection,
- %Mediasync.RoomConnection.State{
- room_pid: pid,
- room_id: room_id,
- user_token_hash: user_token_hash
- },
- max_frame_size: Application.fetch_env!(:mediasync, :websocket_max_frame_octets)
+ |> put_json_content_type()
+ |> send_resp(
+ 200,
+ Jason.encode!(%{
+ "hostConnected" => Mediasync.Room.host_connected?(pid),
+ "viewersConnected" =>
+ Registry.count_match(Mediasync.RoomSubscriptionRegistry, room_id, nil)
+ })
)
[] ->
@@ -100,14 +175,24 @@ defmodule Mediasync.Router do
end
end
+ get "/room/:room_id/video" do
+ room_id = conn.path_params["room_id"]
+
+ case Registry.lookup(Mediasync.RoomRegistry, room_id) do
+ [{pid, _value}] ->
+ redirect(conn, status: 301, location: Mediasync.Room.get_video_info(pid).url)
+
+ [] ->
+ Mediasync.HTTPErrors.send_not_found(conn)
+ end
+ end
+
match _ do
Mediasync.HTTPErrors.send_not_found(conn)
end
@impl Plug.ErrorHandler
def handle_errors(conn, %{kind: kind, reason: reason, stack: _stack}) do
- IO.inspect({kind, reason})
-
case {kind, reason} do
{:error, %Plug.CSRFProtection.InvalidCSRFTokenError{}} ->
Mediasync.HTTPErrors.send_invalid_csrf_token(conn)
@@ -116,4 +201,30 @@ defmodule Mediasync.Router do
Mediasync.HTTPErrors.send_unknown(conn)
end
end
+
+ defp session_wrapper(conn, _opts) do
+ query_params = fetch_query_params(conn).query_params
+
+ in_discord_activity? =
+ !!(Application.fetch_env!(:mediasync, :enable_discord_activity?) &&
+ (Map.get(query_params, query_param_discord_activity_inner()) ||
+ (conn.request_path == "/" &&
+ Map.get(query_params, query_param_instance_id()))))
+
+ Plug.Session.call(
+ conn,
+ Plug.Session.init(
+ store: :cookie,
+ key: "_mediasync_session",
+ encryption_salt: {Mediasync.Utils, :get_session_encryption_salt, []},
+ signing_salt: {Mediasync.Utils, :get_session_signing_salt, []},
+ extra:
+ if in_discord_activity? do
+ "Domain=#{Application.fetch_env!(:mediasync, :discord_client_id)}.discordsays.com; SameSite=None; Partitioned; Secure;"
+ else
+ ""
+ end
+ )
+ )
+ end
end
diff --git a/lib/mediasync/templates.ex b/lib/mediasync/templates.ex
index fb91aa0..c6b15cc 100644
--- a/lib/mediasync/templates.ex
+++ b/lib/mediasync/templates.ex
@@ -1,5 +1,18 @@
defmodule Mediasync.Templates do
require EEx
- EEx.function_from_file(:def, :home, "priv/home.html.eex")
- EEx.function_from_file(:def, :room, "priv/room.html.eex", [:video_url])
+
+ def home() do
+ home(:normal)
+ end
+
+ EEx.function_from_file(:def, :home, "priv/home.html.eex", [:mode])
+
+ EEx.function_from_file(:def, :room, "priv/room.html.eex", [
+ :video_info,
+ :websocket_path,
+ :state_url,
+ :home_url
+ ])
+
+ EEx.function_from_file(:def, :discord_activity, "priv/discord_activity.html.eex")
end
diff --git a/lib/mediasync/utils.ex b/lib/mediasync/utils.ex
index fa0ab68..407a9a4 100644
--- a/lib/mediasync/utils.ex
+++ b/lib/mediasync/utils.ex
@@ -20,6 +20,21 @@ defmodule Mediasync.Utils do
|> send_resp(status, "Redirecting to #{location}")
end
+ def put_resp_header_or_ignore(conn, key, value) do
+ if value do
+ Plug.Conn.put_resp_header(conn, key, value)
+ else
+ conn
+ end
+ end
+
+ @spec get_req_header_list(Plug.Conn.t(), [String.t()]) :: [{String.t(), String.t()}]
+ def get_req_header_list(conn, keys) do
+ for key <- keys, value = List.first(Plug.Conn.get_req_header(conn, key)) do
+ {key, value}
+ end
+ end
+
@spec put_secret_key_base(Plug.Conn.t()) :: Plug.Conn.t()
@spec put_secret_key_base(Plug.Conn.t(), []) :: Plug.Conn.t()
def put_secret_key_base(conn, _opts \\ []) do
@@ -36,31 +51,9 @@ defmodule Mediasync.Utils do
Application.fetch_env!(:mediasync, :session_signing_salt)
end
- @spec bool_to_int_repr(boolean()) :: 0 | 1
- @doc """
- Convert false to 0 and true to 1. Useful for sending boolean values over binary protocols.
+ def bool_to_int_repr(false), do: 0
+ def bool_to_int_repr(true), do: 1
- Inverse of `int_repr_to_bool/1`.
- """
- def bool_to_int_repr(bool) do
- case bool do
- false -> 0
- true -> 1
- end
- end
-
- @spec int_repr_to_bool!(0 | 1) :: boolean()
- @doc """
- Convert 0 to false and 1 to true. Useful for receiving boolean values over binary protocols.
- Raises `ArgumentError` if given an argument other than 0 or 1.
-
- Inverse of `bool_to_int_repr/1`.
- """
- def int_repr_to_bool!(int_repr) do
- case int_repr do
- 0 -> false
- 1 -> true
- _ -> raise ArgumentError
- end
- end
+ def int_repr_to_bool(0), do: false
+ def int_repr_to_bool(1), do: true
end
diff --git a/lib/mix/tasks/vendor.ex b/lib/mix/tasks/vendor.ex
index 1f64236..06e3aa5 100644
--- a/lib/mix/tasks/vendor.ex
+++ b/lib/mix/tasks/vendor.ex
@@ -3,9 +3,29 @@ defmodule Mix.Tasks.Vendor do
@impl Mix.Task
def run([]) do
- {_, 0} = System.cmd("npm", ~w(install))
+ {_, 0} = System.cmd("npm", ~w(install --include=dev))
+
File.cp_r!("node_modules/video.js/dist", "priv/static/video.js")
File.cp_r!("node_modules/video.js/LICENSE", "priv/static/video.js/LICENSE")
+
+ {_, 0} = System.cmd("npx", ~w(parcel build --target discord-embedded-app-sdk))
+
+ discord_readme_path = "priv/static/discord-embedded-app-sdk/README.md"
+
+ File.cp_r!(
+ "node_modules/@discord/embedded-app-sdk/LICENSE.md",
+ discord_readme_path
+ )
+
+ File.write!(
+ discord_readme_path,
+ """
+ This directory contains a bundled version of https://github.com/discord/embedded-app-sdk/
+ See lib/mix/tasks/vendor.ex for how it was generated. The license for the \
+ original library is reproduced below:\n
+ """ <> File.read!(discord_readme_path)
+ )
+
nil
end
end