Ignore default beer label.

Ignore default beer label.

defmodule Slacktapped.Checkins do
  @instance_name System.get_env("INSTANCE_NAME") ||
    Application.get_env(:slacktapped, :instance_name)
  @redis Application.get_env(:slacktapped, :redis)

  @doc """
  Parses a checkin into an attachment for Slack.
  """
  def process_checkin(checkin) do
    parse_checkin(checkin)
      |> Slacktapped.add_attachment(checkin)
      |> Slacktapped.Checkins.report_checkin_type
  end

  @doc ~S"""
  Parses a checkin into an attachment for Slack.

  ## Examples

  A typical checkin with a rating and comment:

      iex> Slacktapped.Checkins.parse_checkin(%{
      ...>   "attachments" => [],
      ...>   "user" => %{
      ...>     "user_name" => "nicksergeant",
      ...>     "user_avatar" => "http://path/to/user/avatar"
      ...>   },
      ...>   "beer" => %{
      ...>     "bid" => 123,
      ...>     "beer_abv" => 4.5,
      ...>     "beer_label" => "http://path/to/beer/label",
      ...>     "beer_name" => "IPA",
      ...>     "beer_slug" => "two-lake-ipa",
      ...>     "beer_style" => "American IPA"
      ...>   },
      ...>   "brewery" => %{
      ...>     "brewery_id" => 1,
      ...>     "brewery_label" => "http://path/to/brewery/label",
      ...>     "brewery_name" => "Two Lake"
      ...>   },
      ...>   "checkin_comment" => "Lovely!",
      ...>   "checkin_id" => 567,
      ...>   "rating_score" => 3.5
      ...> })
      {:ok,
        %{
          "author_icon" => "http://path/to/user/avatar",
          "author_link" => "https://untappd.com/user/nicksergeant",
          "author_name" => "nicksergeant",
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/1|Two Lake>",
          "footer_icon" => "http://path/to/brewery/label",
          "image_url" => "http://path/to/beer/label",
          "text" => "" <>
            "<https://untappd.com/user/nicksergeant|nicksergeant> is drinking " <>
            "<https://untappd.com/b/two-lake-ipa/123|IPA> " <>
            "(American IPA, 4.5% ABV)." <>
            "\nThey rated it a 3.5 and said \"Lovely!\" " <>
            "<https://untappd.com/user/nicksergeant/checkin/567|Toast »>",
          "title" => "IPA",
          "title_link" => "https://untappd.com/b/two-lake-ipa/123"
        }
      }

  A checkin without a rating:

      iex> Slacktapped.Checkins.parse_checkin(%{
      ...>   "attachments" => [],
      ...>   "checkin_comment" => "Lovely!"
      ...> })
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => nil,
          "text" => "" <>
            "<https://untappd.com/user/|> is drinking " <>
            "<https://untappd.com/b//|> (, % ABV)." <>
            "\nThey said \"Lovely!\" " <>
            "<https://untappd.com/user//checkin/|Toast »>",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  A checkin without a comment:

      iex> Slacktapped.Checkins.parse_checkin(%{"rating_score" => 1.5})
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => nil,
          "text" => "" <>
            "<https://untappd.com/user/|> is drinking " <>
            "<https://untappd.com/b//|> (, % ABV)." <>
            "\nThey rated it a 1.5. " <>
            "<https://untappd.com/user//checkin/|Toast »>",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  A checkin without a comment or rating:

      iex> Slacktapped.Checkins.parse_checkin(%{})
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => nil,
          "text" => "" <>
            "<https://untappd.com/user/|> is drinking " <>
            "<https://untappd.com/b//|> (, % ABV). " <>
            "<https://untappd.com/user//checkin/|Toast »>",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  A checkin with an image:

      iex> Slacktapped.Checkins.parse_checkin(%{
      ...>   "media" => %{
      ...>     "items" => [
      ...>       %{
      ...>         "photo" => %{
      ...>           "photo_id" => 987,
      ...>           "photo_img_lg" => "http://path/to/beer/image"
      ...>         }
      ...>       }
      ...>     ]
      ...>   }
      ...> })
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => "http://path/to/beer/image",
          "text" => "" <>
            "<https://untappd.com/user/|> is drinking " <>
            "<https://untappd.com/b//|> (, % ABV). " <>
            "<https://untappd.com/user//checkin/|Toast »>",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  A checkin with a venue:

      iex> Slacktapped.Checkins.parse_checkin(%{
      ...>   "attachments" => [],
      ...>   "venue" => %{
      ...>     "venue_id" => 789,
      ...>     "venue_name" => "Venue Name",
      ...>     "venue_slug" => "venue-slug"
      ...>   }
      ...> })
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => nil,
          "text" => "" <>
            "<https://untappd.com/user/|> is drinking " <>
            "<https://untappd.com/b//|> (, % ABV) " <>
            "at <https://untappd.com/v/venue-slug/789|Venue Name>. " <>
            "<https://untappd.com/user//checkin/|Toast »>",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  A checkin with an image, with a previously-posted checkin with no image:

      iex> Slacktapped.Checkins.parse_checkin(%{
      ...>   "checkin_id" => 5566,
      ...>   "media" => %{
      ...>     "items" => [
      ...>       %{
      ...>         "photo" => %{
      ...>           "photo_id" => 987,
      ...>           "photo_img_lg" => "http://path/to/beer/image"
      ...>         }
      ...>       }
      ...>     ]
      ...>   }
      ...> })
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => "http://path/to/beer/image",
          "text" => "" <>
            "<https://untappd.com/user/|> added an image to " <>
            "<https://untappd.com/user//checkin/5566|their checkin> of " <>
            "<https://untappd.com/b//|>.",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  The default Untappd beer label is ignored:

      iex> Slacktapped.Checkins.parse_checkin(%{
      ...>   "attachments" => [],
      ...>   "beer" => %{
      ...>     "beer_label" => "https://untappd.akamaized.net/site/assets/images/temp/badge-beer-default.png"
      ...>   }
      ...> })
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => "",
          "text" => "" <>
            "<https://untappd.com/user/|> is drinking " <>
            "<https://untappd.com/b//|> (, % ABV). " <>
            "<https://untappd.com/user//checkin/|Toast »>",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  """
  def parse_checkin(checkin) do
    {:ok, user_name} = Slacktapped.parse_name(checkin["user"])

    beer_abv = checkin["beer"]["beer_abv"]
    beer_id = checkin["beer"]["bid"]
    beer_label = checkin["beer"]["beer_label"]
    beer_name = checkin["beer"]["beer_name"]
    beer_slug = checkin["beer"]["beer_slug"]
    beer_style = checkin["beer"]["beer_style"]
    brewery_id = checkin["brewery"]["brewery_id"]
    brewery_name = checkin["brewery"]["brewery_name"]
    brewery_label = checkin["brewery"]["brewery_label"]
    checkin_comment = checkin["checkin_comment"]
    checkin_id = checkin["checkin_id"]
    checkin_rating = checkin["rating_score"]
    media_items = checkin["media"]["items"]
    user_avatar = checkin["user"]["user_avatar"]
    user_username = checkin["user"]["user_name"]

    beer = "<https://untappd.com/b/#{beer_slug}/#{beer_id}|#{beer_name}>"
    toast = "<https://untappd.com/user/#{user_username}/checkin/#{checkin_id}|Toast »>"
    user = "<https://untappd.com/user/#{user_username}|#{user_name}>"

    rating_and_comment = cond do
      is_binary(checkin_comment)
        and checkin_comment != ""
        and is_number(checkin_rating) ->
          "\nThey rated it a #{checkin_rating} and said \"#{checkin_comment}\""
      is_binary(checkin_comment)
        and checkin_comment != "" ->
          "\nThey said \"#{checkin_comment}\""
      is_number(checkin_rating) ->
        "\nThey rated it a #{checkin_rating}."
      true -> ""
    end

    venue = if is_map(checkin["venue"]) do
      venue_id = checkin["venue"]["venue_id"]
      venue_name = checkin["venue"]["venue_name"]
      venue_slug = checkin["venue"]["venue_slug"]
      " at <https://untappd.com/v/#{venue_slug}/#{venue_id}|#{venue_name}>"
    end

    image_url = cond do
      is_list(media_items) and Enum.count(media_items) >= 1 ->
        media_items
          |> Enum.at(0)
          |> get_in(["photo", "photo_img_lg"])
      beer_label != "https://untappd.akamaized.net/site/assets/images/temp/badge-beer-default.png" ->
        beer_label
      true -> ""
    end

    # If we have an image and there was already a post for this checkin
    # *without* an image, only indicate that an image was added.
    cmd = "GET #{@instance_name}:#{checkin_id}:without-image"
    text = if is_binary(image_url) and
        @redis.command(cmd) == {:ok, "1"} do
      "#{user} added an image to " <>
      "<https://untappd.com/user/#{user_username}/checkin/#{checkin_id}|" <>
      "their checkin> of #{beer}."
    else
      "#{user} is drinking #{beer} (#{beer_style}, #{beer_abv}% ABV)" <>
      "#{venue}.#{rating_and_comment} #{toast}"
    end

    {:ok, %{
      "author_icon" => user_avatar,
      "author_link" => "https://untappd.com/user/#{user_username}",
      "author_name" => user_username,
      "color" => "#FFCF0B",
      "fallback" => "Image of this checkin.",
      "footer" => "<https://untappd.com/brewery/#{brewery_id}|#{brewery_name}>",
      "footer_icon" => brewery_label,
      "image_url" => image_url,
      "text" => text,
      "title" => beer_name,
      "title_link" => "https://untappd.com/b/#{beer_slug}/#{beer_id}"
    }}
  end

  @doc """
  Reports that a checkin was posted to Slack by setting a Redis key. There are
  two possible Redis keys indicating a post was made:

  1. <slacktapped-instance-name>:<checkin_id>:with-image
  2. <slacktapped-instance-name>:<checkin_id>:without-image

  Already-posted checkins that were previously posted without an image are
  posted again indicating that the user added an image to the checkin. If a
  record exists for the checkin with an image, the checkin is not posted again.

  Examples

      iex> Slacktapped.Checkins.report_checkin_type({:ok, %{}})
      {:ok, %{"reported_as" => "without-image"}}

      iex> Slacktapped.Checkins.report_checkin_type({:ok, %{
      ...>   "media" => %{
      ...>     "items" => [
      ...>       %{
      ...>         "photo" => %{
      ...>           "photo_id" => 987,
      ...>           "photo_img_lg" => "http://path/to/beer/image"
      ...>         }
      ...>       }
      ...>     ]
      ...>   }
      ...> }})
      {:ok, %{
        "reported_as" => "with-image",
        "media" => %{
          "items" => [
            %{
              "photo" => %{
                "photo_id" => 987,
                "photo_img_lg" => "http://path/to/beer/image"
              }
            }
          ]
        }
      }}

  """
  def report_checkin_type({:ok, checkin}) do
    checkin_id = checkin["checkin_id"]
    media_items = checkin["media"]["items"]
    has_image = is_list(media_items) and Enum.count(media_items) >= 1

    reported_as = cond do
      has_image == true ->
        @redis.command("SET #{@instance_name}:#{checkin_id}:with-image 1")
        "with-image"
      true ->
        @redis.command("SET #{@instance_name}:#{checkin_id}:without-image 1")
        "without-image"
    end

    checkin = Map.put(checkin, "reported_as", reported_as)

    {:ok, checkin}
  end
end

