Typofix

Typofix

defmodule Phoenix.Socket.Handler do
  @behaviour :cowboy_websocket_handler

  alias Phoenix.Socket
  alias Phoenix.Socket.Message
  alias Phoenix.Channel

  defexception InvalidReturn, message: nil do
    def exception(opts) do
      InvalidReturn[message: "Invalid Handler return: #{inspect opts[:message]}"]
    end
  end

  def init({:tcp, :http}, req, opts) do
    {:upgrade, :protocol, :cowboy_websocket, req, opts}
  end
  def init({:ssl, :http}, req, opts) do
    {:upgrade, :protocol, :cowboy_websocket, req, opts}
  end

  @doc """
  Handles initalization of the websocket

  Possible returns:
    :ok
    {:ok, req, state}
    {:ok, req, state, timeout} # Timeout defines how long it waits for activity
                                 from the client. Default: infinity.
  """
  def websocket_init(_transport, req, opts) do
    router = Dict.fetch! opts, :router

    {:ok, req, %Socket{conn: req, pid: self, router: router}}
  end

  @doc """
  Dispatches multiplexed socket message to Router and handles result

  # Join Event
    "join" events are specially treated.
    When {:ok, socket} is returned from the Channel, the socket is subscribed
    to the channel and authorized to pubsub on the channel/topic pair
    When {:error, socket, reason} is returned, the socket is denied pubsub access

  # Leave Event
    "leave" events call the channels `leave/2` function only if the socket has
    previously been authorized via `join/2`

  # Arbitrary Events
    Any other event calls the channel's `event/3` function, with the event
    name as the first argument. Event handlers are only invoked if the socket
    was previously authorized via `join/2`.

  """
  def websocket_handle({:text, text}, _req, socket) do
    msg = Message.parse!(text)

    socket
    |> Socket.set_current_channel(msg.channel, msg.topic)
    |> dispatch(msg.event, msg.message)
  end

  defp dispatch(socket, "join", msg) do
    socket
    |> socket.router.match(:websocket, socket.channel, "join", msg)
    |> handle_result("join")
  end
  defp dispatch(socket, event, msg) do
    if Socket.authenticated?(socket, socket.channel, socket.topic) do
      socket
      |> socket.router.match(:websocket, socket.channel, event, msg)
      |> handle_result(event)
    else
      handle_result({:error, socket, :unauthenticated}, event)
    end
  end

  defp handle_result({:ok, socket}, "join") do
    {:ok, socket.conn, Channel.subscribe(socket, socket.channel, socket.topic)}
  end
  defp handle_result(socket = %Socket{}, "leave") do
    {:ok, socket.conn, Channel.unsubscribe(socket, socket.channel, socket.topic)}
  end
  defp handle_result(socket = %Socket{}, _event) do
    {:ok, socket.conn, socket}
  end
  defp handle_result({:error, socket, _reason}, _event) do
    {:ok, socket.conn, socket}
  end
  defp handle_result(bad_return, event) when event in ["join", "leave"] do
    raise InvalidReturn, message: """
      expected {:ok, %Socket{}} | {:error, %Socket{}, reason} got #{inspect bad_return}
    """
  end
  defp handle_result(bad_return, _event) do
    raise InvalidReturn, message: """
      expected %Socket{} got #{inspect bad_return}
    """
  end

  @doc """
  Handles recieving messages from processes
  """
  def info(_info, _req, state) do
    {:ok, state}
  end

  def websocket_info({:reply, frame}, req, state) do
    {:reply, frame, req, state}
  end
  def websocket_info(:shutdown, req, state) do
    {:shutdown, req, state}
  end
  def websocket_info(:hibernate, req, state) do
    {:ok, req, state, :hibernate}
  end

  def websocket_info(data, req, socket) do
    Enum.each socket.channels, fn {channel, topic} ->
      socket
      |> Socket.set_current_channel(channel, topic)
      |> socket.router.match(:websocket, channel, "info", data)
      |> handle_result("info")
    end
    {:ok, req, socket}
  end

  @doc """
  This is called right before the websocket is about to be closed.
  Reason is defined as:
   {:normal, :shutdown | :timeout}   Called when erlang closes connection
   {:remote, :closed}                Called if client formally closes connection
   {:remote, close_code(), binary()}
   {:error, :badencoding | :badframe | :closed | atom()}  Called for many reasons
                                                          tab closed, conn dropped.
  """
  def websocket_terminate(reason, _req, socket) do
    Enum.each socket.channels, fn {channel, topic} ->
      socket
      |> Socket.set_current_channel(channel, topic)
      |> socket.router.match(:websocket, channel, "leave", reason: reason)
      |> handle_result("leave")
    end
    :ok
  end

  @doc """
  Sends a reply to the socket. Follow the cowboy websocket frame syntax

  Frame is defined as
    :close | :ping | :pong
    {:text | :binary | :close | :ping | :pong, iodata()}
    {:close, close_code(), iodata()}

  Options:
    :state
    :hibernate # (true | false) if you want to hibernate the connection

  close_code: 1000..4999
  """
  def reply(socket, frame) do
    send(socket.pid, {:reply, frame, socket})
  end

  @doc """
  Terminates a connection.
  """
  def terminate(socket) do
    send(socket.pid, :shutdown)
  end

  @doc """
  Hibernates the socket.
  """
  def hibernate(socket) do
    send(socket.pid, :hibernate)
  end
end

