From aaba05529e36433519067702f2da0c6e97f5ab5a Mon Sep 17 00:00:00 2001
From: Daniel Flanagan <daniel@lyte.dev>
Date: Thu, 3 Dec 2020 19:03:37 -0600
Subject: [PATCH] Add input helper (#3)

Co-authored-by: Mitchell Hanberg <mitch@mitchellhanberg.com>
---
 .gitignore                  |  2 +
 README.md                   | 30 ++++++++++++-
 config/config.exs           | 16 +++++++
 config/test.exs             |  3 ++
 lib/advent_of_code/input.ex | 87 +++++++++++++++++++++++++++++++++++++
 mix.exs                     |  8 ++--
 6 files changed, 141 insertions(+), 5 deletions(-)
 create mode 100644 config/test.exs
 create mode 100644 lib/advent_of_code/input.ex

diff --git a/.gitignore b/.gitignore
index 7bdbd47..6e95091 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,3 +22,5 @@ erl_crash.dump
 # Ignore package tarball (built via "mix hex.build").
 advent_of_code_2018-*.tar
 
+# Ignore configuration secrets
+/config/secrets.exs
diff --git a/README.md b/README.md
index de6e3a9..5efa7a0 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ defmodule Mix.Tasks.D01.P1 do
 
   @shortdoc "Day 01 Part 1"
   def run(args) do
-    input = nil
+    input = AdventOfCode.Input.get!(2020, 1)
 
     if Enum.member?(args, "-b"),
       do: Benchee.run(%{part_1: fn -> input |> part1() end}),
@@ -65,6 +65,34 @@ defmodule Mix.Tasks.D01.P1 do
 end
 ```
 
+### Optional Automatic Input Retriever
+
+This starter comes with a module that will automatically get your inputs so you
+don't have to mess with copy/pasting. Don't worry, it automatically caches your
+inputs to your machine so you don't have to worry about slamming the Advent of
+Code server. You will need to configure it with your cookie and make sure to
+enable it. You can do this by creating a `config/secrets.exs` file containing
+the following:
+
+```elixir
+config :advent_of_code, AdventOfCode.Input,
+  allow_network?: true,
+  session_cookie: "..." # yours will be longer
+```
+
+After which, you can retrieve your inputs using the module:
+
+```elixir
+day = 1
+year = 2020
+AdventOfCode.Input.get!(day, year)
+# or just have it auto-detect the current year
+AdventOfCode.Input.get!(7)
+# and if your input somehow gets mangled and you need a fresh one:
+AdventOfCode.Input.delete!(7, 2019)
+# and the next time you `get!` it will download a fresh one -- use this sparingly!
+```
+
 ## Installation
 
 ```bash
diff --git a/config/config.exs b/config/config.exs
index d2d855e..4422bd5 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -1 +1,17 @@
 use Mix.Config
+
+config :advent_of_code, AdventOfCode.Input,
+  # allow_network?: true,
+  session_cookie: System.get_env("ADVENT_OF_CODE_SESSION_COOKIE")
+
+# If you don't like environment variables, put your cookie in
+# a `config/secrets.exs` file like this:
+#
+# config :advent_of_code, AdventOfCode.Input,
+#   advent_of_code_session_cookie: "session=..."
+
+try do
+  import_config "secrets.exs"
+rescue
+  _ -> :ok
+end
diff --git a/config/test.exs b/config/test.exs
new file mode 100644
index 0000000..e7693e4
--- /dev/null
+++ b/config/test.exs
@@ -0,0 +1,3 @@
+# Don't let CI fetch inputs from the server! Be nice!
+# https://www.reddit.com/r/adventofcode/comments/5h6mmt/how_to_read_input/day6jlw
+config :advent_of_code, AdventOfCode.Input, allow_network?: false
diff --git a/lib/advent_of_code/input.ex b/lib/advent_of_code/input.ex
new file mode 100644
index 0000000..0b2394c
--- /dev/null
+++ b/lib/advent_of_code/input.ex
@@ -0,0 +1,87 @@
+defmodule AdventOfCode.Input do
+  @moduledoc """
+  This module can help with automatically managing your Advent of Code input
+  files. It will retrieve them once from the server and cache them to your
+  machine.
+
+  By default, it is configured to have network requests disabled. You can
+  easily turn it on by editing the configuration.
+  """
+
+  @doc """
+  Retrieves the specified input for your account. If the input is not in your
+  cache, it will be retrieved from the server if `allow_network?: true` is
+  configured and your cookie is setup.
+  """
+  def get!(day, year \\ nil)
+  def get!(day, nil), do: get!(day, default_year())
+
+  def get!(day, year) do
+    cond do
+      in_cache?(day, year) ->
+        from_cache!(day, year)
+
+      allow_network?() ->
+        download!(day, year)
+
+      true ->
+        raise "Cache miss for day #{day} of year #{year} and `:allow_network?` is not `true`"
+    end
+  end
+
+  @doc """
+  If, somehow, your input is invalid or mangled and you want to delete it from
+  your cache so you can re-fetch it, this will save your bacon.
+  Please don't use this to retrieve the input from the server repeatedly!
+  """
+  def delete!(day, year \\ nil)
+  def delete!(day, nil), do: delete!(day, default_year())
+  def delete!(day, year), do: File.rm!(cache_path(day, year))
+
+  defp cache_path(day, year), do: Path.join(cache_dir(), "/#{year}/#{day}.aocinput")
+  defp in_cache?(day, year), do: File.exists?(cache_path(day, year))
+
+  defp store_in_cache!(day, year, input) do
+    path = cache_path(day, year)
+    :ok = path |> Path.dirname() |> File.mkdir_p()
+    :ok = File.write(path, input)
+  end
+
+  defp from_cache!(day, year), do: File.read!(cache_path(day, year))
+
+  defp download!(day, year) do
+    {:ok, {{'HTTP/1.1', 200, 'OK'}, _, input}} =
+      :httpc.request(
+        :get,
+        {'https://adventofcode.com/#{year}/day/#{day}/input', headers()},
+        [],
+        []
+      )
+
+    store_in_cache!(day, year, input)
+
+    to_string(input)
+  end
+
+  defp cache_dir do
+    config()
+    |> Keyword.get(
+      :cache_dir,
+      Path.join([System.get_env("XDG_CACHE_HOME", "~/.cache"), "/advent_of_code_inputs"])
+    )
+    |> Path.expand()
+  end
+
+  defp default_year do
+    case :calendar.local_time() do
+      {{y, 12, _}, _} -> y
+      {{y, _, _}, _} -> y - 1
+    end
+  end
+
+  defp config, do: Application.get_env(:advent_of_code, __MODULE__)
+  defp allow_network?, do: Keyword.get(config(), :allow_network?, false)
+
+  defp headers,
+    do: [{'cookie', String.to_charlist("session=" <> Keyword.get(config(), :session_cookie))}]
+end
diff --git a/mix.exs b/mix.exs
index 7cb9dd5..ccedd9a 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,11 +1,11 @@
-defmodule AdventOfCode2019.MixProject do
+defmodule AdventOfCode.MixProject do
   use Mix.Project
 
   def project do
     [
-      app: :advent_of_code_2019,
+      app: :advent_of_code,
       version: "0.1.0",
-      elixir: "~> 1.7",
+      elixir: "~> 1.9",
       start_permanent: Mix.env() == :prod,
       deps: deps()
     ]
@@ -14,7 +14,7 @@ defmodule AdventOfCode2019.MixProject do
   # Run "mix help compile.app" to learn about applications.
   def application do
     [
-      extra_applications: [:logger]
+      extra_applications: [:logger, :inets]
     ]
   end
 
-- 
2.43.0