defmodule Slacktapped.Checkins do
  @instance_name System.get_env("INSTANCE_NAME") ||
    Application.get_env(:slacktapped, :instance_name)
  @redis Application.get_env(:slacktapped, :redis)

  @doc """
  Parses a checkin into an attachment for Slack.
  """
  def process_checkin(checkin) do
    parse_checkin(checkin)
      |> Slacktapped.add_attachment(checkin)
      |> Slacktapped.Checkins.report_checkin_type
  end

  @doc ~S"""
  Parses a checkin into an attachment for Slack.

  ## Examples

  A typical checkin with a rating and comment:

      iex> Slacktapped.Checkins.parse_checkin(%{
      ...>   "attachments" => [],
      ...>   "user" => %{
      ...>     "user_name" => "nicksergeant",
      ...>     "user_avatar" => "http://path/to/user/avatar"
      ...>   },
      ...>   "beer" => %{
      ...>     "bid" => 123,
      ...>     "beer_abv" => 4.5,
      ...>     "beer_label" => "http://path/to/beer/label",
      ...>     "beer_name" => "IPA",
      ...>     "beer_slug" => "two-lake-ipa",
      ...>     "beer_style" => "American IPA"
      ...>   },
      ...>   "brewery" => %{
      ...>     "brewery_id" => 1,
      ...>     "brewery_label" => "http://path/to/brewery/label",
      ...>     "brewery_name" => "Two Lake"
      ...>   },
      ...>   "checkin_comment" => "Lovely!",
      ...>   "checkin_id" => 567,
      ...>   "rating_score" => 3.5
      ...> })
      {:ok,
        %{
          "author_icon" => "http://path/to/user/avatar",
          "author_link" => "https://untappd.com/user/nicksergeant",
          "author_name" => "nicksergeant",
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/1|Two Lake>",
          "footer_icon" => "http://path/to/brewery/label",
          "image_url" => "http://path/to/beer/label",
          "text" => "" <>
            "<https://untappd.com/user/nicksergeant|nicksergeant> is drinking " <>
            "<https://untappd.com/b/two-lake-ipa/123|IPA> " <>
            "(American IPA, 4.5% ABV)." <>
            "\nThey rated it a 3.5 and said \"Lovely!\" " <>
            "<https://untappd.com/user/nicksergeant/checkin/567|Toast »>",
          "title" => "IPA",
          "title_link" => "https://untappd.com/b/two-lake-ipa/123"
        }
      }

  A checkin without a rating:

      iex> Slacktapped.Checkins.parse_checkin(%{
      ...>   "attachments" => [],
      ...>   "checkin_comment" => "Lovely!"
      ...> })
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => nil,
          "text" => "" <>
            "<https://untappd.com/user/|> is drinking " <>
            "<https://untappd.com/b//|> (, % ABV)." <>
            "\nThey said \"Lovely!\" " <>
            "<https://untappd.com/user//checkin/|Toast »>",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  A checkin without a comment:

      iex> Slacktapped.Checkins.parse_checkin(%{"rating_score" => 1.5})
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => nil,
          "text" => "" <>
            "<https://untappd.com/user/|> is drinking " <>
            "<https://untappd.com/b//|> (, % ABV)." <>
            "\nThey rated it a 1.5. " <>
            "<https://untappd.com/user//checkin/|Toast »>",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  A checkin without a comment or rating:

      iex> Slacktapped.Checkins.parse_checkin(%{})
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => nil,
          "text" => "" <>
            "<https://untappd.com/user/|> is drinking " <>
            "<https://untappd.com/b//|> (, % ABV). " <>
            "<https://untappd.com/user//checkin/|Toast »>",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  A checkin with an image:

      iex> Slacktapped.Checkins.parse_checkin(%{
      ...>   "media" => %{
      ...>     "items" => [
      ...>       %{
      ...>         "photo" => %{
      ...>           "photo_id" => 987,
      ...>           "photo_img_lg" => "http://path/to/beer/image"
      ...>         }
      ...>       }
      ...>     ]
      ...>   }
      ...> })
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => "http://path/to/beer/image",
          "text" => "" <>
            "<https://untappd.com/user/|> is drinking " <>
            "<https://untappd.com/b//|> (, % ABV). " <>
            "<https://untappd.com/user//checkin/|Toast »>",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  A checkin with a venue:

      iex> Slacktapped.Checkins.parse_checkin(%{
      ...>   "attachments" => [],
      ...>   "venue" => %{
      ...>     "venue_id" => 789,
      ...>     "venue_name" => "Venue Name",
      ...>     "venue_slug" => "venue-slug"
      ...>   }
      ...> })
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => nil,
          "text" => "" <>
            "<https://untappd.com/user/|> is drinking " <>
            "<https://untappd.com/b//|> (, % ABV) " <>
            "at <https://untappd.com/v/venue-slug/789|Venue Name>. " <>
            "<https://untappd.com/user//checkin/|Toast »>",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  A checkin with an image, with a previously-posted checkin with no image:

      iex> Slacktapped.Checkins.parse_checkin(%{
      ...>   "checkin_id" => 5566,
      ...>   "media" => %{
      ...>     "items" => [
      ...>       %{
      ...>         "photo" => %{
      ...>           "photo_id" => 987,
      ...>           "photo_img_lg" => "http://path/to/beer/image"
      ...>         }
      ...>       }
      ...>     ]
      ...>   }
      ...> })
      {:ok,
        %{
          "author_icon" => nil,
          "author_link" => "https://untappd.com/user/",
          "author_name" => nil,
          "color" => "#FFCF0B",
          "fallback" => "Image of this checkin.",
          "footer" => "<https://untappd.com/brewery/|>",
          "footer_icon" => nil,
          "image_url" => "http://path/to/beer/image",
          "text" => "" <>
            "<https://untappd.com/user/|> added an image to " <>
            "<https://untappd.com/user//checkin/5566|their checkin> of " <>
            "<https://untappd.com/b//|>.",
          "title" => nil,
          "title_link" => "https://untappd.com/b//"
        }
      }

  """
  def parse_checkin(checkin) do
    {:ok, user_name} = Slacktapped.parse_name(checkin["user"])

    beer_abv = checkin["beer"]["beer_abv"]
    beer_id = checkin["beer"]["bid"]
    beer_label = checkin["beer"]["beer_label"]
    beer_name = checkin["beer"]["beer_name"]
    beer_slug = checkin["beer"]["beer_slug"]
    beer_style = checkin["beer"]["beer_style"]
    brewery_id = checkin["brewery"]["brewery_id"]
    brewery_name = checkin["brewery"]["brewery_name"]
    brewery_label = checkin["brewery"]["brewery_label"]
    checkin_comment = checkin["checkin_comment"]
    checkin_id = checkin["checkin_id"]
    checkin_rating = checkin["rating_score"]
    media_items = checkin["media"]["items"]
    user_avatar = checkin["user"]["user_avatar"]
    user_username = checkin["user"]["user_name"]

    beer = "<https://untappd.com/b/#{beer_slug}/#{beer_id}|#{beer_name}>"
    toast = "<https://untappd.com/user/#{user_username}/checkin/#{checkin_id}|Toast »>"
    user = "<https://untappd.com/user/#{user_username}|#{user_name}>"

    rating_and_comment = cond do
      is_binary(checkin_comment)
        and checkin_comment != ""
        and is_number(checkin_rating) ->
          "\nThey rated it a #{checkin_rating} and said \"#{checkin_comment}\""
      is_binary(checkin_comment)
        and checkin_comment != "" ->
          "\nThey said \"#{checkin_comment}\""
      is_number(checkin_rating) ->
        "\nThey rated it a #{checkin_rating}."
      true -> ""
    end

    venue = if is_map(checkin["venue"]) do
      venue_id = checkin["venue"]["venue_id"]
      venue_name = checkin["venue"]["venue_name"]
      venue_slug = checkin["venue"]["venue_slug"]
      " at <https://untappd.com/v/#{venue_slug}/#{venue_id}|#{venue_name}>"
    end

    image_url = if is_list(media_items) and Enum.count(media_items) >= 1 do
      media_items
        |> Enum.at(0)
        |> get_in(["photo", "photo_img_lg"])
    else
      beer_label
    end

    # If we have an image and there was already a post for this checkin
    # *without* an image, only indicate that an image was added.
    cmd = "GET #{@instance_name}:#{checkin_id}:without-image"
    text = if is_binary(image_url) and
        @redis.command(cmd) == {:ok, "1"} do
      "#{user} added an image to " <>
      "<https://untappd.com/user/#{user_username}/checkin/#{checkin_id}|" <>
      "their checkin> of #{beer}."
    else
      "#{user} is drinking #{beer} (#{beer_style}, #{beer_abv}% ABV)" <>
      "#{venue}.#{rating_and_comment} #{toast}"
    end

    {:ok, %{
      "author_icon" => user_avatar,
      "author_link" => "https://untappd.com/user/#{user_username}",
      "author_name" => user_username,
      "color" => "#FFCF0B",
      "fallback" => "Image of this checkin.",
      "footer" => "<https://untappd.com/brewery/#{brewery_id}|#{brewery_name}>",
      "footer_icon" => brewery_label,
      "image_url" => image_url,
      "text" => text,
      "title" => beer_name,
      "title_link" => "https://untappd.com/b/#{beer_slug}/#{beer_id}"
    }}
  end

  @doc """
  Reports that a checkin was posted to Slack by setting a Redis key. There are
  two possible Redis keys indicating a post was made:

  1. <slacktapped-instance-name>:<checkin_id>:with-image
  2. <slacktapped-instance-name>:<checkin_id>:without-image

  Already-posted checkins that were previously posted without an image are
  posted again indicating that the user added an image to the checkin. If a
  record exists for the checkin with an image, the checkin is not posted again.

  Examples

      iex> Slacktapped.Checkins.report_checkin_type({:ok, %{}})
      {:ok, %{"reported_as" => "without-image"}}

      iex> Slacktapped.Checkins.report_checkin_type({:ok, %{
      ...>   "media" => %{
      ...>     "items" => [
      ...>       %{
      ...>         "photo" => %{
      ...>           "photo_id" => 987,
      ...>           "photo_img_lg" => "http://path/to/beer/image"
      ...>         }
      ...>       }
      ...>     ]
      ...>   }
      ...> }})
      {:ok, %{
        "reported_as" => "with-image",
        "media" => %{
          "items" => [
            %{
              "photo" => %{
                "photo_id" => 987,
                "photo_img_lg" => "http://path/to/beer/image"
              }
            }
          ]
        }
      }}

  """
  def report_checkin_type({:ok, checkin}) do
    checkin_id = checkin["checkin_id"]
    media_items = checkin["media"]["items"]
    has_image = is_list(media_items) and Enum.count(media_items) >= 1

    reported_as = cond do
      has_image == true ->
        @redis.command("SET #{@instance_name}:#{checkin_id}:with-image 1")
        "with-image"
      true ->
        @redis.command("SET #{@instance_name}:#{checkin_id}:without-image 1")
        "without-image"
    end

    checkin = Map.put(checkin, "reported_as", reported_as)

    {:ok, checkin}
  end
end


Do not attempt to compile protocols in test env

