defmodule Nola.Plugins.Coronavirus 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(Nola.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 = %Nola.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 = %Nola.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 = %Nola.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