Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NOT TO BE MERGED - just using this for comparing diffs #2

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ defmodule PhoenixChat.Mixfile do
applications: [
:comeonin,
:cowboy,
:faker,
:gettext,
:logger,
:phoenix,
Expand All @@ -45,6 +46,7 @@ defmodule PhoenixChat.Mixfile do
{:comeonin, "~> 2.3"},
{:corsica, "~> 0.4"},
{:cowboy, "~> 1.0"},
{:faker, "~> 0.7"},
{:gettext, "~> 0.11"},
{:guardian, "~> 0.10"},
{:phoenix, "~> 1.2.0"},
Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"db_connection": {:hex, :db_connection, "1.0.0-rc.5", "1d9ab6e01387bdf2de7a16c56866971f7c2f75aea7c69cae2a0346e4b537ae0d", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: true]}, {:sbroker, "~> 1.0.0-beta.3", [hex: :sbroker, optional: true]}]},
"decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], []},
"ecto": {:hex, :ecto, "2.0.5", "7f4c79ac41ffba1a4c032b69d7045489f0069c256de606523c65d9f8188e502d", [:mix], [{:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.1.2 or ~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.12.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}]},
"faker": {:hex, :faker, "0.7.0", "2c42deeac7be717173c78c77fb3edc749fb5d5e460e33d01fe592ae99acc2f0d", [:mix], []},
"fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], []},
"gettext": {:hex, :gettext, "0.11.0", "80c1dd42d270482418fa158ec5ba073d2980e3718bacad86f3d4ad71d5667679", [:mix], []},
"guardian": {:hex, :guardian, "0.12.0", "ab1f0a1ab0cd8f4f9c8cca6e28d61136ca682684cf0f82e55a50e8061be7575a", [:mix], [{:jose, "~> 1.6", [hex: :jose, optional: false]}, {:plug, "~> 1.0", [hex: :plug, optional: false]}, {:poison, ">= 1.3.0", [hex: :poison, optional: false]}, {:uuid, ">=1.1.1", [hex: :uuid, optional: false]}]},
Expand Down
18 changes: 18 additions & 0 deletions priv/repo/migrations/20160915111446_create_anonymous_users.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule PhoenixChat.Repo.Migrations.CreateAnonymousUsers do
use Ecto.Migration

def change do
# We want to use a `uuid` as primary key so we need to set `primary_key: false`.
create table(:anonymous_users, primary_key: false) do
# We add the `:id` column manually with a type of `uuid` and set
# it as `primary_key`.
add :id, :uuid, primary_key: true
add :name, :string
add :avatar, :string
add :public_key, :string
add :last_viewed_by_admin_at, :datetime

timestamps
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule PhoenixChat.Repo.Migrations.MessageBelongsToAnonymousUser do
use Ecto.Migration

def up do
alter table(:messages) do
# We need to set `type` as `uuid` so it does not default to `integer`.
add :anonymous_user_id, references(:anonymous_users, on_delete: :nilify_all, type: :uuid)
remove :from
end
end

def down do
alter table(:messages) do
remove :anonymous_user_id
add :from, :string
end
end
end
45 changes: 38 additions & 7 deletions web/channels/admin_channel.ex
Original file line number Diff line number Diff line change
@@ -1,33 +1,64 @@
defmodule PhoenixChat.AdminChannel do
@moduledoc """
The channel used to give the administrator access to all users.
The channel used to give the administrator access to all users.
"""

use PhoenixChat.Web, :channel
require Logger

alias PhoenixChat.{Presence}
alias PhoenixChat.{Presence, Repo, AnonymousUser}

intercept ~w(lobby_list)

@doc """
The `admin:active_users` topic is how we identify all users currently using the app.
"""
def join("admin:active_users", payload, socket) do
authorize(payload, fn ->
public_key = socket.assigns.public_key
lobby_list = public_key
|> AnonymousUser.by_public_key
|> Repo.all
|> user_payload
send(self, :after_join)
{:ok, socket}
{:ok, %{lobby_list: lobby_list}, socket}
end)
end

@doc """
This handles the `:after_join` event and tracks the presence of the socket that has subscribed to the `admin:active_users` topic.
This handles the `:after_join` event and tracks the presence of the socket that
has subscribed to the `admin:active_users` topic.
"""
def handle_info(:after_join, socket) do
track_presence(socket, socket.assigns)
{:noreply, socket}
end