defmodule Phoenix.Socket.Handler do
  @behaviour :cowboy_websocket_handler

  alias Phoenix.Socket
  alias Phoenix.Socket.Message
  alias Phoenix.Channel

  defexception InvalidReturn, message: nil do
    def exception(opts) do
      InvalidReturn[message: "Invalid Handler return: #{inspect opts[:message]}"]
    end
  end

  def init({:tcp, :http}, req, opts) do
    {:upgrade, :protocol, :cowboy_websocket, req, opts}
  end
  def init({:ssl, :http}, req, opts) do
    {:upgrade, :protocol, :cowboy_websocket, req, opts}
  end

  @doc """
  Handles initalization of the websocket

  Possible returns:
    :ok
    {:ok, req, state}
    {:ok, req, state, timeout} # Timeout defines how long it waits for activity
                                 from the client. Default: infinity.
  """
  def websocket_init(_transport, req, opts) do
    router = Dict.fetch! opts, :router

    {:ok, req, %Socket{conn: req, pid: self, router: router}}
  end

  @doc """
  Dispatches multiplexed socket message to Router and handles result

  # Join Event
    "join" events are specially treated.
    When {:ok, socket} is returned from the Channel, the socket is subscribed
    to the channel and authorized to pubsub on the channel/topic pair
    When {:error, socket, reason} is returned, the socket is denied pubsub access

  # Leave Event
    "leave" events call the channels `leave/2` function only if the socket has
    previously been authorized via `join/2`

  # Arbitrary Events
    Any other event calls the channel's `event/3` function, with the event
    name as the first argument. Event handlers are only invoked if the socket
    was previously authorized via `join/2`.

  """
  def websocket_handle({:text, text}, _req, socket) do
    msg = Message.parse!(text)

    socket
    |> Socket.set_current_channel(msg.channel, msg.topic)
    |> dispatch(msg.event, msg.message)
  end

  defp dispatch(socket, "join", msg) do
    socket
    |> socket.router.match(:websocket, socket.channel, "join", msg)
    |> handle_result("join")
  end
  defp dispatch(socket, event, msg) do
    if Socket.authenticated?(socket, socket.channel, socket.topic) do
      socket
      |> socket.router.match(:websocket, socket.channel, event, msg)
      |> handle_result(event)
    else
      handle_result({:error, socket, :unauthenticated}, event)
    end
  end

  defp handle_result({:ok, socket}, "join") do
    {:ok, socket.conn, Channel.subscribe(socket, socket.channel, socket.topic)}
  end
  defp handle_result(socket = %Socket{}, "leave") do
    {:ok, socket.conn, Channel.unsubscribe(socket, socket.channel, socket.topic)}
  end
  defp handle_result(socket = %Socket{}, _event) do
    {:ok, socket.conn, socket}
  end
  defp handle_result({:error, socket, _reason}, _event) do
    {:ok, socket.conn, socket}
  end
  defp handle_result(bad_return, event) when event in ["join", "leave"] do
    raise InvalidReturn, message: """
      expected {:ok, %Socket{}} | {:error, %Socket{}, reason} got #{inspect bad_return}
    """
  end
  defp handle_result(bad_return, _event) do
    raise InvalidReturn, message: """
      expected %Socket{} got #{inspect bad_return}
    """
  end

  @doc """
  Handles handles recieving messages from processes
  """
  def info(_info, _req, state) do
    {:ok, state}
  end

  def websocket_info({:reply, frame}, req, state) do
    {:reply, frame, req, state}
  end
  def websocket_info(:shutdown, req, state) do
    {:shutdown, req, state}
  end
  def websocket_info(:hibernate, req, state) do
    {:ok, req, state, :hibernate}
  end

  def websocket_info(data, req, socket) do
    Enum.each socket.channels, fn {channel, topic} ->
      socket
      |> Socket.set_current_channel(channel, topic)
      |> socket.router.match(:websocket, channel, "info", data)
      |> handle_result("info")
    end
    {:ok, req, socket}
  end

  @doc """
  This is called right before the websocket is about to be closed.
  Reason is defined as:
   {:normal, :shutdown | :timeout}   Called when erlang closes connection
   {:remote, :closed}                Called if client formally closes connection
   {:remote, close_code(), binary()}
   {:error, :badencoding | :badframe | :closed | atom()}  Called for many reasons
                                                          tab closed, conn dropped.
  """
  def websocket_terminate(reason, _req, socket) do
    Enum.each socket.channels, fn {channel, topic} ->
      socket
      |> Socket.set_current_channel(channel, topic)
      |> socket.router.match(:websocket, channel, "leave", reason: reason)
      |> handle_result("leave")
    end
    :ok
  end

  @doc """
  Sends a reply to the socket. Follow the cowboy websocket frame syntax

  Frame is defined as
    :close | :ping | :pong
    {:text | :binary | :close | :ping | :pong, iodata()}
    {:close, close_code(), iodata()}

  Options:
    :state
    :hibernate # (true | false) if you want to hibernate the connection

  close_code: 1000..4999
  """
  def reply(socket, frame) do
    send(socket.pid, {:reply, frame, socket})
  end

  @doc """
  Terminates a connection.
  """
  def terminate(socket) do
    send(socket.pid, :shutdown)
  end

  @doc """
  Hibernates the socket.
  """
  def hibernate(socket) do
    send(socket.pid, :hibernate)
  end
end


Fix callback definition for to_form/4

Fix callback definition for to_form/4

