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