Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions lib/realtime/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,9 @@ defmodule Realtime.Api do
end

defp list_extensions(type) do
from(e in Extensions, where: e.type == ^type, select: e)
|> Repo.all()
query = from(e in Extensions, where: e.type == ^type, select: e)

Repo.all(query)
end

def rename_settings_field(from, to) do
Expand Down
2 changes: 1 addition & 1 deletion lib/realtime_web/channels/realtime_channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ defmodule RealtimeWeb.RealtimeChannel do
end

def handle_in("presence", payload, %{assigns: %{private?: false}} = socket) do
with {:ok, socket} <- PresenceHandler.handle(payload, socket) do
with {:ok, socket} <- PresenceHandler.handle(payload, nil, socket) do
{:reply, :ok, socket}
else
{:error, :rate_limit_exceeded} ->
Expand Down
16 changes: 6 additions & 10 deletions lib/realtime_web/channels/realtime_channel/presence_handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,28 +52,22 @@ defmodule RealtimeWeb.RealtimeChannel.PresenceHandler do
end
end

@spec handle(map(), Socket.t()) ::
{:ok, Socket.t()} | {:error, :rls_policy_error | :unable_to_set_policies | :rate_limit_exceeded}
def handle(_, %{assigns: %{presence_enabled?: false}} = socket), do: {:ok, socket}
def handle(payload, socket) when not is_private?(socket), do: handle(payload, nil, socket)

@spec handle(map(), pid() | nil, Socket.t()) ::
{:ok, Socket.t()}
| {:error, :rls_policy_error | :unable_to_set_policies | :rate_limit_exceeded | :unable_to_track_presence}
def handle(_, _, %{assigns: %{presence_enabled?: false}} = socket), do: {:ok, socket}

def handle(%{"event" => event} = payload, db_conn, socket) do
event = String.downcase(event, :ascii)
handle_presence_event(event, payload, db_conn, socket)
end

def handle(_payload, _db_conn, socket), do: {:ok, socket}
def handle(_, _, socket), do: {:ok, socket}

defp handle_presence_event("track", payload, _db_conn, socket) when not is_private?(socket) do
defp handle_presence_event("track", payload, _, socket) when not is_private?(socket) do
track(socket, payload)
end

defp handle_presence_event("track", payload, db_conn, socket) when is_nil(socket.assigns.policies.presence.write) do
defp handle_presence_event("track", payload, db_conn, socket)
when is_private?(socket) and is_nil(socket.assigns.policies.presence.write) do
%{assigns: %{authorization_context: authorization_context, policies: policies}} = socket

case Authorization.get_write_authorizations(policies, db_conn, authorization_context) do
Expand Down Expand Up @@ -111,6 +105,8 @@ defmodule RealtimeWeb.RealtimeChannel.PresenceHandler do
end

defp track(socket, payload) do
socket = assign(socket, :presence_enabled?, true)

