Packages

RIG, the Reactive Interaction Gateway, provides an easy (and scaleable) way to push messages from backend services to connected frontends (and vice versa).

Current section

Files

Jump to
rig lib rig_web presence channel.ex
Raw

lib/rig_web/presence/channel.ex

defmodule RigWeb.Presence.Channel do
@moduledoc """
The presence channel is used to track a user's connected devices.
To this end, there is also only a single room for every user. This is used,
for instance, by the Kafka consumer code to broadcast incoming messages to the
target users' channels, in order to distribute the messages to all connected
devices.
Note that keeping track of connected devices is done by the Phoenix PubSub
module, so it also works with distributed nodes.
"""
use RigWeb, :channel
use Rig.Config, :custom_validation
require Logger
alias RigWeb.Presence
# Confex callback
defp validate_config!(config) do
%{
jwt_user_field: config |> Keyword.fetch!(:jwt_user_field),
jwt_roles_field: config |> Keyword.fetch!(:jwt_roles_field),
privileged_roles: MapSet.new(config |> Keyword.fetch!(:privileged_roles))
}
end
@doc """
The room name for a specific user.
"""
@spec user_channel_name(String.t) :: String.t
def user_channel_name(username), do: "user:#{username}"
@doc """
The room name for a specific role.
"""
@spec role_channel_name(String.t) :: String.t
def role_channel_name(role), do: "role:#{role}"
defp extract_username_and_roles(socket) do
user_info = socket.assigns.user_info
conf = config()
{
_username = Map.fetch!(user_info, conf.jwt_user_field),
_roles = Map.fetch!(user_info, conf.jwt_roles_field)
}
end
@doc """
Join user specific channel. Only owner of given channel or user with authorized
role is able to join.
"""
@spec join(String.t, map, map) :: {atom, map}
def join(room = "user:" <> user_subtopic_name, _params, socket) do
{username, roles} = extract_username_and_roles(socket)
cond do
username == user_subtopic_name ->
send(self(), {:after_join, username, roles})
authorized_join(room, username, socket)
has_authorized_role?(roles) ->
send(self(), {:after_join, user_subtopic_name})
authorized_join(room, username, socket)
true -> unauthorized_join(room, username)
end
end
@doc """
Join common role based channel. Only user with authorized role is able to join.
"""
@spec join(String.t, map, map) :: {atom, map}
def join(room = "role:" <> _, _params, socket) do
{username, roles} = extract_username_and_roles(socket)
if has_authorized_role?(roles) do
authorized_join(room, username, socket)
else
unauthorized_join(room, username)
end
end
@doc """
Start tracking of user in global role baes channels and also in his specific channel.
"""
@spec handle_info({:after_join, String.t, [String.t]}, map) :: {:noreply, map}
def handle_info({:after_join, username, roles}, socket) do
# track global role channels
push(socket, "presence_state", Presence.list(socket))
track_multiple_presences("role", roles, socket)
# track user specific channel
track_presence("user:#{username}", socket)
{:noreply, socket}
end
@doc """
Start tracking of user in his specific channel.
"""
@spec handle_info({:after_join, String.t}, map) :: {:noreply, map}
def handle_info({:after_join, username}, socket) do
# track user specific channel
track_presence("user:#{username}", socket)
{:noreply, socket}
end
@doc """
List all presences in given topic.
"""
@spec channels_list(String.t) :: map
def channels_list(topic), do: Presence.list(topic)
@spec track_multiple_presences(String.t, list(String.t), map) :: any
defp track_multiple_presences(topic_namespace, subtopics, socket) do
Enum.each(subtopics, fn(subtopic) ->
track_presence("#{topic_namespace}:#{subtopic}", socket)
end)
end
@spec track_presence(String.t, map) :: any
defp track_presence(topic, socket) do
%{"username" => username} = socket.assigns.user_info
{:ok, _} = Presence.track(
socket.channel_pid,
topic,
username,
Map.merge(socket.assigns.user_info,
%{
time: System.system_time(:milli_seconds),
address: "192.168.0.1",
device: "desktop",
browser: "Chrome",
}
)
)
end
@spec has_authorized_role?(list(String.t)) :: boolean
defp has_authorized_role?(roles) do
roles
|> MapSet.new
|> MapSet.intersection(config().privileged_roles)
|> MapSet.size
|> Kernel.>(0)
end
@spec authorized_join(String.t, String.t, map) :: {:ok, map}
defp authorized_join(room, username, socket) do
Logger.debug("user #{inspect username} has joined #{room}")
{:ok, socket}
end
@spec unauthorized_join(String.t, String.t) :: {:error, String.t}
defp unauthorized_join(room, username) do
Logger.warn(msg = "unauthorized user with id #{inspect username} tried to join #{inspect room}!")
{:error, msg}
end
end