aboutsummaryrefslogtreecommitdiff
path: root/shardweb/lib
diff options
context:
space:
mode:
authorAlex Auvolat <alex@adnab.me>2018-09-01 16:07:22 +0200
committerAlex Auvolat <alex@adnab.me>2018-09-01 16:07:22 +0200
commit81b5a844a2a155e28c497a8ce671eb5f02803e5d (patch)
tree15e878ebe47d5993e7af46a48a1ccdc1dd1e4b2f /shardweb/lib
parentc6ec33d6e612168e14d77007915a4ea423c55a2e (diff)
downloadshard-81b5a844a2a155e28c497a8ce671eb5f02803e5d.tar.gz
shard-81b5a844a2a155e28c497a8ce671eb5f02803e5d.zip
Import shardweb0.0.2
Diffstat (limited to 'shardweb/lib')
-rw-r--r--shardweb/lib/application.ex29
-rw-r--r--shardweb/lib/shard_web.ex67
-rw-r--r--shardweb/lib/shard_web/channels/room_channel.ex68
-rw-r--r--shardweb/lib/shard_web/channels/user_socket.ex37
-rw-r--r--shardweb/lib/shard_web/controllers/page_controller.ex7
-rw-r--r--shardweb/lib/shard_web/controllers/peer_controller.ex25
-rw-r--r--shardweb/lib/shard_web/controllers/room_controller.ex12
-rw-r--r--shardweb/lib/shard_web/endpoint.ex59
-rw-r--r--shardweb/lib/shard_web/gettext.ex24
-rw-r--r--shardweb/lib/shard_web/router.ex30
-rw-r--r--shardweb/lib/shard_web/templates/layout/app.html.eex38
-rw-r--r--shardweb/lib/shard_web/templates/page/index.html.eex29
-rw-r--r--shardweb/lib/shard_web/templates/room/show.html.eex25
-rw-r--r--shardweb/lib/shard_web/views/error_helpers.ex44
-rw-r--r--shardweb/lib/shard_web/views/error_view.ex16
-rw-r--r--shardweb/lib/shard_web/views/layout_view.ex5
-rw-r--r--shardweb/lib/shard_web/views/page_view.ex14
-rw-r--r--shardweb/lib/shard_web/views/room_view.ex7
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