summaryrefslogblamecommitdiff
path: root/lib/plugins/preums.ex
blob: f7738ad29251a98d90747f76c533e0b81976d9db (plain) (tree)
1
2
3
4
5
6
7
8
                                






                                        




























                                                                                                                    

                



                                                                  




                                                    
                                                                     
                           
                                                                                               






                              






                                                                         
                                                          


                                                             




                               
                                                                                  

     

                             
                                                          

     
             
                                                                

     
                 
                                  


                                                                  




























                                                                                       



                        

                                                                                                             
                                   
                             

                                                          

                                                



                                              
                                                                   




                
                                                  
                                                      


                                                              





                                                           

                                                                                                            
                                   
                                                        
                                                         


                                                                    
                                          
              







                                   



                     
                                                                                                              
                                   

                                                            













                                                                    




































                                                                                   
                           
                                                             





                                                           





                                                                            








                                                          

                                                  

                                                                                         
                                                         

                                        


                                                                            






               




                                                       
   
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