@doc """
Sends the lobby_list only to admins
"""
def handle_out("lobby_list", payload, socket) do
%{assigns: assigns} = socket
if assigns.user_id && assigns.public_key == payload.public_key do
push socket, "lobby_list", payload
end
{:noreply, socket}
end

defp track_presence(socket, %{uuid: uuid}) do
user = get_or_create_anonymous_user!(uuid)

payload = user_payload(user)
# Keep track of rooms to be displayed to admins
broadcast! socket, "lobby_list", payload
# Keep track of users that are online (not keepin track of admin presence)
push socket, "presence_state", Presence.list(socket)
Logger.debug "Presence for socket: #{inspect socket}"
id = socket.assigns.user_id || socket.assigns.uuid
{:ok, _} = Presence.track(socket, id, %{

{:ok, _} = Presence.track(socket, uuid, %{
online_at: inspect(System.system_time(:seconds))
})
{:noreply, socket}
end

defp track_presence(_socket, _), do: nil #noop
end
39 changes: 39 additions & 0 deletions web/channels/channel_helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule PhoenixChat.ChannelHelpers do
Convenience functions imported in all Channels
"""

alias PhoenixChat.{AnonymousUser, Repo, Message}

@doc """
Convenience function for authorization
"""
Expand All @@ -21,4 +23,41 @@ defmodule PhoenixChat.ChannelHelpers do
def authorized?(_payload) do
true
end

@doc """
Returns an anonymous user record.

This either gets or creates an anonymous user with the `uuid` from `socket.assigns.`.
"""
def get_or_create_anonymous_user!(%{uuid: uuid} = assigns) do
if user = Repo.get(AnonymousUser, uuid) do
user
else
params = %{public_key: assigns.public_key, id: uuid}
changeset = AnonymousUser.changeset(%AnonymousUser{}, params)
Repo.insert!(changeset)
end
end

# We do not need to create signed-up users
def get_or_create_anonymous_user!(_socket), do: nil #noop

def user_payload(list) when is_list(list) do
Enum.map(list, &user_payload/1)
end

def user_payload({user, message}) do
%{name: user.name,
avatar: user.avatar,
id: user.id,
public_key: user.public_key,
last_viewed_by_admin_at: user.last_viewed_by_admin_at,
last_message: message && message.body,
last_message_sent_at: message && message.inserted_at}
end

def user_payload(user) do
message = Message.latest_room_messages(user.id, 1) |> Repo.one
user_payload({user, message})
end
end
61 changes: 54 additions & 7 deletions web/channels/room_channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule PhoenixChat.RoomChannel do
use PhoenixChat.Web, :channel
require Logger

alias PhoenixChat.{Message, Repo}
alias PhoenixChat.{Message, Repo, Endpoint, AnonymousUser}

def join("room:" <> room_id, payload, socket) do
authorize(payload, fn ->
Expand All @@ -11,32 +11,79 @@ defmodule PhoenixChat.RoomChannel do
|> Repo.all
|> Enum.map(&message_payload/1)
|> Enum.reverse
send(self, {:after_join, payload})
{:ok, %{messages: messages}, socket}
end)
end

def handle_info({:after_join, payload}, socket) do
# We create the anonymous user in our DB if its `uuid` does not match
# any existing record.
get_or_create_anonymous_user!(socket)

# We record when admin views a room
update_last_viewed_at(payload["previousRoom"])
update_last_viewed_at(payload["nextRoom"])
{:noreply, socket}
end

def handle_in("message", payload, socket) do
payload = payload
|> Map.put("user_id", socket.assigns.user_id)
|> Map.put("from", socket.assigns[:uuid])
changeset = Message.changeset(%Message{}, payload)

case Repo.insert(changeset) do
# This branch gets triggered when a message is sent by an anonymous user
{:ok, %{anonymous_user_id: uuid} = message} when not is_nil(uuid) ->
user = Repo.preload(message, :anonymous_user).anonymous_user
message_payload = message_payload(message, user)
broadcast! socket, "message", message_payload

# Apart from sending the message, we want to update the lobby list
# with the last message sent by the user and its timestamp
Endpoint.broadcast_from! self, "admin:active_users",
"lobby_list", user_payload({user, message})

# We also send the message via the "notifications" event. This event
# will be listened to in the frontend and will publish an Notification
# via the browser when admin is not viewing the sender's chatroom.
Endpoint.broadcast_from! self, "admin:active_users",
"notifications", message_payload

# This branch gets triggered when a message is sent by admin
{:ok, message} ->
payload = message_payload(message)
broadcast! socket, "message", payload
{:reply, :ok, socket}
broadcast! socket, "message", message_payload(message)
{:error, changeset} ->
{:reply, {:error, %{errors: changeset}}, socket}
end
end

defp message_payload(message) do
from = message.user_id || message.from
defp update_last_viewed_at(nil), do: nil #noop

defp update_last_viewed_at(uuid) do
user = Repo.get(AnonymousUser, uuid)
changeset = AnonymousUser.last_viewed_changeset(user)
user = Repo.update!(changeset)
Endpoint.broadcast_from! self, "admin:active_users",
"lobby_list", user_payload(user)
end

defp message_payload(%{anonymous_user_id: nil} = message) do
%{body: message.body,
timestamp: message.timestamp,
room: message.room,
from: message.user_id,
id: message.id}
end

defp message_payload(message, user \\ nil) do
user = user || Repo.preload(message, :anonymous_user).anonymous_user
%{body: message.body,
timestamp: message.timestamp,
room: message.room,
from: from,
from: user.name,
uuid: user.id,
id: message.id}
end
end
9 changes: 5 additions & 4 deletions web/channels/user_socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,20 @@ defmodule PhoenixChat.UserSocket do
user = user_id && Repo.get(User, user_id)

socket = if user do
socket
socket
|> assign(:user_id, user_id)
|> assign(:username, user.username)
|> assign(:email, user.email)
else
socket
|> assign(:user_id, nil)
|> assign(:uuid, params["uuid"])
|> assign(:user_id, nil)
|> assign(:uuid, params["uuid"])
end
|> assign(:public_key, params["public_key"])

{:ok, socket}
end

# Socket id's are topics that allow you to identify all sockets for a given user:
#
# def id(socket), do: "users_socket:#{socket.assigns.user_id}"
Expand Down
65 changes: 65 additions & 0 deletions web/models/anonymous_user.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
defmodule PhoenixChat.AnonymousUser do
use PhoenixChat.Web, :model

alias PhoenixChat.Message

# Since we provide the `id` for our AnonymousUser record, we will need to set
# the primary key to not autogenerate it.
@primary_key {:id, :binary_id, autogenerate: false}
# We need to set `@foreign_key_type` below since it defaults to `:integer`.
# We are using a UUID as `id` so we need to set type as `:binary_id`.
@foreign_key_type :binary_id

schema "anonymous_users" do
field :name
field :avatar
field :public_key
field :last_viewed_by_admin_at, PhoenixChat.DateTime
has_many :messages, Message

timestamps
end

def changeset(model, params \\ :empty) do
model
|> cast(params, ~w(public_key id), ~w())
|> put_avatar
|> put_name
end

def last_viewed_changeset(model) do
params = %{last_viewed_by_admin_at: System.system_time(:milliseconds)}
model
|> cast(params, ~w(last_viewed_by_admin_at), [])
end

@doc """
This query returns all users and the respective last messages they
have sent.

Once the query is run, the return value is a tuple of two elements:
`{user, message}`
"""
def by_public_key(public_key, limit \\ 20) do
from u in __MODULE__,
join: m in Message, on: m.anonymous_user_id == u.id,
where: u.public_key == ^public_key,
limit: ^limit,
distinct: u.id,
order_by: [desc: m.inserted_at],
select: {u, m}
end

# Set a fake name for our anonymous user every time we create one
defp put_name(changeset) do
name = (Faker.Color.fancy_name <> " " <> Faker.Company.buzzword()) |> String.downcase
changeset
|> put_change(:name, name)
end

# Set a fake avatar for our anonymous user every time we create one
defp put_avatar(changeset) do
changeset
|> put_change(:avatar, Faker.Avatar.image_url(25, 25))
end
end
11 changes: 8 additions & 3 deletions web/models/message.ex
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
defmodule PhoenixChat.Message do
use PhoenixChat.Web, :model

alias PhoenixChat.{DateTime}

schema "messages" do
field :body, :string
field :timestamp, PhoenixChat.DateTime
field :timestamp, DateTime
field :room, :string
field :from, :string

belongs_to :user, PhoenixChat.User
# Note that we set `:type` below. This is so Ecto is aware the type of the
# foreign_key is not an `:integer` but a `:binary_id`.
belongs_to :anonymous_user, PhoenixChat.AnonymousUser, type: :binary_id

timestamps
end

@required_fields ~w(body timestamp room)
@optional_fields ~w(user_id from)
@optional_fields ~w(anonymous_user_id user_id)

@doc """
Creates a changeset based on the `model` and `params`.
Expand Down