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

Add Channel for Broadcasting Hit Data in Realtime issue #79 #80

Merged
merged 9 commits into from
Jun 2, 2019
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -884,6 +884,30 @@ Open `test/hits_web/controllers/hit_controller_test.exs` in your editor.

-->

## Add Channel

If you are new to Phoenix Channels, please recap:
https://github.com/dwyl/phoenix-chat-example
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


In your terminal, run the following command:
```sh
mix phx.gen.channel Hit
```
You should see the following output:

```
* creating lib/hits_web/channels/hit_channel.ex
* creating test/hits_web/channels/hit_channel_test.exs

Add the channel to your `lib/hits_web/channels/user_socket.ex` handler, for example:

channel "hit:lobby", HitsWeb.HitChannel
```

> If you want to see the code required
to render the hits on the homepage in realtime,
please see: https://github.com/dwyl/hits/pull/80/files


## Research & Background Reading

Expand All @@ -905,3 +929,5 @@ https://medium.com/@kansi/elixir-plug-unveiled-bf354e364641
+ Building a web framework from scratch in Elixir:
https://codewords.recurse.com/issues/five/building-a-web-framework-from-scratch-in-elixir
+ Testing Plugs: https://robots.thoughtbot.com/testing-elixir-plugs
+ How to broadcast a message from a Phoenix Controller to a Channel?
https://stackoverflow.com/questions/33960207/how-to-broadcast-a-message-from-a-phoenix-controller-to-a-channel
39 changes: 36 additions & 3 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ import "phoenix_html"

console.log('hello!');
// Import local files
//
// Local files can be imported directly using relative paths, for example:
// import socket from "./socket"
import socket from "./socket"

// Get Markdown Template from HTML:
var mt = document.getElementById('badge').innerHTML;

Expand Down Expand Up @@ -47,3 +46,37 @@ setTimeout(function () {
}
}
}, 500);

// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("hit:lobby", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })

channel.on('hit', function (payload) { // listen to the 'shout' event
console.log('hit', payload);
append_hit(payload);
// var li = document.createElement("li"); // creaet new list item DOM element
// var name = payload.name || 'guest'; // get name from payload or set default
// li.innerHTML = '<b>' + name + '</b>: ' + payload.message;
// ul.appendChild(li); // append to list
});

const root = document.getElementById("hits");
function append_hit (data) {
const previous = root.childNodes[0];
const DATE = new Date();
const date = Date.now();
const time = DATE.toUTCString().replace('GMT', '');
const text = time + ' /' + data.user + '/' + data.repo + ' ' + data.count
root.insertBefore(div(date, text), previous);
}

// borrowed from: https://git.io/v536m
function div(divid, text) {
let div = document.createElement('div');
div.id = divid;
const txt = document.createTextNode(text);
div.appendChild(txt);
return div;
}
49 changes: 1 addition & 48 deletions assets/js/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,54 +10,7 @@ import {Socket} from "phoenix"

let socket = new Socket("/socket", {params: {token: window.userToken}})

// When you connect, you'll often need to authenticate the client.
// For example, imagine you have an authentication plug, `MyAuth`,
// which authenticates the session and assigns a `:current_user`.
// If the current user exists you can assign the user's token in
// the connection for use in the layout.
//
// In your "lib/web/router.ex":
//
// pipeline :browser do
// ...
// plug MyAuth
// plug :put_user_token
// end
//
// defp put_user_token(conn, _) do
// if current_user = conn.assigns[:current_user] do
// token = Phoenix.Token.sign(conn, "user socket", current_user.id)
// assign(conn, :user_token, token)
// else
// conn
// end
// end
//
// Now you need to pass this token to JavaScript. You can do so
// inside a script tag in "lib/web/templates/layout/app.html.eex":
//
// <script>window.userToken = "<%= assigns[:user_token] %>";</script>
//
// You will need to verify the user token in the "connect/3" function
// in "lib/web/channels/user_socket.ex":
//
// def connect(%{"token" => token}, socket, _connect_info) do
// # max_age: 1209600 is equivalent to two weeks in seconds
// case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
// {:ok, user_id} ->
// {:ok, assign(socket, :user, user_id)}
// {:error, reason} ->
// :error
// end
// end
//
// Finally, connect to the socket:
// Connect to the socket:
socket.connect()

