defmodule Nola.Plugins.Preums 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(Nola.PubSub, "account", regopts) {:ok, _} = Registry.register(Nola.PubSub, "messages", regopts) {:ok, _} = Registry.register(Nola.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.Plugins.Preums, []) 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