diff options
Diffstat (limited to 'lib/nola_plugins/coronavirus_plugin.ex')
-rw-r--r-- | lib/nola_plugins/coronavirus_plugin.ex | 172 |
1 files changed, 172 insertions, 0 deletions
diff --git a/lib/nola_plugins/coronavirus_plugin.ex b/lib/nola_plugins/coronavirus_plugin.ex new file mode 100644 index 0000000..d04d8f9 --- /dev/null +++ b/lib/nola_plugins/coronavirus_plugin.ex @@ -0,0 +1,172 @@ +defmodule Nola.IRC.CoronavirusPlugin do + require Logger + NimbleCSV.define(CovidCsv, separator: ",", escape: "\"") + @moduledoc """ + # Corona Virus + + Données de [Johns Hopkins University](https://github.com/CSSEGISandData/COVID-19) et mises à jour a peu près tous les jours. + + * `!coronavirus [France | Country]`: :-) + * `!coronavirus`: top 10 confirmés et non guéris + * `!coronavirus confirmés`: top 10 confirmés + * `!coronavirus morts`: top 10 morts + * `!coronavirus soignés`: top 10 soignés + """ + def irc_doc, do: @moduledoc + + def start_link() do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + def init(_) do + {:ok, _} = Registry.register(IRC.PubSub, "trigger:coronavirus", [plugin: __MODULE__]) + {:ok, nil, {:continue, :init}} + :ignore + end + + def handle_continue(:init, _) do + date = Date.add(Date.utc_today(), -2) + {data, _} = fetch_data(%{}, date) + {data, next} = fetch_data(data) + :timer.send_after(next, :update) + {:noreply, %{data: data}} + end + + def handle_info(:update, state) do + {data, next} = fetch_data(state.data) + :timer.send_after(next, :update) + {:noreply, %{data: data}} + end + + def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :bang, args: args}}}, state) when args in [ + [], ["morts"], ["confirmés"], ["soignés"], ["malades"], ["n"], ["nmorts"], ["nsoignés"], ["nconfirmés"]] do + {field, name} = case args do + ["confirmés"] -> {:confirmed, "confirmés"} + ["morts"] -> {:deaths, "morts"} + ["soignés"] -> {:recovered, "soignés"} + ["nmorts"] -> {:new_deaths, "nouveaux morts"} + ["nconfirmés"] -> {:new_confirmed, "nouveaux confirmés"} + ["n"] -> {:new_current, "nouveaux malades"} + ["nsoignés"] -> {:new_recovered, "nouveaux soignés"} + _ -> {:current, "malades"} + end + IO.puts("FIELD #{inspect field}") + field_evol = String.to_atom("new_#{field}") + sorted = state.data + |> Enum.filter(fn({_, %{region: region}}) -> region == true end) + |> Enum.map(fn({location, data}) -> {location, Map.get(data, field, 0), Map.get(data, field_evol, 0)} end) + |> Enum.sort_by(fn({_,count,_}) -> count end, &>=/2) + |> Enum.take(10) + |> Enum.with_index() + |> Enum.map(fn({{location, count, evol}, index}) -> + ev = if String.starts_with?(name, "nouveaux") do + "" + else + " (#{Util.plusminus(evol)})" + end + "##{index+1}: #{location} #{count}#{ev}" + end) + |> Enum.intersperse(" - ") + |> Enum.join() + m.replyfun.("CORONAVIRUS TOP10 #{name}: " <> sorted) + {:noreply, state} + end + + def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :bang, args: location}}}, state) do + location = Enum.join(location, " ") |> String.downcase() + if data = Map.get(state.data, location) do + m.replyfun.("coronavirus: #{location}: " + <> "#{data.current} malades (#{Util.plusminus(data.new_current)}), " + <> "#{data.confirmed} confirmés (#{Util.plusminus(data.new_confirmed)}), " + <> "#{data.deaths} morts (#{Util.plusminus(data.new_deaths)}), " + <> "#{data.recovered} soignés (#{Util.plusminus(data.new_recovered)}) (@ #{data.update})") + end + {:noreply, state} + end + + def handle_info({:irc, :trigger, "coronavirus", m = %IRC.Message{trigger: %{type: :query, args: location}}}, state) do + m.replyfun.("https://github.com/CSSEGISandData/COVID-19") + {:noreply, state} + end + + # 1. Try to fetch data for today + # 2. Fetch yesterday if no results + defp fetch_data(current_data, date \\ nil) do + now = Date.utc_today() + url = fn(date) -> + "https://github.com/CSSEGISandData/COVID-19/raw/master/csse_covid_19_data/csse_covid_19_daily_reports/#{date}.csv" + end + request_date = date || now + Logger.debug("Coronavirus check date: #{inspect request_date}") + {:ok, date_s} = Timex.format({request_date.year, request_date.month, request_date.day}, "%m-%d-%Y", :strftime) + cur_url = url.(date_s) + Logger.debug "Fetching URL #{cur_url}" + case HTTPoison.get(cur_url, [], follow_redirect: true) do + {:ok, %HTTPoison.Response{status_code: 200, body: csv}} -> + # Parse CSV update data + data = csv + |> CovidCsv.parse_string() + |> Enum.reduce(%{}, fn(line, acc) -> + case line do + # FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key + #0FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key,Incidence_Rate,Case-Fatality_Ratio + [_, _, state, region, update, _lat, _lng, confirmed, deaths, recovered, _active, _combined_key, _incidence_rate, _fatality_ratio] -> + state = String.downcase(state) + region = String.downcase(region) + confirmed = String.to_integer(confirmed) + deaths = String.to_integer(deaths) + recovered = String.to_integer(recovered) + + current = (confirmed - recovered) - deaths + + entry = %{update: update, confirmed: confirmed, deaths: deaths, recovered: recovered, current: current, region: region} + + region_entry = Map.get(acc, region, %{update: nil, confirmed: 0, deaths: 0, recovered: 0, current: 0}) + region_entry = %{ + update: region_entry.update || update, + confirmed: region_entry.confirmed + confirmed, + deaths: region_entry.deaths + deaths, + current: region_entry.current + current, + recovered: region_entry.recovered + recovered, + region: true + } + + changes = if old = Map.get(current_data, region) do + %{ + new_confirmed: region_entry.confirmed - old.confirmed, + new_current: region_entry.current - old.current, + new_deaths: region_entry.deaths - old.deaths, + new_recovered: region_entry.recovered - old.recovered, + } + else + %{new_confirmed: 0, new_current: 0, new_deaths: 0, new_recovered: 0} + end + + region_entry = Map.merge(region_entry, changes) + + acc = Map.put(acc, region, region_entry) + + acc = if state && state != "" do + Map.put(acc, state, entry) + else + acc + end + + other -> + Logger.info("Coronavirus line failed: #{inspect line}") + acc + end + end) + Logger.info "Updated coronavirus database" + {data, :timer.minutes(60)} + {:ok, %HTTPoison.Response{status_code: 404}} -> + Logger.debug "Corona 404 #{cur_url}" + date = Date.add(date || now, -1) + fetch_data(current_data, date) + other -> + Logger.error "Coronavirus: Update failed #{inspect other}" + {current_data, :timer.minutes(5)} + end + end + +end |