// Now that you are connected, you can join channels with a topic:
let channel = socket.channel("topic:subtopic", {})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })

export default socket
31 changes: 31 additions & 0 deletions lib/hits_web/channels/hit_channel.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
defmodule HitsWeb.HitChannel do
use HitsWeb, :channel

def join("hit:lobby", payload, socket) do
if authorized?(payload) do
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end

# Channels can be used in a request/response fashion
# by sending replies to requests from the client
def handle_in("ping", payload, socket) do
{:reply, {:ok, payload}, socket}
end

# It is also common to receive messages from the client and
# broadcast to everyone in the current topic (hit:lobby).
def handle_in("shout", payload, socket) do
broadcast socket, "shout", payload
{:noreply, socket}
end

# Add authorization logic here as required.
defp authorized?(_payload) do
true
end


end
2 changes: 1 addition & 1 deletion lib/hits_web/channels/user_socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule HitsWeb.UserSocket do

## Channels
# channel "room:*", HitsWeb.RoomChannel

channel "hit:lobby", HitsWeb.HitChannel
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into
Expand Down
16 changes: 14 additions & 2 deletions lib/hits_web/controllers/hit_controller.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
defmodule HitsWeb.HitController do
use HitsWeb, :controller
# use Phoenix.Channel
# import Ecto.Query
alias Hits.{Hit, Repository, User, Useragent}

Expand Down Expand Up @@ -30,21 +31,32 @@ defmodule HitsWeb.HitController do

# remote_ip comes in as a Tuple {192, 168, 1, 42} >> 192.168.1.42 (dot quad)
ip = Enum.join(Tuple.to_list(conn.remote_ip), ".")
# TODO: perform IP Geolocation lookup here so we can insert lat/lon for map!

# insert the useragent:
useragent_id = Useragent.insert(%Useragent{name: useragent, ip: ip})

# extract GitHub username from params so it can be saved & sent via channel:
username = params["user"]
# insert the user:
user_id = User.insert(%User{name: params["user"]})
user_id = User.insert(%User{name: username})

# strip ".svg" from repo name and insert:
repository = params["repository"] |> String.split(".svg") |> List.first()

repository_attrs = %Repository{name: repository, user_id: user_id}
repository_id = Repository.insert(repository_attrs)

# insert the hit record:
hit_attrs = %Hit{repo_id: repository_id, useragent_id: useragent_id}
Hit.insert(hit_attrs)
count = Hit.insert(hit_attrs)

# Send hit to connected clients via channel github.com/dwyl/hits/issues/79
HitsWeb.Endpoint.broadcast("hit:lobby", "hit",
%{"user" => username, "repo" => repository, "count" => count})

# return the count for the badge:
count
end

@doc """
Expand Down
2 changes: 1 addition & 1 deletion lib/hits_web/templates/page/index.html.eex
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
</p>

<h2 class="mt0 fw5 tc f4 bg-teal white pa2 mt5"><em>Recently</em> Viewed Projects (<em>tracked by Hits</em>)</h2>
<div class="h5 pl2" id='hits'>
<div class="h5 pl2" id="hits">
<div style="display:none">Dummy Child Node for insertBefore to work</div>
</div>

Expand Down
26 changes: 26 additions & 0 deletions test/hits_web/channels/hit_channel_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule HitsWeb.HitChannelTest do
use HitsWeb.ChannelCase

setup do
{:ok, _, socket} =
socket(HitsWeb.UserSocket, "user_id", %{some: :assign})
|> subscribe_and_join(HitsWeb.HitChannel, "hit:lobby")

{:ok, socket: socket}
end

test "ping replies with status ok", %{socket: socket} do
ref = push socket, "ping", %{"hello" => "there"}
assert_reply ref, :ok, %{"hello" => "there"}
end

test "shout broadcasts to hit:lobby", %{socket: socket} do
push socket, "shout", %{"hello" => "all"}
assert_broadcast "shout", %{"hello" => "all"}
end

test "broadcasts are pushed to the client", %{socket: socket} do
broadcast_from! socket, "broadcast", %{"some" => "data"}
assert_push "broadcast", %{"some" => "data"}
end
end