defmodule ReleaseManager.Plugin.Consolidation do
  @name "protocol.consolidation"
  @shortdoc "Performs protocol consolidation for your release."

  use    ReleaseManager.Plugin
  alias  ReleaseManager.Config
  alias  ReleaseManager.Utils
  import ReleaseManager.Utils

  def before_release(%Config{verbosity: verbosity, env: env}) do
    build_embedded = Keyword.get(Mix.Project.config, :build_embedded, false)
    should_compile = env != :test && !build_embedded
    if should_compile do
      debug "Performing protocol consolidation..."
      with_env env, fn ->
        cond do
          verbosity == :verbose ->
            mix "compile.protocols", env, :verbose
          true ->
            mix "compile.protocols", env
        end
      end
    end

    debug "Packaging consolidated protocols..."

    # Load relx.config
    relx_config = Utils.rel_file_dest_path("relx.config") |> Utils.read_terms
    # Add overlay to relx.config which copies consolidated dir to release
    consolidated_path = Path.join([File.cwd!, "_build", "#{env}", "consolidated"])
    overlays = [overlay: [
      {:copy, '#{consolidated_path}', 'lib'}
    ]]
    updated = Utils.merge(relx_config, overlays)
    # Persist relx.config
    Utils.write_terms(Utils.rel_file_dest_path("relx.config"), updated)
  end

  def after_release(_), do: nil
  def after_package(_), do: nil
  def after_cleanup(_), do: nil
end

defmodule ReleaseManager.Plugin.Consolidation do
  @name "protocol.consolidation"
  @shortdoc "Performs protocol consolidation for your release."

  use    ReleaseManager.Plugin
  alias  ReleaseManager.Config
  alias  ReleaseManager.Utils
  import ReleaseManager.Utils

  def before_release(%Config{verbosity: verbosity, env: env}) do
    build_embedded = Keyword.get(Mix.Project.config, :build_embedded, false)
    should_compile = env == :prod && !build_embedded
    if should_compile do
      debug "Performing protocol consolidation..."
      with_env env, fn ->
        cond do
          verbosity == :verbose ->
            mix "compile.protocols", env, :verbose
          true ->
            mix "compile.protocols", env
        end
      end
    end

    debug "Packaging consolidated protocols..."

    # Load relx.config
    relx_config = Utils.rel_file_dest_path("relx.config") |> Utils.read_terms
    # Add overlay to relx.config which copies consolidated dir to release
    consolidated_path = Path.join([File.cwd!, "_build", "#{env}", "consolidated"])
    overlays = [overlay: [
      {:copy, '#{consolidated_path}', 'lib'}
    ]]
    updated = Utils.merge(relx_config, overlays)
    # Persist relx.config
    Utils.write_terms(Utils.rel_file_dest_path("relx.config"), updated)
  end

  def after_release(_), do: nil
  def after_package(_), do: nil
  def after_cleanup(_), do: nil
end


remove useless @duplication

remove useless @duplication

according to dialyzer and @Dalgon, this is a much more sensible style

(now that i see it, I agree!)

defmodule Serum.Build.FileProcessor do
  @moduledoc "Processes the input files to produce the intermediate data."

  alias Serum.Build.FileLoader
  alias Serum.GlobalBindings
  alias Serum.Markdown
  alias Serum.Page
  alias Serum.Plugin
  alias Serum.Post
  alias Serum.PostList
  alias Serum.Project
  alias Serum.Result
  alias Serum.Tag
  alias Serum.Template
  alias Serum.Template.Compiler, as: TC

  @type tag_groups() :: [{Tag.t(), [Post.t()]}]
  @type tag_counts() :: [{Tag.t(), integer()}]

  @type result() :: %{
          pages: [Page.t()],
          posts: [Post.t()],
          lists: [PostList.t()]
        }

  @doc """
  Processes the input files.

  ## Procedure

  - Compiles includable templates.
  - Compiles regular templates. Any call to the `include/1` macro in a template
    will be expanded into the corresponding includable template by the
    `Serum.Template.Compiler` module.
  - Processes page files.
  - Processes blog post files.
  - Generates post lists using information generated just before. A list of
    tags and their use counts is also created.
  - Updates the `Serum.GlobalBindings` agent so that the above information is
    available later, when rendering pages into fragments or full HTML pages.
  """
  @spec process_files(FileLoader.result(), Project.t()) :: Result.t(result())
  def process_files(files, proj) do
    %{pages: page_files, posts: post_files} = files

    with :ok <- compile_templates(files),
         {:ok, {pages, compact_pages}} <- process_pages(page_files, proj),
         {:ok, {posts, compact_posts}} <- process_posts(post_files, proj),
         {:ok, {lists, tag_counts}} <- generate_lists(compact_posts, proj) do
      update_global_bindings(compact_pages, compact_posts, tag_counts)

      {:ok, %{pages: pages, posts: posts, lists: lists}}
    else
      {:error, _} = error -> error
    end
  end

  @spec compile_templates(map()) :: Result.t()
  defp compile_templates(files) do
    IO.puts("Compiling templates...")

    with {:ok, includes} <- TC.compile_files(files.includes, :include),
         :ok <- Template.load(includes, :include),
         {:ok, templates} <- TC.compile_files(files.templates, :template) do
      Template.load(templates, :template)
    else
      {:error, _} = error -> error
    end
  end

  @doc false
  @spec process_pages([Serum.File.t()], Project.t()) :: Result.t({[Page.t()], [map()]})
  def process_pages(files, proj) do
    IO.puts("Processing page files...")

    files
    |> Task.async_stream(&process_page(&1, proj))
    |> Enum.map(&elem(&1, 1))
    |> Result.aggregate_values(:file_processor)
    |> case do
      {:ok, pages} ->
        sorted_pages = Enum.sort(pages, &(&1.order < &2.order))

        {:ok, {sorted_pages, Enum.map(sorted_pages, &Page.compact/1)}}

      {:error, _} = error ->
        error
    end
  end

  @spec process_page(Serum.File.t(), Project.t()) :: Result.t(Page.t())
  defp process_page(file, proj) do
    import Serum.HeaderParser

    opts = [
      title: :string,
      label: :string,
      group: :string,
      order: :integer
    ]

    required = [:title]

    with {:ok, file2} <- Plugin.processing_page(file),
         {:ok, {header, rest}} <- parse_header(file2.in_data, opts, required) do
      header = Map.put(header, :label, header[:label] || header.title)
      page = Page.new(file2.src, header, rest, proj)

      Plugin.processed_page(page)
    else
      {:invalid, message} -> {:error, {message, file.src, 0}}
      {:error, _} = plugin_error -> plugin_error
    end
  end

  @doc false
  @spec process_posts([Serum.File.t()], Project.t()) :: Result.t({[Post.t()], [map()]})
  def process_posts(files, proj)

  def process_posts([], _proj), do: {:ok, {[], []}}

  def process_posts(files, proj) do
    IO.puts("Processing post files...")

    files
    |> Task.async_stream(&process_post(&1, proj))
    |> Enum.map(&elem(&1, 1))
    |> Result.aggregate_values(:file_processor)
    |> case do
      {:ok, posts} ->
        sorted_posts = Enum.sort(posts, &(&1.raw_date > &2.raw_date))

        {:ok, {sorted_posts, Enum.map(sorted_posts, &Post.compact/1)}}

      {:error, _} = error ->
        error
    end
  end

  @spec process_post(Serum.File.t(), Project.t()) :: Result.t(Post.t())
  defp process_post(file, proj) do
    import Serum.HeaderParser

    opts = [
      title: :string,
      tags: {:list, :string},
      date: :datetime
    ]

    required = [:title, :date]

    with {:ok, file2} <- Plugin.processing_post(file),
         {:ok, {header, rest}} <- parse_header(file2.in_data, opts, required) do
      header = %{
        header
        | date: header[:date] || Timex.to_datetime(Timex.zero(), :local)
      }

      post = Post.new(file2.src, header, Markdown.to_html(rest, proj), proj)

      Plugin.processed_post(post)
    else
      {:invalid, message} -> {:error, {message, file.src, 0}}
      {:error, _} = plugin_error -> plugin_error
    end
  end

  @doc false
  @spec generate_lists([map()], Project.t()) :: Result.t({[PostList.t()], tag_counts()})
  def generate_lists(compact_posts, proj)

  def generate_lists([], _proj), do: {:ok, []}

  def generate_lists(compact_posts, proj) do
    IO.puts("Generating post lists...")

    tag_groups = group_posts_by_tag(compact_posts)

    [{nil, compact_posts} | tag_groups]
    |> Task.async_stream(fn {tag, posts} ->
      PostList.generate(tag, posts, proj)
    end)
    |> Enum.map(&elem(&1, 1))
    |> Result.aggregate_values(:file_processor)
    |> case do
      {:ok, lists} -> {:ok, {List.flatten(lists), get_tag_counts(tag_groups)}}
      {:error, _} = error -> error
    end
  end

  @spec group_posts_by_tag([map()], map()) :: tag_groups()
  defp group_posts_by_tag(posts, acc \\ %{})

  defp group_posts_by_tag([], acc) do
    Enum.map(acc, fn {tag, posts} -> {tag, Enum.reverse(posts)} end)
  end

  defp group_posts_by_tag([post | posts], acc1) do
    new_acc =
      Enum.reduce(post.tags, acc1, fn tag, acc2 ->
        acc2
        |> Map.get_and_update(tag, fn
          nil -> {nil, [post]}
          posts when is_list(posts) -> {posts, [post | posts]}
        end)
        |> elem(1)
      end)

    group_posts_by_tag(posts, new_acc)
  end

  @spec get_tag_counts(tag_groups()) :: tag_counts()
  defp get_tag_counts(tags) do
    Enum.map(tags, fn {k, v} -> {k, Enum.count(v)} end)
  end

  @spec update_global_bindings([map()], [map()], tag_counts()) :: :ok
  def update_global_bindings(compact_pages, compact_posts, tag_counts) do
    GlobalBindings.put(:all_pages, compact_pages)
    GlobalBindings.put(:all_posts, compact_posts)
    GlobalBindings.put(:all_tags, tag_counts)
  end
end

