summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorhref <href@random.sh>2020-04-17 15:53:14 +0200
committerhref <href@random.sh>2020-04-17 15:53:14 +0200
commit919725a6941830ce82c835ed3288c1722ddd8c9f (patch)
tree49a95b0ce716a24c7e056036d3353ceca1debe4a /lib
parentwelp (diff)
bleh
Diffstat (limited to '')
-rw-r--r--lib/alcool.ex16
-rw-r--r--lib/irc/pubsub_handler.ex9
-rw-r--r--lib/irc/user_track.ex6
-rw-r--r--lib/lsg/application.ex1
-rw-r--r--lib/lsg/token.ex38
-rw-r--r--lib/lsg_irc.ex4
-rw-r--r--lib/lsg_irc/alcolog_plugin.ex898
-rw-r--r--lib/lsg_irc/alcoolog_announcer_plugin.ex198
-rw-r--r--lib/lsg_irc/bourosama_plugin.ex57
-rw-r--r--lib/lsg_irc/coronavirus_plugin.ex116
-rw-r--r--lib/lsg_irc/correction_plugin.ex46
-rw-r--r--lib/lsg_irc/link_plugin.ex97
-rw-r--r--lib/lsg_irc/link_plugin/imgur.ex8
-rw-r--r--lib/lsg_irc/seen_plugin.ex58
-rw-r--r--lib/lsg_irc/txt_plugin.ex39
-rw-r--r--lib/lsg_irc/wolfram_alpha_plugin.ex47
-rw-r--r--lib/lsg_web/controllers/alcoolog_controller.ex48
-rw-r--r--lib/lsg_web/router.ex2
-rw-r--r--lib/lsg_web/templates/alcoolog/index.html.eex55
-rw-r--r--lib/lsg_web/views/alcoolog_view.ex4
-rw-r--r--lib/lsg_web/views/layout_view.ex17
-rw-r--r--lib/util.ex13
22 files changed, 1620 insertions, 157 deletions
diff --git a/lib/alcool.ex b/lib/alcool.ex
new file mode 100644
index 0000000..40384ba
--- /dev/null
+++ b/lib/alcool.ex
@@ -0,0 +1,16 @@
+defmodule Alcool do
+
+ @spec units(Float.t, Float.t) :: Float.t
+ def units(cl, degrees) do
+ kg(cl, degrees)*100
+ end
+
+ def grams(cl, degrees) do
+ kg(cl, degrees)*1000
+ end
+
+ def kg(cl, degrees) do
+ (((cl/100) * 0.8) * (degrees/100))
+ end
+
+end
diff --git a/lib/irc/pubsub_handler.ex b/lib/irc/pubsub_handler.ex
index 450dc2f..61ae55c 100644
--- a/lib/irc/pubsub_handler.ex
+++ b/lib/irc/pubsub_handler.ex
@@ -23,6 +23,10 @@ defmodule IRC.PubSubHandler do
GenServer.start_link(__MODULE__, [client], [name: __MODULE__])
end
+ def privmsg(channel, line) do
+ GenServer.cast(__MODULE__, {:privmsg, channel, line})
+ end
+
def init([client]) do
ExIRC.Client.add_handler(client, self())
{:ok, client}
@@ -37,6 +41,11 @@ defmodule IRC.PubSubHandler do
"~" => :tilde
}
+ def handle_cast({:privmsg, channel, line}, client) do
+ irc_reply(client, {channel, nil}, line)
+ {:noreply, client}
+ end
+
def handle_info({:received, text, sender, chan}, client) do
reply_fun = fn(text) -> irc_reply(client, {chan, sender}, text) end
message = %IRC.Message{text: text, sender: sender, channel: chan, replyfun: reply_fun, trigger: extract_trigger(text)}
diff --git a/lib/irc/user_track.ex b/lib/irc/user_track.ex
index 2614d98..02d5752 100644
--- a/lib/irc/user_track.ex
+++ b/lib/irc/user_track.ex
@@ -85,6 +85,12 @@ defmodule IRC.UserTrack do
end
end
+ def channel(channel) do
+ Enum.filter(to_list(), fn({_, nick, _, _, _, _, channels}) ->
+ Map.get(channels, channel)
+ end)
+ end
+
def joined(c, s), do: joined(c,s,[])
def joined(channel, sender=%{nick: nick, user: uname, host: host}, privileges) do
diff --git a/lib/lsg/application.ex b/lib/lsg/application.ex
index 8ee8aa2..972767f 100644
--- a/lib/lsg/application.ex
+++ b/lib/lsg/application.ex
@@ -14,6 +14,7 @@ defmodule LSG.Application do
# worker(LSG.Worker, [arg1, arg2, arg3]),
worker(Registry, [[keys: :duplicate, name: LSG.BroadcastRegistry]], id: :registry_broadcast),
worker(LSG.IcecastAgent, []),
+ worker(LSG.Token, []),
#worker(LSG.Icecast, []),
] ++ LSG.IRC.application_childs
diff --git a/lib/lsg/token.ex b/lib/lsg/token.ex
new file mode 100644
index 0000000..33946d4
--- /dev/null
+++ b/lib/lsg/token.ex
@@ -0,0 +1,38 @@
+defmodule LSG.Token do
+ use GenServer
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [], [name: __MODULE__])
+ end
+
+ def lookup(id) do
+ with \
+ [{_, cred, date}] <- :ets.lookup(__MODULE__.ETS, id),
+ IO.inspect("cred: #{inspect cred} valid for #{inspect date} now #{inspect DateTime.utc_now()}"),
+ d when d > 0 <- DateTime.diff(date, DateTime.utc_now())
+ do
+ {:ok, cred}
+ else
+ err -> {:error, err}
+ end
+ end
+
+ def new(cred) do
+ GenServer.call(__MODULE__, {:new, cred})
+ end
+
+ def init(_) do
+ ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
+ {:ok, ets}
+ end
+
+ def handle_call({:new, cred}, _, ets) do
+ id = IRC.UserTrack.Id.large_id()
+ expire = DateTime.utc_now()
+ |> DateTime.add(15*60, :second)
+ obj = {id, cred, expire}
+ :ets.insert(ets, obj)
+ {:reply, {:ok, id}, ets}
+ end
+
+end
diff --git a/lib/lsg_irc.ex b/lib/lsg_irc.ex
index 482cb5d..cb1e382 100644
--- a/lib/lsg_irc.ex
+++ b/lib/lsg_irc.ex
@@ -19,7 +19,11 @@ defmodule LSG.IRC do
for plugin <- Application.get_env(:lsg, :irc)[:plugins] do
worker(plugin, [], [name: plugin])
end
+ ++ [
+ worker(LSG.IRC.AlcoologAnnouncerPlugin, [])
+ ]
end
+
end
diff --git a/lib/lsg_irc/alcolog_plugin.ex b/lib/lsg_irc/alcolog_plugin.ex
index b5392a3..f27dcd8 100644
--- a/lib/lsg_irc/alcolog_plugin.ex
+++ b/lib/lsg_irc/alcolog_plugin.ex
@@ -1,13 +1,27 @@
-defmodule LSG.IRC.AlcologPlugin do
+defmodule LSG.IRC.AlcoologPlugin do
require Logger
@moduledoc """
- # alcoolisme _(v2)_
+ # alcoolisme
- * **`!santai <montant> <degrés d'alcool> [annotation]`** enregistre un nouveau verre de `montant` d'une boisson à `degrés d'alcool`.
+ * **`!santai <montant> <degrés d'alcool> [annotation]`**: enregistre un nouveau verre de `montant` d'une boisson à `degrés d'alcool`.
+ * **-santai**: annule la dernière entrée d'alcoolisme.
+ * **.alcoolisme**: état du channel en temps réel.
+ * **!alcoolisme `[pseudo]`**: affiche les points d'alcoolisme.
+ * **!alcoolisme `semaine`**: affiche le top des 7 derniers jours
+ * **+alcoolisme `<h|f>` `<poids en kg>` `[facteur de perte en mg/l (10, 15, 20, 25)]`**: Configure votre profil d'alcoolisme.
+ * **.sobre**: affiche quand la sobriété frappera sur le chan.
+ * **!sobre `[pseudo]`**: affiche quand la sobriété frappera pour `[pseudo]`.
+ * **!sobrepour `<date>`**: affiche tu pourras être sobre pour `<date>`, et si oui, combien de volumes d'alcool peuvent encore être consommés.
+ * **!alcoolog**: lien pour voir l'état/statistiques et historique de l'alcoolémie du channel.
+ * **!alcool `<cl>` `<degrés>`**: donne le nombre d'unités d'alcool dans `<cl>` à `<degrés>°`.
+ * **!soif**: c'est quand l'apéro ?
- * **!`<trigger>` `[coeff]` `[annotation]`** enregistre de l'alcoolisme.
- * **!alcoolisme `[pseudo]`** affiche les points d'alcoolisme.
+ 1 point = 1 volume d'alcool
+
+ Anciennes commandes:
+
+ * **!`<trigger>` `[coeff]` `[annotation]`**: enregistre de l'alcoolisme.
Triggers/Coeffs:
@@ -23,13 +37,14 @@ defmodule LSG.IRC.AlcologPlugin do
@triggers %{
"apero" =>
%{
- triggers: ["apero", "apéro", "apairo", "santai"],
- default_coeff: "25",
+ triggers: ["apero", "apéro", "apairo", "apaireau"],
+ default_coeff: "",
coeffs: %{
- "+" => 2,
- "++" => 3,
- "+++" => 4,
- "++++" => 5,
+ "" => 1.0,
+ "+" => 2.0,
+ "++" => 3.0,
+ "+++" => 4.0,
+ "++++" => 5.0,
}
},
"bière" =>
@@ -37,11 +52,11 @@ defmodule LSG.IRC.AlcologPlugin do
triggers: ["beer", "bière", "biere", "biaire"],
default_coeff: "25",
coeffs: %{
- "25" => 1,
- "50" => 2,
- "75" => 3,
- "100" => 4,
- "+" => 2,
+ "25" => 1.0,
+ "50" => 2.0,
+ "75" => 3.0,
+ "100" => 4.0,
+ "+" => 2.0,
"++" => 3,
"+++" => 4,
"++++" => 5,
@@ -64,34 +79,104 @@ defmodule LSG.IRC.AlcologPlugin do
triggers: ["shot", "royaume", "whisky", "rhum", "armagnac", "dijo"],
default_coeff: "",
coeffs: %{
- "" => 3,
- "+" => 5,
- "++" => 7,
- "+++" => 9,
- "++++" => 11
- }
- },
- "eau" => %{
- triggers: ["eau"],
- default_coeff: "1",
- coeffs: %{
- "1" => -2,
- "+" => -3,
- "++" => -4,
- "+++" => -6,
- "++++" => -8
+ "" => 1,
+ "+" => 2,
+ "++" => 3,
+ "+++" => 4,
+ "++++" => 5
}
}
}
+ @user_states %{
+ :sober => %{
+ false => ["n'a pas encore bu", "est actuellement sobre"],
+ true => ["commence a peine... santé"],
+ },
+ :low => %{
+ false => ["est quasiment sobre", "est entrain de redescendre et devrait reboire"],
+ true => ["commence a monter"],
+ },
+ :legal => %{
+ true => ["peut encore conduire, mais plus pour longtemps"],
+ false => ["peut reconduire", "est en route vers la sobriété"],
+ },
+ :legalhigh => %{
+ true => ["est bientôt bourraide", "peut plus conduire"],
+ false => ["commence a débourrai"],
+ },
+ :high => %{
+ true => ["est bourraide!", "est raide"],
+ false => ["est bourraide mais redescend!"],
+ },
+ :toohigh => ["est totalement bourraide", "est raiiideeeeee"],
+ :sick => ["est BEAUCOUP trop bourraide"],
+ }
+
- @santai ["SANTÉ", "SANTÉ", "SANTAIIII", "SANTAIIIIIIIIII", "SANTAI", "A LA TIENNE"]
+ @santai [
+ "SANTÉ",
+ "SANTAIIII",
+ "SANTAI",
+ "A LA TIENNE",
+ "CUL SEC CUL SEC",
+ "TCHIN TCHIN",
+ "LONGUE VIE",
+ "ZUM WOHL", "PROST",
+ "Կէնաձդ",
+ "Наздраве",
+ "SANTAIIIIIIIIII",
+ "AGLOU",
+ "AUNG MYIN PAR SAY",
+ "SALUD",
+ "SANTAIIIIII",
+ "ΥΓΕΙΑ",
+ "Å’KåLÀ MA’LUNA",
+ "EGÉSZSÉGEDRE",
+ "FENÉKIG",
+ "乾杯",
+ "KANPAI",
+ "TULGATSGAAYA",
+ "AGLOU AGLOU",
+ "БУДЕМ ЗДОРОВЫ",
+ "НА ЗДОРОВЬЕ",
+ "SANTAIIIIIIIIIIIII",
+ "MỘT HAI BA, YO",
+ "SKÅL",
+ "NA ZDRAVJE",
+ "AGLOUUUU AGLOUUU AGLOUUUUUUU",
+ "فى صحتك",
+ "GESONDHEID",
+ "ZHU NIN JIANKANG",
+ "祝您健康",
+ "SANTÉ",
+ "CHEERS"
+
+ ]
def irc_doc, do: @moduledoc
- def start_link(), do: GenServer.start_link(__MODULE__, [])
+ def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
+
+ # tuple dets: {nick, date, volumes, current_alcohol_level, nom, commentaire}
+ # tuple ets: {{nick, date}, volumes, current, nom, commentaire}
+ # tuple meta dets: {nick, map}
+ # %{:weight => float, :sex => true(h),false(f)}
+ @default_user_meta %{weight: 77.4, sex: true, loss_factor: 15}
+
+ def data_state() do
+ dets_filename = (LSG.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
+ dets_meta_filename = (LSG.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
+ %{dets: dets_filename, meta: dets_meta_filename, ets: __MODULE__.ETS}
+ end
def init(_) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:santai", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:alcoolog", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:sobre", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:sobrepour", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:soif", [])
{:ok, _} = Registry.register(IRC.PubSub, "trigger:alcoolisme", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:alcool", [])
for {_, config} <- @triggers do
for trigger <- config.triggers do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:#{trigger}", [])
@@ -102,56 +187,539 @@ defmodule LSG.IRC.AlcologPlugin do
end
dets_filename = (LSG.data_path() <> "/" <> "alcoolisme.dets") |> String.to_charlist
{:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
- {:ok, dets}
+ ets = :ets.new(__MODULE__.ETS, [:ordered_set, :named_table, :protected, {:read_concurrency, true}])
+ dets_meta_filename = (LSG.data_path() <> "/" <> "alcoolisme_meta.dets") |> String.to_charlist
+ {:ok, meta} = :dets.open_file(dets_meta_filename, [{:type,:set}])
+ state = %{dets: dets, meta: meta, ets: ets}
+
+ # Upgrade dets format
+ update_fun = fn(obj, dets) ->
+ case obj do
+ object = {nick, date, volumes, name, comment} ->
+ :dets.delete_object(dets, object)
+ :dets.insert(dets, {nick, date, volumes || 0.0, 0, name, comment})
+ dets
+ object = {nick, date, volumes, active, name, comment} ->
+ volumes = if is_integer(volumes) do
+ volumes = volumes+0.0
+ :dets.delete_object(dets, object)
+ :dets.insert(dets, {nick, date, volumes || 0.0, 0, name, comment})
+ volumes
+ else
+ volumes
+ end
+ :ets.insert(ets, {{nick, date}, volumes, active, name, comment})
+ dets
+ _ ->
+ dets
+ end
+ end
+ :dets.foldl(update_fun, dets, dets)
+ :dets.sync(dets)
+ {:ok, state}
+ end
+
+ def handle_info({:irc, :trigger, "soif", m = %IRC.Message{trigger: %IRC.Trigger{args: _, type: :bang}}}, state) do
+ now = DateTime.utc_now()
+ |> Timex.Timezone.convert("Europe/Paris")
+ apero = format_duration_from_now(%DateTime{now | hour: 18, minute: 0, second: 0}, false)
+ txt = cond do
+ now.hour >= 0 && now.hour < 6 ->
+ ["apéro tardif ? Je dis OUI ! SANTAI !"]
+ now.hour >= 6 && now.hour < 12 ->
+ ["C'est quand même un peu tôt non ? Prochain apéro #{apero}"]
+ now.hour >= 12 && (now.hour < 14) ->
+ ["oui! c'est l'apéro de midi! (et apéro #{apero})",
+ "tu peux attendre #{apero} ou y aller, il est midi !"
+ ]
+ now.hour == 17 ->
+ [
+ "ÇA APPROCHE !!! Apéro #{apero}",
+ "BIENTÔT !!! Apéro #{apero}",
+ "achetez vite les teilles, apéro dans #{apero}!",
+ "préparez les teilles, apéro dans #{apero}!"
+ ]
+ now.hour >= 14 && now.hour < 18 ->
+ ["tiens bon! apéro #{apero}",
+ "courage... apéro dans #{apero}",
+ "pas encore :'( apéro dans #{apero}"
+ ]
+ true ->
+ [
+ "C'EST L'HEURE DE L'APÉRO !!! SANTAIIIIIIIIIIII !!!!"
+ ]
+ end
+
+ txt = txt
+ |> Enum.shuffle()
+ |> Enum.random()
+
+ m.replyfun.(txt)
+
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "sobrepour", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
+ args = Enum.join(args, " ")
+ {:ok, now} = DateTime.now("Europe/Paris", Tzdata.TimeZoneDatabase)
+ time = case args do
+ "demain " <> time ->
+ {h, m} = case String.split(time, [":", "h"]) do
+ [hour, ""] ->
+ IO.puts ("h #{inspect hour}")
+ {h, _} = Integer.parse(hour)
+ {h, 0}
+ [hour, min] when min != "" ->
+ {h, _} = Integer.parse(hour)
+ {m, _} = Integer.parse(min)
+ {h, m}
+ [hour] ->
+ IO.puts ("h #{inspect hour}")
+ {h, _} = Integer.parse(hour)
+ {h, 0}
+ _ -> {0, 0}
+ end
+ secs = ((60*60)*24)
+ day = DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase)
+ %DateTime{day | hour: h, minute: m, second: 0}
+ "après demain " <> time ->
+ secs = 2*((60*60)*24)
+ DateTime.add(now, secs, :second, Tzdata.TimeZoneDatabase)
+ datetime ->
+ case Timex.Parse.DateTime.Parser.parse(datetime, "{}") do
+ {:ok, dt} -> dt
+ _ -> nil
+ end
+ end
+
+ if time do
+ meta = get_user_meta(state, m.sender.nick)
+ stats = get_full_statistics(state, m.sender.nick)
+
+ duration = round(DateTime.diff(time, now)/60.0)
+
+ IO.puts "diff #{inspect duration} sober in #{inspect stats.sober_in}"
+
+ if duration < stats.sober_in do
+ int = stats.sober_in - duration
+ m.replyfun.("désolé, aucune chance! tu seras sobre #{format_minute_duration(int)} après!")
+ else
+ remaining = duration - stats.sober_in
+ if remaining < 30 do
+ m.replyfun.("moins de 30 minutes de sobriété, c'est impossible de boire plus")
+ else
+ loss_per_minute = ((meta.loss_factor/100)/60)
+ remaining_gl = (remaining-30)*loss_per_minute
+ m.replyfun.("marge de boisson: #{inspect remaining} minutes, #{remaining_gl} g/l")
+ end
+ end
+
+ end
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "alcoolog", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :bang}}}, state) do
+ {:ok, token} = LSG.Token.new({:alcoolog, :index, m.channel})
+ url = LSGWeb.Router.Helpers.alcoolog_url(LSGWeb.Endpoint, :index, token)
+ m.replyfun.("-> #{url}")
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "alcool", m = %IRC.Message{trigger: %IRC.Trigger{args: args = [cl, deg], type: :bang}}}, state) do
+ {cl, _} = Util.float_paparse(cl)
+ {deg, _} = Util.float_paparse(deg)
+ points = Alcool.units(cl, deg)
+ meta = get_user_meta(state, m.sender.nick)
+ k = if meta.sex, do: 0.7, else: 0.6
+ weight = meta.weight
+ gl = (10*points)/(k*weight)
+ duration = round(gl/((meta.loss_factor/100)/60))+30
+ sober_in_s = if duration > 0 do
+ duration = Timex.Duration.from_minutes(duration)
+ Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
+ else
+ ""
+ end
+
+ m.replyfun.("Il y a #{Float.round(points+0.0, 4)} unités d'alcool dans #{cl}cl à #{deg}° (#{Float.round(gl + 0.0, 4)} g/l, #{sober_in_s})")
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
+ case args do
+ [cl, deg | comment] ->
+ comment = if comment == [] do
+ nil
+ else
+ Enum.join(comment, " ")
+ end
+
+ {cl, _} = Util.float_paparse(cl)
+ {deg, _} = Util.float_paparse(deg)
+ points = Alcool.units(cl, deg)
+
+ if cl > 0 && deg > 0 do
+ now = DateTime.to_unix(DateTime.utc_now(), :millisecond)
+ user_meta = get_user_meta(state, m.sender.nick)
+ name = "#{cl}cl #{deg}°"
+ old_stats = get_full_statistics(state, m.sender.nick)
+ :ok = :dets.insert(state.dets, {String.downcase(m.sender.nick), now, points, if(old_stats, do: old_stats.active, else: 0), name, comment})
+ true = :ets.insert(state.ets, {{String.downcase(m.sender.nick), now}, points, if(old_stats, do: old_stats.active, else: 0),name, comment})
+ sante = @santai |> Enum.shuffle() |> Enum.random()
+ meta = get_user_meta(state, m.sender.nick)
+ k = if meta.sex, do: 0.7, else: 0.6
+ weight = meta.weight
+ peak = Float.round((10*points||0.0)/(k*weight), 4)
+ stats = get_full_statistics(state, m.sender.nick)
+ sober_add = if old_stats && Map.get(old_stats || %{}, :sober_in) do
+ mins = round(stats.sober_in - old_stats.sober_in)
+ " [+#{mins}m]"
+ else
+ ""
+ end
+ nonow = DateTime.utc_now()
+ sober = nonow |> DateTime.add(round(stats.sober_in*60), :second)
+ |> Timex.Timezone.convert("Europe/Paris")
+ at = if nonow.day == sober.day do
+ {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
+ detail
+ else
+ {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
+ detail
+ end
+
+ up = if stats.active_drinks > 1 do
+ " " <> Enum.join(for(_ <- 1..stats.active_drinks, do: "▲")) <> ""
+ else
+ ""
+ end
+
+ m.replyfun.("#{sante} #{m.sender.nick}#{up} #{format_points(points)} @#{stats.active}g/l [+#{peak} g/l]"
+ <> " (15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) (sobriété #{at} (dans #{stats.sober_in_s})#{sober_add}) !"
+ <> " (aujourd'hui #{stats.daily_volumes} points - #{stats.daily_gl} g/l)")
+ else
+ m.replyfun.("on ne peut pas boire en négatif...")
+ end
+ _ ->
+ m.replyfun.("!santai <cl> <degrés> [commentaire]")
+ end
+ {:noreply, state}
+ end
+
+ def get_channel_statistics(channel) do
+ IRC.UserTrack.to_list()
+ |> Enum.filter(fn({_, nick, _, _, _, _, channels}) -> Map.get(channels, channel) end)
+ |> Enum.map(fn({_, nick, _, _, _, _, _}) -> nick end)
+ |> Enum.map(fn(nick) -> {nick, get_full_statistics(nick)} end)
+ |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
+ |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
+ end
+
+ def get_full_statistics(nick) do
+ get_full_statistics(data_state(), nick)
+ end
+
+ defp get_full_statistics(state, nick) do
+ case get_statistics_for_nick(state, nick) do
+ {count, {_, last_at, last_points, last_active, last_type, last_descr}} ->
+ {active, active_drinks} = current_alcohol_level(state, nick)
+ {rising, m30} = alcohol_level_rising(state, nick)
+ {rising, m15} = alcohol_level_rising(state, nick, 15)
+ {_, h1} = alcohol_level_rising(state, nick, 60)
+
+ trend = if rising do
+ "▲"
+ else
+ "▼"
+ end
+ user_state = cond do
+ active <= 0.0 -> :sober
+ active <= 0.25 -> :low
+ active <= 0.50 -> :legal
+ active <= 1.0 -> :legalhigh
+ active <= 2.5 -> :high
+ active < 3 -> :toohigh
+ true -> :sick
+ end
+ states = Map.get(@user_states, user_state)
+ list = if is_list(states) do
+ states
+ else
+ Map.get(states, rising)
+ end
+ meta = get_user_meta(state, nick)
+ minutes_til_sober = h1/((meta.loss_factor/100)/60)
+ minutes_til_sober = cond do
+ active < 0 -> 0
+ m15 < 0 -> 15
+ m30 < 0 -> 30
+ h1 < 0 -> 60
+ minutes_til_sober > 0 ->
+ Float.round(minutes_til_sober+60)
+ true -> 0
+ end
+
+ duration = Timex.Duration.from_minutes(minutes_til_sober)
+ sober_in_s = if minutes_til_sober > 0 do
+ Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
+ else
+ nil
+ end
+
+ user_status = list
+ |> Enum.shuffle()
+ |> Enum.random()
+ {total_volumes, total_gl} = user_stats(state, nick)
+
+
+ %{active: active, last_at: last_at, last_points: last_points, last_type: last_type, last_descr: last_descr,
+ trend_symbol: trend,
+ active15m: m15, active30m: m30, active1h: h1,
+ rising: rising,
+ active_drinks: active_drinks,
+ user_status: user_status,
+ daily_gl: total_gl, daily_volumes: total_volumes,
+ sober_in: minutes_til_sober, sober_in_s: sober_in_s
+ }
+ _ ->
+ nil
+ end
+ end
+
+ def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :dot}}}, state) do
+ nicks = Enum.filter(IRC.UserTrack.to_list, fn({_, nick, _, _, _, _, channels}) -> Map.get(channels, m.channel) end) |> Enum.map(fn({_, nick, _, _, _, _, _}) -> nick end)
+ nicks = nicks
+ |> Enum.map(fn(nick) -> {nick, get_full_statistics(state, nick)} end)
+ |> Enum.filter(fn({_nick, status}) -> status && status.sober_in && status.sober_in > 0 end)
+ |> Enum.sort_by(fn({_, status}) -> status.sober_in end, &</2)
+ |> Enum.map(fn({nick, stats}) ->
+ now = DateTime.utc_now()
+ sober = now |> DateTime.add(round(stats.sober_in*60), :second)
+ |> Timex.Timezone.convert("Europe/Paris")
+ at = if now.day == sober.day do
+ {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
+ detail
+ else
+ {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
+ detail
+ end
+ "#{nick} sobre #{at} (dans #{stats.sober_in_s})"
+ end)
+ |> Enum.intersperse(", ")
+ |> Enum.join("")
+ |> (fn(line) ->
+ case line do
+ "" -> "tout le monde est sobre......."
+ line -> line
+ end
+ end).()
+ |> m.replyfun.()
+ {:noreply, state}
end
+ def handle_info({:irc, :trigger, "sobre", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
+ nick = case args do
+ [nick] -> nick
+ [] -> m.sender.nick
+ end
+
+ stats = get_full_statistics(state, nick)
+ if stats && stats.sober_in > 0 do
+ now = DateTime.utc_now()
+ sober = now |> DateTime.add(round(stats.sober_in*60), :second)
+ |> Timex.Timezone.convert("Europe/Paris")
+ at = if now.day == sober.day do
+ {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "aujourd'hui {h24}:{m}", "fr")
+ detail
+ else
+ {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(sober, "{WDfull} {h24}:{m}", "fr")
+ detail
+ end
+ m.replyfun.("#{nick} sera sobre #{at} (dans #{stats.sober_in_s})!")
+ else
+ m.replyfun.("#{nick} est déjà sobre. aidez le !")
+ end
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :dot}}}, state) do
+ nicks = Enum.filter(IRC.UserTrack.to_list, fn({_, nick, _, _, _, _, channels}) -> Map.get(channels, m.channel) end) |> Enum.map(fn({_, nick, _, _, _, _, _}) -> nick end)
+ nicks = nicks
+ |> Enum.map(fn(nick) -> {nick, get_full_statistics(state, nick)} end)
+ |> Enum.filter(fn({_nick, status}) -> status && (status.active > 0 || status.active30m > 0) end)
+ |> Enum.sort_by(fn({_, status}) -> status.active end, &>/2)
+ |> Enum.map(fn({nick, status}) ->
+ trend_symbol = if status.active_drinks > 1 do
+ Enum.join(for(_ <- 1..status.active_drinks, do: status.trend_symbol))
+ else
+ status.trend_symbol
+ end
+ "#{nick} #{status.user_status} #{trend_symbol} #{status.active} g/l"
+ end)
+ |> Enum.intersperse(", ")
+ |> Enum.join("")
+
+ msg = if nicks == "" do
+ "wtf?!?! personne n'a bu!"
+ else
+ nicks
+ end
+
+ m.replyfun.(msg)
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: ["semaine"], type: :bang}}}, state) do
+ aday = 7*((24 * 60)*60)
+ now = DateTime.utc_now()
+ before = now
+ |> DateTime.add(-aday, :second)
+ |> DateTime.to_unix(:millisecond)
+ #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
+ match = [
+ {{{:_, :"$1"}, :_, :_, :_, :_},
+ [
+ {:>, :"$1", {:const, before}},
+ ], [:"$_"]}
+ ]
+ # tuple ets: {{nick, date}, volumes, current, nom, commentaire}
+ drinks = :ets.select(state.ets, match)
+ |> Enum.sort_by(fn({{_, ts}, _, _, _, _}) -> ts end, &>/2)
+
+ top = Enum.reduce(drinks, %{}, fn({{nick, _}, vol, _, _, _}, acc) ->
+ all = Map.get(acc, nick, 0)
+ Map.put(acc, nick, all + vol)
+ end)
+ |> Enum.sort_by(fn({_nick, count}) -> count end, &>/2)
+ |> Enum.map(fn({nick, count}) -> "#{nick}: #{Float.round(count, 4)}" end)
+ |> Enum.intersperse(", ")
+
+ m.replyfun.("sur 7 jours: #{top}")
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :plus}}}, state) do
+ meta = get_user_meta(state, m.sender.nick)
+ hf = if meta.sex, do: "h", else: "f"
+ m.replyfun.("+alcoolisme sexe: #{hf} poids: #{meta.weight} facteur de perte: #{meta.loss_factor}")
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: [h, weight | rest], type: :plus}}}, state) do
+ h = case h do
+ "h" -> true
+ "f" -> false
+ _ -> nil
+ end
+
+ weight = case Util.float_paparse(weight) do
+ {weight, _} -> weight
+ _ -> nil
+ end
+
+ {factor} = case rest do
+ [factor] ->
+ case Util.float_paparse(factor) do
+ {float, _} -> {float}
+ _ -> {@default_user_meta.loss_factor}
+ end
+ _ -> {@default_user_meta.loss_factor}
+ end
+
+ if h == nil || weight == nil do
+ m.replyfun.("paramètres invalides")
+ else
+ old_meta = get_user_meta(state, m.sender.nick)
+ meta = Map.merge(@default_user_meta, %{sex: h, weight: weight, loss_factor: factor})
+ put_user_meta(state, m.sender.nick, meta)
+ msg = if old_meta.weight < meta.weight do
+ "t'as grossi..."
+ else
+ "ok"
+ end
+ m.replyfun.(msg)
+ end
+
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, "santai", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :minus}}}, state) do
+ case get_statistics_for_nick(state, m.sender.nick) do
+ {_, obj = {_, date, points, _last_active, type, descr}} ->
+ :dets.delete_object(state.dets, obj)
+ :ets.delete(state.ets, {String.downcase(m.sender.nick), date})
+ m.replyfun.("supprimé: #{m.sender.nick} #{points} #{type} #{descr}")
+ m.replyfun.("faudrait quand même penser à boire")
+ {:noreply, state}
+ _ ->
+ {:noreply, state}
+ end
+ end
+
+ def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
+ nick = case args do
+ [nick] -> nick
+ [] -> m.sender.nick
+ end
+ if stats = get_full_statistics(state, nick) do
+ trend_symbol = if stats.active_drinks > 1 do
+ Enum.join(for(_ <- 1..stats.active_drinks, do: stats.trend_symbol))
+ else
+ stats.trend_symbol
+ end
+ msg = "#{nick} #{stats.user_status} "
+ <> (if stats.active > 0 || stats.active15m > 0 || stats.active30m > 0 || stats.active1h > 0, do: ": #{trend_symbol} #{stats.active}g/l ", else: "")
+ <> (if stats.active30m > 0 || stats.active1h > 0, do: "(15m: #{stats.active15m}, 30m: #{stats.active30m}, 1h: #{stats.active1h}) ", else: "")
+ <> (if stats.sober_in > 0, do: "— Sobre dans #{stats.sober_in_s} ", else: "")
+ <> "— Dernier verre: #{present_type(stats.last_type, stats.last_descr)} [#{Float.round(stats.last_points+0.0, 4)}] "
+ <> "#{format_duration_from_now(stats.last_at)} "
+ <> (if stats.daily_volumes > 0, do: "— Aujourd'hui: #{stats.daily_volumes} #{stats.daily_gl}g/l", else: "")
+
+ m.replyfun.(msg)
+ else
+ m.replyfun.("honteux mais #{nick} n'a pas l'air alcoolique du tout. /kick")
+ end
+ {:noreply, state}
+ end
+
+
for {name, config} <- @triggers do
coeffs = Map.get(config, "coeffs")
default_coeff_value = Map.get(config.coeffs, config.default_coeff)
- IO.puts "at triggers #{inspect config}"
+ #IO.puts "at triggers #{inspect config}"
# Handle each trigger
for trigger <- config.triggers do
# … with a known coeff …
for {coef, value} when byte_size(coef) > 0 <- config.coeffs do
- def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: [unquote(coef)<>_ | args], type: :bang}}}, dets) do
- handle_bang(unquote(name), unquote(value), m, args, dets)
- {:noreply, dets}
+ def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: [unquote(coef)<>_ | args], type: :bang}}}, state) do
+ handle_bang(unquote(name), unquote(value), m, args, state)
+ {:noreply, state}
end
- def handle_info({:irc, :trigger, unquote(trigger)<>unquote(coef), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
- handle_bang(unquote(name), unquote(value), m, args, dets)
- {:noreply, dets}
+ def handle_info({:irc, :trigger, unquote(trigger)<>unquote(coef), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
+ handle_bang(unquote(name), unquote(value), m, args, state)
+ {:noreply, state}
end
end
# … or without
- def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
- handle_bang(unquote(name), unquote(default_coeff_value), m, args, dets)
- {:noreply, dets}
+ def handle_info({:irc, :trigger, unquote(trigger), m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, state) do
+ handle_bang(unquote(name), unquote(default_coeff_value), m, args, state)
+ {:noreply, state}
end
end
end
- def handle_info({:irc, :trigger, "alcoolisme", m = %IRC.Message{trigger: %IRC.Trigger{args: args, type: :bang}}}, dets) do
- nick = case args do
- [nick] -> nick
- [] -> m.sender.nick
- end
- case get_statistics_for_nick(dets, nick) do
- {count, {_, last_at, last_points, last_type, last_descr}} ->
- m.replyfun.("#{nick} a #{count} points d'alcoolisme. Dernier verre: #{present_type(last_type,
- last_descr)} [#{last_points}] #{format_relative_timestamp(last_at)}")
- _ ->
- m.replyfun.("honteux mais #{nick} n'a pas l'air alcoolique du tout. /kick")
- end
- {:noreply, dets}
+ def handle_info(t, state) do
+ Logger.debug("AlcoologPlugin: unhandled info #{inspect t}")
+ {:noreply, state}
end
- defp handle_bang(name, points, message, args, dets) do
+ defp handle_bang(name, points, message, args, state) do
description = case args do
[] -> nil
[something] -> something
@@ -159,16 +727,29 @@ defmodule LSG.IRC.AlcologPlugin do
_ -> nil
end
now = DateTime.to_unix(DateTime.utc_now(), :milliseconds)
- :ok = :dets.insert(dets, {String.downcase(message.sender.nick), now, points, name, description})
- {count, {_, last_at, last_points, last_type, last_descr}} = get_statistics_for_nick(dets, message.sender.nick)
+ points = points+0.0
+ user_meta = get_user_meta(state, message.sender.nick)
+ # TODO: Calculer la perte sur last_active depuis last_at.
+ # TODO: Ajouter les g/L de la nouvelle boisson.
+ :ok = :dets.insert(state.dets, {String.downcase(message.sender.nick), now, points, 0, name, description})
+ true = :ets.insert(state.ets, {{String.downcase(message.sender.nick), now}, points, 0, name, description})
+ {active,_} = current_alcohol_level(state, message.sender.nick)
+ {count, {_, last_at, last_points, _active, last_type, last_descr}} = get_statistics_for_nick(state, message.sender.nick)
sante = @santai |> Enum.shuffle() |> Enum.random()
- message.replyfun.("#{sante} #{message.sender.nick} #{format_points(points)}! (total #{count} points)")
+ k = if user_meta.sex, do: 0.7, else: 0.6
+ weight = user_meta.weight
+ peak = (10*points)/(k*weight)
+ {_, m30} = alcohol_level_rising(state, message.sender.nick)
+ {_, m15} = alcohol_level_rising(state, message.sender.nick, 15)
+ {_, h1} = alcohol_level_rising(state, message.sender.nick, 60)
+ #message.replyfun.("#{sante} #{message.sender.nick} #{format_points(points)}! #{active}~ g/L (total #{Float.round(count+0.0, 4)} points)")
+ message.replyfun.("#{sante} #{message.sender.nick} #{format_points(points)} @#{active}~ g/l [+#{Float.round(peak+0.0, 4)} g/l] (15m: #{m15}, 30m: #{m30}, 1h: #{h1})! (total #{Float.round(count+0.0, 4)} points)")
end
- defp get_statistics_for_nick(dets, nick) do
- qvc = :dets.lookup(dets, String.downcase(nick))
- IO.puts inspect(qvc)
- count = Enum.reduce(qvc, 0, fn({_nick, _ts, points, _type, _descr}, acc) -> acc + points end)
+ defp get_statistics_for_nick(state, nick) do
+ qvc = :dets.lookup(state.dets, String.downcase(nick))
+ |> Enum.sort_by(fn({_, ts, _, _, _, _}) -> ts end, &</2)
+ count = Enum.reduce(qvc, 0, fn({_nick, _ts, points, _active, _type, _descr}, acc) -> acc + (points||0) end)
last = List.last(qvc) || nil
{count, last}
end
@@ -176,19 +757,26 @@ defmodule LSG.IRC.AlcologPlugin do
def present_type(type, descr) when descr in [nil, ""], do: "#{type}"
def present_type(type, description), do: "#{type} (#{description})"
- def format_points(int) when int > 0 do
+ def format_points(int) when is_integer(int) and int > 0 do
"+#{Integer.to_string(int)}"
end
- def format_points(int) when int < 0 do
+ def format_points(int) when is_integer(int) and int < 0 do
Integer.to_string(int)
end
+ def format_points(int) when is_float(int) and int > 0 do
+ "+#{Float.to_string(Float.round(int,4))}"
+ end
+ def format_points(int) when is_float(int) and int < 0 do
+ Float.to_string(Float.round(int,4))
+ end
def format_points(0), do: "0"
+ def format_points(0.0), do: "0"
defp format_relative_timestamp(timestamp) do
alias Timex.Format.DateTime.Formatters
alias Timex.Timezone
date = timestamp
- |> DateTime.from_unix!(:milliseconds)
+ |> DateTime.from_unix!(:millisecond)
|> Timezone.convert("Europe/Paris")
{:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
@@ -196,5 +784,177 @@ defmodule LSG.IRC.AlcologPlugin do
relative <> detail
end
+
+ defp put_user_meta(state, nick, meta) do
+ :dets.insert(state.meta, {{:meta, String.downcase(nick)}, meta})
+ :ok
+ end
+
+ defp get_user_meta(%{meta: meta}, nick) do
+ case :dets.lookup(meta, {:meta, String.downcase(nick)}) do
+ [{{:meta, _}, meta}] ->
+ Map.merge(@default_user_meta, meta)
+ _ ->
+ @default_user_meta
+ end
+ end
+ # Calcul g/l actuel:
+ # 1. load user meta
+ # 2. foldr ets
+ # for each object
+ # get_current_alcohol
+ # ((object g/l) - 0,15/l/60)* minutes_since_drink
+ # if minutes_since_drink < 10, reduce g/l (?!)
+ # acc + current_alcohol
+ # stop folding when ?
+ #
+
+ defp user_stats(state = %{ets: ets}, nick) do
+ meta = get_user_meta(state, nick)
+ aday = (10 * 60)*60
+ now = DateTime.utc_now()
+ before = now
+ |> DateTime.add(-aday, :second)
+ |> DateTime.to_unix(:millisecond)
+ #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
+ match = [
+ {{{:"$1", :"$2"}, :_, :_, :_, :_},
+ [
+ {:>, :"$2", {:const, before}},
+ {:"=:=", {:const, String.downcase(nick)}, :"$1"}
+ ], [:"$_"]}
+]
+ # tuple ets: {{nick, date}, volumes, current, nom, commentaire}
+ drinks = :ets.select(ets, match)
+ # {date, single_peak}
+ total_volume = Enum.reduce(drinks, 0.0, fn({{_, date}, volume, _, _, _}, acc) ->
+ acc + volume
+ end)
+ k = if meta.sex, do: 0.7, else: 0.6
+ weight = meta.weight
+ gl = (10*total_volume)/(k*weight)
+ {Float.round(total_volume + 0.0, 4), Float.round(gl + 0.0, 4)}
+ end
+
+ defp alcohol_level_rising(state, nick, minutes \\ 30) do
+ {now, _} = current_alcohol_level(state, nick)
+ soon_date = DateTime.utc_now
+ |> DateTime.add(minutes*60, :second)
+ {soon, _} = current_alcohol_level(state, nick, soon_date)
+ soon = cond do
+ soon < 0 -> 0.0
+ true -> soon
+ end
+ #IO.puts "soon #{soon_date} - #{inspect soon} #{inspect now}"
+ {soon > now, Float.round(soon+0.0, 4)}
+ end
+
+ defp current_alcohol_level(state = %{ets: ets}, nick, now \\ nil) do
+ meta = get_user_meta(state, nick)
+ aday = ((24*7) * 60)*60
+ now = if now do
+ now
+ else
+ DateTime.utc_now()
+ end
+ before = now
+ |> DateTime.add(-aday, :second)
+ |> DateTime.to_unix(:millisecond)
+ nick = String.downcase(nick)
+ #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
+ match = [
+ {{{:"$1", :"$2"}, :_, :_, :_, :_},
+ [
+ {:>, :"$2", {:const, before}},
+ {:"=:=", {:const, nick}, :"$1"}
+ ], [:"$_"]}
+]
+ # tuple ets: {{nick, date}, volumes, current, nom, commentaire}
+ drinks = :ets.select(ets, match)
+ |> Enum.sort_by(fn({{_, date}, _, _, _, _}) -> date end, &</2)
+ # {date, single_peak}
+ {all, last_drink_at, gl, active_drinks} = Enum.reduce(drinks, {0.0, nil, [], 0}, fn({{_, date}, volume, _, _, _}, {all, last_at, acc, active_drinks}) ->
+ k = if meta.sex, do: 0.7, else: 0.6
+ weight = meta.weight
+ peak = (10*volume)/(k*weight)
+ date = DateTime.from_unix!(date, :millisecond)
+ last_at = last_at || date
+ mins_since = round(DateTime.diff(now, date)/60.0)
+ IO.puts "Drink: #{inspect({date, volume})} - mins since: #{inspect mins_since} - last drink at #{inspect last_at}"
+ # Apply loss since `last_at` on `all`
+ #
+ all = if last_at do
+ mins_since_last = round(DateTime.diff(date, last_at)/60.0)
+ loss = ((meta.loss_factor/100)/60)*(mins_since_last)
+ #IO.puts "Applying last drink loss: from #{all}, loss of #{inspect loss} (mins since #{inspect mins_since_first})"
+ cond do
+ (all-loss) > 0 -> all - loss
+ true -> 0.0
+ end
+ else
+ all
+ end
+ #IO.puts "Applying last drink current before drink: #{inspect all}"
+ if mins_since < 30 do
+ per_min = (peak)/30.0
+ current = (per_min*mins_since)
+ IO.puts "Applying current drink 30m: from #{peak}, loss of #{inspect per_min}/min (mins since #{inspect mins_since})"
+ {all + current, date, [{date, current} | acc], active_drinks + 1}
+ else
+ {all + peak, date, [{date, peak} | acc], active_drinks}
+ end
+ end)
+ #IO.puts "last drink #{inspect last_drink_at}"
+ mins_since_last = if last_drink_at do
+ round(DateTime.diff(now, last_drink_at)/60.0)
+ else
+ 0
+ end
+ # Si on a déjà bu y'a déjà moins 15 minutes (big up le binge drinking), on applique plus de perte
+ level = if mins_since_last > 15 do
+ loss = ((meta.loss_factor/100)/60)*(mins_since_last)
+ Float.round(all - loss, 4)
+ else
+ all
+ end
+ #IO.puts "\n LEVEL #{inspect level}\n\n\n\n"
+ cond do
+ level < 0 -> {0.0, 0}
+ true -> {level, active_drinks}
+ end
+ end
+
+ defp format_duration_from_now(date, with_detail \\ true) do
+ date = if is_integer(date) do
+ date = DateTime.from_unix!(date, :millisecond)
+ |> Timex.Timezone.convert("Europe/Paris")
+ else
+ date
+ end
+ now = DateTime.utc_now()
+ |> Timex.Timezone.convert("Europe/Paris")
+ {:ok, detail} = Timex.Format.DateTime.Formatters.Default.lformat(date, "({h24}:{m})", "fr")
+ mins_since = round(DateTime.diff(now, date)/60.0)
+ if ago = format_minute_duration(mins_since) do
+ word = if mins_since > 0 do
+ "il y a "
+ else
+ "dans "
+ end
+ word <> ago <> if(with_detail, do: " #{detail}", else: "")
+ else
+ "maintenant #{detail}"
+ end
+ end
+
+ defp format_minute_duration(minutes) do
+ sober_in_s = if (minutes != 0) do
+ duration = Timex.Duration.from_minutes(minutes)
+ Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
+ else
+ nil
+ end
+ end
+
end
diff --git a/lib/lsg_irc/alcoolog_announcer_plugin.ex b/lib/lsg_irc/alcoolog_announcer_plugin.ex
new file mode 100644
index 0000000..214debd
--- /dev/null
+++ b/lib/lsg_irc/alcoolog_announcer_plugin.ex
@@ -0,0 +1,198 @@
+defmodule LSG.IRC.AlcoologAnnouncerPlugin do
+ require Logger
+
+ @moduledoc """
+ Annonce changements d'alcoolog
+ """
+
+ @channel "#dmz"
+
+ @seconds 30
+
+ @apero [
+ "C'EST L'HEURE DE L'APÉRRROOOOOOOO !!",
+ "SAAAAANNNNNNNTTTTTTTTAAAAAAAAIIIIIIIIIIIIIIIIIIIIII",
+ "APÉRO ? APÉRO !",
+ {:timed, ["/!\\ à vos tires bouchons…", "… débouchez …", "… BUVEZ! SANTAIII!"]},
+ "/!\\ ALERTE APÉRO /!\\",
+ "CED !!! VASE DE ROUGE !",
+ "DIDI UN PETIT RICARD™??!",
+ "ALLEZ GUIGUI UNE PETITE BIERE ?",
+ {:timed, ["/!\\ à vos tires bouchons…", "… débouchez …", "… BUVEZ! SANTAIII!"]},
+ "APPPPAIIIRRREAAUUUUUUUUUUU"
+ ]
+
+ def irc_doc, do: nil
+
+ def start_link(), do: GenServer.start_link(__MODULE__, [])
+
+ def init(_) do
+ stats = get_stats()
+ Process.send_after(self(), :stats, :timer.seconds(30))
+ dets_filename = (LSG.data_path() <> "/" <> "alcoologlog.dets") |> String.to_charlist
+ {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}])
+ {:ok, {stats, now(), dets}}
+ end
+
+ def alcohol_reached(old, new, level) do
+ (old.active < level && new.active >= level)
+ end
+
+ def alcohol_below(old, new, level) do
+ (old.active > level && new.active <= level)
+ end
+
+
+ def handle_info(:stats, {old_stats, old_now, dets}) do
+ stats = get_stats()
+ now = now()
+
+ if old_now.hour < 18 && now.hour == 18 do
+ apero = Enum.shuffle(@apero)
+ |> Enum.random()
+
+ case apero do
+ {:timed, list} ->
+ spawn(fn() ->
+ for line <- list do
+ IRC.PubSubHandler.privmsg(@channel, line)
+ :timer.sleep(:timer.seconds(5))
+ end
+ end)
+ string ->
+ IRC.PubSubHandler.privmsg(@channel, string)
+ end
+
+ end
+
+ IO.puts "newstats #{inspect stats}"
+ events = for {nick, old} <- old_stats do
+ new = Map.get(stats, nick, nil)
+ IO.puts "#{nick}: #{inspect(old)} -> #{inspect(new)}"
+
+ if new && new[:active] do
+ :dets.insert(dets, {nick, DateTime.utc_now(), new[:active]})
+ else
+ :dets.insert(dets, {nick, DateTime.utc_now(), 0.0})
+ end
+
+ event = cond do
+ old == nil -> nil
+ (old.active > 0) && (new == nil) -> :sober
+ new == nil -> nil
+ alcohol_reached(old, new, 0.5) -> :stopconduire
+ alcohol_reached(old, new, 1.0) -> :g1
+ alcohol_reached(old, new, 2.0) -> :g2
+ alcohol_reached(old, new, 3.0) -> :g3
+ alcohol_reached(old, new, 4.0) -> :g4
+ alcohol_reached(old, new, 5.0) -> :g5
+ alcohol_reached(old, new, 6.0) -> :g6
+ alcohol_reached(old, new, 7.0) -> :g7
+ alcohol_reached(old, new, 10.0) -> :g10
+ alcohol_reached(old, new, 13.74) -> :record
+ alcohol_below(old, new, 0.5) -> :conduire
+ alcohol_below(old, new, 1.0) -> :fini1g
+ alcohol_below(old, new, 2.0) -> :fini2g
+ alcohol_below(old, new, 3.0) -> :fini3g
+ alcohol_below(old, new, 4.0) -> :fini4g
+ (old.rising) && (!new.rising) -> :lowering
+ true -> nil
+ end
+ {nick, event}
+ end
+
+ for {nick, event} <- events do
+ message = case event do
+ :g1 -> [
+ "[vigicuite jaune] LE GRAMME! LE GRAMME O/",
+ "début de vigicuite jaune ! LE GRAMME ! \\O/",
+ "waiiiiiiii le grammmeee",
+ "bourraiiiiiiiiiiide 1 grammeeeeeeeeeee",
+ ]
+ :g2 -> [
+ "[vigicuite orange] \\o_YAY 2 GRAMMES ! _o/",
+ "PAITAIIIIIIIIII DEUX GRAMMEESSSSSSSSSSSSSSSSS",
+ "bourrrrrraiiiiiiiiiiiiiiiide 2 grammeeeeeeeeeees",
+ ]
+ :g3 -> [
+ "et un ! et deux ! et TROIS GRAMMEEESSSSSSS",
+ "[vigicuite rouge] _o/ BOURRAIIDDDEEEE 3 GRAMMESSSSSSSSS \\o/ \\o/"
+ ]
+ :g4 -> [
+ "[vigicuite écarlate] et un, et deux, et trois, ET QUATRES GRAMMEESSSSSSSSSSSSSSSSSSSssssss"
+ ]
+ :g5 -> "[vigicuite écarlate+] PUTAIN 5 GRAMMES !"
+ :g6 -> "[vigicuite écarlate++] 6 grammes ? Vous pouvez joindre Alcool info service au 0 980 980 930"
+ :g7 -> "[vigicuite c'est la merde] 7 grammes. Le SAMU, c'est le 15."
+ :g10 -> "BORDLE 10 GRAMMES"
+ :record -> "RECORD DU MONDE BATTU ! >13.74g/l !!"
+ :fini1g -> [
+ "fin d'alerte vigicuite jaune, passage en vert (<1g/l)",
+ "/!\\ alerte moins de 1g/l /!\\"
+ ]
+ :fini2g -> [
+ "t'as moins de 2 g/l, faut se reprendre là [vigicuite jaune]"
+ ]
+ :fini3g -> [
+ "fin d'alerte vigicuite rouge, passage en orange (<3g/l)"
+ ]
+ :fini4g -> [
+ "fin d'alerte vigicuite écarlate, passage en rouge (<4g/l)"
+ ]
+ :lowering -> [
+ "attention ça baisse!",
+ "taux d'alcoolémie en chute ! agissez avant qu'il soit trop tard!",
+ "ÇA BAISSE !!"
+ ]
+ :stopconduire -> [
+ "0.5g! bientot le gramme?",
+ "tu peux plus prendre la route... mais... tu peux prendre la route du gramme! !santai !",
+ "fini la conduite!",
+ "ça monte! 0.5g/l!"
+ ]
+ :conduire -> [
+ "tu peux conduire, ou recommencer à boire! niveau critique!",
+ "!santai ?",
+ "tu peux reprendre la route, ou reprendre la route du gramme..",
+ "attention, niveau critique!",
+ "il faut boire !!",
+ "trop de sang dans ton alcool, c'est mauvais pour la santé",
+ ]
+ :sober -> [
+ "sobre…",
+ "/!\\ alerte sobriété /!\\",
+ "... sobre?!?!",
+ "attention, t'es sobre :/",
+ "danger, alcoolémie à 0.0 !",
+ ]
+ _ -> nil
+ end
+ message = case message do
+ m when is_binary(m) -> m
+ m when is_list(m) -> m |> Enum.shuffle() |> Enum.random()
+ nil -> nil
+ end
+ if message, do: IO.puts "#{nick}: #{message}"
+ if message, do: IRC.PubSubHandler.privmsg(@channel, "#{nick}: #{message}")
+ end
+
+ timer()
+
+ IO.puts "tick stats ok"
+ {:noreply, {stats,now,dets}}
+ end
+
+ defp now() do
+ DateTime.utc_now()
+ |> Timex.Timezone.convert("Europe/Paris")
+ end
+
+ defp get_stats() do
+ Enum.into(LSG.IRC.AlcoologPlugin.get_channel_statistics(@channel), %{})
+ end
+
+ defp timer() do
+ Process.send_after(self(), :stats, :timer.seconds(@seconds))
+ end
+
+end
diff --git a/lib/lsg_irc/bourosama_plugin.ex b/lib/lsg_irc/bourosama_plugin.ex
new file mode 100644
index 0000000..7dde662
--- /dev/null
+++ b/lib/lsg_irc/bourosama_plugin.ex
@@ -0,0 +1,57 @@
+defmodule LSG.IRC.BoursoramaPlugin do
+
+ def irc_doc() do
+ """
+ # bourses
+
+ Un peu comme [finance](#finance), mais en un peu mieux, et un peu moins bien.
+
+ Source: [boursorama.com](https://boursorama.com)
+
+ * **!caca40** affiche l'état du cac40
+ """
+ end
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ @cac40_url "https://www.boursorama.com/bourse/actions/palmares/france/?france_filter%5Bmarket%5D=1rPCAC&france_filter%5Bsector%5D=&france_filter%5Bvariation%5D=50002&france_filter%5Bperiod%5D=1&france_filter%5Bfilter%5D="
+
+ def init(_) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:cac40", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:caca40", [])
+ {:ok, nil}
+ end
+
+ def handle_info({:irc, :trigger, cac, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) when cac in ["cac40", "caca40"] do
+ case HTTPoison.get(@cac40_url, [], []) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ html = Floki.parse(body)
+ board = Floki.find(body, "div.c-tradingboard")
+
+ cac40 = Floki.find(board, ".c-tradingboard__main > .c-tradingboard__infos")
+ instrument = Floki.find(cac40, ".c-instrument")
+ last = Floki.find(instrument, "span[data-ist-last]")
+ |> Floki.text()
+ |> String.replace(" ", "")
+ variation = Floki.find(instrument, "span[data-ist-variation]")
+ |> Floki.text()
+
+ sign = case variation do
+ "-"<>_ -> "▼"
+ "+" -> "▲"
+ _ -> ""
+ end
+
+ m.replyfun.("caca40: #{sign} #{variation} #{last}")
+
+ {:error, %HTTPoison.Response{status_code: code}} ->
+ m.replyfun.("caca40: erreur http #{code}")
+
+ _ ->
+ m.replyfun.("caca40: erreur http")
+ end
+ end
+
+end
diff --git a/lib/lsg_irc/coronavirus_plugin.ex b/lib/lsg_irc/coronavirus_plugin.ex
index 9a017f3..debefa3 100644
--- a/lib/lsg_irc/coronavirus_plugin.ex
+++ b/lib/lsg_irc/coronavirus_plugin.ex
@@ -1,5 +1,6 @@
defmodule LSG.IRC.CoronavirusPlugin do
require Logger
+ NimbleCSV.define(CovidCsv, separator: ",", escape: "\"")
@moduledoc """
# Corona Virus
@@ -17,7 +18,9 @@ defmodule LSG.IRC.CoronavirusPlugin do
def init(_) do
{:ok, _} = Registry.register(IRC.PubSub, "trigger:coronavirus", [])
- {data, next} = fetch_data(%{})
+ date = Date.add(Date.utc_today(), -2)
+ {data, _} = fetch_data(%{}, date)
+ {data, next} = fetch_data(data)
:timer.send_after(next, :update)
{:ok, %{data: data}}
end
@@ -28,32 +31,48 @@ defmodule LSG.IRC.CoronavirusPlugin do
{: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"]] do
+ 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)} end)
- |> Enum.sort_by(fn({_,count}) -> count end, &>=/2)
+ |> 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}, index}) ->
- "##{index+1}: #{location} #{count}"
+ |> 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.("Corona virus top 10 #{name}: " <> sorted)
+ 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.("Corona virus: #{location}: #{data.current} malades, #{data.confirmed} confirmés, #{data.deaths} morts, #{data.recovered} soignés")
+ 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
@@ -79,41 +98,56 @@ defmodule LSG.IRC.CoronavirusPlugin do
{:ok, %HTTPoison.Response{status_code: 200, body: csv}} ->
# Parse CSV update data
data = csv
- |> String.strip()
- |> String.split("\n")
- |> Enum.drop(1)
+ |> CovidCsv.parse_string()
|> Enum.reduce(%{}, fn(line, acc) ->
- [state, region, update, confirmed, deaths, recovered,_lat, _lng] = line
- |> String.strip()
- |> String.split(",")
-
- 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}
-
- acc = if state && state != "" do
- Map.put(acc, state, entry)
- else
- acc
+ case line do
+ # FIPS,Admin2,Province_State,Country_Region,Last_Update,Lat,Long_,Confirmed,Deaths,Recovered,Active,Combined_Key
+ [_, _, state, region, update, _lat, _lng, confirmed, deaths, recovered, _active, _combined_key] ->
+ 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
-
- 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
- }
-
- Map.put(acc, region, region_entry)
end)
Logger.info "Updated coronavirus database"
{data, :timer.minutes(60)}
diff --git a/lib/lsg_irc/correction_plugin.ex b/lib/lsg_irc/correction_plugin.ex
new file mode 100644
index 0000000..a5d3d31
--- /dev/null
+++ b/lib/lsg_irc/correction_plugin.ex
@@ -0,0 +1,46 @@
+defmodule LSG.IRC.CorrectionPlugin do
+ @moduledoc """
+ # correction
+
+ * `s/pattern/replace` replace `pattern` by `replace` in the last matching message
+ """
+
+ def irc_doc, do: @moduledoc
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def init(_) do
+ {:ok, _} = Registry.register(IRC.PubSub, "message", [])
+ {:ok, []}
+ end
+
+ def handle_info({:irc, :text, m = %IRC.Message{}}, history) do
+ if String.starts_with?(m.text, "s/") do
+ case String.split(m.text, "/") do
+ ["s", match, replace | _] ->
+ case Regex.compile(match) do
+ {:ok, reg} ->
+ repl = Enum.find(history, fn(m) -> Regex.match?(reg, m.text) end)
+ if repl do
+ new_text = String.replace(repl.text, reg, replace)
+ m.replyfun.("correction: <#{repl.sender.nick}> #{new_text}")
+ end
+ _ ->
+ m.replyfun.("correction: invalid regex")
+ end
+ _ -> m.replyfun.("correction: invalid regex format")
+ end
+ {:noreply, history}
+ else
+ history = if length(history) > 100 do
+ {_, history} = List.pop_at(history, 99)
+ [m | history]
+ else
+ [m | history]
+ end
+ {:noreply, history}
+ end
+ end
+
+end
diff --git a/lib/lsg_irc/link_plugin.ex b/lib/lsg_irc/link_plugin.ex
index 61bdbf9..bc9764a 100644
--- a/lib/lsg_irc/link_plugin.ex
+++ b/lib/lsg_irc/link_plugin.ex
@@ -127,40 +127,83 @@ defmodule LSG.IRC.LinkPlugin do
expand_default(acc)
end
+ defp get(url, headers \\ [], options \\ []) do
+ get_req(:hackney.get(url, headers, <<>>, options))
+ end
+
+ defp get_req({:error, reason}) do
+ {:error, reason}
+ end
+
+ defp get_req({:ok, 200, headers, client}) do
+ headers = Enum.reduce(headers, %{}, fn({key, value}, acc) ->
+ Map.put(acc, String.downcase(key), value)
+ end)
+ content_type = Map.get(headers, "content-type", "application/octect-stream")
+ length = Map.get(headers, "content-length", "0")
+ {length, _} = Integer.parse(length)
+
+ cond do
+ String.starts_with?(content_type, "text/html") && length <= 30_000_000 ->
+ get_body(30_000_000, client, <<>>)
+ true ->
+ :hackney.close(client)
+ {:ok, "file: #{content_type}, size: #{length} bytes"}
+ end
+ end
+
+ defp get_req({:ok, redirect, headers, client}) when redirect in 300..399 do
+ headers = Enum.reduce(headers, %{}, fn({key, value}, acc) ->
+ Map.put(acc, String.downcase(key), value)
+ end)
+ location = Map.get(headers, "location")
+
+ :hackney.close(client)
+ {:redirect, location}
+ end
+
+ defp get_req({:ok, status, headers, client}) do
+ :hackney.close(client)
+ {:error, status, headers}
+ end
+
+ defp get_body(len, client, acc) when len >= byte_size(acc) do
+ case :hackney.stream_body(client) do
+ {:ok, data} ->
+ get_body(len, client, << acc::binary, data::binary >>)
+ :done ->
+ html = Floki.parse(acc)
+ title = case Floki.find(html, "title") do
+ [{"title", [], [title]} | _] ->
+ String.trim(title)
+ _ ->
+ nil
+ end
+ {:ok, title}
+ {:error, reason} ->
+ {:ok, "failed to fetch body: #{inspect reason}"}
+ end
+ end
+
+ defp get_body(len, client, _acc) do
+ :hackney.close(client)
+ {:ok, "mais il rentrera jamais en ram ce fichier !"}
+ end
+
def expand_default(acc = [uri = %URI{scheme: scheme} | _]) when scheme in ["http", "https"] do
headers = []
options = [follow_redirect: false, max_body_length: 30_000_000]
- case HTTPoison.get(URI.to_string(uri), headers, options) do
- {:ok, %HTTPoison.Response{status_code: 200, headers: headers, body: body}} ->
- headers = Enum.reduce(headers, %{}, fn({key, value}, acc) ->
- Map.put(acc, String.downcase(key), value)
- end)
- text = case Map.get(headers, "content-type") do
- "text/html"<>_ ->
- html = Floki.parse(body)
- case Floki.find(html, "title") do
- [{"title", [], [title]} | _] ->
- title
- _ ->
- nil
- end
- other ->
- "file: #{other}, size: #{Map.get(headers, "content-length", "?")} bytes"
- end
+ case get(URI.to_string(uri), headers, options) do
+ {:ok, text} ->
{:ok, acc, text}
- {:ok, resp = %HTTPoison.Response{headers: headers, status_code: redirect, body: body}} when redirect in 300..399 ->
- headers = Enum.reduce(headers, %{}, fn({key, value}, acc) ->
- Map.put(acc, String.downcase(key), value)
- end)
- link = Map.get(headers, "location")
+ {:redirect, link} ->
new_uri = URI.parse(link)
+ new_uri = %URI{new_uri | scheme: scheme, authority: uri.authority, host: uri.host, port: uri.port}
expand_link([new_uri | acc])
- {:ok, %HTTPoison.Response{status_code: code}} ->
- {:ok, acc, "Error #{code}"}
- {:error, %HTTPoison.Error{reason: reason}} ->
+ {:error, status, _headers} ->
+ {:ok, acc, "Error #{status}"}
+ {:error, reason} ->
{:ok, acc, "Error #{to_string(reason)}"}
- {:error, error} ->
- {:ok, acc, "Error #{inspect(error)}"}
end
end
diff --git a/lib/lsg_irc/link_plugin/imgur.ex b/lib/lsg_irc/link_plugin/imgur.ex
index 9ce3cf3..1b8173f 100644
--- a/lib/lsg_irc/link_plugin/imgur.ex
+++ b/lib/lsg_irc/link_plugin/imgur.ex
@@ -43,10 +43,12 @@ defmodule LSG.IRC.LinkPlugin.Imgur do
{:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
{:ok, json} = Jason.decode(body)
data = json["data"]
- IO.puts inspect(json)
title = String.slice(data["title"] || data["description"], 0, 180)
- nsfw = if data["nsfw"], do: "(NSFW) - ", else: ""
- {:ok, "#{nsfw}#{title}"}
+ nsfw = if data["nsfw"], do: "(NSFW) - ", else: " "
+ height = Map.get(data, "height")
+ width = Map.get(data, "width")
+ size = Map.get(data, "size")
+ {:ok, "image, #{width}x#{height}, #{size} bytes #{nsfw}#{title}"}
other ->
:error
end
diff --git a/lib/lsg_irc/seen_plugin.ex b/lib/lsg_irc/seen_plugin.ex
new file mode 100644
index 0000000..8b2178f
--- /dev/null
+++ b/lib/lsg_irc/seen_plugin.ex
@@ -0,0 +1,58 @@
+defmodule LSG.IRC.SeenPlugin do
+ @moduledoc """
+ # seen
+
+ * **!seen `<nick>`**
+ """
+
+ def irc_doc, do: @moduledoc
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def init([]) do
+ {:ok, _} = Registry.register(IRC.PubSub, "triggers", [])
+ {:ok, _} = Registry.register(IRC.PubSub, "message", [])
+ dets_filename = (LSG.data_path() <> "/seen.dets") |> String.to_charlist()
+ {:ok, dets} = :dets.open_file(dets_filename, [])
+ {:ok, %{dets: dets}}
+ end
+
+ def handle_info({:irc, :trigger, "seen", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: [nick]}}}, state) do
+ witness(m, state)
+ m.replyfun.(last_seen(m.channel, nick, state))
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :trigger, _, m}, state) do
+ witness(m, state)
+ {:noreply, state}
+ end
+
+ def handle_info({:irc, :text, m}, state) do
+ witness(m, state)
+ {:noreply, state}
+ end
+
+ defp witness(%IRC.Message{channel: channel, text: text, sender: %{nick: nick}}, %{dets: dets}) do
+ :dets.insert(dets, {{channel, nick}, DateTime.utc_now(), text})
+ :ok
+ end
+
+ defp last_seen(channel, nick, %{dets: dets}) do
+ case :dets.lookup(dets, {channel, nick}) do
+ [{_, date, text}] ->
+ diff = round(DateTime.diff(DateTime.utc_now(), date)/60)
+ cond do
+ diff >= 30 ->
+ duration = Timex.Duration.from_minutes(diff)
+ format = Timex.Format.Duration.Formatter.lformat(duration, "fr", :humanized)
+ "#{nick} a parlé pour la dernière fois il y a #{format}: “#{text}”"
+ true -> "#{nick} est là..."
+ end
+ [] ->
+ "je ne connais pas de #{nick}"
+ end
+ end
+
+end
diff --git a/lib/lsg_irc/txt_plugin.ex b/lib/lsg_irc/txt_plugin.ex
index 6b7edbd..ca1be9c 100644
--- a/lib/lsg_irc/txt_plugin.ex
+++ b/lib/lsg_irc/txt_plugin.ex
@@ -132,6 +132,7 @@ defmodule LSG.IRC.TxtPlugin do
def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: []}}}, state) do
result = Enum.reduce(state.triggers, [], fn({trigger, data}, acc) ->
+ IO.puts inspect(data)
Enum.reduce(data, acc, fn({l, _}, acc) ->
[{trigger, l} | acc]
end)
@@ -140,7 +141,7 @@ defmodule LSG.IRC.TxtPlugin do
if !Enum.empty?(result) do
{source, line} = Enum.random(result)
- msg.replyfun.("#{source}: #{line}")
+ msg.replyfun.(format_line(line, "#{source}: "))
end
{:noreply, state}
end
@@ -157,7 +158,7 @@ defmodule LSG.IRC.TxtPlugin do
if !Enum.empty?(result) do
{source, line} = Enum.random(result)
- msg.replyfun.("#{source}: #{line}")
+ msg.replyfun.(["#{source}: ", line])
end
{:noreply, state}
end
@@ -208,11 +209,12 @@ defmodule LSG.IRC.TxtPlugin do
# TXT: RANDOM
#
- def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :bang, args: []}}}, state) do
- {trigger, opts} = clean_trigger(trigger)
- line = get_random(state.triggers, trigger, opts)
+ def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :bang, args: opts}}}, state) do
+ {trigger, _} = clean_trigger(trigger)
+ IO.puts "OPTS : #{inspect {trigger, opts}}"
+ line = get_random(state.triggers, trigger, String.trim(Enum.join(opts, " ")))
if line do
- msg.replyfun.(line)
+ msg.replyfun.(format_line(line))
end
{:noreply, state}
end
@@ -317,7 +319,7 @@ defmodule LSG.IRC.TxtPlugin do
end
end
- defp get_random(triggers, trigger, [opt]) do
+ defp get_random(triggers, trigger, opt) do
arg = case Integer.parse(opt) do
{pos, ""} -> {:index, pos}
{_pos, _some_string} -> {:grep, opt}
@@ -372,21 +374,24 @@ defmodule LSG.IRC.TxtPlugin do
trigger = trigger
|> String.downcase
- |> String.replace("à", "a")
- |> String.replace("ä", "a")
- |> String.replace("â", "a")
- |> String.replace("é", "e")
- |> String.replace("è", "e")
- |> String.replace("ê", "e")
- |> String.replace("ë", "e")
- |> String.replace("ç", "c")
- |> String.replace("ï", "i")
- |> String.replace("î", "i")
+ |> :unicode.characters_to_nfd_binary()
|> String.replace(~r/[^a-z0-9]/, "")
{trigger, opts}
end
+ defp format_line(line, prefix \\ "") do
+ prefix <> line
+ |> String.split("\\\\")
+ |> Enum.map(fn(line) ->
+ String.split(line, "\\\\\\\\")
+ end)
+ |> List.flatten()
+ |> Enum.map(fn(line) ->
+ String.trim(line)
+ end)
+ end
+
def directory() do
Application.get_env(:lsg, :data_path) <> "/irc.txt/"
end
diff --git a/lib/lsg_irc/wolfram_alpha_plugin.ex b/lib/lsg_irc/wolfram_alpha_plugin.ex
new file mode 100644
index 0000000..b34db56
--- /dev/null
+++ b/lib/lsg_irc/wolfram_alpha_plugin.ex
@@ -0,0 +1,47 @@
+defmodule LSG.IRC.WolframAlphaPlugin do
+ use GenServer
+ require Logger
+
+ @moduledoc """
+ # wolfram alpha
+
+ * **`!wa <requête>`** lance `<requête>` sur WolframAlpha
+ """
+
+ def irc_doc, do: @moduledoc
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, [])
+ end
+
+ def init(_) do
+ {:ok, _} = Registry.register(IRC.PubSub, "trigger:wa", [])
+ {:ok, nil}
+ end
+
+ def handle_info({:irc, :trigger, _, m = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: query}}}, state) do
+ query = Enum.join(query, " ")
+ params = %{
+ "appid" => Keyword.get(Application.get_env(:lsg, :wolframalpha, []), :app_id, "NO_APP_ID"),
+ "units" => "metric",
+ "i" => query
+ }
+ url = "https://www.wolframalpha.com/input/?i=" <> URI.encode(query)
+ case HTTPoison.get("http://api.wolframalpha.com/v1/result", [], [params: params]) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ m.replyfun.(["#{query} -> #{body}", url])
+ {:ok, %HTTPoison.Response{status_code: code, body: body}} ->
+ error = case {code, body} do
+ {501, b} -> "input invalide: #{body}"
+ {code, error} -> "erreur #{code}: #{body || ""}"
+ end
+ m.replyfun.("wa: #{error}")
+ {:error, %HTTPoison.Error{reason: reason}} ->
+ m.replyfun.("wa: erreur http: #{to_string(reason)}")
+ _ ->
+ m.replyfun.("wa: erreur http")
+ end
+ {:noreply, state}
+ end
+
+end
diff --git a/lib/lsg_web/controllers/alcoolog_controller.ex b/lib/lsg_web/controllers/alcoolog_controller.ex
new file mode 100644
index 0000000..9d5d9d9
--- /dev/null
+++ b/lib/lsg_web/controllers/alcoolog_controller.ex
@@ -0,0 +1,48 @@
+defmodule LSGWeb.AlcoologController do
+ use LSGWeb, :controller
+ require Logger
+
+ def index(conn, %{"channel" => channel}) do
+ case LSG.Token.lookup(channel) do
+ {:ok, obj} -> index(conn, obj)
+ err ->
+ Logger.debug("AlcoologControler: token #{inspect err} invalid")
+ conn
+ |> put_status(404)
+ |> text("Page not found")
+ end
+ end
+
+ def index(conn, {:alcoolog, :index, channel}) do
+ aday = 7*((24 * 60)*60)
+ now = DateTime.utc_now()
+ before = now
+ |> DateTime.add(-aday, :second)
+ |> DateTime.to_unix(:millisecond)
+ #match = :ets.fun2ms(fn(obj = {{^nick, date}, _, _, _, _}) when date > before -> obj end)
+ match = [
+ {{{:_, :"$1"}, :_, :_, :_, :_},
+ [
+ {:>, :"$1", {:const, before}},
+ ], [:"$_"]}
+ ]
+
+ nicks_in_channel = IRC.UserTrack.channel(channel)
+ |> Enum.map(fn({_, nick, _, _, _, _, _}) -> String.downcase(nick) end)
+ # tuple ets: {{nick, date}, volumes, current, nom, commentaire}
+ drinks = :ets.select(LSG.IRC.AlcoologPlugin.ETS, match)
+ #|> Enum.filter(fn({{nick, _}, _, _, _, _}) -> Enum.member?(nicks_in_channel, nick) end)
+ |> Enum.sort_by(fn({{_, ts}, _, _, _, _}) -> ts end, &>/2)
+
+ stats = LSG.IRC.AlcoologPlugin.get_channel_statistics(channel)
+
+ top = Enum.reduce(drinks, %{}, fn({{nick, _}, vol, _, _, _}, acc) ->
+ all = Map.get(acc, nick, 0)
+ Map.put(acc, nick, all + vol)
+ end)
+ |> Enum.sort_by(fn({_nick, count}) -> count end, &>/2)
+ # {date, single_peak}
+ render(conn, "index.html", channel: channel, drinks: drinks, top: top, stats: stats)
+ end
+
+end
diff --git a/lib/lsg_web/router.ex b/lib/lsg_web/router.ex
index c47e422..a834083 100644
--- a/lib/lsg_web/router.ex
+++ b/lib/lsg_web/router.ex
@@ -22,6 +22,8 @@ defmodule LSGWeb.Router do
get "/irc", IrcController, :index
get "/irc/txt", IrcController, :txt
get "/irc/txt/:name", IrcController, :txt
+ get "/irc/alcoolog/:channel", AlcoologController, :index
+ get "/irc/alcoolog/~/:nick", AlcoologController, :nick
end
scope "/api", LSGWeb do
diff --git a/lib/lsg_web/templates/alcoolog/index.html.eex b/lib/lsg_web/templates/alcoolog/index.html.eex
new file mode 100644
index 0000000..507be71
--- /dev/null
+++ b/lib/lsg_web/templates/alcoolog/index.html.eex
@@ -0,0 +1,55 @@
+<style type="text/css">
+h1 small {
+ font-size: 14px;
+}
+ol li {
+ margin-bottom: 5px
+}
+</style>
+
+<h1>
+ <small><a href="/irc"><%= Keyword.get(Application.get_env(:lsg, :irc), :name, "ircbot") %></a> &rsaquo; </small><br/>
+ alcoolog <%= @channel %>
+</h1>
+
+<ul>
+ <%= for {nick, status} <- @stats do %>
+ <li><strong><%= nick %> <%= status.user_status %> - <%= status.trend_symbol %> <%= status.active %> g/l</strong><br/>
+ &mdash; 15m: <%= status.active15m %> g/l - 30m: <%= status.active30m %> g/l - 1h: <%= status.active1h %> g/l<br />
+ &mdash; dernier verre: <%= status.last_type %> <%= status.last_descr %> (<%= status.last_points %>)
+ <%= LSGWeb.LayoutView.format_time(status.last_at) %>
+ <br />
+ &mdash; sobre dans: <%= status.sober_in_s %>
+ <br />
+ <small>
+ &mdash; aujourd'hui: <%= status.daily_volumes %> points, <%= status.daily_gl %> g/l
+ </small>
+ </li>
+ <% end %>
+</ul>
+
+<p>
+ top consommateur par volume, les 7 derniers jours: <%= Enum.intersperse(for({nick, count} <- @top, do: "#{nick}: #{Float.round(count,4)}"), ", ") %>
+</p>
+
+<table class="table">
+ <thead>
+ <tr>
+ <th scope="col">date</th>
+ <th scope="col">nick</th>
+ <th scope="col">points</th>
+ <th scope="col">nom</th>
+ </tr>
+ </thead>
+ <tbody>
+ <%= for {{nick, date}, points, _, nom, comment} <- @drinks do %>
+ <% date = DateTime.from_unix!(date, :millisecond) %>
+ <th scope="row"><%= LSGWeb.LayoutView.format_time(date, false) %></th>
+ <td><%= nick %></td>
+ <td><%= Float.round(points+0.0, 5) %></td>
+ <td><%= nom||"" %> <%= comment||"" %></td>
+ </tr>
+ <% end %>
+ </tbody>
+</table>
+
diff --git a/lib/lsg_web/views/alcoolog_view.ex b/lib/lsg_web/views/alcoolog_view.ex
new file mode 100644
index 0000000..2c55c09
--- /dev/null
+++ b/lib/lsg_web/views/alcoolog_view.ex
@@ -0,0 +1,4 @@
+defmodule LSGWeb.AlcoologView do
+ use LSGWeb, :view
+end
+
diff --git a/lib/lsg_web/views/layout_view.ex b/lib/lsg_web/views/layout_view.ex
index 39216ed..956d703 100644
--- a/lib/lsg_web/views/layout_view.ex
+++ b/lib/lsg_web/views/layout_view.ex
@@ -1,3 +1,20 @@
defmodule LSGWeb.LayoutView do
use LSGWeb, :view
+
+ def format_time(date, with_relative \\ true) do
+ alias Timex.Format.DateTime.Formatters
+ alias Timex.Timezone
+ date = if is_integer(date) do
+ date
+ |> DateTime.from_unix!(:millisecond)
+ |> Timezone.convert("Europe/Paris")
+ else
+ date
+ end
+ {:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr")
+ {:ok, detail} = Formatters.Default.lformat(date, "{D}/{M}/{YYYY} {h24}:{m}", "fr")
+
+ content_tag(:time, if(with_relative, do: relative, else: detail), [title: detail])
+ end
+
end
diff --git a/lib/util.ex b/lib/util.ex
new file mode 100644
index 0000000..f515936
--- /dev/null
+++ b/lib/util.ex
@@ -0,0 +1,13 @@
+defmodule Util do
+
+ def plusminus(number) when number > 0, do: "+#{number}"
+ def plusminus(0), do: "0"
+ def plusminus(number) when number < 0, do: "#{number}"
+
+ def float_paparse(string) do
+ string
+ |> String.replace(",", ".")
+ |> Float.parse()
+ end
+
+end