%{assigns: %{presence_key: presence_key, tenant_topic: tenant_topic}} = socket
payload = Map.get(payload, "payload", %{})

Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Realtime.MixProject do
def project do
[
app: :realtime,
version: "2.47.1",
version: "2.47.2",
elixir: "~> 1.17.3",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
Expand Down
50 changes: 50 additions & 0 deletions test/integration/rt_channel_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,56 @@ defmodule Realtime.Integration.RtChannelTest do
assert_receive %Message{event: "phx_reply", payload: %{"status" => "ok"}}, 500
refute_receive %Message{event: "presence_state"}, 500
end

test "presence automatically enabled when user sends track message for public channel", %{tenant: tenant} do
{socket, _} = get_connection(tenant)
config = %{presence: %{key: "", enabled: false}, private: false}
topic = "realtime:any"

WebsocketClient.join(socket, topic, %{config: config})

assert_receive %Message{event: "phx_reply", payload: %{"status" => "ok"}, topic: ^topic}, 300
refute_receive %Message{event: "presence_state"}, 500

payload = %{
type: "presence",
event: "TRACK",
payload: %{name: "realtime_presence_96", t: 1814.7000000029802}
}

WebsocketClient.send_event(socket, topic, "presence", payload)

assert_receive %Message{event: "presence_diff", payload: %{"joins" => joins, "leaves" => %{}}, topic: ^topic}

join_payload = joins |> Map.values() |> hd() |> get_in(["metas"]) |> hd()
assert get_in(join_payload, ["name"]) == payload.payload.name
assert get_in(join_payload, ["t"]) == payload.payload.t
end

@tag policies: [:authenticated_read_broadcast_and_presence, :authenticated_write_broadcast_and_presence]
test "presence automatically enabled when user sends track message for private channel",
%{tenant: tenant, topic: topic} do
{socket, _} = get_connection(tenant, "authenticated")
config = %{presence: %{key: "", enabled: false}, private: true}
topic = "realtime:#{topic}"

WebsocketClient.join(socket, topic, %{config: config})
assert_receive %Message{event: "phx_reply", payload: %{"status" => "ok"}, topic: ^topic}, 300
refute_receive %Message{event: "presence_state"}, 500

payload = %{
type: "presence",
event: "TRACK",
payload: %{name: "realtime_presence_96", t: 1814.7000000029802}
}

WebsocketClient.send_event(socket, topic, "presence", payload)

assert_receive %Message{event: "presence_diff", payload: %{"joins" => joins, "leaves" => %{}}, topic: ^topic}, 500
join_payload = joins |> Map.values() |> hd() |> get_in(["metas"]) |> hd()
assert get_in(join_payload, ["name"]) == payload.payload.name
assert get_in(join_payload, ["t"]) == payload.payload.t
end
end

describe "token handling" do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ defmodule RealtimeWeb.RealtimeChannel.PresenceHandlerTest do
end
end

describe "handle/2" do
describe "handle/3" do
test "with true policy and is private, user can track their presence and changes", %{
tenant: tenant,
topic: topic,
Expand Down Expand Up @@ -142,7 +142,7 @@ defmodule RealtimeWeb.RealtimeChannel.PresenceHandlerTest do
policies = %Policies{presence: %PresencePolicies{read: false, write: false}}
socket = socket_fixture(tenant, topic, key, policies: policies, private?: false)

assert {:ok, _socket} = PresenceHandler.handle(%{"event" => "track"}, socket)
assert {:ok, _socket} = PresenceHandler.handle(%{"event" => "track"}, nil, socket)

topic = socket.assigns.tenant_topic
assert_receive %Broadcast{topic: ^topic, event: "presence_diff", payload: %{joins: joins, leaves: %{}}}
Expand Down Expand Up @@ -229,6 +229,7 @@ defmodule RealtimeWeb.RealtimeChannel.PresenceHandlerTest do
assert {:ok, socket} =
PresenceHandler.handle(
%{"event" => "track", "payload" => %{"metadata" => random_string()}},
nil,
socket
)

Expand All @@ -248,20 +249,20 @@ defmodule RealtimeWeb.RealtimeChannel.PresenceHandlerTest do
assert log =~ "UnknownPresenceEvent"
end

test "socket with presence enabled false will ignore presence events in public channel", %{
test "socket with presence enabled false will ignore non-track presence events in public channel", %{
tenant: tenant,
topic: topic
} do
key = random_string()
policies = %Policies{presence: %PresencePolicies{read: true, write: true}}
socket = socket_fixture(tenant, topic, key, policies: policies, private?: false, enabled?: false)

assert {:ok, _socket} = PresenceHandler.handle(%{"event" => "track"}, socket)
assert {:ok, _socket} = PresenceHandler.handle(%{"event" => "untrack"}, nil, socket)
topic = socket.assigns.tenant_topic
refute_receive %Broadcast{topic: ^topic, event: "presence_diff"}
end

test "socket with presence enabled false will ignore presence events in private channel", %{
test "socket with presence enabled false will ignore non-track presence events in private channel", %{
tenant: tenant,
topic: topic,
db_conn: db_conn
Expand All @@ -270,11 +271,80 @@ defmodule RealtimeWeb.RealtimeChannel.PresenceHandlerTest do
policies = %Policies{presence: %PresencePolicies{read: true, write: true}}
socket = socket_fixture(tenant, topic, key, policies: policies, private?: false, enabled?: false)

assert {:ok, _socket} = PresenceHandler.handle(%{"event" => "track"}, db_conn, socket)
assert {:ok, _socket} = PresenceHandler.handle(%{"event" => "untrack"}, db_conn, socket)
topic = socket.assigns.tenant_topic
refute_receive %Broadcast{topic: ^topic, event: "presence_diff"}
end

test "socket with presence disabled will enable presence on track message for public channel", %{
tenant: tenant,
topic: topic
} do
key = random_string()
policies = %Policies{presence: %PresencePolicies{read: true, write: true}}
socket = socket_fixture(tenant, topic, key, policies: policies, private?: false, enabled?: false)

refute socket.assigns.presence_enabled?

assert {:ok, updated_socket} = PresenceHandler.handle(%{"event" => "track"}, nil, socket)

assert updated_socket.assigns.presence_enabled?
topic = socket.assigns.tenant_topic
assert_receive %Broadcast{topic: ^topic, event: "presence_diff", payload: %{joins: joins, leaves: %{}}}
assert Map.has_key?(joins, key)
end

test "socket with presence disabled will enable presence on track message for private channel", %{
tenant: tenant,
topic: topic,
db_conn: db_conn
} do
key = random_string()
policies = %Policies{presence: %PresencePolicies{read: true, write: true}}
socket = socket_fixture(tenant, topic, key, policies: policies, private?: true, enabled?: false)

refute socket.assigns.presence_enabled?

assert {:ok, updated_socket} = PresenceHandler.handle(%{"event" => "track"}, db_conn, socket)

assert updated_socket.assigns.presence_enabled?
topic = socket.assigns.tenant_topic
assert_receive %Broadcast{topic: ^topic, event: "presence_diff", payload: %{joins: joins, leaves: %{}}}
assert Map.has_key?(joins, key)
end

test "socket with presence disabled will not enable presence on untrack message", %{
tenant: tenant,
topic: topic,
db_conn: db_conn
} do
key = random_string()
policies = %Policies{presence: %PresencePolicies{read: true, write: true}}
socket = socket_fixture(tenant, topic, key, policies: policies, enabled?: false)

refute socket.assigns.presence_enabled?

assert {:ok, updated_socket} = PresenceHandler.handle(%{"event" => "untrack"}, db_conn, socket)

refute updated_socket.assigns.presence_enabled?
topic = socket.assigns.tenant_topic
refute_receive %Broadcast{topic: ^topic, event: "presence_diff"}
end

test "socket with presence disabled will not enable presence on unknown event", %{
tenant: tenant,
topic: topic,
db_conn: db_conn
} do
key = random_string()
policies = %Policies{presence: %PresencePolicies{read: true, write: true}}
socket = socket_fixture(tenant, topic, key, policies: policies, enabled?: false)

refute socket.assigns.presence_enabled?

assert {:error, :unknown_presence_event} = PresenceHandler.handle(%{"event" => "unknown"}, db_conn, socket)
end

@tag policies: [:authenticated_read_broadcast_and_presence, :authenticated_write_broadcast_and_presence]
test "rate limit is checked on private channel", %{tenant: tenant, topic: topic, db_conn: db_conn} do
key = random_string()
Expand Down
Loading