Upgrade to Pro — share decks privately, control downloads, hide ads and more …

MongooseIM + Phoenix

MongooseIM + Phoenix

Erlang Factory SF 2015 - Michal Slaski & Sonny Scroggin - MongooseIM + Phoenix

https://www.youtube.com/watch?v=1TpK4NDEEVA

Sonny Scroggin

March 26, 2015
Tweet

More Decks by Sonny Scroggin

Other Decks in Programming

Transcript

  1. • "Distributed Web Services" framework • MVC • Convention over

    Configuration • WebSockets • Realtime events • No productivity sacrifices for performance • No performance sacrifices for productivity
  2. Install λ git clone git://github.com/phoenixframework/phoenix Cloning into 'phoenix'... remote: Counting

    objects: 13259, done. remote: Compressing objects: 100% (92/92), done. remote: Total 13259 (delta 44), reused 0 (delta 0), pack-reused 13161 Receiving objects: 100% (13259/13259), 2.35 MiB | 1.12 MiB/s, done. Resolving deltas: 100% (8825/8825), done. Checking connectivity... done. λ cd phoenix λ mix do deps.get, compile, phoenix.new ../my_app
  3. Install * creating ../my_app/README.md * creating ../my_app/config/config.exs * creating ../my_app/lib/my_app.ex

    ... * creating ../my_app/lib/my_app/endpoint.ex * creating ../my_app/mix.exs * creating ../my_app/web/router.ex * creating ../my_app/lib/my_app/repo.ex Install mix dependencies? [Yn] * running mix deps.get Install brunch.io dependencies? [Yn] * running npm install We are all set! Run your Phoenix application: $ cd ../my_app $ mix phoenix.server
  4. Generators (Mix tasks) λ mix help | grep ecto mix

    ecto.create # Create the storage for the repo mix ecto.drop # Drop the storage for the repo mix ecto.gen.migration # Generate a new migration for the repo mix ecto.gen.repo # Generate a new repository mix ecto.migrate # Run migrations up on a repo mix ecto.rollback # Rollback migrations from a repo λ mix ecto.create The database for repo MyApp.Repo has been created. λ mix help | grep phoenix mix phoenix.gen.html # Generates HTML files for a resource mix phoenix.gen.model # Generates an Ecto model mix phoenix.new # Create a new Phoenix application mix phoenix.routes # Prints all routes mix phoenix.server # Starts applications and their servers
  5. Generators λ mix phoenix.gen.html Post posts title body:text author_id:integer *

    creating priv/repo/migrations/20150324194548_create_post.exs * creating web/models/post.ex * creating web/controllers/post_controller.ex * creating web/templates/post/edit.html.eex * creating web/templates/post/form.html.eex * creating web/templates/post/index.html.eex * creating web/templates/post/new.html.eex * creating web/templates/post/show.html.eex * creating web/views/post_view.ex Add the resource to the proper scope in web/router.ex: resources "/posts", PostController and then update your repository by running migrations: $ mix ecto.migrate
  6. Migrations defmodule MyApp.Repo.Migrations.CreatePost do use Ecto.Migration def change do create

    table(:posts) do add :title, :string add :body, :text add :author_id, :integer timestamps end end end
  7. Migrations λ mix ecto.migrate Compiled lib/my_app.ex Compiled web/models/post.ex Compiled web/router.ex

    Compiled web/views/error_view.ex Compiled web/controllers/page_controller.ex Compiled web/views/page_view.ex Compiled web/controllers/post_controller.ex Compiled web/views/layout_view.ex Compiled lib/my_app/endpoint.ex Compiled web/views/post_view.ex Generated my_app.app The database for repo MyApp.Repo has already been created. [info] == Running MyApp.Repo.Migrations.CreatePost.change/0 forward [info] create table posts [info] == Migrated in 0.1s
  8. Start the server λ mix phoenix.server [info] Running MyApp.Endpoint with

    Cowboy on port 4000 (http) 23 Mar 17:28:51 - info: compiled 3 files into 2 files in 241ms
  9. Models defmodule MyApp.Post do use MyApp.Web, :model schema "posts" do

    field :title, :string field :body, :string field :author_id, :integer timestamps end @required_fields ~w(title body author_id) @optional_fields ~w() def changeset(model, params \\ nil) do cast(model, params, @required_fields, @optional_fields) end end
  10. Router The router provides a set of macros for generating

    routes that dispatch to specific controllers and actions. Those macros are named after HTTP verbs.
  11. Router defmodule MyApp.Router do use Phoenix.Router pipeline :browser do plug

    :accepts, ~w(html) plug :fetch_session end socket "/ws", MyApp do channel "xmpp:*", XMPPChannel end scope "/", alias: MyApp do pipe_through :browser get "/", PageController, :index resources "/posts", PostController do resources "/comments", CommentController end end end
  12. Router resources "/posts", PostController do resources "/comments", CommentController end defp

    match(conn, "GET", ["posts"], _) defp match(conn, "GET", ["posts", id, "edit"], _) defp match(conn, "GET", ["posts", "new"], _) defp match(conn, "GET", ["posts", id], _) defp match(conn, "POST", ["posts"], _) defp match(conn, "PATCH", ["posts", id], _) defp match(conn, "PUT", ["posts", id], _) defp match(conn, "DELETE", ["posts", id], _) defp match(conn, "GET", ["posts", post_id, "comments"], _) defp match(conn, "GET", ["posts", post_id, "comments", id, "edit"], _) defp match(conn, "GET", ["posts", post_id, "comments", "new"], _) defp match(conn, "GET", ["posts", post_id, "comments", id], _) defp match(conn, "POST", ["posts", post_id, "comments"], _) defp match(conn, "PATCH", ["posts", post_id, "comments", id], _) defp match(conn, "PUT", ["posts", post_id, "comments", id], _) defp match(conn, "DELETE", ["posts", post_id, "comments", id], _) This Compiles to (showing only the function heads)
  13. Router get "/", PageController, :index defp match(conn, "GET", [], _)

    do conn |> Plug.Conn.put_private(:phoenix_pipelines, [:browser]) |> Plug.Conn.put_private(:phoenix_route, fn conn -> opts = MyApp.PageController.init(:index) MyApp.PageController.call(conn, opts) end) |> browser([]) end This Compiles to (showing full function definition)
  14. Router λ mix phoenix.routes page_path GET / MyApp.PageController.index/2 user_path GET

    /users MyApp.UserController.index/2 user_path GET /users/:id/edit MyApp.UserController.edit/2 user_path GET /users/new MyApp.UserController.new/2 user_path GET /users/:id MyApp.UserController.show/2 user_path POST /users MyApp.UserController.create/2 user_path PATCH /users/:id MyApp.UserController.update/2 PUT /users/:id MyApp.UserController.update/2 user_path DELETE /users/:id MyApp.UserController.delete/2
  15. Controllers defmodule MyApp.PageController do use MyApp.Web, :controller plug :find_page when

    action in [:show] plug :action def index(conn, _params) do render conn, "index.html" end def show(conn, _params) do render conn, "show.html" end defp find_page(conn, %{"page" => page}) do assign conn, :page, Repo.get(Page, page) end end
  16. Controllers def index(conn, _params) do users = Repo.all(User) render conn,

    "index.html", users: users end def show(conn, %{"id" => id}) do user = Repo.get(User, id) render conn, "show.html", user: user end GET /users GET /users/:id
  17. Controllers def new(conn, _params) do changeset = User.changeset(%User{}) render conn,

    "new.html", changeset: changeset end def create(conn, %{"user" => user_params}) do changeset = User.changeset(%User{}, user_params) if changeset.valid? do Repo.insert(changeset) conn |> put_flash(:info, "User created succesfully.") |> redirect(to: user_path(conn, :index)) else render conn, "new.html", changeset: changeset end end GET /users/new POST /users
  18. Controllers def edit(conn, %{"id" => id}) do user = Repo.get(User,

    id) changeset = User.changeset(user) render conn, "edit.html", user: user, changeset: changeset end def update(conn, %{"id" => id, "user" => user_params}) do user = Repo.get(User, id) changeset = User.changeset(user, user_params) if changeset.valid? do Repo.update(changeset) conn |> put_flash(:info, "User updated succesfully.") |> redirect(to: user_path(conn, :index)) else render conn, "edit.html", user: user, changeset: changeset end end GET /users/:id/edit PUT or PATCH /users/:id
  19. Controllers def delete(conn, %{"id" => id}) do user = Repo.get(User,

    id) Repo.delete(user) conn |> put_flash(:info, "User deleted succesfully.") |> redirect(to: user_path(conn, :index)) end DELETE /users/:id
  20. Views & Templates • Views render templates • Views serve

    as a presentation layer • Module hierarchy for shared context • Templates are precompiled into views • EEx & Haml engine support
  21. Views defmodule MyApp.UserView do use MyApp.Web, :view @attributes ~W(id name

    inserted_at) alias MyApp.User def name(%User{first_name: first_name, last_name: last_name}) do "#{first_name} #{last_name}" end def render("index.json", %{data: users}) do for user <- users, do: render("show.json", %{data: user}) end def render("show.json", %{data: user}) do user |> Map.take(@attributes) end end
  22. Templates <h2>New User</h2> <%= render "form.html", changeset: @changeset, action: user_path(@conn,

    :create) %> <%= link "Back", to: user_path(@conn, :index) %> web/templates/user/new.html.eex <h2>Edit User</h2> <%= render "form.html", changeset: @changeset, action: user_path(@conn, :update, @user) %> <%= link "Back", to: user_path(@conn, :index) %> web/templates/user/edit.html.eex
  23. Templates <%= form_for @changeset, @action, fn f -> %> <%=

    if @changeset.errors != [] do %> <div class="alert alert-danger"> <p>Oops, something went wrong! Please check the errors below:</p> <ul> <%= for {attr, message} <- @changeset.errors do %> <li><%= humanize(attr) %> <%= message %></li> <% end %> </ul> </div> <% end %> <%= text_input f, :name, placeholder: "Name" %> <%= text_input f, :login, placeholder: "Login" %> <%= text_input f, :email, placeholder: "Email" %> <%= submit "Submit" %> <% end %> web/templates/user/form.html.eex
  24. Channels Channels allow you to route pub/sub events to channel

    handlers in your application. By default, Phoenix supports both WebSocket and LongPoller transports. • WebSocket / PubSub Abstraction • Similar to Controllers, but bi-directional • Handle socket events and broadcast • phoenix.js - JavaScript client
  25. Channels defmodule MyApp.Router do use Phoenix.Router pipeline :browser do plug

    :accepts, ~w(html) plug :fetch_session end socket "/ws", MyApp do channel "xmpp:*", XMPPChannel end scope "/", alias: MyApp do pipe_through :browser get "/", PageController, :index resources "/posts", PostController do resources "/comments", CommentController end end end
  26. Channels import Channel from "./channel"; import {Socket} from "./phoenix"; class

    App { constructor(user) { this.user = user; this.socket = new Socket("/ws"); this.socket.connect(); this.channel = new Channel(this); } } $(() => { let user = JSON.parse($('meta[name="current-user"]').attr('content')); new App(user); }); web/static/js/app.js
  27. Channels import MessageHandler from "./handlers/message"; class Channel { constructor(app) {

    this.user = app.user; this.socket = app.socket; this.initHandlers(); return this; } initHandlers() { this.socket.join(`xmpp:${this.user.uuid}`, this.user, (channel) => { new MessageHandler(channel, this.user, new MessagesView()); }); } } export default Channel; web/static/js/channel.js
  28. Channels class MessageHandler { constructor(channel, user, view) { channel.on("new:message", (msg)

    => { if (msg.type === "groupchat") { if (msg.body != "") { view.collection.add(msg); } } }); } } export default MessageHandler; web/static/js/handlers/message.js
  29. Channels defmodule MyApp.XMPPChannel do use Phoenix.Channel def join("xmpp:" <> uuid,

    %{"uuid" => uuid} = user, socket) do client = find_or_start_client(user) socket = assign(socket, :client, client) |> assign(:user, user) {:ok, socket} end def join(_uuid, _data, socket) do {:error, :unauthorized, socket} end def leave(msg, socket) do pid = socket.assigns[:client] send(pid, {:stop, :normal}) {:ok, socket} end def handle_in("new:message", msg, socket) do send_stanza(socket, Stanza.message(msg)) end end
  30. Hedwig Hedwig is an XMPP client and bot framework written

    in Elixir. It allows you to build custom response handlers to fit your business logic. Simply configure Hedwig with credentials and custom handlers and you're set!
  31. Config use Mix.Config alias Hedwig.Handlers config :hedwig, clients: [ %{

    jid: "[email protected]", password: System.get_env("TEST_XMPP_PASS"), nickname: "scrogson", resource: "hedwig", rooms: [ "[email protected]" ], handlers: [ {Handlers.Help, %{}}, {Handlers.GreatSuccess, %{}}, {Handlers.Panzy, %{}} ] } ]
  32. Handlers defmodule Issues.XMPPHandler do use Hedwig.Handler alias Issues.Endpoint def handle_event(%Message{}

    = stanza, opts) do Endpoint.broadcast! "xmpp:#{opts.topic}", "new:message", stanza {:ok, opts} end def handle_event(%Presence{} = stanza, opts) do case stanza.type do "subscribe" -> stanza = %Presence{stanza | to: stanza.from, type: "subscribed"} Hedwig.Client.reply(stanza.client, stanza) _ -> Endpoint.broadcast! "xmpp:#{opts.topic}", "new:presence", stanza end {:ok, opts} end def handle_event(%IQ{type: "result"} = stanza, opts) do Endpoint.broadcast! "xmpp:#{opts.topic}", "iq:result", stanza {:ok, opts} end def handle_event(_, opts), do: {:ok, opts} end
  33. XML -> JSON defimpl Poison.Encoder, for: Hedwig.JID do def encode(%Hedwig.JID{user:

    user, server: server, resource: resource} = jid, opts) do %{raw: to_string(jid), user: user, server: server, resource: resource} |> Poison.Encoder.encode(opts) end end
  34. XML -> JSON defimpl Poison.Encoder, for: Hedwig.Stanzas.Message do def encode(%Hedwig.Stanzas.Message{}

    = stanza, opts) do %{name: "message", id: stanza.id, to: stanza.to, from: stanza.from, type: stanza.type, body: stanza.body, payload: stanza.payload} |> Poison.Encoder.encode(opts) end end
  35. XML -> JSON defimpl Poison.Encoder, for: Tuple do def encode({:xmlel,

    "message", attrs, children}, opts) do %{name: "message", attrs: attrs, payload: children} |> Poison.Encoder.encode(opts) end end
  36. XMPP Channel defmodule MyApp.XMPPChannel do use Phoenix.Channel def join("xmpp:" <>

    uuid, %{"uuid" => uuid} = user, socket) do client = find_or_start_client(user) socket = assign(socket, :client, client) |> assign(:user, user) {:ok, socket} end def join(_uuid, _data, socket) do {:error, :unauthorized, socket} end def leave(msg, socket) do pid = socket.assigns[:client] send(pid, {:stop, :normal}) {:ok, socket} end def handle_in("new:message", msg, socket) do send_stanza(socket, Stanza.message(msg)) end ...
  37. XMPP Channel defp find_or_start_client(user) do case Process.whereis(String.to_atom(User.jid(user))) do nil ->

    start_client(user) pid -> pid end end defp start_client(user) do {:ok, pid} = Hedwig.Client.start_link(client_spec(user)) pid end
  38. XMPP Channel defp client_spec(user) do %{ jid: User.jid(user), password: one_time_password,

    nickname: User.nickname(user), resource: "issues", config: %{ ignore_from_self?: false }, handlers: [ {Issues.XMPPHandler, %{topic: user["uuid"]}} ] } end defp one_time_password do :crypto.rand_bytes(16) |> Base.encode16(case: :lower) end