29
lib/anonbb_web/channels/room_channel.ex
Normal file
29
lib/anonbb_web/channels/room_channel.ex
Normal 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
|
||||
33
lib/anonbb_web/channels/user_socket.ex
Normal file
33
lib/anonbb_web/channels/user_socket.ex
Normal 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
|
||||
7
lib/anonbb_web/controllers/page_controller.ex
Normal file
7
lib/anonbb_web/controllers/page_controller.ex
Normal file
@@ -0,0 +1,7 @@
|
||||
defmodule AnonbbWeb.PageController do
|
||||
use AnonbbWeb, :controller
|
||||
|
||||
def index(conn, _params) do
|
||||
render(conn, "index.html")
|
||||
end
|
||||
end
|
||||
7
lib/anonbb_web/controllers/room_controller.ex
Normal file
7
lib/anonbb_web/controllers/room_controller.ex
Normal 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
|
||||
46
lib/anonbb_web/endpoint.ex
Normal file
46
lib/anonbb_web/endpoint.ex
Normal 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
24
lib/anonbb_web/gettext.ex
Normal 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
27
lib/anonbb_web/router.ex
Normal 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
|
||||
21
lib/anonbb_web/templates/layout/app.html.eex
Normal file
21
lib/anonbb_web/templates/layout/app.html.eex
Normal 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>
|
||||
26
lib/anonbb_web/templates/page/index.html.eex
Normal file
26
lib/anonbb_web/templates/page/index.html.eex
Normal 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>
|
||||
-->
|
||||
81
lib/anonbb_web/templates/room/show.html.eex
Normal file
81
lib/anonbb_web/templates/room/show.html.eex
Normal 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%>">>><%= 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>
|
||||
44
lib/anonbb_web/views/error_helpers.ex
Normal file
44
lib/anonbb_web/views/error_helpers.ex
Normal 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
|
||||
16
lib/anonbb_web/views/error_view.ex
Normal file
16
lib/anonbb_web/views/error_view.ex
Normal 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
|
||||
3
lib/anonbb_web/views/layout_view.ex
Normal file
3
lib/anonbb_web/views/layout_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule AnonbbWeb.LayoutView do
|
||||
use AnonbbWeb, :view
|
||||
end
|
||||
3
lib/anonbb_web/views/page_view.ex
Normal file
3
lib/anonbb_web/views/page_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule AnonbbWeb.PageView do
|
||||
use AnonbbWeb, :view
|
||||
end
|
||||
3
lib/anonbb_web/views/room_view.ex
Normal file
3
lib/anonbb_web/views/room_view.ex
Normal file
@@ -0,0 +1,3 @@
|
||||
defmodule AnonbbWeb.RoomView do
|
||||
use AnonbbWeb, :view
|
||||
end
|
||||
Reference in New Issue
Block a user