| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- defmodule Vaccins.LocationStore do
- defmodule Location do
- alias __MODULE__, as: Location
- @limit 60 * 60 * 24
- defstruct [
- :id,
- :name,
- :location,
- :geographic_area,
- :availability_query,
- :booking_page,
- :provider,
- availability_query_params: []
- ]
- def from_json(m = %{provider: "Elixir.Vaccins.Queries.Doctolib"}),
- do: struct(Location, m) |> Map.replace(:provider, Vaccins.Queries.Doctolib)
- def set_id(l = %Location{name: name}), do: %{l | id: name |> String.to_atom()}
- def build_query(l = %Location{availability_query_params: params, provider: provider})
- when params != [],
- do: %{l | availability_query: params |> provider.new}
- def query_availability(%Location{availability_query: q, provider: provider}) do
- with url <- q |> provider.to_url() |> URI.to_string(),
- {:ok, result} <- url |> Vaccins.Scraper.get_json() do
- case result |> provider.analyze_result() do
- {:ok, slots} ->
- case slots
- |> Enum.map(&DateTime.truncate(&1, :second))
- |> Enum.group_by(&(&1 |> DateTime.diff(DateTime.utc_now()) |> abs < @limit)) do
- grouped = %{true: before_limit} ->
- {:ok, before_limit |> sort, grouped |> Map.get(false, []) |> sort}
- %{false: after_limit} ->
- {:ok, after_limit |> sort}
- end
- error = {:error, reason} when reason in [:no_availability] ->
- error
- end
- end
- end
- defp sort(list) when is_list(list), do: list |> Enum.sort_by(& &1, {:asc, DateTime})
- end
- defmodule LocationRaw do
- use Ecto.Schema
- import Ecto.Changeset
- @primary_key {:id, :id, autogenerate: false}
- embedded_schema do
- field(:name, :string)
- field(:location, :string)
- field(:geographic_area, :string)
- field(:booking_page, :string)
- field(:raw_query, :string)
- end
- @doc false
- def changeset(location \\ %__MODULE__{}, attrs),
- do:
- location
- |> cast(attrs, [:name, :location, :booking_page, :geographic_area, :raw_query])
- |> validate_required([:name, :location, :booking_page, :geographic_area, :raw_query])
- def to_query_params(%__MODULE__{raw_query: raw}) do
- raw
- |> URI.parse()
- |> Map.get(:query)
- |> URI.decode_query()
- |> Map.new(fn {k, v} -> {k |> String.to_atom(), v} end)
- |> Map.take([:agenda_ids, :limit, :practice_ids, :visit_motive_ids])
- end
- end
- use GenServer
- require Ex2ms
- import Ecto.Changeset
- alias Vaccins.Queries.Doctolib
- @name Vaccins.LocationStore
- @file_path "./location_store.json"
- def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: @name)
- @impl true
- def init(_opts) do
- {:ok, load_state()}
- end
- def reload() do
- GenServer.call(@name, :reload)
- end
- def get_locations() do
- GenServer.call(@name, :get_locations)
- end
- def add_location(params) do
- GenServer.call(@name, {:add_location, params})
- end
- def delete_location(id) do
- GenServer.call(@name, {:delete_location, id})
- end
- @impl true
- def handle_call(:reload, _, _), do: {:reply, :ok, load_state()}
- @impl true
- def handle_call(:get_locations, _, state = %{locations: locations}),
- do: {:reply, locations |> Map.values() |> Enum.map(&Location.build_query/1), state}
- @impl true
- def handle_call({:add_location, params}, _, state = %{locations: locations}) do
- cs = params |> LocationRaw.changeset()
- with {:ok, raw_location} <- cs |> apply_action(:insert),
- processed <-
- %Location{
- name: raw_location.name,
- location: raw_location.location,
- booking_page: raw_location.booking_page,
- availability_query_params: raw_location |> LocationRaw.to_query_params(),
- provider: Doctolib
- }
- |> Location.set_id(),
- locations <- locations |> Map.put(processed.id, processed),
- new_state <- %{locations: locations},
- :ok <- new_state |> dump_state do
- :ok
- {:reply, :ok, new_state}
- else
- e = {:error, _} -> {:reply, e, state}
- end
- end
- @impl true
- def handle_call({:delete_location, id}, _, %{locations: locations}) do
- with locations <- locations |> Map.delete(id),
- new_state <- %{locations: locations},
- :ok <- new_state |> dump_state,
- do: {:reply, :ok, new_state}
- end
- defp load_state(), do: %{locations: read_file() |> Map.new(&{&1.id, &1})}
- defp read_file(),
- do:
- with(
- {:ok, content} <- File.read(@file_path),
- {:ok, decoded} <- Jason.decode(content, keys: :atoms),
- do: decoded |> Enum.map(&Location.from_json/1)
- )
- defp dump_state(%{locations: locations}), do: locations |> Map.values() |> write_file()
- defp write_file(locations),
- do:
- with(
- {:ok, encoded} <-
- Jason.encode(locations |> Enum.map(&(&1 |> Map.from_struct())), pretty: true),
- do: File.write(@file_path, encoded)
- )
- end
|