defmodule Serum.Build.FileProcessor do
  @moduledoc "Processes the input files to produce the intermediate data."

  alias Serum.Build.FileLoader
  alias Serum.GlobalBindings
  alias Serum.Markdown
  alias Serum.Page
  alias Serum.Plugin
  alias Serum.Post
  alias Serum.PostList
  alias Serum.Project
  alias Serum.Result
  alias Serum.Tag
  alias Serum.Template
  alias Serum.Template.Compiler, as: TC

  @type tag_groups() :: [{Tag.t(), [Post.t()]}]
  @type tag_counts() :: [{Tag.t(), integer()}]

  @type result() :: %{
          pages: [Page.t()],
          posts: [Post.t()],
          lists: [PostList.t()]
        }

  @doc """
  Processes the input files.

  ## Procedure

  - Compiles includable templates.
  - Compiles regular templates. Any call to the `include/1` macro in a template
    will be expanded into the corresponding includable template by the
    `Serum.Template.Compiler` module.
  - Processes page files.
  - Processes blog post files.
  - Generates post lists using information generated just before. A list of
    tags and their use counts is also created.
  - Updates the `Serum.GlobalBindings` agent so that the above information is
    available later, when rendering pages into fragments or full HTML pages.
  """
  @spec process_files(FileLoader.result(), Project.t()) :: Result.t(result())
  def process_files(files, proj) do
    %{pages: page_files, posts: post_files} = files

    with :ok <- compile_templates(files),
         {:ok, {pages, compact_pages}} <- process_pages(page_files, proj),
         {:ok, {posts, compact_posts}} <- process_posts(post_files, proj),
         {:ok, {lists, tag_counts}} <- generate_lists(compact_posts, proj) do
      update_global_bindings(compact_pages, compact_posts, tag_counts)

      {:ok, %{pages: pages, posts: posts, lists: lists}}
    else
      {:error, _} = error -> error
    end
  end

  @spec compile_templates(map()) :: Result.t()
  defp compile_templates(files) do
    IO.puts("Compiling templates...")

    with {:ok, includes} <- TC.compile_files(files.includes, :include),
         :ok <- Template.load(includes, :include),
         {:ok, templates} <- TC.compile_files(files.templates, :template) do
      Template.load(templates, :template)
    else
      {:error, _} = error -> error
    end
  end

  @doc false
  @spec process_pages([Serum.File.t()], Project.t()) :: Result.t({[Page.t()], [map()]})
  def process_pages(files, proj) do
    IO.puts("Processing page files...")

    files
    |> Task.async_stream(&process_page(&1, proj))
    |> Enum.map(&elem(&1, 1))
    |> Result.aggregate_values(:file_processor)
    |> case do
      {:ok, pages} ->
        sorted_pages = Enum.sort(pages, &(&1.order < &2.order))

        {:ok, {sorted_pages, Enum.map(sorted_pages, &Page.compact/1)}}

      {:error, _} = error ->
        error
    end
  end

  @spec process_page(Serum.File.t(), Project.t()) :: Result.t(Page.t())
  defp process_page(file, proj) do
    import Serum.HeaderParser

    opts = [
      title: :string,
      label: :string,
      group: :string,
      order: :integer
    ]

    required = [:title]

    with {:ok, file2} <- Plugin.processing_page(file),
         {:ok, {header, rest}} <- parse_header(file2.in_data, opts, required) do
      header = Map.put(header, :label, header[:label] || header.title)
      page = Page.new(file2.src, header, rest, proj)

      Plugin.processed_page(page)
    else
      {:invalid, message} -> {:error, {message, file.src, 0}}
      {:error, _} = plugin_error -> plugin_error
    end
  end

  @doc false
  @spec process_posts([], Project.t()) :: Result.t({[Post.t()], [map()]})
  def process_posts([], _proj) do
    {:ok, {[], []}}
  end

  @doc false
  @spec process_posts([Serum.File.t()], Project.t()) :: Result.t({[Post.t()], [map()]})
  def process_posts(files, proj) do
    IO.puts("Processing post files...")

    files
    |> Task.async_stream(&process_post(&1, proj))
    |> Enum.map(&elem(&1, 1))
    |> Result.aggregate_values(:file_processor)
    |> case do
      {:ok, posts} ->
        sorted_posts = Enum.sort(posts, &(&1.raw_date > &2.raw_date))

        {:ok, {sorted_posts, Enum.map(sorted_posts, &Post.compact/1)}}

      {:error, _} = error ->
        error
    end
  end

  @spec process_post(Serum.File.t(), Project.t()) :: Result.t(Post.t())
  defp process_post(file, proj) do
    import Serum.HeaderParser

    opts = [
      title: :string,
      tags: {:list, :string},
      date: :datetime
    ]

    required = [:title, :date]

    with {:ok, file2} <- Plugin.processing_post(file),
         {:ok, {header, rest}} <- parse_header(file2.in_data, opts, required) do
      header = %{
        header
        | date: header[:date] || Timex.to_datetime(Timex.zero(), :local)
      }

      post = Post.new(file2.src, header, Markdown.to_html(rest, proj), proj)

      Plugin.processed_post(post)
    else
      {:invalid, message} -> {:error, {message, file.src, 0}}
      {:error, _} = plugin_error -> plugin_error
    end
  end

  @doc false
  @spec generate_lists([], Project.t()) :: Result.t({[PostList.t()], tag_counts()})
  def generate_lists([], _proj) do
    {:ok, []}
  end

  @doc false
  @spec generate_lists([map()], Project.t()) :: Result.t({[PostList.t()], tag_counts()})
  def generate_lists(compact_posts, proj) do
    IO.puts("Generating post lists...")

    tag_groups = group_posts_by_tag(compact_posts)

    [{nil, compact_posts} | tag_groups]
    |> Task.async_stream(fn {tag, posts} ->
      PostList.generate(tag, posts, proj)
    end)
    |> Enum.map(&elem(&1, 1))
    |> Result.aggregate_values(:file_processor)
    |> case do
      {:ok, lists} -> {:ok, {List.flatten(lists), get_tag_counts(tag_groups)}}
      {:error, _} = error -> error
    end
  end

  @spec group_posts_by_tag([map()], map()) :: tag_groups()
  defp group_posts_by_tag(posts, acc \\ %{})

  defp group_posts_by_tag([], acc) do
    Enum.map(acc, fn {tag, posts} -> {tag, Enum.reverse(posts)} end)
  end

  defp group_posts_by_tag([post | posts], acc1) do
    new_acc =
      Enum.reduce(post.tags, acc1, fn tag, acc2 ->
        acc2
        |> Map.get_and_update(tag, fn
          nil -> {nil, [post]}
          posts when is_list(posts) -> {posts, [post | posts]}
        end)
        |> elem(1)
      end)

    group_posts_by_tag(posts, new_acc)
  end

  @spec get_tag_counts(tag_groups()) :: tag_counts()
  defp get_tag_counts(tags) do
    Enum.map(tags, fn {k, v} -> {k, Enum.count(v)} end)
  end

  @spec update_global_bindings([map()], [map()], tag_counts()) :: :ok
  def update_global_bindings(compact_pages, compact_posts, tag_counts) do
    GlobalBindings.put(:all_pages, compact_pages)
    GlobalBindings.put(:all_posts, compact_posts)
    GlobalBindings.put(:all_tags, tag_counts)
  end
end


Remove memberships when a user is deleted

defmodule Officetournament.Repo.Migrations.CreateMembership do
  use Ecto.Migration

  def change do
    create table(:memberships) do
      add :user_id, references(:users, on_delete: :delete_all)
      add :league_id, references(:leagues, on_delete: :delete_all)

      timestamps
    end

    create unique_index(:memberships, [:user_id, :league_id])

  end
end

defmodule Officetournament.Repo.Migrations.CreateMembership do
  use Ecto.Migration

  def change do
    create table(:memberships) do
      add :user_id, references(:users)
      add :league_id, references(:leagues)

      timestamps
    end

    create unique_index(:memberships, [:user_id, :league_id])

  end
end


Provide full url to process_page when there is more than 50 entries.

The previous variable next_page_uri was returning the end point

`/2010-04-01/Accounts/xxxxxxx/Messages.json`

and causing HTTPotion to raise a HTTPotion.HTTPError because it wasn't a
full url.

defmodule ExTwilio.ResultStream do
  @moduledoc """
  Generate a stream of results for a given Twilio API URL. Pages are lazily
  loaded on demand.

  ## Example

      ExTwilio.ResultStream.new(ExTwilio.Call)
      |> Stream.map(fn call -> call.sid end)
      |> Enum.take(5)

      # => [%ExTwilio.Call{ ... }, %ExTwilio.Call{ ... }, ...]
  """

  alias ExTwilio.Api
  alias ExTwilio.Parser
  alias ExTwilio.UrlGenerator
  alias ExTwilio.Config

  @type url :: String.t

  @doc """
  Create a new Stream.

  ## Parameters

  - `module`: The name of the module to create a Stream of results for.

  ## Example

  ExTwilio.ResultStream.new(ExTwilio.Call)
  """
  def new(module, options \\ []) do
    url = UrlGenerator.build_url(module, nil, options)

    Stream.resource(
      fn -> fetch_page(url, module) end,
      &process_page/1,
      fn _ -> nil end
    )
  end

  @spec fetch_page(url, module) :: {list, url, module}
  defp fetch_page(url, module) do
    results = Api.get(url)
    {:ok, items, meta} = Parser.parse_list(results, module, module.resource_collection_name)
    {items, meta["next_page_uri"], module}
  end

  @spec process_page({list | nil, url | nil, module}) :: {:halt, nil} |
                                                         {list, {nil, url, module}}
  defp process_page({nil, nil, _module}) do
    {:halt, nil}
  end

  defp process_page({nil, next_page_uri, module}) do
    next_page_url(next_page_uri)
    |> fetch_page(module)
    |> process_page
  end

  defp process_page({items, next_page_url, module}) do
    {items, {nil, next_page_url, module}}
  end

  defp next_page_url(uri), do: "https://#{Config.api_domain}" <> uri
end

defmodule ExTwilio.ResultStream do
  @moduledoc """
  Generate a stream of results for a given Twilio API URL. Pages are lazily
  loaded on demand.

  ## Example

      ExTwilio.ResultStream.new(ExTwilio.Call)
      |> Stream.map(fn call -> call.sid end)
      |> Enum.take(5)

      # => [%ExTwilio.Call{ ... }, %ExTwilio.Call{ ... }, ...]
  """

  alias ExTwilio.Api
  alias ExTwilio.Parser
  alias ExTwilio.UrlGenerator

  @type url :: String.t

  @doc """
  Create a new Stream.

  ## Parameters

  - `module`: The name of the module to create a Stream of results for.

  ## Example

      ExTwilio.ResultStream.new(ExTwilio.Call)
  """
  def new(module, options \\ []) do
    url = UrlGenerator.build_url(module, nil, options)

    Stream.resource(
      fn -> fetch_page(url, module) end,
      &process_page/1,
      fn _ -> nil end
    )
  end

  @spec fetch_page(url, module) :: {list, url, module}
  defp fetch_page(url, module) do
    results = Api.get(url)
    {:ok, items, meta} = Parser.parse_list(results, module, module.resource_collection_name)
    {items, meta["next_page_uri"], module}
  end

  @spec process_page({list | nil, url | nil, module}) :: {:halt, nil} |
                                                         {list, {nil, url, module}}
  defp process_page({nil, nil, _module}) do
    {:halt, nil}
  end

  defp process_page({nil, next_page_url, module}) do
    next_page_url
    |> fetch_page(module)
    |> process_page
  end

  defp process_page({items, next_page_url, module}) do
    {items, {nil, next_page_url, module}}
  end
end


Fix login bugs

Fix login bugs

defmodule TheJuice.SessionController do
  use TheJuice.Web, :controller
  alias TheJuice.User
  import Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0]

  plug :scrub_params, "user" when action in [:create]

  def new(conn, _params) do
    render conn, "new.html", changeset: User.changeset(%User{})
  end

  def create(conn, %{"user" => %{"username" => username, "password" => password}})
  when not is_nil(username) and not is_nil(password) do
    user = Repo.get_by(User, username: username)
    sign_in(user, password, conn)
  end

  def create(conn, _) do
    failed_login(conn)
  end

  def delete(conn, _params) do
    conn
    |> delete_session(:current_user)
    |> put_flash(:info, "Signed out successfully!")
    |> redirect(to: page_path(conn, :index))
  end

  defp sign_in(user, _password, conn) when is_nil(user) do
    failed_login(conn)
  end

  defp sign_in(user, password, conn) do
    if checkpw(password, user.password_digest) do
      conn
      |> put_session(:current_user, %{id: user.id, username: user.username})
      |> put_flash(:info, "Sign in successful!")
      |> redirect(to: page_path(conn, :index))
    else
      failed_login(conn)
    end
  end

  defp failed_login(conn) do
    # Call dummy_checkpw so hackers can't iterate through usernames with
    # bad passwords and check to see if timing changes when username doesn't
    # exist in our system.
    dummy_checkpw()
    conn
    |> put_session(:current_user, nil)
    |> put_flash(:error, "Invalid username/password combination!")
    |> redirect(to: page_path(conn, :index))
    |> halt()
  end
end

