summaryrefslogtreecommitdiff
path: root/lib/nola_plugins/preums_plugin.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/nola_plugins/preums_plugin.ex')
-rw-r--r--lib/nola_plugins/preums_plugin.ex276
1 files changed, 276 insertions, 0 deletions
diff --git a/lib/nola_plugins/preums_plugin.ex b/lib/nola_plugins/preums_plugin.ex
new file mode 100644
index 0000000..f250e85
--- /dev/null
+++ b/lib/nola_plugins/preums_plugin.ex
@@ -0,0 +1,276 @@
+defmodule Nola.IRC.PreumsPlugin do
+ @moduledoc """
+ # preums !!!
+
+ * `!preums`: affiche le preums du jour
+ * `.preums`: stats des preums
+ """
+
+ # WIP Scores
+ # L'idée c'est de donner un score pour mettre un peu de challenge en pénalisant les preums faciles.
+ #
+ # Un preums ne vaut pas 1 point, mais plutôt 0.10 ou 0.05, et on arrondi au plus proche. C'est un jeu sur le long
+ # terme. Un gros bonus pourrait apporter beaucoup de points.
+ #
+ # Il faudrait ces données:
+ # - moyenne des preums
+ # - activité récente du channel et par nb actifs d'utilisateurs
+ # (aggréger memberships+usertrack last_active ?)
+ # (faire des stats d'activité habituelle (un peu a la pisg) ?)
+ # - preums consécutifs
+ #
+ # Malus:
+ # - est proche de la moyenne en faible activité
+ # - trop consécutif de l'utilisateur sauf si activité
+ #
+ # Bonus:
+ # - plus le preums est éloigné de la moyenne
+ # - après 18h double
+ # - plus l'activité est élévée, exponentiel selon la moyenne
+ # - derns entre 4 et 6 (pourrait être adapté selon les stats d'activité)
+ #
+ # WIP Badges:
+ # - derns
+ # - streaks
+ # - faciles
+ # - ?
+
+ require Logger
+
+ @perfects [~r/preum(s|)/i]
+
+ # dets {{chan, day = {yyyy, mm, dd}}, nick, now, perfect?, text}
+
+ def all(dets) do
+ :dets.foldl(fn(i, acc) -> [i|acc] end, [], dets)
+ end
+
+ def all(dets, channel) do
+ fun = fn({{chan, date}, account_id, time, perfect, text}, acc) ->
+ if channel == chan do
+ [%{date: date, account_id: account_id, time: time, perfect: perfect, text: text} | acc]
+ else
+ acc
+ end
+ end
+ :dets.foldl(fun, [], dets)
+ end
+
+ def topnicks(dets, channel, options \\ []) do
+ sort_elem = case Keyword.get(options, :sort_by, :score) do
+ :score -> 1
+ :count -> 0
+ end
+
+ fun = fn(x = {{chan, date}, account_id, time, perfect, text}, acc) ->
+ if (channel == nil and chan) or (channel == chan) do
+ {count, points} = Map.get(acc, account_id, {0, 0})
+ score = score(chan, account_id, time, perfect, text)
+ Map.put(acc, account_id, {count + 1, points + score})
+ else
+ acc
+ end
+ end
+ :dets.foldl(fun, %{}, dets)
+ |> Enum.sort_by(fn({_account_id, value}) -> elem(value, sort_elem) end, &>=/2)
+ end
+
+ def irc_doc, do: @moduledoc
+ def start_link() do
+ GenServer.start_link(__MODULE__, [], name: __MODULE__)
+ end
+
+ def dets do
+ (Nola.data_path() <> "/preums.dets") |> String.to_charlist()
+ end
+
+ def init([]) do
+ regopts = [plugin: __MODULE__]
+ {:ok, _} = Registry.register(IRC.PubSub, "account", regopts)
+ {:ok, _} = Registry.register(IRC.PubSub, "messages", regopts)
+ {:ok, _} = Registry.register(IRC.PubSub, "triggers", regopts)
+ {:ok, dets} = :dets.open_file(dets(), [{:repair, :force}])
+ Util.ets_mutate_select_each(:dets, dets, [{:"$1", [], [:"$1"]}], fn(table, obj) ->
+ {key, nick, now, perfect, text} = obj
+ case key do
+ {{net, {bork,chan}}, date} ->
+ :dets.delete(table, key)
+ nick = if IRC.Account.get(nick) do
+ nick
+ else
+ if acct = IRC.Account.find_always_by_nick(net, nil, nick) do
+ acct.id
+ else
+ nick
+ end
+ end
+ :dets.insert(table, { { {net,chan}, date }, nick, now, perfect, text})
+ {{_net, nil}, _} ->
+ :dets.delete(table, key)
+ {{net, chan}, date} ->
+ if !IRC.Account.get(nick) do
+ if acct = IRC.Account.find_always_by_nick(net, chan, nick) do
+ :dets.delete(table, key)
+ :dets.insert(table, { { {net,chan}, date }, acct.id, now, perfect, text})
+ end
+ end
+ _ ->
+ Logger.debug("DID NOT FIX: #{inspect key}")
+ end
+ end)
+ {:ok, %{dets: dets}}
+ end
+
+ # Latest
+ def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}}}, state) do
+ channelkey = {m.network, m.channel}
+ state = handle_preums(m, state)
+ tz = timezone(channelkey)
+ {:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
+ date = {now.year, now.month, now.day}
+ key = {channelkey, date}
+ chan_cache = Map.get(state, channelkey, %{})
+ item = if i = Map.get(chan_cache, date) do
+ i
+ else
+ case :dets.lookup(state.dets, key) do
+ [item = {^key, _account_id, _now, _perfect, _text}] -> item
+ _ -> nil
+ end
+ end
+
+ if item do
+ {_, account_id, date, _perfect, text} = item
+ h = "#{date.hour}:#{date.minute}:#{date.second}"
+ account = IRC.Account.get(account_id)
+ user = IRC.UserTrack.find_by_account(m.network, account)
+ nick = if(user, do: user.nick, else: account.name)
+ m.replyfun.("preums: #{nick} à #{h}: “#{text}”")
+ end
+ {:noreply, state}
+ end
+
+ # Stats
+ def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :dot}}}, state) do
+ channel = {m.network, m.channel}
+ state = handle_preums(m, state)
+ top = topnicks(state.dets, channel, sort_by: :score)
+ |> Enum.map(fn({account_id, {count, score}}) ->
+ account = IRC.Account.get(account_id)
+ user = IRC.UserTrack.find_by_account(m.network, account)
+ nick = if(user, do: user.nick, else: account.name)
+ "#{nick}: #{score} (#{count})"
+ end)
+ |> Enum.intersperse(", ")
+ |> Enum.join("")
+ msg = unless top == "" do
+ "top preums: #{top}"
+ else
+ "vous êtes tous nuls"
+ end
+ m.replyfun.(msg)
+ {:noreply, state}
+ end
+
+ # Help
+ def handle_info({:irc, :trigger, "preums", m = %IRC.Message{trigger: %IRC.Trigger{type: :query}}}, state) do
+ state = handle_preums(m, state)
+ msg = "!preums - preums du jour, .preums top preumseurs"
+ m.replymsg.(msg)
+ {:noreply, state}
+ end
+
+ # Trigger fallback
+ def handle_info({:irc, :trigger, _, m = %IRC.Message{}}, state) do
+ state = handle_preums(m, state)
+ {:noreply, state}
+ end
+
+ # Message fallback
+ def handle_info({:irc, :text, m = %IRC.Message{}}, state) do
+ {:noreply, handle_preums(m, state)}
+ end
+
+ # Account
+ def handle_info({:account_change, old_id, new_id}, state) do
+ spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, old_id}}], [:"$_"]}]
+ Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
+ rename_object_owner(table, obj, new_id)
+ end)
+ {:noreply, state}
+ end
+
+ # Account: move from nick to account id
+ # FIXME: Doesn't seem to work.
+ def handle_info({:accounts, accounts}, state) do
+ for x={:account, _net, _chan, _nick, _account_id} <- accounts do
+ handle_info(x, state)
+ end
+ {:noreply, state}
+ end
+ def handle_info({:account, _net, _chan, nick, account_id}, state) do
+ nick = String.downcase(nick)
+ spec = [{{:_, :"$1", :_, :_, :_}, [{:==, :"$1", {:const, nick}}], [:"$_"]}]
+ Util.ets_mutate_select_each(:dets, state.dets, spec, fn(table, obj) ->
+ Logger.debug("account:: merging #{nick} -> #{account_id}")
+ rename_object_owner(table, obj, account_id)
+ end)
+ {:noreply, state}
+ end
+
+ def handle_info(_, dets) do
+ {:noreply, dets}
+ end
+
+
+ defp rename_object_owner(table, object = {key, _, now, perfect, time}, new_id) do
+ :dets.delete_object(table, key)
+ :dets.insert(table, {key, new_id, now, perfect, time})
+ end
+
+ defp timezone(channel) do
+ env = Application.get_env(:nola, Nola.IRC.PreumsPlugin, [])
+ channels = Keyword.get(env, :channels, %{})
+ channel_settings = Map.get(channels, channel, [])
+ default = Keyword.get(env, :default_tz, "Europe/Paris")
+ Keyword.get(channel_settings, :tz, default) || default
+ end
+
+ defp handle_preums(%IRC.Message{channel: nil}, state) do
+ state
+ end
+
+ defp handle_preums(m = %IRC.Message{text: text, sender: sender}, state) do
+ channel = {m.network, m.channel}
+ tz = timezone(channel)
+ {:ok, now} = DateTime.now(tz, Tzdata.TimeZoneDatabase)
+ date = {now.year, now.month, now.day}
+ key = {channel, date}
+ chan_cache = Map.get(state, channel, %{})
+ unless i = Map.get(chan_cache, date) do
+ case :dets.lookup(state.dets, key) do
+ [item = {^key, _nick, _now, _perfect, _text}] ->
+ # Preums lost, but wasn't cached
+ Map.put(state, channel, %{date => item})
+ [] ->
+ # Preums won!
+ perfect? = Enum.any?(@perfects, fn(perfect) -> Regex.match?(perfect, text) end)
+ item = {key, m.account.id, now, perfect?, text}
+ :dets.insert(state.dets, item)
+ :dets.sync(state.dets)
+ Map.put(state, channel, %{date => item})
+ {:error, _} = error ->
+ Logger.error("#{__MODULE__} dets lookup failed: #{inspect error}")
+ state
+ end
+ else
+ state
+ end
+ end
+
+ def score(_chan, _account, _time, _perfect, _text) do
+ 1
+ end
+
+
+end