diff options
Diffstat (limited to 'shardweb/lib')
18 files changed, 536 insertions, 0 deletions
diff --git a/shardweb/lib/application.ex b/shardweb/lib/application.ex new file mode 100644 index 0000000..2de7bff --- /dev/null +++ b/shardweb/lib/application.ex @@ -0,0 +1,29 @@ +defmodule Shardweb.Application do + use Application + + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + def start(_type, _args) do + import Supervisor.Spec + + # Define workers and child supervisors to be supervised + children = [ + # Start the endpoint when the application starts + supervisor(ShardWeb.Endpoint, []), + # Start your own worker by calling: Shardweb.Worker.start_link(arg1, arg2, arg3) + # worker(Shardweb.Worker, [arg1, arg2, arg3]), + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: Shardweb.Supervisor] + Supervisor.start_link(children, opts) + end + + # Tell Phoenix to update the endpoint configuration + # whenever the application is updated. + def config_change(changed, _new, removed) do + ShardWeb.Endpoint.config_change(changed, removed) + :ok + end +end diff --git a/shardweb/lib/shard_web.ex b/shardweb/lib/shard_web.ex new file mode 100644 index 0000000..5192806 --- /dev/null +++ b/shardweb/lib/shard_web.ex @@ -0,0 +1,67 @@ +defmodule ShardWeb do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, views, channels and so on. + + This can be used in your application as: + + use ShardWeb, :controller + use ShardWeb, :view + + The definitions below will be executed for every view, + controller, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define any helper function in modules + and import those modules here. + """ + + def controller do + quote do + use Phoenix.Controller, namespace: ShardWeb + import Plug.Conn + import ShardWeb.Router.Helpers + import ShardWeb.Gettext + end + end + + def view do + quote do + use Phoenix.View, root: "lib/shard_web/templates", + namespace: ShardWeb + + # Import convenience functions from controllers + import Phoenix.Controller, only: [get_flash: 2, view_module: 1] + + # Use all HTML functionality (forms, tags, etc) + use Phoenix.HTML + + import ShardWeb.Router.Helpers + import ShardWeb.ErrorHelpers + import ShardWeb.Gettext + end + end + + def router do + quote do + use Phoenix.Router + import Plug.Conn + import Phoenix.Controller + end + end + + def channel do + quote do + use Phoenix.Channel + import ShardWeb.Gettext + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/shardweb/lib/shard_web/channels/room_channel.ex b/shardweb/lib/shard_web/channels/room_channel.ex new file mode 100644 index 0000000..2dd733b --- /dev/null +++ b/shardweb/lib/shard_web/channels/room_channel.ex @@ -0,0 +1,68 @@ +defmodule ShardWeb.RoomChannel do + use ShardWeb, :channel + + require Logger + + def join("room:" <> room_name, payload, socket) do + if authorized?(payload) do + + list = for {_chid, manifest, chpid} <- :ets.tab2list(:shard_db), + {:chat, chan} = manifest, + do: {chan, chpid} + pid = case List.keyfind(list, room_name, 0) do + nil -> + {:ok, pid} = DynamicSupervisor.start_child(Shard.DynamicSupervisor, {SApp.Chat, room_name}) + pid + {_, pid} -> + pid + end + socket = assign(socket, :pid, pid) + + GenServer.cast(pid, {:subscribe, self()}) + send(self(), :after_join) + + {:ok, socket} + else + {:error, %{reason: "unauthorized"}} + end + end + + def handle_info(:after_join, socket) do + GenServer.call(socket.assigns.pid, {:read_history, nil, 100}) + |> Enum.each(fn {{_ts, nick, msg}, true} -> push(socket, "shout", %{ + name: nick, + message: msg, + }) end) + {:noreply, socket} + end + + def handle_info({:chat_recv, _chan, {_ts, from, msg}}, socket) do + Logger.info("#{inspect self()} :chat_recv #{inspect msg}") + push socket, "shout", %{"name" => from, "message" => msg} + {:noreply, socket} + end + + def handle_info({:chat_send, _, _}, socket) do + {:noreply, socket} + 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 (room:lobby). + def handle_in("shout", payload, socket) do + broadcast socket, "shout", payload + Shard.Identity.set_nickname(payload["name"]) + GenServer.cast(socket.assigns.pid, {:chat_send, payload["message"]}) + {:noreply, socket} + end + + # Add authorization logic here as required. + defp authorized?(_payload) do + true + end +end diff --git a/shardweb/lib/shard_web/channels/user_socket.ex b/shardweb/lib/shard_web/channels/user_socket.ex new file mode 100644 index 0000000..4fb76dc --- /dev/null +++ b/shardweb/lib/shard_web/channels/user_socket.ex @@ -0,0 +1,37 @@ +defmodule ShardWeb.UserSocket do + use Phoenix.Socket + + ## Channels + channel "room:*", ShardWeb.RoomChannel + + ## Transports + transport :websocket, Phoenix.Transports.WebSocket + # transport :longpoll, Phoenix.Transports.LongPoll + + # 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 + # the socket that will be set for all channels, ie + # + # {:ok, assign(socket, :user_id, verified_user_id)} + # + # To deny connection, return `:error`. + # + # See `Phoenix.Token` documentation for examples in + # performing token verification on connect. + def connect(_params, socket) do + {:ok, socket} + end + + # Socket id's are topics that allow you to identify all sockets for a given user: + # + # def id(socket), do: "user_socket:#{socket.assigns.user_id}" + # + # Would allow you to broadcast a "disconnect" event and terminate + # all active sockets and channels for a given user: + # + # ShardWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) + # + # Returning `nil` makes this socket anonymous. + def id(_socket), do: nil +end diff --git a/shardweb/lib/shard_web/controllers/page_controller.ex b/shardweb/lib/shard_web/controllers/page_controller.ex new file mode 100644 index 0000000..a590630 --- /dev/null +++ b/shardweb/lib/shard_web/controllers/page_controller.ex @@ -0,0 +1,7 @@ +defmodule ShardWeb.PageController do + use ShardWeb, :controller + + def index(conn, _params) do + render conn, "index.html" + end +end diff --git a/shardweb/lib/shard_web/controllers/peer_controller.ex b/shardweb/lib/shard_web/controllers/peer_controller.ex new file mode 100644 index 0000000..0bf6ded --- /dev/null +++ b/shardweb/lib/shard_web/controllers/peer_controller.ex @@ -0,0 +1,25 @@ +defmodule ShardWeb.PeerController do + use ShardWeb, :controller + + require Logger + + def add(conn, _params) do + try do + ip = conn.params["ip"] + port = conn.params["port"] + {:ok, ip_tuple} = case :inet.parse_address(to_charlist(ip)) do + {:ok, tup} -> {:ok, tup} + _ -> + case :inet.gethostbyname(to_charlist(ip)) do + {:ok, {:hostent, _, _, :inet, 4, [ip_tup | _]}} -> {:ok, ip_tup} + _ -> :error + end + end + {port_num, _} = Integer.parse port + Shard.Manager.add_peer(ip_tuple, port_num) + rescue + _ -> nil + end + redirect conn, to: page_path(conn, :index) + end +end diff --git a/shardweb/lib/shard_web/controllers/room_controller.ex b/shardweb/lib/shard_web/controllers/room_controller.ex new file mode 100644 index 0000000..4d9adb4 --- /dev/null +++ b/shardweb/lib/shard_web/controllers/room_controller.ex @@ -0,0 +1,12 @@ +defmodule ShardWeb.RoomController do + use ShardWeb, :controller + + import PhoenixGon.Controller + + def show(conn, %{"room" => room}) do + conn = put_gon(conn, chat_room: room) + render conn, "show.html", + room: room, + name: Shard.Identity.get_nickname + end +end diff --git a/shardweb/lib/shard_web/endpoint.ex b/shardweb/lib/shard_web/endpoint.ex new file mode 100644 index 0000000..6883493 --- /dev/null +++ b/shardweb/lib/shard_web/endpoint.ex @@ -0,0 +1,59 @@ +defmodule ShardWeb.Endpoint do + use Phoenix.Endpoint, otp_app: :shardweb + + socket "/socket", ShardWeb.UserSocket + + # Serve at "/" the static files from "priv/static" directory. + # + # You should set gzip to true if you are running phoenix.digest + # when deploying your static files in production. + plug Plug.Static, + at: "/", from: :shardweb, gzip: false, + only: ~w(css fonts images js favicon.ico robots.txt) + + # Code reloading can be explicitly enabled under the + # :code_reloader configuration of your endpoint. + if code_reloading? do + socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket + plug Phoenix.LiveReloader + plug Phoenix.CodeReloader + end + + plug Plug.Logger + + plug Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Poison + + plug Plug.MethodOverride + plug Plug.Head + + # The session will be stored in the cookie and signed, + # this means its contents can be read but not tampered with. + # Set :encryption_salt if you would also like to encrypt it. + plug Plug.Session, + store: :cookie, + key: "_shardweb_key", + signing_salt: "BkoHycu8" + + + plug PhoenixGon.Pipeline + + plug ShardWeb.Router + + @doc """ + Callback invoked for dynamically configuring the endpoint. + + It receives the endpoint configuration and checks if + configuration should be loaded from the system environment. + """ + def init(_key, config) do + if config[:load_from_system_env] do + port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" + {:ok, Keyword.put(config, :http, [:inet6, port: port])} + else + {:ok, config} + end + end +end diff --git a/shardweb/lib/shard_web/gettext.ex b/shardweb/lib/shard_web/gettext.ex new file mode 100644 index 0000000..e74cd43 --- /dev/null +++ b/shardweb/lib/shard_web/gettext.ex @@ -0,0 +1,24 @@ +defmodule ShardWeb.Gettext do + @moduledoc """ + A module providing Internationalization with a gettext-based API. + + By using [Gettext](https://hexdocs.pm/gettext), + your module gains a set of macros for translations, for example: + + import ShardWeb.Gettext + + # Simple translation + gettext "Here is the string to translate" + + # Plural translation + ngettext "Here is the string to translate", + "Here are the strings to translate", + 3 + + # Domain-based translation + dgettext "errors", "Here is the error message to translate" + + See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage. + """ + use Gettext, otp_app: :shardweb +end diff --git a/shardweb/lib/shard_web/router.ex b/shardweb/lib/shard_web/router.ex new file mode 100644 index 0000000..180ebdd --- /dev/null +++ b/shardweb/lib/shard_web/router.ex @@ -0,0 +1,30 @@ +defmodule ShardWeb.Router do + use ShardWeb, :router + + pipeline :browser do + plug :accepts, ["html"] + plug :fetch_session + plug :fetch_flash + plug :protect_from_forgery + plug :put_secure_browser_headers + plug Plug.Parsers, parsers: [:urlencoded] + end + + pipeline :api do + plug :accepts, ["json"] + end + + scope "/", ShardWeb do + pipe_through :browser # Use the default browser stack + + get "/", PageController, :index + get "/room/:room", RoomController, :show + + post "/peer/add", PeerController, :add + end + + # Other scopes may use custom stacks. + # scope "/api", ShardWeb do + # pipe_through :api + # end +end diff --git a/shardweb/lib/shard_web/templates/layout/app.html.eex b/shardweb/lib/shard_web/templates/layout/app.html.eex new file mode 100644 index 0000000..9903775 --- /dev/null +++ b/shardweb/lib/shard_web/templates/layout/app.html.eex @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + <meta name="description" content=""> + <meta name="author" content=""> + + <title>Shard.</title> + <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>"> + </head> + + <body> + <div class="container"> + <header class="header"> + <nav role="navigation"> + <ul class="nav nav-pills pull-right"> + <li><a href="<%= page_path(@conn, :index) %>">Home</a></li> + <li><a href="<%= room_path(@conn, :show, "lobby") %>">Chat</a></li> + </ul> + </nav> + <span class="logo"></span> + </header> + + <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p> + <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p> + + + <main role="main"> + <%= render @view_module, @view_template, assigns %> + </main> + + </div> <!-- /container --> + <%= render_gon_script(@conn) %> + <script src="<%= static_path(@conn, "/js/app.js") %>"></script> + </body> +</html> diff --git a/shardweb/lib/shard_web/templates/page/index.html.eex b/shardweb/lib/shard_web/templates/page/index.html.eex new file mode 100644 index 0000000..412cbe5 --- /dev/null +++ b/shardweb/lib/shard_web/templates/page/index.html.eex @@ -0,0 +1,29 @@ +<h4>Peer list</h4> + +<table class="table table-striped"> + <tr> + <th>Peer ID</th> + <th>Address</th> + <th>Port</th> + </tr> + <%= for {id, pid, ip, port} <- peer_list() do %> + <tr> + <td> + <%= if pid == nil do %> + <%= peer_id_to_str(id) %> + <% else %> + <strong><%= peer_id_to_str(id) %></strong> + <% end %> + </td> + <td><%= :inet_parse.ntoa(ip) %></td> + <td><%= port %></td> + </tr> + <% end %> +</table> + +<%= form_for @conn, peer_path(@conn, :add), [class: "form-inline"], fn f -> %> + <%= text_input f, :ip, [class: "form-control", placeholder: "Hostname / IP address"] %> + <%= text_input f, :port, [class: "form-control", placeholder: "Port", value: "4044"] %> + <%= submit "Add peer", [class: "btn btn-default"] %> +<% end %> + diff --git a/shardweb/lib/shard_web/templates/room/show.html.eex b/shardweb/lib/shard_web/templates/room/show.html.eex new file mode 100644 index 0000000..a689017 --- /dev/null +++ b/shardweb/lib/shard_web/templates/room/show.html.eex @@ -0,0 +1,25 @@ + <ul class="nav nav-tabs"> + <%= for shard <- shard_list() do %> + <%= case shard do %> + <%= {_, {:chat, name}, _} -> %> + <li class="<%= if name == @room do "active" else "" end %>"> + <a href="<%= room_path(@conn, :show, name) %>">#<%= name %></a> + </li> + <% end %> + <% end %> + <li> + <a href="#" onclick="if(new_room=prompt('Enter name of room to join, without preceding # sign:'))window.location.href='/room/'+new_room;">Join room</a> + </li> + </ul> + +<!-- The list of messages will appear here: --> +<ul id='msg-list' class='row' style='list-style: none; min-height:400px; padding: 10px; max-height: 400px; overflow: scroll'></ul> + +<div class="row"> + <div class="col-xs-3"> + <input type="text" value="<%= @name %>" id="name" class="form-control" placeholder="Your Name"> + </div> + <div class="col-xs-9"> + <input type="text" id="msg" class="form-control" placeholder="Your Message" autofocus> + </div> +</div> diff --git a/shardweb/lib/shard_web/views/error_helpers.ex b/shardweb/lib/shard_web/views/error_helpers.ex new file mode 100644 index 0000000..f476548 --- /dev/null +++ b/shardweb/lib/shard_web/views/error_helpers.ex @@ -0,0 +1,44 @@ +defmodule ShardWeb.ErrorHelpers do + @moduledoc """ + Conveniences for translating and building error messages. + """ + + use Phoenix.HTML + + @doc """ + Generates tag for inlined form input errors. + """ + def error_tag(form, field) do + Enum.map(Keyword.get_values(form.errors, field), fn (error) -> + content_tag :span, translate_error(error), class: "help-block" + end) + end + + @doc """ + Translates an error message using gettext. + """ + def translate_error({msg, opts}) do + # When using gettext, we typically pass the strings we want + # to translate as a static argument: + # + # # Translate "is invalid" in the "errors" domain + # dgettext "errors", "is invalid" + # + # # Translate the number of files with plural rules + # dngettext "errors", "1 file", "%{count} files", count + # + # Because the error messages we show in our forms and APIs + # are defined inside Ecto, we need to translate them dynamically. + # This requires us to call the Gettext module passing our gettext + # backend as first argument. + # + # Note we use the "errors" domain, which means translations + # should be written to the errors.po file. The :count option is + # set by Ecto and indicates we should also apply plural rules. + if count = opts[:count] do + Gettext.dngettext(ShardWeb.Gettext, "errors", msg, msg, count, opts) + else + Gettext.dgettext(ShardWeb.Gettext, "errors", msg, opts) + end + end +end diff --git a/shardweb/lib/shard_web/views/error_view.ex b/shardweb/lib/shard_web/views/error_view.ex new file mode 100644 index 0000000..a4b6eed --- /dev/null +++ b/shardweb/lib/shard_web/views/error_view.ex @@ -0,0 +1,16 @@ +defmodule ShardWeb.ErrorView do + use ShardWeb, :view + + # If you want to customize a particular status code + # for a certain format, you may uncomment below. + # def render("500.html", _assigns) do + # "Internal Server Error" + # end + + # By default, Phoenix returns the status message from + # the template name. For example, "404.html" becomes + # "Not Found". + def template_not_found(template, _assigns) do + Phoenix.Controller.status_message_from_template(template) + end +end diff --git a/shardweb/lib/shard_web/views/layout_view.ex b/shardweb/lib/shard_web/views/layout_view.ex new file mode 100644 index 0000000..514779b --- /dev/null +++ b/shardweb/lib/shard_web/views/layout_view.ex @@ -0,0 +1,5 @@ +defmodule ShardWeb.LayoutView do + use ShardWeb, :view + + import PhoenixGon.View +end diff --git a/shardweb/lib/shard_web/views/page_view.ex b/shardweb/lib/shard_web/views/page_view.ex new file mode 100644 index 0000000..8d39191 --- /dev/null +++ b/shardweb/lib/shard_web/views/page_view.ex @@ -0,0 +1,14 @@ +defmodule ShardWeb.PageView do + use ShardWeb, :view + + def peer_list do + :ets.tab2list(:peer_db) + end + + def peer_id_to_str(id) do + id + |> binary_part(0, 8) + |> Base.encode16 + |> String.downcase + end +end diff --git a/shardweb/lib/shard_web/views/room_view.ex b/shardweb/lib/shard_web/views/room_view.ex new file mode 100644 index 0000000..b2d7ebe --- /dev/null +++ b/shardweb/lib/shard_web/views/room_view.ex @@ -0,0 +1,7 @@ +defmodule ShardWeb.RoomView do + use ShardWeb, :view + + def shard_list do + :ets.tab2list(:shard_db) + end +end |