defmodule TheJuice.SessionController do
  use TheJuice.Web, :controller
  alias TheJuice.User
  import Comeonin.Bcrypt, only: [checkpw: 2]

  plug :scrub_params, "user" when action in [:create]

  def new(conn, _params) do
    render conn, "new.html", changeset: User.changeset(%User{})
  end

  def create(conn, %{"user" => user_params}) do
    Repo.get_by(User, username: user_params["username"])
    |> sign_in(user_params["password"], conn)
  end

  def delete(conn, _params) do
    conn
    |> delete_session(:current_user)
    |> put_flash(:info, "Signed out successfully!")
    |> redirect(to: page_path(conn, :index))
  end

  defp sign_in(user, _, conn) when is_nil(user) do
    conn
    |> put_flash(:error, "Invalid username/password combination!")
    |> redirect(to: page_path(conn, :index))
  end

  defp sign_in(user, password, conn) do
    if checkpw(password, user.password_digest) do
      conn
      |> put_session(:current_user, %{id: user.id, username: user.username})
      |> put_flash(:info, "Sign in successful!")
      |> redirect(to: page_path(conn, :index))
    else
      conn
      |> put_session(:current_user, nil)
      |> put_flash(:error, "Invalid username/password combination!")
      |> redirect(to: page_path(conn, :index))
    end
  end
end


use pipe operator instead

use pipe operator instead

defmodule Tesla.Middleware.Logger.Formatter do
  @moduledoc false

  # Heavily based on Elixir's Logger.Formatter
  # https://github.com/elixir-lang/elixir/blob/v1.6.4/lib/logger/lib/logger/formatter.ex

  @default_format "$method $url -> $status ($time ms)"
  @keys ~w(method url status time query)

  @type format :: [atom | binary]

  @spec compile(binary | nil) :: format
  def compile(nil), do: compile(@default_format)

  def compile(binary) do
    ~r/(?<h>)\$[a-z]+(?<t>)/
    |> Regex.split(binary, on: [:h, :t], trim: true)
    |> Enum.map(&compile_key/1)
  end

  defp compile_key("$" <> key) when key in @keys, do: String.to_atom(key)
  defp compile_key("$" <> key), do: raise(ArgumentError, "$#{key} is an invalid format pattern.")
  defp compile_key(part), do: part

  @spec format(Tesla.Env.t(), Tesla.Env.result(), integer, format) :: IO.chardata()
  def format(request, response, time, format) do
    Enum.map(format, &output(&1, request, response, time))
  end

  defp output(:query, env, _, _), do: env.query |> Tesla.encode_query()
  defp output(:method, env, _, _), do: env.method |> to_string() |> String.upcase()
  defp output(:url, env, _, _), do: env.url
  defp output(:status, _, {:ok, env}, _), do: to_string(env.status)
  defp output(:status, _, {:error, reason}, _), do: "error: " <> inspect(reason)
  defp output(:time, _, _, time), do: :io_lib.format("~.3f", [time / 1000])
  defp output(binary, _, _, _), do: binary
end

defmodule Tesla.Middleware.Logger do
  @moduledoc """
  Log requests using Elixir's Logger.

  With the default settings it logs request method, url, response status and time taken in milliseconds.

  ## Example usage
  ```
  defmodule MyClient do
    use Tesla

    plug Tesla.Middleware.Logger
  end
  ```

  ## Options
  - `:log_level` - custom function for calculating log level (see below)
  - `:filter_headers` - sanitizes sensitive headers before logging in debug mode (see below)
  - `:debug` - show detailed request/response logging

  ## Custom log format

  The default log format is `"$method $url -> $status ($time ms)"`
  which shows in logs like:

  ```
  2018-03-25 18:32:40.397 [info]  GET https://bitebot.io -> 200 (88.074 ms)
  ```

  Because log format is processed during compile time it needs to be set in config:

  ```
  config :tesla, Tesla.Middleware.Logger, format: "$method $url ====> $status / time=$time"
  ```

  ## Custom log levels

  By default, the following log levels will be used:
  - `:error` - for errors, 5xx and 4xx responses
  - `:warn` - for 3xx responses
  - `:info` - for 2xx responses

  You can customize this setting by providing your own `log_level/1` function:

  ```
  defmodule MyClient do
    use Tesla

    plug Tesla.Middleware.Logger, log_level: &my_log_level/1

    def my_log_level(env) do
      case env.status do
        404 -> :info
        _ -> :default
      end
    end
  end
  ```

  ## Logger Debug output

  When the Elixir Logger log level is set to `:debug`
  Tesla Logger will show full request & response.

  If you want to disable detailed request/response logging
  but keep the `:debug` log level (i.e. in development)
  you can set `debug: false` in your config:

  ```
  # config/dev.local.exs
  config :tesla, Tesla.Middleware.Logger, debug: false
  ```

  Note that the logging configuration is evaluated at compile time,
  so Tesla must be recompiled for the configuration to take effect:

  ```
  mix deps.clean --build tesla
  mix deps.compile tesla
  ```

  In order to be able to set `:debug` at runtime we can
  pass it as a option to the middleware at runtime.

  ```elixir
  def client do
    middleware = [
      # ...
      {Tesla.Middleware.Logger, debug: false}
    ]

    Tesla.client(middleware)
  end
  ```

  ### Filter headers

  To sanitize sensitive headers such as `authorization` in
  debug logs, add them to the `:filter_headers` option.
  `:filter_headers` expects a list of header names as strings.

  ```
  # config/dev.local.exs
  config :tesla, Tesla.Middleware.Logger,
    filter_headers: ["authorization"]
  ```
  """

  @behaviour Tesla.Middleware

  alias Tesla.Middleware.Logger.Formatter

  @config Application.get_env(:tesla, __MODULE__, [])

  @type log_level :: :info | :warn | :error

  require Logger

  @impl Tesla.Middleware
  def call(env, next, opts) do
    {time, response} = :timer.tc(Tesla, :run, [env, next])

    config = Keyword.merge(@config, opts)

    format =
      config
      |> Keyword.get(:format)
      |> Formatter.compile()

    level = log_level(response, config)
    Logger.log(level, fn -> Formatter.format(env, response, time, format) end)

    if Keyword.get(config, :debug, true) do
      Logger.debug(fn -> debug(env, response, config) end)
    end

    response
  end

  defp log_level({:error, _}, _), do: :error

  defp log_level({:ok, env}, config) do
    case Keyword.get(config, :log_level) do
      nil ->
        default_log_level(env)

      fun when is_function(fun) ->
        case fun.(env) do
          :default -> default_log_level(env)
          level -> level
        end

      atom when is_atom(atom) ->
        atom
    end
  end

  @spec default_log_level(Tesla.Env.t()) :: log_level
  def default_log_level(env) do
    cond do
      env.status >= 400 -> :error
      env.status >= 300 -> :warn
      true -> :info
    end
  end

  @debug_no_query "(no query)"
  @debug_no_headers "(no headers)"
  @debug_no_body "(no body)"
  @debug_stream "[Elixir.Stream]"

  defp debug(request, {:ok, response}, config) do
    [
      "\n>>> REQUEST >>>\n",
      debug_query(request.query),
      ?\n,
      debug_headers(request.headers, config),
      ?\n,
      debug_body(request.body),
      ?\n,
      "\n<<< RESPONSE <<<\n",
      debug_headers(response.headers, config),
      ?\n,
      debug_body(response.body)
    ]
  end

  defp debug(request, {:error, error}, config) do
    [
      "\n>>> REQUEST >>>\n",
      debug_query(request.query),
      ?\n,
      debug_headers(request.headers, config),
      ?\n,
      debug_body(request.body),
      ?\n,
      "\n<<< RESPONSE ERROR <<<\n",
      inspect(error)
    ]
  end

  defp debug_query([]), do: @debug_no_query

  defp debug_query(query) do
    query
    |> Enum.flat_map(&Tesla.encode_pair/1)
    |> Enum.map(fn {k, v} -> ["Query: ", to_string(k), ": ", to_string(v), ?\n] end)
  end

  defp debug_headers([], _config), do: @debug_no_headers

  defp debug_headers(headers, config) do
    filtered = Keyword.get(config, :filter_headers, [])

    Enum.map(headers, fn {k, v} ->
      v = if k in filtered, do: "[FILTERED]", else: v
      [k, ": ", v, ?\n]
    end)
  end

  defp debug_body(nil), do: @debug_no_body
  defp debug_body([]), do: @debug_no_body
  defp debug_body(%Stream{}), do: @debug_stream
  defp debug_body(stream) when is_function(stream), do: @debug_stream

  defp debug_body(%Tesla.Multipart{} = mp) do
    [
      "[Tesla.Multipart]\n",
      "boundary: ",
      mp.boundary,
      ?\n,
      "content_type_params: ",
      inspect(mp.content_type_params),
      ?\n
      | Enum.map(mp.parts, &[inspect(&1), ?\n])
    ]
  end

  defp debug_body(data) when is_binary(data) or is_list(data), do: data
  defp debug_body(term), do: inspect(term)
end

defmodule Tesla.Middleware.Logger.Formatter do
  @moduledoc false

  # Heavily based on Elixir's Logger.Formatter
  # https://github.com/elixir-lang/elixir/blob/v1.6.4/lib/logger/lib/logger/formatter.ex

  @default_format "$method $url -> $status ($time ms)"
  @keys ~w(method url status time query)

  @type format :: [atom | binary]

  @spec compile(binary | nil) :: format
  def compile(nil), do: compile(@default_format)

  def compile(binary) do
    ~r/(?<h>)\$[a-z]+(?<t>)/
    |> Regex.split(binary, on: [:h, :t], trim: true)
    |> Enum.map(&compile_key/1)
  end

  defp compile_key("$" <> key) when key in @keys, do: String.to_atom(key)
  defp compile_key("$" <> key), do: raise(ArgumentError, "$#{key} is an invalid format pattern.")
  defp compile_key(part), do: part

  @spec format(Tesla.Env.t(), Tesla.Env.result(), integer, format) :: IO.chardata()
  def format(request, response, time, format) do
    Enum.map(format, &output(&1, request, response, time))
  end

  defp output(:query, env, _, _), do: env.query |> Tesla.encode_query()
  defp output(:method, env, _, _), do: env.method |> to_string() |> String.upcase()
  defp output(:url, env, _, _), do: env.url
  defp output(:status, _, {:ok, env}, _), do: to_string(env.status)
  defp output(:status, _, {:error, reason}, _), do: "error: " <> inspect(reason)
  defp output(:time, _, _, time), do: :io_lib.format("~.3f", [time / 1000])
  defp output(binary, _, _, _), do: binary
end

