From 2d83df8b32bff7f0028923bb5b64dc0b55f20d03 Mon Sep 17 00:00:00 2001 From: Jordan Bracco Date: Tue, 20 Dec 2022 00:21:54 +0000 Subject: Nola rename: The Big Move, Refs T77 --- lib/nola_plugins/preums_plugin.ex | 276 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 lib/nola_plugins/preums_plugin.ex (limited to 'lib/nola_plugins/preums_plugin.ex') 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 -- cgit v1.2.3