Use Ecto.Type if possible to provide new callbacks (#108)

Closes #107 and prevents other issues of its type through
`use Ecto.Type` if available to provide default implementations
of `c:embed_as/1`, `c:equal/1`, and anything else they add later
that's backward-compatible.

Removes compilation warning for Ecto 3.2 or above, but not necessary for
correct operation because `embed_as/2` checks before calling.

if Code.ensure_loaded?(Ecto) do
  defmodule Honeydew.EctoSource.ErlangTerm do
    @moduledoc false

    if macro_exported?(Ecto.Type, :__using__, 1) do
      use Ecto.Type
    else
      @behaviour Ecto.Type
    end

    @impl true
    def type, do: :binary

    @impl true
    def cast(term) do
      {:ok, term}
    end

    @impl true
    def load(binary) when is_binary(binary) do
      {:ok, :erlang.binary_to_term(binary)}
    end

    @impl true
    def dump(term) do
      {:ok, :erlang.term_to_binary(term)}
    end
  end
end

if Code.ensure_loaded?(Ecto) do
  defmodule Honeydew.EctoSource.ErlangTerm do
    @moduledoc false

    @behaviour Ecto.Type

    @impl true
    def type, do: :binary

    @impl true
    def cast(term) do
      {:ok, term}
    end

    @impl true
    def load(binary) when is_binary(binary) do
      {:ok, :erlang.binary_to_term(binary)}
    end

    @impl true
    def dump(term) do
      {:ok, :erlang.term_to_binary(term)}
    end
  end
end


Update controller_helpers.ex

Update controller_helpers.ex

Ensure expired? returns for nil data

defmodule Coherence.ControllerHelpers do
  @moduledoc """
  Common helper functions for Coherence Controllers.
  """
  alias Coherence.Config
  require Logger
  import Phoenix.Controller, only: [put_flash: 3, redirect: 2]
  import Plug.Conn, only: [halt: 1]
  alias Coherence.Schema.{Confirmable}
  @lockable_failure "Failed to update lockable attributes "

  @doc """
  Get the MyProject.Router.Helpers module.

  Returns the projects Router.Helpers module.
  """
  def router_helpers do
    Module.concat(Config.module, Router.Helpers)
  end

  @doc """
  Get the configured logged_out_url.
  """
  def logged_out_url(conn) do
    Config.logged_out_url || Module.concat(Config.module, Router.Helpers).session_path(conn, :new)
  end

  @doc """
  Get the configured logged_in_url.
  """
  def logged_in_url(_conn) do
    Config.logged_in_url || "/"
  end

  @doc """
  Get a random string of given length.

  Returns a random url safe encoded64 string of the given length.
  Used to generate tokens for the various modules that require unique tokens.
  """
  def random_string(length) do
    :crypto.strong_rand_bytes(length)
    |> Base.url_encode64
    |> binary_part(0, length)
  end

  @doc """
  Test if a datetime has expired.

  Convert the datetime from Ecto.DateTime format to Timex format to do
  the comparison given the time during in opts.

  ## Examples

      expired?(user.expire_at, days: 5)
      expired?(user.expire_at, minutes: 10)
  """
  def expired?(nil, _), do: true
  def expired?(datetime, opts) do
    expire_on? = datetime
    |> Ecto.DateTime.to_erl
    |> Timex.DateTime.from_erl
    |> Timex.shift(opts)
    not Timex.before?(Timex.DateTime.now, expire_on?)
  end

  @doc """
  Log an error message when lockable update fails.
  """
  def lockable_failure(changeset) do
    Logger.error @lockable_failure <> inspect(changeset.errors)
  end

  @doc """
  Send a user email.

  Sends a user email given the module, model, and url. Logs the email for
  debug purposes.

  Note: This function uses an apply to avoid compile warnings if the
  mailer is not selected as an option.
  """
  def send_user_email(fun, model, url) do
    email = apply(Module.concat(Config.module, Coherence.UserEmail), fun, [model, url])
    Logger.debug fn -> "#{fun} email: #{inspect email}" end
    apply(Module.concat(Config.module, Coherence.Mailer), :deliver, [email])
  end

  @doc """
  Send confirmation email with token.

  If the user supports confirmable, generate a token and send the email.
  """
  def send_confirmation(conn, user, user_schema) do
    if user_schema.confirmable? do
      token = random_string 48
      url = router_helpers.confirmation_url(conn, :edit, token)
      Logger.debug "confirmation email url: #{inspect url}"
      dt = Ecto.DateTime.utc
      user_schema.changeset(user,
        %{confirmation_token: token, confirmation_sent_at: dt})
      |> Config.repo.update!

      send_user_email :confirmation, user, url
      conn
      |> put_flash(:info, "Confirmation email sent.")
    else
      conn
      |> put_flash(:info, "Registration created successfully.")
    end
  end

  #############
  # User Schema

  @doc """
  Confirm a user account.

  Adds the `:confirmed_at` datetime field on the user model and updates the database
  """
  def confirm!(user) do
    changeset = Confirmable.confirm(user)
    unless Confirmable.confirmed? user do
      Config.repo.update changeset
    else
      changeset = Ecto.Changeset.add_error changeset, :confirmed_at, "already confirmed"
      {:error, changeset}
    end
  end

  @doc """
  Lock a use account.

  Sets the `:locked_at` field on the user model to the current date and time unless
  provided a value for the optional parameter.

  You can provide a date in the future to override the configured lock expiry time. You
  can set this data far in the future to do a pseudo permanent lock.
  """

  def lock!(user, locked_at \\ Ecto.DateTime.utc) do
    user_schema = Config.user_schema
    changeset = user_schema.lock user, locked_at
    unless user_schema.locked?(user) do
      changeset
      |> Config.repo.update
    else
      changeset = Ecto.Changeset.add_error changeset, :locked_at, "already locked"
      {:error, changeset}
    end
  end

  @doc """
  Unlock a user account.

  Clears the `:locked_at` field on the user model and updates the database.
  """
  def unlock!(user) do
    user_schema = Config.user_schema
    changeset = user_schema.unlock user
    if user_schema.locked?(user) do
      changeset
      |> Config.repo.update
    else
      changeset = Ecto.Changeset.add_error changeset, :locked_at, "not locked"
      {:error, changeset}
    end
  end

  @doc """
  Plug to redirect already logged in users.
  """
  def redirect_logged_in(conn, _params) do
    if Coherence.logged_in?(conn) do
      conn
      |> put_flash(:info, "Already logged in." )
      |> redirect(to: logged_in_url(conn))
      |> halt
    else
      conn
    end
  end

  def redirect_to(conn, path, params) do
    apply(Coherence.Redirects, path, [conn, params])
  end
  def redirect_to(conn, path, params, user) do
    apply(Coherence.Redirects, path, [conn, params, user])
  end

  def changeset(which, module, model, params \\ %{}) do
    {mod, fun, args} = case Application.get_env :coherence, :changeset do
      nil -> {module, :changeset, [model, params]}
      {mod, fun} -> {mod, fun, [model, params, which]}
    end
    apply mod, fun, args
  end

  @doc """
  Login a user.

  Logs in a user and redirects them to the session_create page.
  """
  def login_user(conn, user, params) do
     apply(Config.auth_module, Config.create_login, [conn, user, [id_key: Config.schema_key]])
     |> track_login(user, Config.user_schema.trackable?)
     |> redirect_to(:session_create, params)
  end

  @doc """
  Track user login details.

  Saves the ip address and timestamp when the user logs in.
  """
  def track_login(conn, _, false), do: conn
  def track_login(conn, user, true) do
    ip = conn.peer |> elem(0) |> inspect
    now = Ecto.DateTime.utc
    {last_at, last_ip} = cond do
      is_nil(user.last_sign_in_at) and is_nil(user.current_sign_in_at) ->
        {now, ip}
      !!user.current_sign_in_at ->
        {user.current_sign_in_at, user.current_sign_in_ip}
      true ->
        {user.last_sign_in_at, user.last_sign_in_ip}
    end

    changeset(:session, user.__struct__, user,
      %{
        sign_in_count: user.sign_in_count + 1,
        current_sign_in_at: Ecto.DateTime.utc,
        current_sign_in_ip: ip,
        last_sign_in_at: last_at,
        last_sign_in_ip: last_ip
      })
    |> Config.repo.update
    |> case do
      {:ok, _} -> nil
      {:error, _changeset} ->
        Logger.error ("Failed to update tracking!")
    end
    conn
  end

end

defmodule Coherence.ControllerHelpers do
  @moduledoc """
  Common helper functions for Coherence Controllers.
  """
  alias Coherence.Config
  require Logger
  import Phoenix.Controller, only: [put_flash: 3, redirect: 2]
  import Plug.Conn, only: [halt: 1]
  alias Coherence.Schema.{Confirmable}
  @lockable_failure "Failed to update lockable attributes "

  @doc """
  Get the MyProject.Router.Helpers module.

  Returns the projects Router.Helpers module.
  """
  def router_helpers do
    Module.concat(Config.module, Router.Helpers)
  end

  @doc """
  Get the configured logged_out_url.
  """
  def logged_out_url(conn) do
    Config.logged_out_url || Module.concat(Config.module, Router.Helpers).session_path(conn, :new)
  end

  @doc """
  Get the configured logged_in_url.
  """
  def logged_in_url(_conn) do
    Config.logged_in_url || "/"
  end

  @doc """
  Get a random string of given length.

  Returns a random url safe encoded64 string of the given length.
  Used to generate tokens for the various modules that require unique tokens.
  """
  def random_string(length) do
    :crypto.strong_rand_bytes(length)
    |> Base.url_encode64
    |> binary_part(0, length)
  end

  @doc """
  Test if a datetime has expired.

  Convert the datetime from Ecto.DateTime format to Timex format to do
  the comparison given the time during in opts.

  ## Examples

      expired?(user.expire_at, days: 5)
      expired?(user.expire_at, minutes: 10)
  """
  def expired?(datetime, opts) do
    expire_on? = datetime
    |> Ecto.DateTime.to_erl
    |> Timex.DateTime.from_erl
    |> Timex.shift(opts)
    not Timex.before?(Timex.DateTime.now, expire_on?)
  end

  @doc """
  Log an error message when lockable update fails.
  """
  def lockable_failure(changeset) do
    Logger.error @lockable_failure <> inspect(changeset.errors)
  end

  @doc """
  Send a user email.

  Sends a user email given the module, model, and url. Logs the email for
  debug purposes.

  Note: This function uses an apply to avoid compile warnings if the
  mailer is not selected as an option.
  """
  def send_user_email(fun, model, url) do
    email = apply(Module.concat(Config.module, Coherence.UserEmail), fun, [model, url])
    Logger.debug fn -> "#{fun} email: #{inspect email}" end
    apply(Module.concat(Config.module, Coherence.Mailer), :deliver, [email])
  end

  @doc """
  Send confirmation email with token.

  If the user supports confirmable, generate a token and send the email.
  """
  def send_confirmation(conn, user, user_schema) do
    if user_schema.confirmable? do
      token = random_string 48
      url = router_helpers.confirmation_url(conn, :edit, token)
      Logger.debug "confirmation email url: #{inspect url}"
      dt = Ecto.DateTime.utc
      user_schema.changeset(user,
        %{confirmation_token: token, confirmation_sent_at: dt})
      |> Config.repo.update!

      send_user_email :confirmation, user, url
      conn
      |> put_flash(:info, "Confirmation email sent.")
    else
      conn
      |> put_flash(:info, "Registration created successfully.")
    end
  end

  #############
  # User Schema

  @doc """
  Confirm a user account.

  Adds the `:confirmed_at` datetime field on the user model and updates the database
  """
  def confirm!(user) do
    changeset = Confirmable.confirm(user)
    unless Confirmable.confirmed? user do
      Config.repo.update changeset
    else
      changeset = Ecto.Changeset.add_error changeset, :confirmed_at, "already confirmed"
      {:error, changeset}
    end
  end

  @doc """
  Lock a use account.

  Sets the `:locked_at` field on the user model to the current date and time unless
  provided a value for the optional parameter.

  You can provide a date in the future to override the configured lock expiry time. You
  can set this data far in the future to do a pseudo permanent lock.
  """

  def lock!(user, locked_at \\ Ecto.DateTime.utc) do
    user_schema = Config.user_schema
    changeset = user_schema.lock user, locked_at
    unless user_schema.locked?(user) do
      changeset
      |> Config.repo.update
    else
      changeset = Ecto.Changeset.add_error changeset, :locked_at, "already locked"
      {:error, changeset}
    end
  end

  @doc """
  Unlock a user account.

  Clears the `:locked_at` field on the user model and updates the database.
  """
  def unlock!(user) do
    user_schema = Config.user_schema
    changeset = user_schema.unlock user
    if user_schema.locked?(user) do
      changeset
      |> Config.repo.update
    else
      changeset = Ecto.Changeset.add_error changeset, :locked_at, "not locked"
      {:error, changeset}
    end
  end

  @doc """
  Plug to redirect already logged in users.
  """
  def redirect_logged_in(conn, _params) do
    if Coherence.logged_in?(conn) do
      conn
      |> put_flash(:info, "Already logged in." )
      |> redirect(to: logged_in_url(conn))
      |> halt
    else
      conn
    end
  end

  def redirect_to(conn, path, params) do
    apply(Coherence.Redirects, path, [conn, params])
  end
  def redirect_to(conn, path, params, user) do
    apply(Coherence.Redirects, path, [conn, params, user])
  end

  def changeset(which, module, model, params \\ %{}) do
    {mod, fun, args} = case Application.get_env :coherence, :changeset do
      nil -> {module, :changeset, [model, params]}
      {mod, fun} -> {mod, fun, [model, params, which]}
    end
    apply mod, fun, args
  end

  @doc """
  Login a user.

  Logs in a user and redirects them to the session_create page.
  """
  def login_user(conn, user, params) do
     apply(Config.auth_module, Config.create_login, [conn, user, [id_key: Config.schema_key]])
     |> track_login(user, Config.user_schema.trackable?)
     |> redirect_to(:session_create, params)
  end

  @doc """
  Track user login details.

  Saves the ip address and timestamp when the user logs in.
  """
  def track_login(conn, _, false), do: conn
  def track_login(conn, user, true) do
    ip = conn.peer |> elem(0) |> inspect
    now = Ecto.DateTime.utc
    {last_at, last_ip} = cond do
      is_nil(user.last_sign_in_at) and is_nil(user.current_sign_in_at) ->
        {now, ip}
      !!user.current_sign_in_at ->
        {user.current_sign_in_at, user.current_sign_in_ip}
      true ->
        {user.last_sign_in_at, user.last_sign_in_ip}
    end

    changeset(:session, user.__struct__, user,
      %{
        sign_in_count: user.sign_in_count + 1,
        current_sign_in_at: Ecto.DateTime.utc,
        current_sign_in_ip: ip,
        last_sign_in_at: last_at,
        last_sign_in_ip: last_ip
      })
    |> Config.repo.update
    |> case do
      {:ok, _} -> nil
      {:error, _changeset} ->
        Logger.error ("Failed to update tracking!")
    end
    conn
  end

end


Emails now use a proper to: field

Emails now use a proper to: field

Closes #302

defmodule Vutuv.Emailer do
  import Bamboo.Email
  require Vutuv.Gettext
  use Bamboo.Phoenix, view: Vutuv.EmailView

  def login_email(link, email, user) do
    gen_email(link, email, user, "login_email_#{if (user.locale), do: user.locale, else: "en"}")
  end

  def email_creation_email(link, email, user) do
    gen_email(link, email, user,"email_creation_email_#{if (user.locale), do: user.locale, else: "en"}")
  end

  def user_deletion_email(link, email, user) do
    gen_email(link, email, user,"user_deletion_email_#{if (user.locale), do: user.locale, else: "en"}")
  end

  defp gen_email(link, email, user, template) do
    url = Application.get_env(:vutuv, Vutuv.Endpoint)[:public_url]

    new_email
    |> put_text_layout({Vutuv.EmailView, "#{template}.text"})
    |> assign(:link, link)
    |> assign(:url, url)
    |> assign(:user, user)
    |> to("#{Vutuv.UserHelpers.full_name(user)} <#{email}>")
    |> from("vutuv <info@vutuv.de>")
    |> subject(Vutuv.Gettext.gettext("vutuv verification email"))
    |> render("#{template}.text")
  end
end

defmodule Vutuv.Emailer do
  import Bamboo.Email
  require Vutuv.Gettext
  use Bamboo.Phoenix, view: Vutuv.EmailView

  def login_email(link, email, user) do
    gen_email(link, email, user, "login_email_#{if (user.locale), do: user.locale, else: "en"}")
  end

  def email_creation_email(link, email, user) do
    gen_email(link, email, user,"email_creation_email_#{if (user.locale), do: user.locale, else: "en"}")
  end

  def user_deletion_email(link, email, user) do
    gen_email(link, email, user,"user_deletion_email_#{if (user.locale), do: user.locale, else: "en"}")
  end

  defp gen_email(link, email, user, template) do
    url = Application.get_env(:vutuv, Vutuv.Endpoint)[:public_url]

    new_email
    |> put_text_layout({Vutuv.EmailView, "#{template}.text"})
    |> assign(:link, link)
    |> assign(:url, url)
    |> assign(:user, user)
    |> to(email)
    |> from("vutuv <info@vutuv.de>")
    |> subject(Vutuv.Gettext.gettext("vutuv verification email"))
    |> render("#{template}.text")
  end
end


Raise when attempting to prepare simple query

defmodule Xandra.Simple do
  @moduledoc false

  defstruct [:statement, :values]

  @opaque t :: %__MODULE__{
    statement: Xandra.statement,
    values: Xandra.values | nil,
  }

  defimpl DBConnection.Query do
    alias Xandra.{Frame, Protocol}

    def parse(query, _options) do
      raise "cannot prepare #{inspect(query)}"
    end

    def encode(query, values, options) do
      Frame.new(:query)
      |> Protocol.encode_request(%{query | values: values}, options)
      |> Frame.encode(options[:compressor])
    end

    def decode(query, %Frame{} = frame, _options) do
      Protocol.decode_response(frame, query)
    end

    def describe(query, _options) do
      raise "cannot prepare #{inspect(query)}"
    end
  end
end

defmodule Xandra.Simple do
  @moduledoc false

  defstruct [:statement, :values]

  @opaque t :: %__MODULE__{
    statement: Xandra.statement,
    values: Xandra.values | nil,
  }

  defimpl DBConnection.Query do
    alias Xandra.{Frame, Protocol}

    def parse(query, _options) do
      query
    end

    def encode(query, values, options) do
      Frame.new(:query)
      |> Protocol.encode_request(%{query | values: values}, options)
      |> Frame.encode(options[:compressor])
    end

    def decode(query, %Frame{} = frame, _options) do
      Protocol.decode_response(frame, query)
    end

    def describe(query, _options) do
      query
    end
  end
end


tidy up

tidy up

defmodule Bluex.DBusDiscovery do
  @callback device_found(%Bluex.Device{}) :: :ok | :ignore | :error

  @dbus_name Application.get_env(:bluex, :dbus_name)
  @iface_dbus_name Application.get_env(:bluex, :iface_dbus_name)
  @device_dbus_name Application.get_env(:bluex, :device_dbus_name)
  @dbus_bluez_path Application.get_env(:bluex, :dbus_bluez_path)
  @dbus_type Application.get_env(:bluex, :bus_type)

  use GenServer

  @doc """
  Gets list of adapters
  """
  def get_adapters(pid) do
    GenServer.call(pid, :get_adapters)
  end

  @doc """
  Gets list of devices
  """
  def get_devices(pid) do
    GenServer.call(pid, :get_devices)
  end

  @doc false
  def device_found(pid, device) do
    GenServer.cast(pid, {:device_found, device})
  end

  def start_link(module, opts \\ []) do
    GenServer.start_link(__MODULE__, [module, opts], name: module)
  end

  @doc """
  Starts Bluetooth discovery
  """
  def start_discovery(pid) do
    GenServer.cast(pid, :start_discovery)
  end

  @doc false
  def init([module, opts]) do
    {:ok, bus} = :dbus_bus_connection.connect(@dbus_type)
    {:ok, adapter_manager} = :dbus_proxy.start_link(bus, @dbus_name, @dbus_bluez_path)
    {:ok, bluez_manager} = :dbus_proxy.start_link(bus, @dbus_name, "/")

    {:ok, %{bus: bus, adapter_manager: adapter_manager, devices: [], adapters: [], bluez_manager: bluez_manager, module: module}}
  end

  @doc false
  def handle_call(:get_adapters, _, state) do
    {:reply, state[:adapters], state}
  end

  @doc false
  def handle_call(:get_devices, _, state) do
    {:reply, state[:devices], state}
  end

  @doc false
  def handle_info({:dbus_signal, _}, state) do
    GenServer.cast(state[:module], :get_adapters)
    {:noreply, state}
  end

  @doc false
  def handle_cast(:get_adapters, state) do
    adapters = :dbus_proxy.children(state[:adapter_manager])
    |> Stream.map(&Path.basename/1)
    |> Stream.map(fn (adapter) ->
      bluez_path = "#{@dbus_bluez_path}/#{adapter}"
      {:ok, adapter_proxy} = :dbus_proxy.start_link(state[:bus], @dbus_name, bluez_path)
      :dbus_proxy.has_interface(adapter_proxy, @iface_dbus_name)
      {adapter, %{path: bluez_path, proxy: adapter_proxy}}
    end)
    |> Stream.filter(fn ({_, %{proxy: adapter_proxy}}) ->
      #TODO close invalid adapter
      :dbus_proxy.has_interface(adapter_proxy, @iface_dbus_name)
    end)
    |> Enum.into(%{})

    state = Map.put(state, :adapters, adapters)
    {:noreply, state}
  end

  @doc false
  def handle_cast(:start_discovery, state) do
    #TODO: ???get list of devices now and call the device_found callback before leaving this function
    add_interface = fn(sender, "org.freedesktop.DBus.ObjectManager", "InterfacesAdded", path, args, pid) ->
      case args do
        {interface_bluez_path, %{@device_dbus_name => device_details}} ->
          device = %Bluex.Device{mac_address: device_details["Address"], manufacturer_data: device_details["ManufacturerData"], rssi: device_details["RSSI"], uuids: device_details["UUIDs"], adapter: "hci1"}
          Bluex.DBusDiscovery.device_found(pid, device)
        _ -> :ok
      end
    end
    :dbus_proxy.connect_signal(state[:bluez_manager],
                               "org.freedesktop.DBus.ObjectManager",
                               "InterfacesAdded",
                               {add_interface, self})
    :dbus_proxy.children(state[:bluez_manager])
    state[:adapters]
    |> Enum.each(fn ({_, %{proxy: adapter_proxy}}) ->
      #TODO: to filter_services
      #:ok = :dbus_proxy.call(adapter_proxy, @iface_dbus_name, "SetDiscoveryFilter", [%{"UUIDs" => ["5842aec9-3aee-a150-5a8c-159d686d6363"]}])
      :ok = :dbus_proxy.call(adapter_proxy, @iface_dbus_name, "StartDiscovery", [])
    end)
    {:noreply, state}
  end

  @doc false
  def handle_cast({:device_found, device}, state) do
    state = case apply(state[:module], :device_found, [device]) do
      :ok ->
        Map.put(state, :devices, [device])
      _ ->
        state
    end
    {:noreply, state}
  end

  defmacro __using__(opts) do
    quote [unquote: false, location: :keep] do
      @behaviour Bluex.DBusDiscovery
      import Bluex.DBusDiscovery
      use GenServer

      @doc false
      def start_link do
        Bluex.DBusDiscovery.start_link(__MODULE__, [])
      end

      @doc """
      Starts Bluetooth discovery
      """
      def start_discovery do
        Bluex.DBusDiscovery.start_discovery(__MODULE__)
      end
    end
  end
end

defmodule Bluex.DBusDiscovery do
  @callback device_found(%Bluex.Device{}) :: :ok | :ignore | :error

  @dbus_name Application.get_env(:bluex, :dbus_name)
  @iface_dbus_name Application.get_env(:bluex, :iface_dbus_name)
  @device_dbus_name Application.get_env(:bluex, :device_dbus_name)
  @dbus_bluez_path Application.get_env(:bluex, :dbus_bluez_path)
  @dbus_type Application.get_env(:bluex, :bus_type)

  use GenServer

  @doc """
  Gets list of adapters
  """
  def get_adapters(pid) do
    GenServer.call(pid, :get_adapters)
  end

  @doc """
  Gets list of devices
  """
  def get_devices(pid) do
    GenServer.call(pid, :get_devices)
  end

  @doc false
  def device_found(pid, device) do
    GenServer.cast(pid, {:device_found, device})
  end

  def start_link(module, opts \\ []) do
    GenServer.start_link(__MODULE__, [module, opts], name: module)
  end

  @doc """
  Starts Bluetooth discovery
  """
  def start_discovery(pid) do
    GenServer.cast(pid, :start_discovery)
  end

  @doc false
  def init([module, opts]) do
    {:ok, bus} = :dbus_bus_connection.connect(@dbus_type)
    {:ok, adapter_manager} = :dbus_proxy.start_link(bus, @dbus_name, @dbus_bluez_path)
    {:ok, bluez_manager} = :dbus_proxy.start_link(bus, @dbus_name, "/")

    {:ok, %{bus: bus, adapter_manager: adapter_manager, devices: [], adapters: [], bluez_manager: bluez_manager, module: module}}
  end

  @doc false
  def handle_call(:get_adapters, _, state) do
    {:reply, state[:adapters], state}
  end

  @doc false
  def handle_call(:get_devices, _, state) do
    {:reply, state[:devices], state}
  end

  @doc false
  def handle_info({:dbus_signal, _}, state) do
    GenServer.cast(state[:module], :get_adapters)
    {:noreply, state}
  end

  @doc false
  def handle_cast(:get_adapters, state) do
    adapters = :dbus_proxy.children(state[:adapter_manager])
    |> Stream.map(&Path.basename/1)
    |> Stream.map(fn (adapter) ->
      bluez_path = "#{@dbus_bluez_path}/#{adapter}"
      {:ok, adapter_proxy} = :dbus_proxy.start_link(state[:bus], @dbus_name, bluez_path)
      :dbus_proxy.has_interface(adapter_proxy, @iface_dbus_name)
      {adapter, %{path: bluez_path, proxy: adapter_proxy}}
    end)
    |> Stream.filter(fn ({_, %{proxy: adapter_proxy}}) ->
      #TODO close invalid adapter
      :dbus_proxy.has_interface(adapter_proxy, @iface_dbus_name)
    end)
    |> Enum.into(%{})

    state = Map.put(state, :adapters, adapters)
    {:noreply, state}
  end

  #path: "/", args: {"/org/bluez/hci1/dev_C0_CB_38_EB_74_13", %{"org.bluez.Device1" => %{"Adapter" => "/org/bluez/hci1", "Address" => "C0:CB:38:EB:74:13", "Alias" => "ubuntu-0", "Blocked" => false, "Class" => 7078144, "Connected" => false, "Icon" => "computer", "LegacyPairing" => false, "Name" => "ubuntu-0", "Paired" => false, "RSSI" => -71, "Trusted" => false, "TxPower" => 0, "UUIDs" => ["0000112d-0000-1000-8000-00805f9b34fb", "00001112-0000-1000-8000-00805f9b34fb", "0000111f-0000-1000-8000-00805f9b34fb", "0000111e-0000-1000-8000-00805f9b34fb", "0000110c-0000-1000-8000-00805f9b34fb", "0000110e-0000-1000-8000-00805f9b34fb", "0000110a-0000-1000-8000-00805f9b34fb", "0000110b-0000-1000-8000-00805f9b34fb"]}, "org.freedesktop.DBus.Introspectable" => %{}, "org.freedesktop.DBus.Properties" => %{}}}

  @doc false
  def handle_cast(:start_discovery, state) do
    #TODO: ???get list of devices now and call the device_found callback before leaving this function
    add_interface = fn(sender, "org.freedesktop.DBus.ObjectManager", "InterfacesAdded", path, args, pid) ->
      case args do
        {interface_bluez_path, %{@device_dbus_name => device_details}} ->
          device = %Bluex.Device{mac_address: device_details["Address"], manufacturer_data: device_details["ManufacturerData"], rssi: device_details["RSSI"], uuids: device_details["UUIDs"], adapter: "hci1"}
          Bluex.DBusDiscovery.device_found(pid, device)
        _ -> :ok
      end
    end
    :dbus_proxy.connect_signal(state[:bluez_manager],
                               "org.freedesktop.DBus.ObjectManager",
                               "InterfacesAdded",
                               {add_interface, self})
    :dbus_proxy.children(state[:bluez_manager])
    state[:adapters]
    |> Enum.each(fn ({_, %{proxy: adapter_proxy}}) ->
      #TODO: to filter_services
      #:ok = :dbus_proxy.call(adapter_proxy, @iface_dbus_name, "SetDiscoveryFilter", [%{"UUIDs" => ["5842aec9-3aee-a150-5a8c-159d686d6363"]}])
      :ok = :dbus_proxy.call(adapter_proxy, @iface_dbus_name, "StartDiscovery", [])
    end)
    {:noreply, state}
  end

  @doc false
  def handle_cast({:device_found, device}, state) do
    state = case apply(state[:module], :device_found, [device]) do
      :ok ->
        Map.put(state, :devices, [device])
      _ ->
        state
    end
    {:noreply, state}
  end

  defmacro __using__(opts) do
    quote [unquote: false, location: :keep] do
      @behaviour Bluex.DBusDiscovery
      import Bluex.DBusDiscovery
      use GenServer

      @doc false
      def start_link do
        Bluex.DBusDiscovery.start_link(__MODULE__, [])
      end

      @doc """
      Starts Bluetooth discovery
      """
      def start_discovery do
        Bluex.DBusDiscovery.start_discovery(__MODULE__)
      end
    end
  end
end


Use newer child syntax

Use newer child syntax

defmodule Wallaby do
  @moduledoc """
  A concurrent feature testing library.

  ## Configuration

  Wallaby supports the following options:

  * `:otp_app` - The name of your OTP application. This is used to check out your Ecto repos into the SQL Sandbox.
  * `:screenshot_dir` - The directory to store screenshots.
  * `:screenshot_on_failure` - if Wallaby should take screenshots on test failures (defaults to `false`).
  * `:max_wait_time` - The amount of time that Wallaby should wait to find an element on the page. (defaults to `3_000`)
  * `:js_errors` - if Wallaby should re-throw javascript errors in elixir (defaults to true).
  * `:js_logger` - IO device where javascript console logs are written to. Defaults to :stdio. This option can also be set to a file or any other io device. You can disable javascript console logging by setting this to `nil`.
  """

  @drivers %{
    "chrome" => Wallaby.Chrome,
    "selenium" => Wallaby.Selenium
  }

  use Application

  alias Wallaby.Session
  alias Wallaby.SessionStore

  @doc false
  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    case driver().validate() do
      :ok -> :ok
      {:error, exception} -> raise exception
    end

    children = [
      {driver(), [name: Wallaby.Driver.Supervisor]},
      :hackney_pool.child_spec(:wallaby_pool, timeout: 15_000, max_connections: 4),
      {Wallaby.SessionStore, [name: Wallaby.SessionStore]}
    ]

    opts = [strategy: :one_for_one, name: Wallaby.Supervisor]
    Supervisor.start_link(children, opts)
  end

  @type reason :: any
  @type start_session_opts :: {atom, any}

  @doc """
  Starts a browser session.

  ## Multiple sessions

  Each session runs in its own browser so that each test runs in isolation.
  Because of this isolation multiple sessions can be created for a test:

  ```
  @message_field Query.text_field("Share Message")
  @share_button Query.button("Share")
  @message_list Query.css(".messages")

  test "That multiple sessions work" do
    {:ok, user1} = Wallaby.start_session
    user1
    |> visit("/page.html")
    |> fill_in(@message_field, with: "Hello there!")
    |> click(@share_button)

    {:ok, user2} = Wallaby.start_session
    user2
    |> visit("/page.html")
    |> fill_in(@message_field, with: "Hello yourself")
    |> click(@share_button)

    assert user1 |> find(@message_list) |> List.last |> text == "Hello yourself"
    assert user2 |> find(@message_list) |> List.first |> text == "Hello there"
  end
  ```
  """
  @spec start_session([start_session_opts]) :: {:ok, Session.t()} | {:error, reason}
  def start_session(opts \\ []) do
    with {:ok, session} <- driver().start_session(opts),
         :ok <- SessionStore.monitor(session),
         do: {:ok, session}
  end

  @doc """
  Ends a browser session.
  """
  @spec end_session(Session.t()) :: :ok | {:error, reason}
  def end_session(%Session{driver: driver} = session) do
    with :ok <- SessionStore.demonitor(session),
         :ok <- driver.end_session(session),
         do: :ok
  end

  @doc false
  def screenshot_on_failure? do
    Application.get_env(:wallaby, :screenshot_on_failure)
  end

  @doc false
  def js_errors? do
    Application.get_env(:wallaby, :js_errors, true)
  end

  @doc false
  def js_logger do
    Application.get_env(:wallaby, :js_logger, :stdio)
  end

  def driver do
    Map.get(
      @drivers,
      System.get_env("WALLABY_DRIVER"),
      Application.get_env(:wallaby, :driver, Wallaby.Chrome)
    )
  end
end

defmodule Wallaby do
  @moduledoc """
  A concurrent feature testing library.

  ## Configuration

  Wallaby supports the following options:

  * `:otp_app` - The name of your OTP application. This is used to check out your Ecto repos into the SQL Sandbox.
  * `:screenshot_dir` - The directory to store screenshots.
  * `:screenshot_on_failure` - if Wallaby should take screenshots on test failures (defaults to `false`).
  * `:max_wait_time` - The amount of time that Wallaby should wait to find an element on the page. (defaults to `3_000`)
  * `:js_errors` - if Wallaby should re-throw javascript errors in elixir (defaults to true).
  * `:js_logger` - IO device where javascript console logs are written to. Defaults to :stdio. This option can also be set to a file or any other io device. You can disable javascript console logging by setting this to `nil`.
  """

  @drivers %{
    "chrome" => Wallaby.Chrome,
    "selenium" => Wallaby.Selenium
  }

  use Application

  alias Wallaby.Session
  alias Wallaby.SessionStore

  @doc false
  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    case driver().validate() do
      :ok -> :ok
      {:error, exception} -> raise exception
    end

    children = [
      supervisor(Wallaby.Driver.ProcessWorkspace.ServerSupervisor, []),
      supervisor(driver(), [[name: Wallaby.Driver.Supervisor]]),
      :hackney_pool.child_spec(:wallaby_pool, timeout: 15_000, max_connections: 4),
      worker(Wallaby.SessionStore, [])
    ]

    opts = [strategy: :one_for_one, name: Wallaby.Supervisor]
    Supervisor.start_link(children, opts)
  end

  @type reason :: any
  @type start_session_opts :: {atom, any}

  @doc """
  Starts a browser session.

  ## Multiple sessions

  Each session runs in its own browser so that each test runs in isolation.
  Because of this isolation multiple sessions can be created for a test:

  ```
  @message_field Query.text_field("Share Message")
  @share_button Query.button("Share")
  @message_list Query.css(".messages")

  test "That multiple sessions work" do
    {:ok, user1} = Wallaby.start_session
    user1
    |> visit("/page.html")
    |> fill_in(@message_field, with: "Hello there!")
    |> click(@share_button)

    {:ok, user2} = Wallaby.start_session
    user2
    |> visit("/page.html")
    |> fill_in(@message_field, with: "Hello yourself")
    |> click(@share_button)

    assert user1 |> find(@message_list) |> List.last |> text == "Hello yourself"
    assert user2 |> find(@message_list) |> List.first |> text == "Hello there"
  end
  ```
  """
  @spec start_session([start_session_opts]) :: {:ok, Session.t()} | {:error, reason}
  def start_session(opts \\ []) do
    with {:ok, session} <- driver().start_session(opts),
         :ok <- SessionStore.monitor(session),
         do: {:ok, session}
  end

  @doc """
  Ends a browser session.
  """
  @spec end_session(Session.t()) :: :ok | {:error, reason}
  def end_session(%Session{driver: driver} = session) do
    with :ok <- SessionStore.demonitor(session),
         :ok <- driver.end_session(session),
         do: :ok
  end

  @doc false
  def screenshot_on_failure? do
    Application.get_env(:wallaby, :screenshot_on_failure)
  end

  @doc false
  def js_errors? do
    Application.get_env(:wallaby, :js_errors, true)
  end

  @doc false
  def js_logger do
    Application.get_env(:wallaby, :js_logger, :stdio)
  end

  def driver do
    Map.get(
      @drivers,
      System.get_env("WALLABY_DRIVER"),
      Application.get_env(:wallaby, :driver, Wallaby.Chrome)
    )
  end
end


fix bug in random episode display

fix bug in random episode display

defmodule Pan.RecommendationFrontendController do
  use Pan.Web, :controller
  use Crutches
  alias Pan.Podcast
  alias Pan.Episode
  alias Pan.Category
  alias Pan.Chapter
  alias Pan.Recommendation
  alias Pan.Subscription
  alias Pan.Message

  def action(conn, _) do
    apply(__MODULE__, action_name(conn), [conn, conn.params, conn.assigns.current_user])
  end

  def index(conn, params, _) do
    recommendations = from(p in Recommendation, order_by: [desc: :inserted_at],
                                                preload: [:user, :podcast, episode: :podcast,
                                                          chapter: [episode: :podcast]])
                      |> Repo.paginate(params)

    render(conn, "index.html", recommendations: recommendations)
  end

  def my_recommendations(conn,_params, user) do
    podcast_recommendations = Repo.all(from r in Recommendation, where: r.user_id == ^user.id and
                                                                        not is_nil(r.podcast_id) and
                                                                        is_nil(r.episode_id) and
                                                                        is_nil(r.chapter_id),
                                                                 order_by: [desc: :inserted_at],
                                                                 preload: :podcast)

    subscribed_podcast_ids = Repo.all(from s in Subscription, where: s.user_id == ^user.id,
                                                              select: s.podcast_id)
    recommended_podcast_ids = Enum.map(podcast_recommendations, fn(recommendation) -> recommendation.podcast_id end)
    unrecommonded_podcast_ids = Enum.filter(subscribed_podcast_ids, fn(id) ->
      not Enum.member?(recommended_podcast_ids, id)
    end)
    unrecommended_podcasts = Repo.all(from p in Podcast, where: p.id in ^unrecommonded_podcast_ids)

    changeset = Recommendation.changeset(%Recommendation{})

    render(conn, "my_recommendations.html", podcast_recommendations: podcast_recommendations,
                                            unrecommended_podcasts: unrecommended_podcasts,
                                            changeset: changeset)
  end

  def random(conn, _params, _user) do
    podcast = Repo.all(Podcast)
               |> Enum.random
               |> Repo.preload(:categories)

    category = Repo.get(Category, List.first(podcast.categories).id)
               |> Repo.preload([:parent, :children, :podcasts])

    podcast = Repo.get(Podcast, podcast.id)
              |> Repo.preload([:episodes, :languages, :categories, :feeds, engagements: :persona])

    episode = Enum.random(podcast.episodes)
    episode = Repo.get(Episode, episode.id)
              |> Repo.preload([:podcast, :enclosures, [chapters: :recommendations],
                               :contributors, :recommendations])

    changeset = Recommendation.changeset(%Recommendation{})

    render(conn, "random.html", category: category,
                                podcast: podcast,
                                episode: episode,
                                player: "podigee",
                                changeset: changeset)
  end

  def create(conn, %{"recommendation" => recommendation_params}, user) do
    recommendation_params = Map.put(recommendation_params, "user_id", user.id)
    changeset = Recommendation.changeset(%Recommendation{}, recommendation_params)

    e =
    cond do
      podcast_id_param = recommendation_params["podcast_id"] ->
        podcast_id = String.to_integer(podcast_id_param)

        %Event{topic:      "podcasts",
               subtopic:   podcast_id_param,
               podcast_id: podcast_id,
               episode_id: nil,
               chapter_id: nil,
               notification_text: "Podcast " <> Repo.get!(Podcast, podcast_id).title <> ""}

      episode_id_param = recommendation_params["episode_id"] ->
        episode_id = String.to_integer(episode_id_param)
        episode = Repo.get!(Episode, episode_id)
                  |> Repo.preload(:podcast)

        %Event{topic: "podcasts",
               subtopic: Integer.to_string(episode.podcast.id),
               podcast_id: episode.podcast.id,
               episode_id: episode_id,
               chapter_id: nil,
               notification_text: "Episode " <> episode.title <>
                                  " from " <> episode.podcast.title <> ""}

      chapter_id_param = recommendation_params["chapter_id"] ->
        chapter_id = String.to_integer(chapter_id_param)
        chapter = Repo.get!(Chapter, chapter_id)
                  |> Repo.preload([episode: :podcast])

        %Event{topic:      "podcasts",
               subtopic:   Integer.to_string(chapter.episode.podcast.id),
               podcast_id: chapter.episode.podcast.id,
               episode_id: chapter.episode.id,
               chapter_id: chapter_id,
               notification_text: "Chapter " <> chapter.title <> " in " <>
                                  chapter.episode.title <> " from " <>
                                  chapter.episode.podcast.title <> ""}
    end

    e = %{e | current_user_id: user.id,
              type:            "success",
              event:           "recommend",
              content:         C.String.truncate("« recommended " <>
                               e.notification_text <> " » " <>
                               recommendation_params["comment"], 255) }

    Repo.insert(changeset)
    Message.persist_event(e)
    Event.notify_subscribers(e)

    conn
    |> put_flash(:info, "Your recommendation has been added.")
    |> redirect_to_back
  end

  defp redirect_to_back( conn) do
    path =
      conn
      |> Plug.Conn.get_req_header("referer")
      |> List.first
      |> URI.parse
      |> Map.get(:path)

    conn
    |> assign(:refer_path, path)
    |> redirect(to: path)
  end
end

defmodule Pan.RecommendationFrontendController do
  use Pan.Web, :controller
  use Crutches
  alias Pan.Podcast
  alias Pan.Episode
  alias Pan.Category
  alias Pan.Chapter
  alias Pan.Recommendation
  alias Pan.Subscription
  alias Pan.Message

  def action(conn, _) do
    apply(__MODULE__, action_name(conn), [conn, conn.params, conn.assigns.current_user])
  end

  def index(conn, params, _) do
    recommendations = from(p in Recommendation, order_by: [desc: :inserted_at],
                                                preload: [:user, :podcast, episode: :podcast,
                                                          chapter: [episode: :podcast]])
                      |> Repo.paginate(params)

    render(conn, "index.html", recommendations: recommendations)
  end

  def my_recommendations(conn,_params, user) do
    podcast_recommendations = Repo.all(from r in Recommendation, where: r.user_id == ^user.id and
                                                                        not is_nil(r.podcast_id) and
                                                                        is_nil(r.episode_id) and
                                                                        is_nil(r.chapter_id),
                                                                 order_by: [desc: :inserted_at],
                                                                 preload: :podcast)

    subscribed_podcast_ids = Repo.all(from s in Subscription, where: s.user_id == ^user.id,
                                                              select: s.podcast_id)
    recommended_podcast_ids = Enum.map(podcast_recommendations, fn(recommendation) -> recommendation.podcast_id end)
    unrecommonded_podcast_ids = Enum.filter(subscribed_podcast_ids, fn(id) ->
      not Enum.member?(recommended_podcast_ids, id)
    end)
    unrecommended_podcasts = Repo.all(from p in Podcast, where: p.id in ^unrecommonded_podcast_ids)

    changeset = Recommendation.changeset(%Recommendation{})

    render(conn, "my_recommendations.html", podcast_recommendations: podcast_recommendations,
                                            unrecommended_podcasts: unrecommended_podcasts,
                                            changeset: changeset)
  end

  def random(conn, _params, _user) do
    podcast = Repo.all(Podcast)
               |> Enum.random
               |> Repo.preload(:categories)

    category = Repo.get(Category, List.first(podcast.categories).id)
               |> Repo.preload([:parent, :children, :podcasts])

    podcast = Repo.get(Podcast, podcast.id)
              |> Repo.preload([:episodes, :languages, :categories, :feeds])

    episode = Enum.random(podcast.episodes)
    episode = Repo.get(Episode, episode.id)
              |> Repo.preload([:podcast, :enclosures, [chapters: :recommendations],
                               :contributors, :recommendations])

    changeset = Recommendation.changeset(%Recommendation{})

    render(conn, "random.html", category: category,
                                podcast: podcast,
                                episode: episode,
                                player: "podigee",
                                changeset: changeset)
  end

  def create(conn, %{"recommendation" => recommendation_params}, user) do
    recommendation_params = Map.put(recommendation_params, "user_id", user.id)
    changeset = Recommendation.changeset(%Recommendation{}, recommendation_params)

    e =
    cond do
      podcast_id_param = recommendation_params["podcast_id"] ->
        podcast_id = String.to_integer(podcast_id_param)

        %Event{topic:      "podcasts",
               subtopic:   podcast_id_param,
               podcast_id: podcast_id,
               episode_id: nil,
               chapter_id: nil,
               notification_text: "Podcast " <> Repo.get!(Podcast, podcast_id).title <> ""}

      episode_id_param = recommendation_params["episode_id"] ->
        episode_id = String.to_integer(episode_id_param)
        episode = Repo.get!(Episode, episode_id)
                  |> Repo.preload(:podcast)

        %Event{topic: "podcasts",
               subtopic: Integer.to_string(episode.podcast.id),
               podcast_id: episode.podcast.id,
               episode_id: episode_id,
               chapter_id: nil,
               notification_text: "Episode " <> episode.title <>
                                  " from " <> episode.podcast.title <> ""}

      chapter_id_param = recommendation_params["chapter_id"] ->
        chapter_id = String.to_integer(chapter_id_param)
        chapter = Repo.get!(Chapter, chapter_id)
                  |> Repo.preload([episode: :podcast])

        %Event{topic:      "podcasts",
               subtopic:   Integer.to_string(chapter.episode.podcast.id),
               podcast_id: chapter.episode.podcast.id,
               episode_id: chapter.episode.id,
               chapter_id: chapter_id,
               notification_text: "Chapter " <> chapter.title <> " in " <>
                                  chapter.episode.title <> " from " <>
                                  chapter.episode.podcast.title <> ""}
    end

    e = %{e | current_user_id: user.id,
              type:            "success",
              event:           "recommend",
              content:         C.String.truncate("« recommended " <>
                               e.notification_text <> " » " <>
                               recommendation_params["comment"], 255) }

    Repo.insert(changeset)
    Message.persist_event(e)
    Event.notify_subscribers(e)

    conn
    |> put_flash(:info, "Your recommendation has been added.")
    |> redirect_to_back
  end

  defp redirect_to_back( conn) do
    path =
      conn
      |> Plug.Conn.get_req_header("referer")
      |> List.first
      |> URI.parse
      |> Map.get(:path)

    conn
    |> assign(:refer_path, path)
    |> redirect(to: path)
  end
end


phoenix template: use path as title instead of name to avoid grouping of templates that belong to different views

if Appsignal.phoenix? do
  defmodule Appsignal.Phoenix.TemplateInstrumenter do
    @moduledoc """
    Instrument Phoenix template engines

    As documented in the [phoenix
    guidelines](http://docs.appsignal.com/elixir/integrations/phoenix.html),
    The AppSignal Elixir library comes with default template engines to
    instrument renders to `.eex` and `.exs` template files.

    When you use another template engine in your Phoenix project, you
    can create a module which wraps the template renderer to also
    instrument those template files.

    To instrument the
    [phoenix_markdown](https://github.com/boydm/phoenix_markdown)
    library, you would create the following renderer engine module:

    ```
    defmodule MyApp.InstrumentedMarkdownEngine do
      use Appsignal.Phoenix.TemplateInstrumenter, engine: PhoenixMarkdown.Engine
    endmodule
    ```

    And then register the `.md` extension as a template as follows:

    ```
    config :phoenix, :template_engines,
      md: MyApp.InstrumentedMarkdownEngine
    ```

    """

    @doc false
    defmacro __using__(opts) do
      quote do
        @behaviour Phoenix.Template.Engine

        def compile(path, name) do
          expr = unquote(opts[:engine]).compile(path, name)
          quote do
            Appsignal.Instrumentation.Helpers.instrument(
              self(),
              "render.phoenix_template",
              unquote(path),
              fn() -> unquote(expr) end
            )
          end
        end
      end
    end
  end
end

if Appsignal.phoenix? do
  defmodule Appsignal.Phoenix.TemplateInstrumenter do
    @moduledoc """
    Instrument Phoenix template engines

    As documented in the [phoenix
    guidelines](http://docs.appsignal.com/elixir/integrations/phoenix.html),
    The AppSignal Elixir library comes with default template engines to
    instrument renders to `.eex` and `.exs` template files.

    When you use another template engine in your Phoenix project, you
    can create a module which wraps the template renderer to also
    instrument those template files.

    To instrument the
    [phoenix_markdown](https://github.com/boydm/phoenix_markdown)
    library, you would create the following renderer engine module:

    ```
    defmodule MyApp.InstrumentedMarkdownEngine do
      use Appsignal.Phoenix.TemplateInstrumenter, engine: PhoenixMarkdown.Engine
    endmodule
    ```

    And then register the `.md` extension as a template as follows:

    ```
    config :phoenix, :template_engines,
      md: MyApp.InstrumentedMarkdownEngine
    ```

    """

    @doc false
    defmacro __using__(opts) do
      quote do
        @behaviour Phoenix.Template.Engine

        def compile(path, name) do
          expr = unquote(opts[:engine]).compile(path, name)
          quote do
            Appsignal.Instrumentation.Helpers.instrument(
              self(),
              "render.phoenix_template",
              unquote(name),
              fn() -> unquote(expr) end
            )
          end
        end
      end
    end
  end
end


Improve error message in case of lockfile eval failure

# This module keeps a lock file and the manifest for the lock file.
# The lockfile keeps the latest dependency information while the
# manifest is used whenever a dependency is affected via any of the
# deps.* tasks. We also keep the Elixir version in the manifest file.
defmodule Mix.Dep.Lock do
  @moduledoc false
  @manifest ".compile.lock"

  @doc """
  Returns the manifest file for dependencies.
  """
  @spec manifest(Path.t) :: Path.t
  def manifest(manifest_path \\ Mix.Project.manifest_path) do
    Path.join(manifest_path, @manifest)
  end

  @doc """
  Touches the manifest timestamp unless it is an umbrella application.
  """
  @spec touch() :: :ok
  def touch() do
    _ = unless Mix.Project.umbrella?, do: touch(Mix.Project.manifest_path)
    :ok
  end

  @doc """
  Touches the manifest timestamp and updates the elixir version.
  """
  @spec touch(Path.t) :: :ok
  def touch(manifest_path) do
    File.mkdir_p!(manifest_path)
    File.write!(Path.join(manifest_path, @manifest), System.version)
  end

  @doc """
  Returns the elixir version in the lock manifest.
  """
  @spec elixir_vsn() :: binary | nil
  def elixir_vsn() do
    elixir_vsn(Mix.Project.manifest_path)
  end

  @doc """
  Returns the elixir version in the lock manifest in the given path.
  """
  @spec elixir_vsn(Path.t) :: binary | nil
  def elixir_vsn(manifest_path) do
    case File.read(manifest(manifest_path)) do
      {:ok, contents} ->
        contents
      {:error, _} ->
        nil
    end
  end

  @doc """
  Read the lockfile, returns a map containing
  each app name and its current lock information.
  """
  @spec read() :: map
  def read() do
    case File.read(lockfile) do
      {:ok, info} ->
        case Code.eval_string(info, [], file: lockfile) do
          # lock could be a keyword list
          {lock, _binding} when is_list(lock) -> Enum.into(lock, %{})
          {lock, _binding} when is_map(lock)  -> lock
          {nil, _binding}                     -> %{}
        end
      {:error, _} ->
        %{}
    end
  end

  @doc """
  Receives a map and writes it as the latest lock.
  """
  @spec write(map) :: :ok
  def write(map) do
    unless map == read do
      lines =
        for {app, rev} <- Enum.sort(map), rev != nil do
          ~s("#{app}": #{inspect rev, limit: :infinity})
        end
      File.write! lockfile, "%{" <> Enum.join(lines, ",\n  ") <> "}\n"
      touch
    end
    :ok
  end

  defp lockfile do
    Mix.Project.config[:lockfile]
  end
end

# This module keeps a lock file and the manifest for the lock file.
# The lockfile keeps the latest dependency information while the
# manifest is used whenever a dependency is affected via any of the
# deps.* tasks. We also keep the Elixir version in the manifest file.
defmodule Mix.Dep.Lock do
  @moduledoc false
  @manifest ".compile.lock"

  @doc """
  Returns the manifest file for dependencies.
  """
  @spec manifest(Path.t) :: Path.t
  def manifest(manifest_path \\ Mix.Project.manifest_path) do
    Path.join(manifest_path, @manifest)
  end

  @doc """
  Touches the manifest timestamp unless it is an umbrella application.
  """
  @spec touch() :: :ok
  def touch() do
    _ = unless Mix.Project.umbrella?, do: touch(Mix.Project.manifest_path)
    :ok
  end

  @doc """
  Touches the manifest timestamp and updates the elixir version.
  """
  @spec touch(Path.t) :: :ok
  def touch(manifest_path) do
    File.mkdir_p!(manifest_path)
    File.write!(Path.join(manifest_path, @manifest), System.version)
  end

  @doc """
  Returns the elixir version in the lock manifest.
  """
  @spec elixir_vsn() :: binary | nil
  def elixir_vsn() do
    elixir_vsn(Mix.Project.manifest_path)
  end

  @doc """
  Returns the elixir version in the lock manifest in the given path.
  """
  @spec elixir_vsn(Path.t) :: binary | nil
  def elixir_vsn(manifest_path) do
    case File.read(manifest(manifest_path)) do
      {:ok, contents} ->
        contents
      {:error, _} ->
        nil
    end
  end

  @doc """
  Read the lockfile, returns a map containing
  each app name and its current lock information.
  """
  @spec read() :: map
  def read() do
    case File.read(lockfile) do
      {:ok, info} ->
        case Code.eval_string(info) do
          # lock could be a keyword list
          {lock, _binding} when is_list(lock) -> Enum.into(lock, %{})
          {lock, _binding} when is_map(lock)  -> lock
          {nil, _binding}                     -> %{}
        end
      {:error, _} ->
        %{}
    end
  end

  @doc """
  Receives a map and writes it as the latest lock.
  """
  @spec write(map) :: :ok
  def write(map) do
    unless map == read do
      lines =
        for {app, rev} <- Enum.sort(map), rev != nil do
          ~s("#{app}": #{inspect rev, limit: :infinity})
        end
      File.write! lockfile, "%{" <> Enum.join(lines, ",\n  ") <> "}\n"
      touch
    end
    :ok
  end

  defp lockfile do
    Mix.Project.config[:lockfile]
  end
end


Code style

Code style

defmodule Tokenizer.Supervisor do
  @moduledoc """
  This supervisor allows to asynchronously start workers and maintain they availability.
  Polling is done on AMQP level by `prefetch_count` option.
  """
  use Supervisor
  require Logger
  alias Repo.Schemas.Card
  alias Tokenizer.Encryptor

  @token_prefix "card-token"

  def start_link do
    Supervisor.start_link(__MODULE__, [], name: __MODULE__)
  end

  @doc false
  def init(_) do
    children = [
      supervisor(Tokenizer.Card, [], restart: :transient)
    ]

    supervise(children, strategy: :simple_one_for_one)
  end

  @doc """
  Save card to a GenServer process that will allow to read it's data only once.

  Card data is encrypted in two rounds:
    1. By user-token that is generated by this Supervisor
    2. By key that is set in environment config.

  In this way we make sure that card data can't be decoded without server-key if user-key is stolen,
  and by user-key if server is hacked.
  """
  def save_card(%Card{number: card_number} = card) do
    token = card_number
    |> generate_token

    card_data = card
    |> Poison.encode!
    |> Encryptor.encrypt(get_encryption_keys(token))

    child_name = get_card_process_via_tuple(token)
    expires_in = Confex.get(:gateway_api, :card_token_expires_in)

    {:ok, _pid} = Supervisor.start_child(__MODULE__, [[
      card_data: card_data,
      expires_in: expires_in,
      name: child_name]])

    expires_at = Timex.now
    |> Timex.shift(microseconds: expires_in)

    {:ok, %Repo.Schemas.CardToken{token: token, token_expires_at: expires_at}}
  end

  @doc """
  Get saved card data.

  This method requires user card token to find card process and decrypt card data.

  It can return:
    * `{:error, reason}` - when card decryption is impossible (`:invalid_key_or_signature`)
    or card is not found (`:card_not_found`).
    * `{:ok, card}` - where `card` is decrypted card details.

  For a single card this method can be called only once, because card data will destroy itself after decryption.
  """
  def get_card(token) do
    name = token
    |> get_card_process_name()

    Tokenizer.Registry
    |> Registry.lookup(name)
    |> get_card_data(token)
  end

  defp get_card_data([], _token),
    do: {:error, :card_not_found}
  defp get_card_data([{pid, _}], token) do
    message = pid
    |> Tokenizer.Card.get_data()
    |> Encryptor.decrypt(get_decryption_keys(token))

    case message do
      {:error, reason} ->
        {:error, reason}
      _ ->
        card = message
        |> Poison.decode!
        |> to_struct(Card)

        {:ok, card}
    end
  end

  defp to_struct(attrs, kind) do
    struct = struct(kind)
    Enum.reduce Map.to_list(struct), struct, fn {k, _}, acc ->
      case Map.fetch(attrs, Atom.to_string(k)) do
        {:ok, v} -> %{acc | k => v}
        :error -> acc
      end
    end
  end

  @doc """
  Generate card token for a given card number or last 4 digits of card number.
  """
  def generate_token(card_number) when byte_size(card_number) > 4 do
    card_number
    |> String.slice(-4..-1)
    |> generate_token
  end

  def generate_token(last4) when is_binary(last4) do
    salt = {last4, :os.timestamp()}
    |> :erlang.term_to_binary

    id = :sha
    |> :crypto.hash(salt <> :crypto.strong_rand_bytes(16))
    |> Base.encode16
    |> String.downcase

    @token_prefix <> "-" <> last4 <> "-" <> id
  end

  defp get_encryption_keys(<<@token_prefix, "-", _last4::bytes-size(4), "-", user_key::bytes>>) do
    [
      user_key,
      Confex.get(:gateway_api, :card_data_encryption_key)
    ]
  end

  defp get_decryption_keys(<<@token_prefix, "-", _last4::bytes-size(4), "-", _user_key::bytes>> = token) do
    token
    |> get_encryption_keys
    |> Enum.reverse
  end

  defp get_card_process_via_tuple(token),
    do: {:via, Registry, {Tokenizer.Registry, get_card_process_name(token)}}

  defp get_card_process_name(token),
    do: "Cards." <> token
end

defmodule Tokenizer.Supervisor do
  @moduledoc """
  This supervisor allows to asynchronously start workers and maintain they availability.
  Polling is done on AMQP level by `prefetch_count` option.
  """
  use Supervisor
  require Logger
  alias Repo.Schemas.Card
  alias Tokenizer.Encryptor

  @token_prefix "card-token"

  def start_link do
    Supervisor.start_link(__MODULE__, [], name: __MODULE__)
  end

  @doc """
  Save card to a GenServer process that will allow to read it's data only once.

  Card data is encrypted in two rounds:
    1. By user-token that is generated by this Supervisor
    2. By key that is set in environment config.

  In this way we make sure that card data can't be decoded without server-key if user-key is stolen,
  and by user-key if server is hacked.
  """
  def save_card(%Card{number: card_number} = card) do
    token = card_number
    |> generate_token

    card_data = card
    |> Poison.encode!
    |> Encryptor.encrypt(get_encryption_keys(token))

    child_name = get_card_process_via_tuple(token)
    expires_in = Confex.get(:gateway_api, :card_token_expires_in)

    {:ok, _pid} = Supervisor.start_child(__MODULE__, [[
      card_data: card_data,
      expires_in: expires_in,
      name: child_name]])

    expires_at = Timex.now
    |> Timex.shift(microseconds: expires_in)

    {:ok, %Repo.Schemas.CardToken{token: token, token_expires_at: expires_at}}
  end

  @doc """
  Get saved card data.

  This method requires user card token to find card process and decrypt card data.

  It can return:
    * `{:error, reason}` - when card decryption is impossible (`:invalid_key_or_signature`)
    or card is not found (`:card_not_found`).
    * `{:ok, card}` - where `card` is decrypted card details.

  For a single card this method can be called only once, because card data will destroy itself after decryption.
  """
  def get_card(token) do
    name = token
    |> get_card_process_name()

    Tokenizer.Registry
    |> Registry.lookup(name)
    |> get_card_data(token)
  end

  defp get_card_data([], _token),
    do: {:error, :card_not_found}
  defp get_card_data([{pid, _}], token) do
    message = pid
    |> Tokenizer.Card.get_data()
    |> Encryptor.decrypt(get_decryption_keys(token))

    case message do
      {:error, reason} ->
        {:error, reason}
      _ ->
        card = message
        |> Poison.decode!
        |> to_struct(Card)

        {:ok, card}
    end
  end

  defp to_struct(attrs, kind) do
    struct = struct(kind)
    Enum.reduce Map.to_list(struct), struct, fn {k, _}, acc ->
      case Map.fetch(attrs, Atom.to_string(k)) do
        {:ok, v} -> %{acc | k => v}
        :error -> acc
      end
    end
  end

  @doc """
  Generate card token for a given card number or last 4 digits of card number.
  """
  def generate_token(card_number) when byte_size(card_number) > 4 do
    card_number
    |> String.slice(-4..-1)
    |> generate_token
  end

  def generate_token(last4) when is_binary(last4) do
    salt = {last4, :os.timestamp()}
    |> :erlang.term_to_binary

    id = :sha
    |> :crypto.hash(salt <> :crypto.strong_rand_bytes(16))
    |> Base.encode16
    |> String.downcase

    @token_prefix <> "-" <> last4 <> "-" <> id
  end

  defp get_encryption_keys(<<@token_prefix, "-", _last4::bytes-size(4), "-", user_key::bytes>>) do
    [
      user_key,
      Confex.get(:gateway_api, :card_data_encryption_key)
    ]
  end

  defp get_decryption_keys(<<@token_prefix, "-", _last4::bytes-size(4), "-", _user_key::bytes>> = token) do
    token
    |> get_encryption_keys
    |> Enum.reverse
  end

  def get_card_process_via_tuple(token),
    do: {:via, Registry, {Tokenizer.Registry, get_card_process_name(token)}}

  def get_card_process_name(token),
    do: "Cards." <> token

  @doc false
  def init(_) do
    children = [
      supervisor(Tokenizer.Card, [], restart: :transient)
    ]

    supervise(children, strategy: :simple_one_for_one)
  end
end


Update broken link to Message API

Update broken link to Message API

defmodule ExTwilio.Message do
  @moduledoc """
  Represents an Message resource in the Twilio API.

  - [Twilio docs](https://www.twilio.com/docs/sms/api/message-resource)

  Here is an example of sending an SMS message:

    {target_number, twilio_number_you_own, body} = {"+12223334444", "+19223334444", "Hello World"}

    ExTwilio.Message.create(to: target_number, from: twilio_number_you_own, body: body)
  """

  defstruct sid: nil,
            date_created: nil,
            date_updated: nil,
            date_sent: nil,
            account_sid: nil,
            from: nil,
            to: nil,
            body: nil,
            num_media: nil,
            num_segments: nil,
            status: nil,
            error_code: nil,
            error_message: nil,
            direction: nil,
            price: nil,
            price_unit: nil,
            api_version: nil,
            uri: nil,
            subresource_uri: nil,
            messaging_service_sid: nil

  use ExTwilio.Resource,
    import: [
      :stream,
      :all,
      :find,
      :create,
      :update,
      :destroy
    ]

  def parents, do: [:account]
end

defmodule ExTwilio.Message do
  @moduledoc """
  Represents an Message resource in the Twilio API.

  - [Twilio docs](https://www.twilio.com/docs/api/rest/messages)

  Here is an example of sending an SMS message:

    {target_number, twilio_number_you_own, body} = {"+12223334444", "+19223334444", "Hello World"}

    ExTwilio.Message.create(to: target_number, from: twilio_number_you_own, body: body)
  """

  defstruct sid: nil,
            date_created: nil,
            date_updated: nil,
            date_sent: nil,
            account_sid: nil,
            from: nil,
            to: nil,
            body: nil,
            num_media: nil,
            num_segments: nil,
            status: nil,
            error_code: nil,
            error_message: nil,
            direction: nil,
            price: nil,
            price_unit: nil,
            api_version: nil,
            uri: nil,
            subresource_uri: nil,
            messaging_service_sid: nil

  use ExTwilio.Resource,
    import: [
      :stream,
      :all,
      :find,
      :create,
      :update,
      :destroy
    ]

  def parents, do: [:account]
end