defprotocol Phoenix.HTML.FormData do
  @moduledoc """
  Converts a data structure into a `Phoenix.HTML.Form` struct.
  """

  @doc """
  Converts a data structure into a `Phoenix.HTML.Form` struct.

  The options are the same options given to `form_for/4`. It
  can be used by implementations to configure their behaviour
  and it must be stored in the underlying struct, with any
  custom field removed.
  """
  @spec to_form(t, Keyword.t()) :: Phoenix.HTML.Form.t()
  def to_form(data, options)

  @doc """
  Converts the field in the given form based on the data structure
  into a list of `Phoenix.HTML.Form` structs.

  The options are the same options given to `inputs_for/4`. It
  can be used by implementations to configure their behaviour
  and it must be stored in the underlying struct, with any
  custom field removed.
  """
  @spec to_form(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field(), Keyword.t()) ::
          [Phoenix.HTML.Form.t()]
  def to_form(data, form, field, options)

  @doc """
  Returns the value for the given field.
  """
  @spec input_value(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: term
  def input_value(data, form, field)

  @doc """
  Returns the HTML5 validations that would apply to
  the given field.
  """
  @spec input_validations(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: Keyword.t()
  def input_validations(data, form, field)

  @doc """
  Receives the given field and returns its input type (:text_input,
  :select, etc). Returns `nil` if the type is unknown.
  """
  @spec input_type(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: atom | nil
  def input_type(data, form, field)
end

defimpl Phoenix.HTML.FormData, for: Plug.Conn do
  def to_form(conn, opts) do
    {name, params, opts} =
      case Keyword.pop(opts, :as) do
        {nil, opts} ->
          {nil, conn.params, opts}

        {name, opts} ->
          name = to_string(name)
          {name, Map.get(conn.params, name) || %{}, opts}
      end

    {errors, opts} = Keyword.pop(opts, :errors, [])

    %Phoenix.HTML.Form{
      source: conn,
      impl: __MODULE__,
      id: name,
      name: name,
      params: params,
      data: %{},
      errors: errors,
      options: opts
    }
  end

  def to_form(conn, form, field, opts) when is_atom(field) or is_binary(field) do
    {default, opts} = Keyword.pop(opts, :default, %{})
    {prepend, opts} = Keyword.pop(opts, :prepend, [])
    {append, opts} = Keyword.pop(opts, :append, [])
    {name, opts} = Keyword.pop(opts, :as)
    {id, opts} = Keyword.pop(opts, :id)

    id = to_string(id || form.id <> "_#{field}")
    name = to_string(name || form.name <> "[#{field}]")
    params = Map.get(form.params, field_to_string(field))

    cond do
      # cardinality: one
      is_map(default) ->
        [
          %Phoenix.HTML.Form{
            source: conn,
            impl: __MODULE__,
            id: id,
            name: name,
            data: default,
            params: params || %{},
            options: opts
          }
        ]

      # cardinality: many
      is_list(default) ->
        entries =
          if params do
            params
            |> Enum.sort_by(&elem(&1, 0))
            |> Enum.map(&{nil, elem(&1, 1)})
          else
            Enum.map(prepend ++ default ++ append, &{&1, %{}})
          end

        for {{data, params}, index} <- Enum.with_index(entries) do
          index_string = Integer.to_string(index)

          %Phoenix.HTML.Form{
            source: conn,
            impl: __MODULE__,
            index: index,
            id: id <> "_" <> index_string,
            name: name <> "[" <> index_string <> "]",
            data: data,
            params: params,
            options: opts
          }
        end
    end
  end

  def input_value(_conn, %{data: data, params: params}, field)
      when is_atom(field) or is_binary(field) do
    case Map.fetch(params, field_to_string(field)) do
      {:ok, value} ->
        value

      :error ->
        Map.get(data, field)
    end
  end

  def input_type(_conn, _form, _field), do: :text_input
  def input_validations(_conn, _form, _field), do: []

  # Normalize field name to string version
  defp field_to_string(field) when is_atom(field), do: Atom.to_string(field)
  defp field_to_string(field) when is_binary(field), do: field
end

defprotocol Phoenix.HTML.FormData do
  @moduledoc """
  Converts a data structure into a `Phoenix.HTML.Form` struct.
  """

  @doc """
  Converts a data structure into a `Phoenix.HTML.Form` struct.

  The options are the same options given to `form_for/4`. It
  can be used by implementations to configure their behaviour
  and it must be stored in the underlying struct, with any
  custom field removed.
  """
  @spec to_form(t, Keyword.t()) :: Phoenix.HTML.Form.t()
  def to_form(data, options)

  @doc """
  Converts the field in the given form based on the data structure
  into a `Phoenix.HTML.Form` struct.

  The options are the same options given to `inputs_for/4`. It
  can be used by implementations to configure their behaviour
  and it must be stored in the underlying struct, with any
  custom field removed.
  """
  @spec to_form(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field(), Keyword.t()) ::
          Phoenix.HTML.Form.t()
  def to_form(data, form, field, options)

  @doc """
  Returns the value for the given field.
  """
  @spec input_value(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: term
  def input_value(data, form, field)

  @doc """
  Returns the HTML5 validations that would apply to
  the given field.
  """
  @spec input_validations(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: Keyword.t()
  def input_validations(data, form, field)

  @doc """
  Receives the given field and returns its input type (:text_input,
  :select, etc). Returns `nil` if the type is unknown.
  """
  @spec input_type(t, Phoenix.HTML.Form.t(), Phoenix.HTML.Form.field()) :: atom | nil
  def input_type(data, form, field)
end

defimpl Phoenix.HTML.FormData, for: Plug.Conn do
  def to_form(conn, opts) do
    {name, params, opts} =
      case Keyword.pop(opts, :as) do
        {nil, opts} ->
          {nil, conn.params, opts}

        {name, opts} ->
          name = to_string(name)
          {name, Map.get(conn.params, name) || %{}, opts}
      end

    {errors, opts} = Keyword.pop(opts, :errors, [])

    %Phoenix.HTML.Form{
      source: conn,
      impl: __MODULE__,
      id: name,
      name: name,
      params: params,
      data: %{},
      errors: errors,
      options: opts
    }
  end

  def to_form(conn, form, field, opts) when is_atom(field) or is_binary(field) do
    {default, opts} = Keyword.pop(opts, :default, %{})
    {prepend, opts} = Keyword.pop(opts, :prepend, [])
    {append, opts} = Keyword.pop(opts, :append, [])
    {name, opts} = Keyword.pop(opts, :as)
    {id, opts} = Keyword.pop(opts, :id)

    id = to_string(id || form.id <> "_#{field}")
    name = to_string(name || form.name <> "[#{field}]")
    params = Map.get(form.params, field_to_string(field))

    cond do
      # cardinality: one
      is_map(default) ->
        [
          %Phoenix.HTML.Form{
            source: conn,
            impl: __MODULE__,
            id: id,
            name: name,
            data: default,
            params: params || %{},
            options: opts
          }
        ]

      # cardinality: many
      is_list(default) ->
        entries =
          if params do
            params
            |> Enum.sort_by(&elem(&1, 0))
            |> Enum.map(&{nil, elem(&1, 1)})
          else
            Enum.map(prepend ++ default ++ append, &{&1, %{}})
          end

        for {{data, params}, index} <- Enum.with_index(entries) do
          index_string = Integer.to_string(index)

          %Phoenix.HTML.Form{
            source: conn,
            impl: __MODULE__,
            index: index,
            id: id <> "_" <> index_string,
            name: name <> "[" <> index_string <> "]",
            data: data,
            params: params,
            options: opts
          }
        end
    end
  end

  def input_value(_conn, %{data: data, params: params}, field)
      when is_atom(field) or is_binary(field) do
    case Map.fetch(params, field_to_string(field)) do
      {:ok, value} ->
        value

      :error ->
        Map.get(data, field)
    end
  end

  def input_type(_conn, _form, _field), do: :text_input
  def input_validations(_conn, _form, _field), do: []

  # Normalize field name to string version
  defp field_to_string(field) when is_atom(field), do: Atom.to_string(field)
  defp field_to_string(field) when is_binary(field), do: field
end


Ensure the serve endpoints configuration is persistent

defmodule Mix.Tasks.Phoenix.Server do
  use Mix.Task

  @shortdoc "Starts applications and their servers"

  @moduledoc """
  Starts the application by configuring all endpoints servers to run.
  """
  def run(_args) do
    Application.put_env(:phoenix, :serve_endpoints, true, persistent: true)
    Mix.Task.run "app.start", []
    no_halt
  end

  defp no_halt do
    unless iex_running?, do: :timer.sleep(:infinity)
  end

  defp iex_running? do
    Code.ensure_loaded?(IEx) && IEx.started?
  end
end

defmodule Mix.Tasks.Phoenix.Server do
  use Mix.Task

  @shortdoc "Starts applications and their servers"

  @moduledoc """
  Starts the application by configuring all endpoints servers to run.
  """
  def run(_args) do
    Application.put_env :phoenix, :serve_endpoints, true
    Mix.Task.run "app.start", []
    no_halt
  end

  defp no_halt do
    unless iex_running?, do: :timer.sleep(:infinity)
  end

  defp iex_running? do
    Code.ensure_loaded?(IEx) && IEx.started?
  end
end


use consistent 'field name' logic (from str_get), using atom

defmodule Ello.V3.Schema.NetworkTypes do
  import Ello.V3.Schema.Helpers
  use Absinthe.Schema.Notation
  alias Ello.V3.Resolvers
  alias Ello.Core.Network.User

  object :user do
    field :id, :id
    field :username, :string
    field :name, :string
    field :settings, :user_settings
    field :user_stats, :user_stats, resolve: &source_self/2
    field :location, :string
    field :formatted_short_bio, :string
    field :badges, list_of(:string), resolve: &user_badges/2
    field :experimental_features, :boolean, resolve: &experimental_features/2
    field :is_community, :boolean
    field :external_links_list, list_of(:external_link), resolve: fn(_args, %{source: user}) ->
      {:ok, user.rendered_links}
    end
    field :avatar, :tshirt_image_versions, resolve: fn(_args, %{source: user}) ->
      {:ok, user.avatar_struct}
    end
    field :cover_image, :responsive_image_versions, resolve: fn(_args, %{source: user}) ->
      {:ok, user.cover_image_struct}
    end
    field :current_user_state, :user_current_user_state, resolve: &source_self/2
    field :category_users, list_of(:category_user) do
      arg :roles, list_of(:category_user_role)
      resolve &Resolvers.CategoryUsers.call/3
    end
    field :meta_attributes, :user_meta_attributes, resolve: &user_meta/2
  end

  object :user_stream do
    field :next, :string
    field :per_page, :integer
    field :is_last_page, :boolean
    field :users, list_of(:user)
  end

  object :profile do
    field :name, :string
    field :username, :string
    field :email, :string
    field :location, :string
    field :formatted_short_bio, :string
    field :short_bio, :string
    field :web_onboarding_version, :string
    field :allows_analytics, :boolean, resolve: &user_profile/2
    field :discoverable, :boolean, resolve: &user_profile/2
    field :has_ad_notifications_enabled, :boolean, resolve: &user_profile/2
    field :has_announcements_enabled, :boolean, resolve: &user_profile/2
    field :has_auto_watch_enabled, :boolean, resolve: &user_profile/2
    field :has_reposting_enabled, :boolean, resolve: &user_profile/2
    field :has_sharing_enabled, :boolean, resolve: &user_profile/2
    field :is_collaborateable, :boolean, resolve: &user_profile/2
    field :is_hireable, :boolean, resolve: &user_profile/2
    field :notify_of_announcements_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_approved_submissions_from_following_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_approved_submissions_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_featured_category_post_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_featured_category_post_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_approved_submissions_from_following_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_comments_on_post_watch_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_comments_on_post_watch_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_comments_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_comments_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_invitation_acceptances_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_invitation_acceptances_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_loves_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_loves_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_mentions_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_mentions_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_new_followers_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_new_followers_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_reposts_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_reposts_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_watches_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_watches_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_what_you_missed_via_email, :boolean, resolve: &user_profile/2
    field :subscribe_to_daily_ello, :boolean, resolve: &user_profile/2
    field :subscribe_to_onboarding_drip, :boolean, resolve: &user_profile/2
    field :subscribe_to_users_email_list, :boolean, resolve: &user_profile/2
    field :subscribe_to_weekly_ello, :boolean, resolve: &user_profile/2
  end

  object :external_link do
    field :icon, :string
    field :type, :string
    field :text, :string
    field :url, :string
  end

  object :user_settings do
    field :posts_adult_content, :boolean
    field :has_commenting_enabled, :boolean
    field :has_reposting_enabled, :boolean
    field :has_sharing_enabled, :boolean
    field :has_loves_enabled, :boolean
    field :is_collaborateable, :boolean
    field :is_hireable, :boolean
  end

  object :user_stats do
    field :followers_count, :integer
    field :following_count, :integer
    field :loves_count, :integer
    field :posts_count, :integer
    field :total_views_count, :integer
  end

  object :user_meta_attributes do
    field :title, :string
    field :robots, :string
    field :image, :string
    field :description, :string
  end

  object :user_current_user_state do
    field :relationship_priority, :string, resolve: &relationship_priority/2
  end

  defp relationship_priority(_, %{source: %{id: id}, context: %{current_user: %{id: id}}}), do: {:ok, "self"}
  defp relationship_priority(_, %{source: %{relationship_to_current_user: nil}}), do: {:ok, nil}
  defp relationship_priority(_, %{source: %{relationship_to_current_user: %{priority: p}}}), do: {:ok, p}
  defp relationship_priority(_args, _resolution), do: {:ok, nil}

  defp experimental_features(_, %{source: %{is_staff: true}}), do: {:ok, true}
  defp experimental_features(_, %{source: %{has_experimental_features: true}}), do: {:ok, true}
  defp experimental_features(_, _), do: {:ok, false}

  defp user_meta(_, %{source: user}) do
    {:ok, %{
      title: User.title(user),
      robots: User.robots(user),
      image: image(user),
      description: User.seo_description(user),
    }}
  end

  defp user_profile(_, %{source: user, definition: %{schema_node: %{identifier: name}}}) do
    {:ok, Map.get(user.settings, name)}
  end

  defp image(user) do
    version = Enum.find(user.cover_image_struct.versions, &(&1.name == "optimized"))
    image_url(user.cover_image_struct.path, version.filename)
  end

  @sensitive_badges [
    "nsfw",
    "spam",
  ]

  defp user_badges(_, %{source: %{badges: nil}}), do: {:ok, []}
  defp user_badges(_, %{source: %{badges: []}}), do: {:ok, []}
  defp user_badges(_, %{source: %{badges: badges}, context: %{current_user: %{is_staff: true}}}),
    do: {:ok, badges}
  defp user_badges(_, %{source: %{badges: badges}}),
    do: {:ok, Enum.reject(badges, &(Enum.member?(@sensitive_badges, &1)))}
end

defmodule Ello.V3.Schema.NetworkTypes do
  import Ello.V3.Schema.Helpers
  use Absinthe.Schema.Notation
  alias Ello.V3.Resolvers
  alias Ello.Core.Network.User

  object :user do
    field :id, :id
    field :username, :string
    field :name, :string
    field :settings, :user_settings
    field :user_stats, :user_stats, resolve: &source_self/2
    field :location, :string
    field :formatted_short_bio, :string
    field :badges, list_of(:string), resolve: &user_badges/2
    field :experimental_features, :boolean, resolve: &experimental_features/2
    field :is_community, :boolean
    field :external_links_list, list_of(:external_link), resolve: fn(_args, %{source: user}) ->
      {:ok, user.rendered_links}
    end
    field :avatar, :tshirt_image_versions, resolve: fn(_args, %{source: user}) ->
      {:ok, user.avatar_struct}
    end
    field :cover_image, :responsive_image_versions, resolve: fn(_args, %{source: user}) ->
      {:ok, user.cover_image_struct}
    end
    field :current_user_state, :user_current_user_state, resolve: &source_self/2
    field :category_users, list_of(:category_user) do
      arg :roles, list_of(:category_user_role)
      resolve &Resolvers.CategoryUsers.call/3
    end
    field :meta_attributes, :user_meta_attributes, resolve: &user_meta/2
  end

  object :user_stream do
    field :next, :string
    field :per_page, :integer
    field :is_last_page, :boolean
    field :users, list_of(:user)
  end

  object :profile do
    field :name, :string
    field :username, :string
    field :email, :string
    field :location, :string
    field :formatted_short_bio, :string
    field :short_bio, :string
    field :web_onboarding_version, :string
    field :allows_analytics, :boolean, resolve: &user_profile/2
    field :discoverable, :boolean, resolve: &user_profile/2
    field :has_ad_notifications_enabled, :boolean, resolve: &user_profile/2
    field :has_announcements_enabled, :boolean, resolve: &user_profile/2
    field :has_auto_watch_enabled, :boolean, resolve: &user_profile/2
    field :has_reposting_enabled, :boolean, resolve: &user_profile/2
    field :has_sharing_enabled, :boolean, resolve: &user_profile/2
    field :is_collaborateable, :boolean, resolve: &user_profile/2
    field :is_hireable, :boolean, resolve: &user_profile/2
    field :notify_of_announcements_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_approved_submissions_from_following_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_approved_submissions_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_featured_category_post_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_featured_category_post_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_approved_submissions_from_following_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_comments_on_post_watch_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_comments_on_post_watch_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_comments_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_comments_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_invitation_acceptances_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_invitation_acceptances_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_loves_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_loves_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_mentions_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_mentions_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_new_followers_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_new_followers_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_reposts_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_reposts_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_watches_via_email, :boolean, resolve: &user_profile/2
    field :notify_of_watches_via_push, :boolean, resolve: &user_profile/2
    field :notify_of_what_you_missed_via_email, :boolean, resolve: &user_profile/2
    field :subscribe_to_daily_ello, :boolean, resolve: &user_profile/2
    field :subscribe_to_onboarding_drip, :boolean, resolve: &user_profile/2
    field :subscribe_to_users_email_list, :boolean, resolve: &user_profile/2
    field :subscribe_to_weekly_ello, :boolean, resolve: &user_profile/2
  end

  object :external_link do
    field :icon, :string
    field :type, :string
    field :text, :string
    field :url, :string
  end

  object :user_settings do
    field :posts_adult_content, :boolean
    field :has_commenting_enabled, :boolean
    field :has_reposting_enabled, :boolean
    field :has_sharing_enabled, :boolean
    field :has_loves_enabled, :boolean
    field :is_collaborateable, :boolean
    field :is_hireable, :boolean
  end

  object :user_stats do
    field :followers_count, :integer
    field :following_count, :integer
    field :loves_count, :integer
    field :posts_count, :integer
    field :total_views_count, :integer
  end

  object :user_meta_attributes do
    field :title, :string
    field :robots, :string
    field :image, :string
    field :description, :string
  end

  object :user_current_user_state do
    field :relationship_priority, :string, resolve: &relationship_priority/2
  end

  defp relationship_priority(_, %{source: %{id: id}, context: %{current_user: %{id: id}}}), do: {:ok, "self"}
  defp relationship_priority(_, %{source: %{relationship_to_current_user: nil}}), do: {:ok, nil}
  defp relationship_priority(_, %{source: %{relationship_to_current_user: %{priority: p}}}), do: {:ok, p}
  defp relationship_priority(_args, _resolution), do: {:ok, nil}

  defp experimental_features(_, %{source: %{is_staff: true}}), do: {:ok, true}
  defp experimental_features(_, %{source: %{has_experimental_features: true}}), do: {:ok, true}
  defp experimental_features(_, _), do: {:ok, false}

  defp user_meta(_, %{source: user}) do
    {:ok, %{
      title: User.title(user),
      robots: User.robots(user),
      image: image(user),
      description: User.seo_description(user),
    }}
  end

  defp user_profile(_, %{source: user, definition: %{name: name}}) do
    key = String.to_existing_atom(name)
    {:ok, Map.get(user.settings, key)}
  end

  defp image(user) do
    version = Enum.find(user.cover_image_struct.versions, &(&1.name == "optimized"))
    image_url(user.cover_image_struct.path, version.filename)
  end

  @sensitive_badges [
    "nsfw",
    "spam",
  ]

  defp user_badges(_, %{source: %{badges: nil}}), do: {:ok, []}
  defp user_badges(_, %{source: %{badges: []}}), do: {:ok, []}
  defp user_badges(_, %{source: %{badges: badges}, context: %{current_user: %{is_staff: true}}}),
    do: {:ok, badges}
  defp user_badges(_, %{source: %{badges: badges}}),
    do: {:ok, Enum.reject(badges, &(Enum.member?(@sensitive_badges, &1)))}
end


Regenerate Speech client (#403)

Regenerate Speech client (#403)

# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# NOTE: This class is auto generated by the swagger code generator program.
# https://github.com/swagger-api/swagger-codegen.git
# Do not edit the class manually.

defmodule GoogleApi.Speech.V1.Api.Operations do
  @moduledoc """
  API calls for all endpoints tagged `Operations`.
  """

  alias GoogleApi.Speech.V1.Connection
  alias GoogleApi.Gax.{Request, Response}

  @doc """
  Gets the latest state of a long-running operation.  Clients can use this method to poll the operation result at intervals as recommended by the API service.

  ## Parameters

  - connection (GoogleApi.Speech.V1.Connection): Connection to server
  - name (String.t): The name of the operation resource.
  - optional_params (KeywordList): [optional] Optional parameters
    - :$.xgafv (String.t): V1 error format.
    - :access_token (String.t): OAuth access token.
    - :alt (String.t): Data format for response.
    - :callback (String.t): JSONP
    - :fields (String.t): Selector specifying which fields to include in a partial response.
    - :key (String.t): API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.
    - :oauth_token (String.t): OAuth 2.0 token for the current user.
    - :prettyPrint (boolean()): Returns response with indentations and line breaks.
    - :quotaUser (String.t): Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.
    - :upload_protocol (String.t): Upload protocol for media (e.g. \"raw\", \"multipart\").
    - :uploadType (String.t): Legacy upload protocol for media (e.g. \"media\", \"multipart\").

  ## Returns

  {:ok, %GoogleApi.Speech.V1.Model.Operation{}} on success
  {:error, info} on failure
  """
  @spec speech_operations_get(Tesla.Env.client(), String.t(), keyword()) ::
          {:ok, GoogleApi.Speech.V1.Model.Operation.t()} | {:error, Tesla.Env.t()}
  def speech_operations_get(connection, name, optional_params \\ [], opts \\ []) do
    optional_params_config = %{
      :"$.xgafv" => :query,
      :access_token => :query,
      :alt => :query,
      :callback => :query,
      :fields => :query,
      :key => :query,
      :oauth_token => :query,
      :prettyPrint => :query,
      :quotaUser => :query,
      :upload_protocol => :query,
      :uploadType => :query
    }

    request =
      Request.new()
      |> Request.method(:get)
      |> Request.url("/v1/operations/{+name}", %{
        "name" => URI.encode_www_form(name)
      })
      |> Request.add_optional_params(optional_params_config, optional_params)

    connection
    |> Connection.execute(request)
    |> Response.decode(opts ++ [struct: %GoogleApi.Speech.V1.Model.Operation{}])
  end

  @doc """
  Lists operations that match the specified filter in the request. If the server doesn't support this method, it returns `UNIMPLEMENTED`.  NOTE: the `name` binding allows API services to override the binding to use different resource name schemes, such as `users/*/operations`. To override the binding, API services can add a binding such as `\"/v1/{name=users/*}/operations\"` to their service configuration. For backwards compatibility, the default name includes the operations collection id, however overriding users must ensure the name binding is the parent resource, without the operations collection id.

  ## Parameters

  - connection (GoogleApi.Speech.V1.Connection): Connection to server
  - optional_params (KeywordList): [optional] Optional parameters
    - :$.xgafv (String.t): V1 error format.
    - :access_token (String.t): OAuth access token.
    - :alt (String.t): Data format for response.
    - :callback (String.t): JSONP
    - :fields (String.t): Selector specifying which fields to include in a partial response.
    - :key (String.t): API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.
    - :oauth_token (String.t): OAuth 2.0 token for the current user.
    - :prettyPrint (boolean()): Returns response with indentations and line breaks.
    - :quotaUser (String.t): Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.
    - :upload_protocol (String.t): Upload protocol for media (e.g. \"raw\", \"multipart\").
    - :uploadType (String.t): Legacy upload protocol for media (e.g. \"media\", \"multipart\").
    - :filter (String.t): The standard list filter.
    - :name (String.t): The name of the operation's parent resource.
    - :pageSize (integer()): The standard list page size.
    - :pageToken (String.t): The standard list page token.

  ## Returns

  {:ok, %GoogleApi.Speech.V1.Model.ListOperationsResponse{}} on success
  {:error, info} on failure
  """
  @spec speech_operations_list(Tesla.Env.client(), keyword()) ::
          {:ok, GoogleApi.Speech.V1.Model.ListOperationsResponse.t()} | {:error, Tesla.Env.t()}
  def speech_operations_list(connection, optional_params \\ [], opts \\ []) do
    optional_params_config = %{
      :"$.xgafv" => :query,
      :access_token => :query,
      :alt => :query,
      :callback => :query,
      :fields => :query,
      :key => :query,
      :oauth_token => :query,
      :prettyPrint => :query,
      :quotaUser => :query,
      :upload_protocol => :query,
      :uploadType => :query,
      :filter => :query,
      :name => :query,
      :pageSize => :query,
      :pageToken => :query
    }

    request =
      Request.new()
      |> Request.method(:get)
      |> Request.url("/v1/operations")
      |> Request.add_optional_params(optional_params_config, optional_params)

    connection
    |> Connection.execute(request)
    |> Response.decode(opts ++ [struct: %GoogleApi.Speech.V1.Model.ListOperationsResponse{}])
  end
end

# Copyright 2017 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# NOTE: This class is auto generated by the swagger code generator program.
# https://github.com/swagger-api/swagger-codegen.git
# Do not edit the class manually.

defmodule GoogleApi.Speech.V1.Api.Operations do
  @moduledoc """
  API calls for all endpoints tagged `Operations`.
  """

  alias GoogleApi.Speech.V1.Connection
  alias GoogleApi.Gax.{Request, Response}

  @doc """
  Gets the latest state of a long-running operation.  Clients can use this method to poll the operation result at intervals as recommended by the API service.

  ## Parameters

  - connection (GoogleApi.Speech.V1.Connection): Connection to server
  - name (String.t): The name of the operation resource.
  - optional_params (KeywordList): [optional] Optional parameters
    - :$.xgafv (String.t): V1 error format.
    - :access_token (String.t): OAuth access token.
    - :alt (String.t): Data format for response.
    - :callback (String.t): JSONP
    - :fields (String.t): Selector specifying which fields to include in a partial response.
    - :key (String.t): API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.
    - :oauth_token (String.t): OAuth 2.0 token for the current user.
    - :prettyPrint (boolean()): Returns response with indentations and line breaks.
    - :quotaUser (String.t): Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.
    - :upload_protocol (String.t): Upload protocol for media (e.g. \"raw\", \"multipart\").
    - :uploadType (String.t): Legacy upload protocol for media (e.g. \"media\", \"multipart\").

  ## Returns

  {:ok, %GoogleApi.Speech.V1.Model.Operation{}} on success
  {:error, info} on failure
  """
  @spec speech_operations_get(Tesla.Env.client(), String.t(), keyword()) ::
          {:ok, GoogleApi.Speech.V1.Model.Operation.t()} | {:error, Tesla.Env.t()}
  def speech_operations_get(connection, name, optional_params \\ [], opts \\ []) do
    optional_params_config = %{
      :"$.xgafv" => :query,
      :access_token => :query,
      :alt => :query,
      :callback => :query,
      :fields => :query,
      :key => :query,
      :oauth_token => :query,
      :prettyPrint => :query,
      :quotaUser => :query,
      :upload_protocol => :query,
      :uploadType => :query
    }

    request =
      Request.new()
      |> Request.method(:get)
      |> Request.url("/v1/operations/{+name}", %{
        "name" => URI.encode_www_form(name)
      })
      |> Request.add_optional_params(optional_params_config, optional_params)

    connection
    |> Connection.execute(request)
    |> Response.decode(opts ++ [struct: %GoogleApi.Speech.V1.Model.Operation{}])
  end

  @doc """
  Lists operations that match the specified filter in the request. If the server doesn't support this method, it returns `UNIMPLEMENTED`.  NOTE: the `name` binding allows API services to override the binding to use different resource name schemes, such as `users/*/operations`. To override the binding, API services can add a binding such as `\"/v1/{name=users/*}/operations\"` to their service configuration. For backwards compatibility, the default name includes the operations collection id, however overriding users must ensure the name binding is the parent resource, without the operations collection id.

  ## Parameters

  - connection (GoogleApi.Speech.V1.Connection): Connection to server
  - optional_params (KeywordList): [optional] Optional parameters
    - :$.xgafv (String.t): V1 error format.
    - :access_token (String.t): OAuth access token.
    - :alt (String.t): Data format for response.
    - :callback (String.t): JSONP
    - :fields (String.t): Selector specifying which fields to include in a partial response.
    - :key (String.t): API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.
    - :oauth_token (String.t): OAuth 2.0 token for the current user.
    - :prettyPrint (boolean()): Returns response with indentations and line breaks.
    - :quotaUser (String.t): Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters.
    - :upload_protocol (String.t): Upload protocol for media (e.g. \"raw\", \"multipart\").
    - :uploadType (String.t): Legacy upload protocol for media (e.g. \"media\", \"multipart\").
    - :pageSize (integer()): The standard list page size.
    - :filter (String.t): The standard list filter.
    - :name (String.t): The name of the operation's parent resource.
    - :pageToken (String.t): The standard list page token.

  ## Returns

  {:ok, %GoogleApi.Speech.V1.Model.ListOperationsResponse{}} on success
  {:error, info} on failure
  """
  @spec speech_operations_list(Tesla.Env.client(), keyword()) ::
          {:ok, GoogleApi.Speech.V1.Model.ListOperationsResponse.t()} | {:error, Tesla.Env.t()}
  def speech_operations_list(connection, optional_params \\ [], opts \\ []) do
    optional_params_config = %{
      :"$.xgafv" => :query,
      :access_token => :query,
      :alt => :query,
      :callback => :query,
      :fields => :query,
      :key => :query,
      :oauth_token => :query,
      :prettyPrint => :query,
      :quotaUser => :query,
      :upload_protocol => :query,
      :uploadType => :query,
      :pageSize => :query,
      :filter => :query,
      :name => :query,
      :pageToken => :query
    }

    request =
      Request.new()
      |> Request.method(:get)
      |> Request.url("/v1/operations")
      |> Request.add_optional_params(optional_params_config, optional_params)

    connection
    |> Connection.execute(request)
    |> Response.decode(opts ++ [struct: %GoogleApi.Speech.V1.Model.ListOperationsResponse{}])
  end
end


Rule template trigger/actions config is not param

Also do not return __struct__ on rule listings.

require Logger

defmodule Serverboards.Rules do
  alias Serverboards.Rules.Model
  alias Serverboards.Rules
  alias Serverboards.Repo
  use GenServer

  def start_link options do
    {:ok, es} = EventSourcing.start_link( [name: :rules] ++ options )
    Serverboards.Rules.Rule.setup_eventsourcing(es)
    Serverboards.Rules.RPC.start_link
    GenServer.start_link(__MODULE__, :ok, name: Serverboards.Rules)
  end

  defp get_actions(rule_id) do
    import Ecto.Query
    Repo.all( from action in Model.ActionAtState,
       where: action.rule_id == ^rule_id,
      select: {
        action.state,  %{
          action: action.action,
          params: action.params
        }
      }
    ) |> Map.new
  end

  def list(), do: list([])
  def list(map) when is_map(map) do
    list(Map.to_list(map))
  end
  def list(filter) do
    import Ecto.Query
    alias Serverboards.Serverboard.Model.Serverboard
    alias Serverboards.Service.Model.Service

    Logger.debug("All rules: #{inspect Repo.all(from rule in Model.Rule)}")

    q = from rule in Model.Rule,
      left_join: service in Service,
              on: service.id == rule.service_id,
      left_join: serverboard in Serverboard,
              on: serverboard.id == rule.serverboard_id

    q = Enum.reduce(filter, q, fn
      {"serverboard", v}, q ->
        q |> where([_rule, _service, serverboard], serverboard.shortname == ^v )
      {:serverboard, v}, q ->
        q |> where([_rule, _service, serverboard], serverboard.shortname == ^v )
      {"uuid", v}, q ->
        q |> where([rule, _service, _serverboard], rule.uuid == ^v )
      {:uuid, v}, q ->
        q |> where([rule, _service, _serverboard], rule.uuid == ^v )
      end)
    q = q |> select( [rule, service, serverboard], %{
      id: rule.id,
      is_active: rule.is_active,
      uuid: rule.uuid,
      name: rule.name,
      description: rule.description,
      serverboard: serverboard.shortname,
      service: service.uuid,
      trigger: %{
        trigger: rule.trigger,
        params: rule.params
      }
    } )

    Repo.all(q)
      |> Enum.map( fn rule ->
        name = case Serverboards.Rules.Trigger.find(id: rule.trigger.trigger ) do
          [tr] -> tr.name
          [] -> ""
        end
        Map.merge(%Serverboards.Rules.Rule{},
          Map.merge(rule, %{
            actions: get_actions( rule.id ),
            trigger: Map.merge(rule.trigger, %{ name: name })
            }) |> Map.drop([:id])
          ) |> Map.from_struct
        end)
  end

  def rule_templates(filter \\ []) do
    Serverboards.Plugin.Registry.filter_component([type: "rule template"] ++ filter)
    |> Enum.map(fn %{ name: name, traits: traits, id: id, description: description, plugin: plugin, extra: extra} ->
      %{
        id: id,
        name: name,
        traits: traits,
        description: description,
        plugin: plugin,
        trigger: %{
          trigger: extra["trigger"]["trigger"],
          params: extra["trigger"]["params"]
        },
        actions: Map.new(Enum.map(Map.to_list(extra["actions"]), fn {k,v}->
          {k, %{
            params: v["params"],
            action: v["action"]
          } }
        end))
      }
    end)
  end

  def ps do
    GenServer.call(Serverboards.Rules, {:ps})
  end

  def ensure_rule_active(%Serverboards.Rules.Rule{} = rule) do
    GenServer.call(Serverboards.Rules, {:ensure_rule_active, rule})

  end
  def ensure_rule_not_active(%Serverboards.Rules.Rule{} = rule) do
    GenServer.call(Serverboards.Rules, {:ensure_rule_not_active, rule})

  end

  # server impl
  def init(:ok) do
    {:ok, %{}}
  end

  def handle_call({:ps}, _from, status) do
    {:reply, Map.keys(status), status}
  end
  def handle_call({:ensure_rule_active, %Serverboards.Rules.Rule{} = rule}, _from, status) do
    status = if not Map.has_key?(status, rule.uuid) do
      {:ok, trigger} = Rules.Rule.start_link(rule)
      Map.put(status, rule.uuid, trigger)
    end
    #Logger.debug("Ensure active #{inspect rule.uuid} #{inspect status}")
    {:reply, :ok, status}
  end
  def handle_call({:ensure_rule_not_active, %Serverboards.Rules.Rule{} = rule}, _from, status) do
    #Logger.debug("Ensure not active #{inspect rule.uuid} #{inspect status}")
    status = if Map.has_key?(status, rule.uuid) do
      trigger = status[rule.uuid]
      Rules.Rule.stop(trigger)
      Map.drop(status, [rule.uuid])
    else
      status
    end

    {:reply, :ok, status}
  end
end

require Logger

defmodule Serverboards.Rules do
  alias Serverboards.Rules.Model
  alias Serverboards.Rules
  alias Serverboards.Repo
  use GenServer

  def start_link options do
    {:ok, es} = EventSourcing.start_link( [name: :rules] ++ options )
    Serverboards.Rules.Rule.setup_eventsourcing(es)
    Serverboards.Rules.RPC.start_link
    GenServer.start_link(__MODULE__, :ok, name: Serverboards.Rules)
  end

  defp get_actions(rule_id) do
    import Ecto.Query
    Repo.all( from action in Model.ActionAtState,
       where: action.rule_id == ^rule_id,
      select: {
        action.state,  %{
          action: action.action,
          params: action.params
        }
      }
    ) |> Map.new
  end

  def list(), do: list([])
  def list(map) when is_map(map) do
    list(Map.to_list(map))
  end
  def list(filter) do
    import Ecto.Query
    alias Serverboards.Serverboard.Model.Serverboard
    alias Serverboards.Service.Model.Service

    Logger.debug("All rules: #{inspect Repo.all(from rule in Model.Rule)}")

    q = from rule in Model.Rule,
      left_join: service in Service,
              on: service.id == rule.service_id,
      left_join: serverboard in Serverboard,
              on: serverboard.id == rule.serverboard_id

    q = Enum.reduce(filter, q, fn
      {"serverboard", v}, q ->
        q |> where([_rule, _service, serverboard], serverboard.shortname == ^v )
      {:serverboard, v}, q ->
        q |> where([_rule, _service, serverboard], serverboard.shortname == ^v )
      {"uuid", v}, q ->
        q |> where([rule, _service, _serverboard], rule.uuid == ^v )
      {:uuid, v}, q ->
        q |> where([rule, _service, _serverboard], rule.uuid == ^v )
      end)
    q = q |> select( [rule, service, serverboard], %{
      id: rule.id,
      is_active: rule.is_active,
      uuid: rule.uuid,
      name: rule.name,
      description: rule.description,
      serverboard: serverboard.shortname,
      service: service.uuid,
      trigger: %{
        trigger: rule.trigger,
        params: rule.params
      }
    } )

    Repo.all(q)
      |> Enum.map( fn rule ->
        name = case Serverboards.Rules.Trigger.find(id: rule.trigger.trigger ) do
          [tr] -> tr.name
          [] -> ""
        end
        Map.merge(%Serverboards.Rules.Rule{},
          Map.merge(rule, %{
            actions: get_actions( rule.id ),
            trigger: Map.merge(rule.trigger, %{ name: name })
            }) |> Map.drop([:id])
          )
        end)
  end

  def rule_templates(filter \\ []) do
    Serverboards.Plugin.Registry.filter_component([type: "rule template"] ++ filter)
    |> Enum.map(fn %{ name: name, traits: traits, id: id, description: description, plugin: plugin, extra: extra} ->
      %{
        id: id,
        name: name,
        traits: traits,
        description: description,
        plugin: plugin,
        trigger: %{
          id: extra["trigger"]["id"],
          config: extra["trigger"]["config"]
        },
        actions: Map.new(Enum.map(Map.to_list(extra["actions"]), fn {k,v}->
          {k, %{
            config: v["config"],
            id: v["id"]
          } }
        end))
      }
    end)
  end

  def ps do
    GenServer.call(Serverboards.Rules, {:ps})
  end

  def ensure_rule_active(%Serverboards.Rules.Rule{} = rule) do
    GenServer.call(Serverboards.Rules, {:ensure_rule_active, rule})

  end
  def ensure_rule_not_active(%Serverboards.Rules.Rule{} = rule) do
    GenServer.call(Serverboards.Rules, {:ensure_rule_not_active, rule})

  end

  # server impl
  def init(:ok) do
    {:ok, %{}}
  end

  def handle_call({:ps}, _from, status) do
    {:reply, Map.keys(status), status}
  end
  def handle_call({:ensure_rule_active, %Serverboards.Rules.Rule{} = rule}, _from, status) do
    status = if not Map.has_key?(status, rule.uuid) do
      {:ok, trigger} = Rules.Rule.start_link(rule)
      Map.put(status, rule.uuid, trigger)
    end
    #Logger.debug("Ensure active #{inspect rule.uuid} #{inspect status}")
    {:reply, :ok, status}
  end
  def handle_call({:ensure_rule_not_active, %Serverboards.Rules.Rule{} = rule}, _from, status) do
    #Logger.debug("Ensure not active #{inspect rule.uuid} #{inspect status}")
    status = if Map.has_key?(status, rule.uuid) do
      trigger = status[rule.uuid]
      Rules.Rule.stop(trigger)
      Map.drop(status, [rule.uuid])
    else
      status
    end

    {:reply, :ok, status}
  end
end


Don't check if the player list is nil.

Don't check if the player list is nil.

defmodule SunshinesBattleacademy.Web.LobbyChannel do
  use Phoenix.Channel
  require Logger

  # handles the special `"lobby"` subtopic
  def join("room:lobby", message, socket) do
    # init(%{socket: socket})
    ConCache.put(:game_map, :player_list, Map.new)
    send(self, :after_join)
    {:ok, socket}
  end

  def terminate(reason, socket) do
    ConCache.update(:game_map, :player_list, fn(old_value) ->
        {:ok, Map.delete(old_value, socket.assigns[:user_id])}
    end)
    Logger.debug"> leave #{inspect reason}"
    :ok
  end

  def handle_info(:after_join, socket) do
    uid = UUID.uuid4
    push socket, "welcome", %{id: uid}
    ConCache.put(:game_map, socket.assigns[:user_id], %{id: uid})
    {:noreply, socket}
  end

  def handle_in("gotit", %{"nickname" => nickname, "hue" => hue}, socket) do
    ConCache.update_existing(:game_map, socket.assigns[:user_id], fn(old_player) ->
      ConCache.update(:game_map, :player_list, fn(old_value) ->
          {:ok, Map.put(old_value, socket.assigns[:user_id], old_player[:id])}
      end)
      {:ok, %{id: old_player[:id], nickname: nickname, hue: hue, target: %{x: 0,y: 0}, position: %{x: 0,y: 0}}}
    end)
    {:noreply, socket}
  end

  def handle_in("movement", %{"target" => %{"x" => tx, "y" => ty}}, socket) do
    Logger.debug inspect socket.assigns[:user_id]
    ConCache.update_existing(:game_map, socket.assigns[:user_id], fn(old_value) ->
      {:ok, %{old_value | target: %{x: tx, y: ty}}}
    end)

    players = ConCache.get(:game_map, :player_list)
    #Logger.debug inspect players
    map = for n <- Map.to_list(players) do
      #Logger.debug inspect n
      {user_id, id} = n

      ConCache.update_existing(:game_map, user_id, fn(elem) ->
        #Logger.debug inspect elem
        # normalize the target direction, if necessary, and add to position
        tx = elem.target[:x] / 10
        ty = elem.target[:y] / 10
        length = :math.sqrt((tx*tx) + (ty*ty))
        target = if length > 15 do
          %{x: (tx / length) * 15, y: (ty / length) * 15}
        else
          %{x: tx, y: ty}
        end
        new_position = %{x: elem.position[:x] + target[:x], y: elem.position[:y] + target[:y]}
        {:ok, %{elem | position: new_position}}
      end)

      ConCache.get(:game_map, user_id)[:position]
      # Use target to calculate a new position for this player
    end
    push socket, "state_update", %{map: map}
    {:noreply, socket}
  end

  def init(state) do
    #schedule_work()
    {:ok, state}
  end

  # def handle_info(:work, state) do
  #   schedule_work() # Reschedule once more
  #   {:noreply, state.socket}
  # end

  # defp schedule_work() do
  #   Process.send_after(self(), :work, div(1000, 10))
  # end
end

defmodule SunshinesBattleacademy.Web.LobbyChannel do
  use Phoenix.Channel
  require Logger

  # handles the special `"lobby"` subtopic
  def join("room:lobby", message, socket) do
    # init(%{socket: socket})
    ConCache.put(:game_map, :player_list, Map.new)
    send(self, :after_join)
    {:ok, socket}
  end

  def terminate(reason, socket) do
    ConCache.update(:game_map, :player_list, fn(old_value) ->
      case old_value do
        nil -> {:ok, nil}
        value -> {:ok, Map.delete(old_value, socket.assigns[:user_id])}
      end
    end)
    Logger.debug"> leave #{inspect reason}"
    :ok
  end

  def handle_info(:after_join, socket) do
    uid = UUID.uuid4
    push socket, "welcome", %{id: uid}
    ConCache.put(:game_map, socket.assigns[:user_id], %{id: uid})
    {:noreply, socket}
  end

  def handle_in("gotit", %{"nickname" => nickname, "hue" => hue}, socket) do
    ConCache.update_existing(:game_map, socket.assigns[:user_id], fn(old_player) ->
      ConCache.update(:game_map, :player_list, fn(old_value) ->
        if old_value == nil do
          {:ok, Map.put(Map.new, socket.assigns[:user_id], old_player[:id])}
        else
          {:ok, Map.put(old_value, socket.assigns[:user_id], old_player[:id])}
        end
      end)

      {:ok, %{id: old_player[:id], nickname: nickname, hue: hue, target: %{x: 0,y: 0}, position: %{x: 0,y: 0}}}
    end)
    {:noreply, socket}
  end

  def handle_in("movement", %{"target" => %{"x" => tx, "y" => ty}}, socket) do
    Logger.debug inspect socket.assigns[:user_id]
    ConCache.update_existing(:game_map, socket.assigns[:user_id], fn(old_value) ->
      {:ok, %{old_value | target: %{x: tx, y: ty}}}
    end)

    players = ConCache.get(:game_map, :player_list)
    #Logger.debug inspect players
    map = for n <- Map.to_list(players) do
      #Logger.debug inspect n
      {user_id, id} = n

      ConCache.update_existing(:game_map, user_id, fn(elem) ->
        #Logger.debug inspect elem
        # normalize the target direction, if necessary, and add to position
        tx = elem.target[:x] / 10
        ty = elem.target[:y] / 10
        length = :math.sqrt((tx*tx) + (ty*ty))
        target = if length > 15 do
          %{x: (tx / length) * 15, y: (ty / length) * 15}
        else
          %{x: tx, y: ty}
        end
        new_position = %{x: elem.position[:x] + target[:x], y: elem.position[:y] + target[:y]}
        {:ok, %{elem | position: new_position}}
      end)

      ConCache.get(:game_map, user_id)[:position]
      # Use target to calculate a new position for this player
    end
    push socket, "state_update", %{map: map}
    {:noreply, socket}
  end

  def init(state) do
    #schedule_work()
    {:ok, state}
  end

  # def handle_info(:work, state) do
  #   schedule_work() # Reschedule once more
  #   {:noreply, state.socket}
  # end

  # defp schedule_work() do
  #   Process.send_after(self(), :work, div(1000, 10))
  # end
end