defmodule LSG.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