defmodule Tesla.Middleware.Logger do
  @moduledoc """
  Log requests using Elixir's Logger.

  With the default settings it logs request method, url, response status and time taken in milliseconds.

  ## Example usage
  ```
  defmodule MyClient do
    use Tesla

    plug Tesla.Middleware.Logger
  end
  ```

  ## Options
  - `:log_level` - custom function for calculating log level (see below)
  - `:filter_headers` - sanitizes sensitive headers before logging in debug mode (see below)
  - `:debug` - show detailed request/response logging

  ## Custom log format

  The default log format is `"$method $url -> $status ($time ms)"`
  which shows in logs like:

  ```
  2018-03-25 18:32:40.397 [info]  GET https://bitebot.io -> 200 (88.074 ms)
  ```

  Because log format is processed during compile time it needs to be set in config:

  ```
  config :tesla, Tesla.Middleware.Logger, format: "$method $url ====> $status / time=$time"
  ```

  ## Custom log levels

  By default, the following log levels will be used:
  - `:error` - for errors, 5xx and 4xx responses
  - `:warn` - for 3xx responses
  - `:info` - for 2xx responses

  You can customize this setting by providing your own `log_level/1` function:

  ```
  defmodule MyClient do
    use Tesla

    plug Tesla.Middleware.Logger, log_level: &my_log_level/1

    def my_log_level(env) do
      case env.status do
        404 -> :info
        _ -> :default
      end
    end
  end
  ```

  ## Logger Debug output

  When the Elixir Logger log level is set to `:debug`
  Tesla Logger will show full request & response.

  If you want to disable detailed request/response logging
  but keep the `:debug` log level (i.e. in development)
  you can set `debug: false` in your config:

  ```
  # config/dev.local.exs
  config :tesla, Tesla.Middleware.Logger, debug: false
  ```

  Note that the logging configuration is evaluated at compile time,
  so Tesla must be recompiled for the configuration to take effect:

  ```
  mix deps.clean --build tesla
  mix deps.compile tesla
  ```

  In order to be able to set `:debug` at runtime we can
  pass it as a option to the middleware at runtime.

  ```elixir
  def client do
    middleware = [
      # ...
      {Tesla.Middleware.Logger, debug: false}
    ]

    Tesla.client(middleware)
  end
  ```

  ### Filter headers

  To sanitize sensitive headers such as `authorization` in
  debug logs, add them to the `:filter_headers` option.
  `:filter_headers` expects a list of header names as strings.

  ```
  # config/dev.local.exs
  config :tesla, Tesla.Middleware.Logger,
    filter_headers: ["authorization"]
  ```
  """

  @behaviour Tesla.Middleware

  alias Tesla.Middleware.Logger.Formatter

  @config Application.get_env(:tesla, __MODULE__, [])

  @type log_level :: :info | :warn | :error

  require Logger

  @impl Tesla.Middleware
  def call(env, next, opts) do
    {time, response} = :timer.tc(Tesla, :run, [env, next])

    config = Keyword.merge(@config, opts)

    format =
      Keyword.get(config, :format)
      |> Formatter.compile()

    level = log_level(response, config)
    Logger.log(level, fn -> Formatter.format(env, response, time, format) end)

    if Keyword.get(config, :debug, true) do
      Logger.debug(fn -> debug(env, response, config) end)
    end

    response
  end

  defp log_level({:error, _}, _), do: :error

  defp log_level({:ok, env}, config) do
    case Keyword.get(config, :log_level) do
      nil ->
        default_log_level(env)

      fun when is_function(fun) ->
        case fun.(env) do
          :default -> default_log_level(env)
          level -> level
        end

      atom when is_atom(atom) ->
        atom
    end
  end

  @spec default_log_level(Tesla.Env.t()) :: log_level
  def default_log_level(env) do
    cond do
      env.status >= 400 -> :error
      env.status >= 300 -> :warn
      true -> :info
    end
  end

  @debug_no_query "(no query)"
  @debug_no_headers "(no headers)"
  @debug_no_body "(no body)"
  @debug_stream "[Elixir.Stream]"

  defp debug(request, {:ok, response}, config) do
    [
      "\n>>> REQUEST >>>\n",
      debug_query(request.query),
      ?\n,
      debug_headers(request.headers, config),
      ?\n,
      debug_body(request.body),
      ?\n,
      "\n<<< RESPONSE <<<\n",
      debug_headers(response.headers, config),
      ?\n,
      debug_body(response.body)
    ]
  end

  defp debug(request, {:error, error}, config) do
    [
      "\n>>> REQUEST >>>\n",
      debug_query(request.query),
      ?\n,
      debug_headers(request.headers, config),
      ?\n,
      debug_body(request.body),
      ?\n,
      "\n<<< RESPONSE ERROR <<<\n",
      inspect(error)
    ]
  end

  defp debug_query([]), do: @debug_no_query

  defp debug_query(query) do
    query
    |> Enum.flat_map(&Tesla.encode_pair/1)
    |> Enum.map(fn {k, v} -> ["Query: ", to_string(k), ": ", to_string(v), ?\n] end)
  end

  defp debug_headers([], _config), do: @debug_no_headers

  defp debug_headers(headers, config) do
    filtered = Keyword.get(config, :filter_headers, [])

    Enum.map(headers, fn {k, v} ->
      v = if k in filtered, do: "[FILTERED]", else: v
      [k, ": ", v, ?\n]
    end)
  end

  defp debug_body(nil), do: @debug_no_body
  defp debug_body([]), do: @debug_no_body
  defp debug_body(%Stream{}), do: @debug_stream
  defp debug_body(stream) when is_function(stream), do: @debug_stream

  defp debug_body(%Tesla.Multipart{} = mp) do
    [
      "[Tesla.Multipart]\n",
      "boundary: ",
      mp.boundary,
      ?\n,
      "content_type_params: ",
      inspect(mp.content_type_params),
      ?\n
      | Enum.map(mp.parts, &[inspect(&1), ?\n])
    ]
  end

  defp debug_body(data) when is_binary(data) or is_list(data), do: data
  defp debug_body(term), do: inspect(term)
end


Fix plugin tests with new tet plugin

Fix plugin tests with new tet plugin

require Logger

defmodule Serverboards.Plugin.Parser do

  @doc ~S"""
  Parses a component dict (from yaml) into a Component struct.

  ## Examples

  Full parse:

    iex> Serverboards.Plugin.Parser.parse_component(%{ "id" => "test", "name" => "mytest", "type" => "iocmd", "traits" => "test one", "other" => "Other"})
    %Serverboards.Plugin.Component{ id: "test", name: "mytest", type: "iocmd", traits: ["test","one"], extra: %{ "other" => "Other" }}

  Only some fields:

    iex> Serverboards.Plugin.Parser.parse_component(%{ "id" => "test"})
    %Serverboards.Plugin.Component{ id: "test"}

  """
  def parse_component(%{} = dict) do
    traits = case Map.get(dict, "traits") do
      nil ->
        []
      str when is_binary(str) ->
        String.split(str," ", trim: true)
    end

    %Serverboards.Plugin.Component{
      id: Map.get(dict, "id"),
      name: Map.get(dict, "name"),
      type: Map.get(dict, "type"),
      traits: traits,
      description: Map.get(dict, "description"),
      extra: Map.drop(dict, ~w(id name type traits description))
    }
  end

  @doc ~S"""
  Parses a dict that describes a plugin into a plugin struct.

  ## Example

    iex> Serverboards.Plugin.Parser.parse(%{ "id" => "test", "name" => "mytest", "components" => [
    ...>    %{ "id" => "test_A", "name" => "mytest", "type" => "iocmd", "traits" => "test one", "other" => "Other"},
    ...>    %{ "id" => "test_B", "name" => "othertest", "type" => "iocmd", "traits" => "auth"}
    ...>  ]
    ...>  })
    %Serverboards.Plugin{author: nil,
      components: [
        %Serverboards.Plugin.Component{
          extra: %{"other" => "Other"}, id: "test_A", name: "mytest",
          traits: ["test", "one"], type: "iocmd"
          },
       %Serverboards.Plugin.Component{
          extra: %{}, id: "test_B", name: "othertest", traits: ["auth"],
          type: "iocmd"
      }],
      description: nil, extra: %{}, id: "test",
      name: "mytest", version: nil
    }
  """
  def parse(%{} = dict) do
    components = Enum.map( Map.get(dict, "components", []), &parse_component(&1) )

    %Serverboards.Plugin{
      id: Map.get(dict, "id"),
      name: Map.get(dict, "name"),
      description: Map.get(dict, "description"),
      author: Map.get(dict, "author"),
      version: Map.get(dict, "version"),
      url: Map.get(dict, "url"),

      components: components,

      extra: Map.drop(dict, ~w(id name description author version url components))
    }
  end

  @doc ~S"""
  Given a string with yaml data, parses it into a plugin struct.

  ## Examples
    iex> {:ok, plugin} = Serverboards.Plugin.Parser.parse_yaml "id: serverboards.ls\nname: \"Ls\"\nauthor: \"David Moreno\"\nversion: 0.0.1\ncomponents:\n  - id: ls\n    name: ls\n    cmd: ./ls\n"
    iex> plugin.id
    "serverboards.ls"
    iex> (hd plugin.components).id
    "ls"

  """

  def parse_yaml(yaml, filename \\ "") do
    try do
      data = YamlElixir.read_from_string(yaml)
      plugin = parse(data)
      {:ok, plugin}
    catch
      {:yamerl_exception, [
        {:yamerl_parsing_error, :error, msg, line, column, _, _ , _} | _]} ->
          {:error, "#{line}:#{column} #{msg}"}
      e ->
        Logger.debug("Error loading yaml. #{inspect e}")
        {:error, e}
      :exit, {_reason, _where} ->
        Logger.debug("Error loading yaml. yamlr exited.")
        {:error, :exit}
    rescue
      e in CaseClauseError->
        %{ term: term } = e
        { _, _, _, where, _} = term
        line=where[:line]
        column=where[:column]
        Logger.error("Error loading yaml. Bad formed #{filename}:#{line}:#{column}")
        {:error, :bad_formed}
      e in FunctionClauseError ->
        Logger.error("Error loading yaml. Bad formed #{filename}")
        {:error, :bad_formed}
    end
  end

  @doc ~S"""
  Reads a plugin yaml from a file.

  ## Examples

    iex> {:ok, plugin} = Serverboards.Plugin.Parser.read("test/data/plugins/auth/manifest.yaml")
    iex> plugin.id
    "serverboards.test.auth"

    iex> Serverboards.Plugin.Parser.read("test/data/plugins/invalid/manifest.yaml")
    {:error, :invalid_yaml}

    iex> Serverboards.Plugin.Parser.read("test/data/non-existant.yaml")
    {:error, :enoent}
  """
  def read(filename) do
    ret = with {:ok, data} <- File.read(filename),
      {:ok, plugin} <- parse_yaml( data, filename ),
      do: {:ok, plugin}
    case ret do
      {:ok, v} -> {:ok, v}
      {:error, :enoent} ->
        {:error, :enoent}
      {:error, :bad_formed} ->
        {:error, :bad_formed}
      {:error, v} when is_binary(v) ->
        Logger.error("Error loading yaml file #{filename}:#{v}")
        {:error, :invalid_yaml}
    end
  end

  @doc ~S"""
  At that directory, checks all subdirectories with manifest.yaml file and
  return a list with the plugin descriptions.

  ## Example

    iex> {:ok, plugins} = read_dir("test/data/plugins/")
    iex> Enum.count plugins
    2
    iex> (hd Enum.sort(plugins)).id
    "test.extractor"

    iex> read_dir("test/data/invalid/")
    {:error, :enoent}

  """
  def read_dir(dirname) do
    is_directory = fn (dirname, filename) ->
      fullpath="#{dirname}/#{filename}"
      case File.stat(fullpath) do
        {:ok, stat} -> stat.type == :directory
        _ -> false
      end
    end

    is_valid_dirname = fn (dirname) ->
      cond do
        String.starts_with?(dirname, "__") -> false
        String.starts_with?(dirname, ".") -> false
        true -> true
      end
    end

    has_manifest = fn (dirname) ->
      fullpath="#{dirname}/manifest.yaml"
      case File.stat(fullpath) do
        {:ok, stat} -> stat.type == :regular
        _ -> false
      end
    end

    import Enum
    case File.ls(dirname) do
      {:ok, allfiles} ->
        plugins = allfiles
            |> filter(&is_directory.(dirname, &1))
            |> filter(&is_valid_dirname.(&1))
            |> filter(&has_manifest.(dirname<>"/"<>&1))
            |> map(&{read("#{dirname}/#{&1}/manifest.yaml"), &1}) # returns {{:ok, plugin}, midpath}
            |> filter(fn
                {{:ok, _}, _} -> true
                _ -> false
                end)
            |> map(fn {{:ok, p}, midpath} ->
              %Serverboards.Plugin{ p | path: "#{dirname}/#{midpath}" } # set plugin dirname
            end)

        {:ok, plugins}
      e ->
        e
    end
  end
