Initial commit

Signed-off-by: Alek Ratzloff <alekratz@gmail.com>
This commit is contained in:
2019-10-09 08:59:53 -04:00
commit fe85447557
62 changed files with 7907 additions and 0 deletions

9
lib/anonbb.ex Normal file
View File

@@ -0,0 +1,9 @@
defmodule Anonbb do
@moduledoc """
Anonbb keeps the contexts that define your domain
and business logic.
Contexts are also responsible for managing your data, regardless
if it comes from the database, an external API or others.
"""
end

31
lib/anonbb/application.ex Normal file
View File

@@ -0,0 +1,31 @@
defmodule Anonbb.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
def start(_type, _args) do
# List all child processes to be supervised
children = [
# Start the Ecto repository
Anonbb.Repo,
# Start the endpoint when the application starts
AnonbbWeb.Endpoint
# Starts a worker by calling: Anonbb.Worker.start_link(arg)
# {Anonbb.Worker, arg},
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Anonbb.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
AnonbbWeb.Endpoint.config_change(changed, removed)
:ok
end
end

24
lib/anonbb/message.ex Normal file
View File

@@ -0,0 +1,24 @@
defmodule Anonbb.Message do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
schema "messages" do
field :text, :string
field :room_id, :id
timestamps()
end
@doc false
def changeset(message, attrs) do
message
|> cast(attrs, [:text])
|> validate_required([:text])
end
def get_messages(count \\ 10) do
query = from m in Anonbb.Message, limit: ^count
Anonbb.Repo.all(query |> reverse_order)
end
end

5
lib/anonbb/repo.ex Normal file
View File

@@ -0,0 +1,5 @@
defmodule Anonbb.Repo do
use Ecto.Repo,
otp_app: :anonbb,
adapter: Ecto.Adapters.Postgres
end

18
lib/anonbb/room.ex Normal file
View File

@@ -0,0 +1,18 @@
defmodule Anonbb.Room do
use Ecto.Schema
import Ecto.Changeset
schema "rooms" do
field :name, :string
timestamps()
end
@doc false
def changeset(room, attrs) do
room
|> cast(attrs, [:name])
|> validate_required([:name])
|> validate_format(:name, ~r/[^[:space:]\/]+/u)
end
end

69
lib/anonbb_web.ex Normal file
View File

@@ -0,0 +1,69 @@
defmodule AnonbbWeb 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 AnonbbWeb, :controller
use AnonbbWeb, :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: AnonbbWeb
import Plug.Conn
import AnonbbWeb.Gettext
alias AnonbbWeb.Router.Helpers, as: Routes
end
end
def view do
quote do
use Phoenix.View,
root: "lib/anonbb_web/templates",
namespace: AnonbbWeb
# Import convenience functions from controllers
import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
import AnonbbWeb.ErrorHelpers
import AnonbbWeb.Gettext
alias AnonbbWeb.Router.Helpers, as: Routes
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 AnonbbWeb.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

View File

@@ -0,0 +1,29 @@
defmodule AnonbbWeb.RoomChannel do
use Phoenix.Channel
alias Anonbb.{Repo, Room, Message}
import Ecto.Query
def join("room:" <> room, _message, socket) do
# Make sure the room exists
room_name = Repo.one(from r in Room, where: r.name == ^room, select: r.name)
if !room_name do
Repo.insert(%Room{name: room})
room_name = room
end
{:ok, socket
|> assign(:room, room_name)}
end
def handle_in("new_msg", %{"body" => body}, socket) do
room = socket.assigns.room
body = String.slice(body, 0..1000)
insert_message(room, body)
broadcast!(socket, "new_msg", %{body: body})
{:noreply, socket}
end
defp insert_message(room_name, message) do
room_id = Repo.one(from r in Room, where: r.name == ^room_name, select: r.id)
Repo.insert(%Message{text: message, room_id: room_id})
end
end

View File

@@ -0,0 +1,33 @@
defmodule AnonbbWeb.UserSocket do
use Phoenix.Socket
## Channels
channel "room:*", AnonbbWeb.RoomChannel
# 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, _connect_info) 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:
#
# AnonbbWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
#
# Returning `nil` makes this socket anonymous.
def id(_socket), do: nil
end

View File

@@ -0,0 +1,7 @@
defmodule AnonbbWeb.PageController do
use AnonbbWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
end

View File

@@ -0,0 +1,7 @@
defmodule AnonbbWeb.RoomController do
use AnonbbWeb, :controller
def show(conn, %{"name" => _name}) do
render(conn, "show.html", messages: Anonbb.Message.get_messages(20))
end
end

View File

@@ -0,0 +1,46 @@
defmodule AnonbbWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :anonbb
socket "/socket", AnonbbWeb.UserSocket,
websocket: true,
longpoll: false
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phx.digest
# when deploying your static files in production.
plug Plug.Static,
at: "/",
from: :anonbb,
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.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
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: "_anonbb_key",
signing_salt: "SyiBAed2"
plug AnonbbWeb.Router
end

24
lib/anonbb_web/gettext.ex Normal file
View File

@@ -0,0 +1,24 @@
defmodule AnonbbWeb.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 AnonbbWeb.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: :anonbb
end

27
lib/anonbb_web/router.ex Normal file
View File

@@ -0,0 +1,27 @@
defmodule AnonbbWeb.Router do
use AnonbbWeb, :router
pipeline :browser do
plug :accepts, ["html", "text/javascript"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", AnonbbWeb do
pipe_through :browser
get "/", PageController, :index
get "/room/:name", RoomController, :show
end
# Other scopes may use custom stacks.
# scope "/api", AnonbbWeb do
# pipe_through :api
# end
end

View File

@@ -0,0 +1,21 @@
<!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.0"/>
<title>Anonbb · Phoenix Framework</title>
<link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
</head>
<body>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/jquery-3.3.1.slim.min.js") %>" ></script>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/popper.min.js") %>" ></script>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/bootstrap.min.js") %>" ></script>
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
<main role="main" class="container">
<!--<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>-->
<!--<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>-->
<%= render @view_module, @view_template, assigns %>
</main>
</body>
</html>

View File

@@ -0,0 +1,26 @@
<div class="row">
<div class="col-md">
<h1 class="my-3">Here's-a some rooms</h1>
<p>No rooms!</p>
</div>
</div>
<div class="row">
<div class="col-md">
<h1 class="mb-3">Join-a a room</h1>
<div class="input-group">
<input type="text" class="form-control" id="joinRoomName" placeholder="Room name" />
<div class="input-group-append">
<button type="button" class="btn btn-primary" id="joinRoomButton" disabled>Join</button>
</div>
</div>
</div>
</div>
<!--
TODO input validation
<script type="text/javascript">
$(document).ready(() => {
});
</script>
-->

View File

@@ -0,0 +1,81 @@
<div class="row justify-content-md-center my-3">
<div class="col-8">
<textarea rows="5" class="form-control" id="messageInput" placeholder="Message..."></textarea>
<button type="button" class="btn btn-primary my-2" id="sendButton" disabled>Send</button>
</div>
</div>
<div class="row justify-content-md-center">
<div class="col-8">
<ul id="messageOutput">
<%= for m <- @messages do %>
<li id="<%= m.id %>">
<p><a href="#<% m.id%>">&gt;&gt;<%= m.id %></a> <%= m.inserted_at %></p>
<p><%= m.text %></p>
</li>
<% end %>
</ul>
</div>
</div>
<script type="text/javascript">
let input;
$(document).ready(() => {
const NEW_MSG = "new_msg";
let room = window.room;
let socket = window.socket;
let channel = socket.channel("room:" + room, {});
channel.join()
.receive("ok", resp => console.log("Joined successfully", resp))
.receive("error", resp => console.log("Could not join", resp));
input = $("#messageInput")[0];
let output = $("#messageOutput");
let sendbutton = $("#sendButton")[0];
function sendMessage() {
try {
let message_text = input.value;
// empty and space-only messages not allowed
if (message_text.match(/^[\s]*$/m)) {
return;
}
try {
input.disabled = true;
channel.push(NEW_MSG, {body: message_text});
} finally {
input.value = "";
}
} finally {
input.disabled = false;
}
}
channel.on(NEW_MSG, payload => {
console.log(payload);
let body = payload.body;
let now = new Date();
let messageItem = $("<li>").html(
`<p>${now.toISOString()}</p><p>${body}</p>`
);
output.prepend(messageItem);
});
input.onkeydown = (e => {
if (e.keyCode == 13 && e.shiftKey) {
sendMessage();
}
sendButton.disabled = input.value === "";
});
input.onkeyup = (e => {
if (e.keyCode == 13 && e.shiftKey) {
input.value = "";
input.disabled = false;
}
});
$("#messageInput").attr("placeholder", "Message to :" + room);
});
</script>

View File

@@ -0,0 +1,44 @@
defmodule AnonbbWeb.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(AnonbbWeb.Gettext, "errors", msg, msg, count, opts)
else
Gettext.dgettext(AnonbbWeb.Gettext, "errors", msg, opts)
end
end
end

View File

@@ -0,0 +1,16 @@
defmodule AnonbbWeb.ErrorView do
use AnonbbWeb, :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

View File

@@ -0,0 +1,3 @@
defmodule AnonbbWeb.LayoutView do
use AnonbbWeb, :view
end

View File

@@ -0,0 +1,3 @@
defmodule AnonbbWeb.PageView do
use AnonbbWeb, :view
end

View File

@@ -0,0 +1,3 @@
defmodule AnonbbWeb.RoomView do
use AnonbbWeb, :view
end