end

require Logger

defmodule Serverboards.Plugin.Parser do

  @doc ~S"""
  Parses a component dict (from yaml) into a Component struct.

  ## Examples

  Full parse:

    iex> Serverboards.Plugin.Parser.parse_component(%{ "id" => "test", "name" => "mytest", "type" => "iocmd", "traits" => "test one", "other" => "Other"})
    %Serverboards.Plugin.Component{ id: "test", name: "mytest", type: "iocmd", traits: ["test","one"], extra: %{ "other" => "Other" }}

  Only some fields:

    iex> Serverboards.Plugin.Parser.parse_component(%{ "id" => "test"})
    %Serverboards.Plugin.Component{ id: "test"}

  """
  def parse_component(%{} = dict) do
    traits = case Map.get(dict, "traits") do
      nil ->
        []
      str when is_binary(str) ->
        String.split(str," ", trim: true)
    end

    %Serverboards.Plugin.Component{
      id: Map.get(dict, "id"),
      name: Map.get(dict, "name"),
      type: Map.get(dict, "type"),
      traits: traits,
      description: Map.get(dict, "description"),
      extra: Map.drop(dict, ~w(id name type traits description))
    }
  end

  @doc ~S"""
  Parses a dict that describes a plugin into a plugin struct.

  ## Example

    iex> Serverboards.Plugin.Parser.parse(%{ "id" => "test", "name" => "mytest", "components" => [
    ...>    %{ "id" => "test_A", "name" => "mytest", "type" => "iocmd", "traits" => "test one", "other" => "Other"},
    ...>    %{ "id" => "test_B", "name" => "othertest", "type" => "iocmd", "traits" => "auth"}
    ...>  ]
    ...>  })
    %Serverboards.Plugin{author: nil,
      components: [
        %Serverboards.Plugin.Component{
          extra: %{"other" => "Other"}, id: "test_A", name: "mytest",
          traits: ["test", "one"], type: "iocmd"
          },
       %Serverboards.Plugin.Component{
          extra: %{}, id: "test_B", name: "othertest", traits: ["auth"],
          type: "iocmd"
      }],
      description: nil, extra: %{}, id: "test",
      name: "mytest", version: nil
    }
  """
  def parse(%{} = dict) do
    components = Enum.map( Map.get(dict, "components", []), &parse_component(&1) )

    %Serverboards.Plugin{
      id: Map.get(dict, "id"),
      name: Map.get(dict, "name"),
      description: Map.get(dict, "description"),
      author: Map.get(dict, "author"),
      version: Map.get(dict, "version"),
      url: Map.get(dict, "url"),

      components: components,

      extra: Map.drop(dict, ~w(id name description author version url components))
    }
  end

  @doc ~S"""
  Given a string with yaml data, parses it into a plugin struct.

  ## Examples
    iex> {:ok, plugin} = Serverboards.Plugin.Parser.parse_yaml "id: serverboards.ls\nname: \"Ls\"\nauthor: \"David Moreno\"\nversion: 0.0.1\ncomponents:\n  - id: ls\n    name: ls\n    cmd: ./ls\n"
    iex> plugin.id
    "serverboards.ls"
    iex> (hd plugin.components).id
    "ls"

  """

  def parse_yaml(yaml, filename \\ "") do
    try do
      data = YamlElixir.read_from_string(yaml)
      plugin = parse(data)
      {:ok, plugin}
    catch
      {:yamerl_exception, [
        {:yamerl_parsing_error, :error, msg, line, column, _, _ , _} | _]} ->
          {:error, "#{line}:#{column} #{msg}"}
      e ->
        Logger.debug("Error loading yaml. #{inspect e}")
        {:error, e}
      :exit, {_reason, _where} ->
        Logger.debug("Error loading yaml. yamlr exited.")
        {:error, :exit}
    rescue
      e in CaseClauseError->
        %{ term: term } = e
        { _, _, _, where, _} = term
        line=where[:line]
        column=where[:column]
        Logger.error("Error loading yaml. Bad formed #{filename}:#{line}:#{column}")
        {:error, :bad_formed}
      e in FunctionClauseError ->
        Logger.error("Error loading yaml. Bad formed #{filename}")
        {:error, :bad_formed}
    end
  end

  @doc ~S"""
  Reads a plugin yaml from a file.

  ## Examples

    iex> {:ok, plugin} = Serverboards.Plugin.Parser.read("test/data/plugins/auth/manifest.yaml")
    iex> plugin.id
    "serverboards.test.auth"

    iex> Serverboards.Plugin.Parser.read("test/data/plugins/invalid/manifest.yaml")
    {:error, :invalid_yaml}

    iex> Serverboards.Plugin.Parser.read("test/data/non-existant.yaml")
    {:error, :enoent}
  """
  def read(filename) do
    ret = with {:ok, data} <- File.read(filename),
      {:ok, plugin} <- parse_yaml( data, filename ),
      do: {:ok, plugin}
    case ret do
      {:ok, v} -> {:ok, v}
      {:error, :enoent} ->
        {:error, :enoent}
      {:error, :bad_formed} ->
        {:error, :bad_formed}
      {:error, v} when is_binary(v) ->
        Logger.error("Error loading yaml file #{filename}:#{v}")
        {:error, :invalid_yaml}
    end
  end

  @doc ~S"""
  At that directory, checks all subdirectories with manifest.yaml file and
  return a list with the plugin descriptions.

  ## Example

    iex> {:ok, plugins} = read_dir("test/data/plugins/")
    iex> Enum.count plugins
    1
    iex> (hd plugins).id
    "serverboards.test.auth"

    iex> read_dir("test/data/invalid/")
    {:error, :enoent}

  """
  def read_dir(dirname) do
    is_directory = fn (dirname, filename) ->
      fullpath="#{dirname}/#{filename}"
      case File.stat(fullpath) do
        {:ok, stat} -> stat.type == :directory
        _ -> false
      end
    end

    is_valid_dirname = fn (dirname) ->
      cond do
        String.starts_with?(dirname, "__") -> false
        String.starts_with?(dirname, ".") -> false
        true -> true
      end
    end

    has_manifest = fn (dirname) ->
      fullpath="#{dirname}/manifest.yaml"
      case File.stat(fullpath) do
        {:ok, stat} -> stat.type == :regular
        _ -> false
      end
    end

    import Enum
    case File.ls(dirname) do
      {:ok, allfiles} ->
        plugins = allfiles
            |> filter(&is_directory.(dirname, &1))
            |> filter(&is_valid_dirname.(&1))
            |> filter(&has_manifest.(dirname<>"/"<>&1))
            |> map(&{read("#{dirname}/#{&1}/manifest.yaml"), &1}) # returns {{:ok, plugin}, midpath}
            |> filter(fn
                {{:ok, _}, _} -> true
                _ -> false
                end)
            |> map(fn {{:ok, p}, midpath} ->
              %Serverboards.Plugin{ p | path: "#{dirname}/#{midpath}" } # set plugin dirname
            end)

        {:ok, plugins}
      e ->
        e
    end
  end
end


Renamed schema

Renamed schema

defmodule API.Controllers.Transfer do
  @moduledoc """
  This controller implements REST API to send different kins of transfers and fetch information about them.
  """
  use API.Web, :controller
  alias Processing.Adapters.Pay2You.Transfer, as: TransferService
  alias API.Repo.Schemas.Transfer, as: TransferSchema
  alias API.Repo.Schemas.Card, as: CardSchema
  alias API.Repo.Schemas.CardNumber, as: CardNumberSchema
  alias API.Repo.Schemas.AuthorizationLookupCode, as: AuthorizationLookupCodeSchema
  alias API.Views.Transfer, as: TransferView
  alias API.Helpers.TokenResolver
  alias API.Repo
  alias Ecto.Changeset

  @transfer_token_prefix "transfer-token"

  @doc """
  POST /transfers
  """
  def create(conn, params) when is_map(params) do
    %TransferSchema{}
    |> TransferSchema.changeset(params)
    |> TokenResolver.resolve_credentials(:sender)
    |> TokenResolver.resolve_credentials(:recipient)
    |> send_transfer()
    |> put_transfer_token()
    |> create_transfer()
    |> render_response(conn, :created)
  end

  # Transfer Gateway delegates
  defp send_transfer(%Changeset{valid?: false} = changeset), do: changeset
  defp send_transfer(%Changeset{valid?: true} = changeset) do
    update = changeset
    |> Changeset.apply_changes()
    |> do_send_transfer()

    changeset
    |> Changeset.change(update)
  end

  # Card2Card transfers
  defp do_send_transfer(%{amount: amount, fee: fee,
                          sender: %{credential: %CardSchema{} = sender_credential, phone: sender_phone},
                          recipient: %{credential: %CardNumberSchema{} = recipient_credential}}) do
    case TransferService.card2card(sender_credential, recipient_credential, amount, fee, sender_phone) do
      {:ok, update} -> update
      {:error, update} -> update
    end
  end

  defp do_send_transfer(_) do
    external_id = 10_000
    |> :rand.uniform()
    |> to_string

    %{
      auth: %API.Repo.Schemas.Authorization3DS{},
      external_id: external_id
    }
  end

  defp put_transfer_token(%Changeset{valid?: false} = changeset), do: changeset
  defp put_transfer_token(%Changeset{valid?: true} = changeset) do
    expires_in = Confex.get(:gateway_api, :transfer_token_expires_in)
    expires_at = Timex.now |> Timex.shift(microseconds: expires_in)

    changeset
    |> Changeset.put_change(:token, @transfer_token_prefix <> "-" <> Ecto.UUID.generate)
    |> Changeset.put_change(:token_expires_at, expires_at)
  end

  # Store transfer changes into DB
  defp create_transfer(%Changeset{valid?: false} = changeset), do: {:error, changeset}
  defp create_transfer(%Changeset{} = changeset) do
    changeset
    |> TransferSchema.insert
  end

  @doc """
  GET /transfers/:id
  """
  def show(conn, %{"id" => id}) do
    TransferSchema
    |> Repo.get_by(id: id)
    |> validate_query_result()
    |> validate_token(conn)
    |> receive_transfer_status()
    |> update_transfer()
    |> render_response(conn)
  end

  defp receive_transfer_status({:error, reason}), do: {:error, reason}
  defp receive_transfer_status({:ok, %TransferSchema{external_id: external_id} = transfer}) do
    {_, update} = Processing.Adapters.Pay2You.Status.get(external_id)

    transfer = transfer
    |> Changeset.change(update)

    {:ok, transfer}
  end

  @doc """
  POST /transfers/:id/auth
  """
  def authentificate(conn, %{"id" => id} = params) do
    TransferSchema
    |> Repo.get_by(id: id)
    |> validate_query_result()
    |> validate_token(conn)
    |> validate_otp_code(params)
    |> update_transfer()
    |> render_response(conn)
  end

  defp validate_query_result(nil), do: {:error, :not_found}
  defp validate_query_result(%TransferSchema{} = transfer), do: {:ok, transfer}

  defp validate_otp_code({:error, reason}, _params), do: {:error, reason}
  defp validate_otp_code({:ok, %TransferSchema{auth: %AuthorizationLookupCodeSchema{} = transfer_auth} = transfer}, params) do
    transfer = case Processing.Adapters.Pay2You.LookupAuth.auth(transfer_auth, params["code"]) do
      {:ok, update} ->
        transfer
        |> Changeset.change(update)
      {:error, :invalid_otp_code} ->
        transfer
        |> Changeset.change()
        |> Changeset.add_error(:code, "is invalid", validation: :otp_code)
      {:error, %{status: _} = error_update} ->
        transfer
        |> Changeset.change(error_update)
    end

    {:ok, transfer}
  end
  defp validate_otp_code({:ok, _transfer}, _params), do: {:error, :not_found}

  defp validate_token({:error, reason}, _conn), do: {:error, reason}
  defp validate_token({:ok, %{token: transfer_token} = transfer}, %Plug.Conn{assigns: %{token: user_token}})
    when transfer_token == user_token,
    do: {:ok, transfer}
  defp validate_token({:ok, _transfer}, _conn), do: {:error, :access_denied}

  defp update_transfer({:error, reason}), do: {:error, reason}
  defp update_transfer({:ok, %Changeset{valid?: false} = changeset}), do: {:error, changeset}
  defp update_transfer({:ok, changeset}) do
    changeset
    |> TransferSchema.update
  end

  # Responses
  defp render_response(state, conn, status \\ :ok)

  defp render_response({:ok, %TransferSchema{} = transfer}, conn, status) do
    conn
    |> put_status(status)
    |> render(TransferView, "transfer.json", transfer: transfer)
  end

  defp render_response({:error, %Changeset{valid?: false} = changeset}, conn, _status) do
    conn
    |> put_status(422)
    |> render(EView.Views.ValidationError, "422.json", changeset)
  end

  defp render_response({:error, :not_found}, conn, _status) do
    conn
    |> put_status(404)
    |> render(EView.Views.Error, "404.json", %{})
  end

  defp render_response({:error, :access_denied}, conn, _status) do
    conn
    |> put_status(401)
    |> render(EView.Views.Error, "401.json", %{})
  end
end

defmodule API.Controllers.Transfer do
  @moduledoc """
  This controller implements REST API to send different kins of transfers and fetch information about them.
  """
  use API.Web, :controller
  alias Processing.Adapters.Pay2You.Transfer, as: TransferService
  alias API.Repo.Schemas.Transfer, as: TransferSchema
  alias API.Repo.Schemas.Card, as: CardSchema
  alias API.Repo.Schemas.CardNumber, as: CardNumberSchema
  alias API.Repo.Schemas.AuthorizationLookupCode
  alias API.Views.Transfer, as: TransferView
  alias API.Helpers.TokenResolver
  alias API.Repo
  alias Ecto.Changeset

  @transfer_token_prefix "transfer-token"

  @doc """
  POST /transfers
  """
  def create(conn, params) when is_map(params) do
    %TransferSchema{}
    |> TransferSchema.changeset(params)
    |> TokenResolver.resolve_credentials(:sender)
    |> TokenResolver.resolve_credentials(:recipient)
    |> send_transfer()
    |> put_transfer_token()
    |> create_transfer()
    |> render_response(conn, :created)
  end

  # Transfer Gateway delegates
  defp send_transfer(%Changeset{valid?: false} = changeset), do: changeset
  defp send_transfer(%Changeset{valid?: true} = changeset) do
    update = changeset
    |> Changeset.apply_changes()
    |> do_send_transfer()

    changeset
    |> Changeset.change(update)
  end

  # Card2Card transfers
  defp do_send_transfer(%{amount: amount, fee: fee,
                          sender: %{credential: %CardSchema{} = sender_credential, phone: sender_phone},
                          recipient: %{credential: %CardNumberSchema{} = recipient_credential}}) do
    case TransferService.card2card(sender_credential, recipient_credential, amount, fee, sender_phone) do
      {:ok, update} -> update
      {:error, update} -> update
    end
  end

  defp do_send_transfer(_) do
    external_id = 10_000
    |> :rand.uniform()
    |> to_string

    %{
      auth: %API.Repo.Schemas.Authorization3DS{},
      external_id: external_id
    }
  end

  defp put_transfer_token(%Changeset{valid?: false} = changeset), do: changeset
  defp put_transfer_token(%Changeset{valid?: true} = changeset) do
    expires_in = Confex.get(:gateway_api, :transfer_token_expires_in)
    expires_at = Timex.now |> Timex.shift(microseconds: expires_in)

    changeset
    |> Changeset.put_change(:token, @transfer_token_prefix <> "-" <> Ecto.UUID.generate)
    |> Changeset.put_change(:token_expires_at, expires_at)
  end

  # Store transfer changes into DB
  defp create_transfer(%Changeset{valid?: false} = changeset), do: {:error, changeset}
  defp create_transfer(%Changeset{} = changeset) do
    changeset
    |> TransferSchema.insert
  end

  @doc """
  GET /transfers/:id
  """
  def show(conn, %{"id" => id}) do
    TransferSchema
    |> Repo.get_by(id: id)
    |> validate_query_result()
    |> validate_token(conn)
    |> receive_transfer_status()
    |> update_transfer()
    |> render_response(conn)
  end

  defp receive_transfer_status({:error, reason}), do: {:error, reason}
  defp receive_transfer_status({:ok, %TransferSchema{external_id: external_id} = transfer}) do
    {_, update} = Processing.Adapters.Pay2You.Status.get(external_id)

    transfer = transfer
    |> Changeset.change(update)

    {:ok, transfer}
  end

  @doc """
  POST /transfers/:id/auth
  """
  def authentificate(conn, %{"id" => id} = params) do
    TransferSchema
    |> Repo.get_by(id: id)
    |> validate_query_result()
    |> validate_token(conn)
    |> validate_otp_code(params)
    |> update_transfer()
    |> render_response(conn)
  end

  defp validate_query_result(nil), do: {:error, :not_found}
  defp validate_query_result(%TransferSchema{} = transfer), do: {:ok, transfer}

  defp validate_otp_code({:error, reason}, _params), do: {:error, reason}
  defp validate_otp_code({:ok, %TransferSchema{auth: %AuthorizationLookupCode{} = transfer_auth} = transfer}, params) do
    transfer = case Processing.Adapters.Pay2You.LookupAuth.auth(transfer_auth, params["code"]) do
      {:ok, update} ->
        transfer
        |> Changeset.change(update)
      {:error, :invalid_otp_code} ->
        transfer
        |> Changeset.change()
        |> Changeset.add_error(:code, "is invalid", validation: :otp_code)
      {:error, %{status: _} = error_update} ->
        transfer
        |> Changeset.change(error_update)
    end

    {:ok, transfer}
  end
  defp validate_otp_code({:ok, _transfer}, _params), do: {:error, :not_found}

  defp validate_token({:error, reason}, _conn), do: {:error, reason}
  defp validate_token({:ok, %{token: transfer_token} = transfer}, %Plug.Conn{assigns: %{token: user_token}})
    when transfer_token == user_token,
    do: {:ok, transfer}
  defp validate_token({:ok, _transfer}, _conn), do: {:error, :access_denied}

  defp update_transfer({:error, reason}), do: {:error, reason}
  defp update_transfer({:ok, %Changeset{valid?: false} = changeset}), do: {:error, changeset}
  defp update_transfer({:ok, changeset}) do
    changeset
    |> TransferSchema.update
  end

  # Responses
  defp render_response(state, conn, status \\ :ok)

  defp render_response({:ok, %TransferSchema{} = transfer}, conn, status) do
    conn
    |> put_status(status)
    |> render(TransferView, "transfer.json", transfer: transfer)
  end

  defp render_response({:error, %Changeset{valid?: false} = changeset}, conn, _status) do
    conn
    |> put_status(422)
    |> render(EView.Views.ValidationError, "422.json", changeset)
  end

  defp render_response({:error, :not_found}, conn, _status) do
    conn
    |> put_status(404)
    |> render(EView.Views.Error, "404.json", %{})
  end

  defp render_response({:error, :access_denied}, conn, _status) do
    conn
    |> put_status(401)
    |> render(EView.Views.Error, "401.json", %{})
  end
end


Better loaded module logging

Better loaded module logging

defmodule Hypnotoad.Modules do
  use Hypnotoad.Common
  use GenServer.Behaviour

  def start_link do
  	:gen_server.start_link {:local, __MODULE__}, __MODULE__, [], []
  end

  def modules do
    :gen_server.call(__MODULE__, :modules)
  end

  defrecordp :state, modules: []

  def init(_) do
  	:gen_server.cast(self, :init)
  	{:ok, state()}
  end

  def handle_cast(:init, state(modules: modules) = s) do
    Enum.each(modules, fn({module, _}) ->
      :code.delete(module)
      :code.purge(module)
    end)
  	modules = Path.join([Hypnotoad.path, "**/**.exs"])
  	|> Path.wildcard
  	|> Enum.map(fn(file) ->
      Enum.reduce(Code.load_file(file), [], fn({module, _binary}, acc) ->
        if hypnotoad_module?(module) do
          [{module, file}|acc]
        else
          acc
        end
      end)
  	end)
    |> List.flatten |> Enum.reverse
    Enum.each(modules, fn({module, _}) ->
      if module.plan?, do: Hypnotoad.Plan.start(module: module)
    end)
    Enum.each(modules, fn({module, file}) ->
      L.debug "Loaded module ${module} (${file})", module: module, file: file
    end)
    :gproc_ps.publish :l, {__MODULE__, :modules_reloaded}, modules
  	{:noreply, state(s, modules: modules)}
  end

  def handle_call(:modules, _from, state(modules: modules) = s) do
    {:reply, modules, s}
  end

  defp hypnotoad_module?(module) do
    if function_exported?(module, :__info__, 1) do
      module.__info__(:attributes)[:hypnotoad_module] == [true]
    else
      false
    end
  end
end

defmodule Hypnotoad.Modules do
  use Hypnotoad.Common
  use GenServer.Behaviour

  def start_link do
  	:gen_server.start_link {:local, __MODULE__}, __MODULE__, [], []
  end

  def modules do
    :gen_server.call(__MODULE__, :modules)
  end

  defrecordp :state, modules: []

  def init(_) do
  	:gen_server.cast(self, :init)
  	{:ok, state()}
  end

  def handle_cast(:init, state(modules: modules) = s) do
    Enum.each(modules, fn({module, _}) ->
      :code.delete(module)
      :code.purge(module)
    end)
  	modules = Path.join([Hypnotoad.path, "**/**.exs"])
  	|> Path.wildcard
  	|> Enum.map(fn(file) ->
      Enum.reduce(Code.load_file(file), [], fn({module, _binary}, acc) ->
        if hypnotoad_module?(module) do
          [{module, file}|acc]
        else
          acc
        end
      end)
  	end)
    |> List.flatten |> Enum.reverse
    Enum.each(modules, fn({module, _}) ->
      if module.plan?, do: Hypnotoad.Plan.start(module: module)
    end)
    L.debug "Loaded modules ${modules}", modules: modules
    :gproc_ps.publish :l, {__MODULE__, :modules_reloaded}, modules
  	{:noreply, state(s, modules: modules)}
  end

  def handle_call(:modules, _from, state(modules: modules) = s) do
    {:reply, modules, s}
  end

  defp hypnotoad_module?(module) do
    if function_exported?(module, :__info__, 1) do
      module.__info__(:attributes)[:hypnotoad_module] == [true]
    else
      false
    end
  end
end