summaryrefslogblamecommitdiff
path: root/lib/lsg_irc/txt_handler.ex
blob: 032c11c66b16a1f561d91c23feac1bfdf8b5c372 (plain) (tree)
1
2
3
4
5
6
7
                               
                     
                
 


                   













                                                                                                                 

     
                                                               





                                              
                                                                         

                       

                                                                                           

                                                                     







                                                            



















































                                                                                                                

                                                         





                                                        





                                                                                                                                  



                                                                






                                                                                     


















                                                                               








                                                               





                                                                                                      


                                                                                                                               
                            


       






                                                                                                              

                                                       
                            


       
                                                                                                         


                                                                   
                                                       















                                                                                                





                                                                                           
 





                                                                   
 



                                







                                  

                                     
                                                     


                                                      
                                













                                


                                






                                            
                                                                        






































                                                                         

                                                     






                                                            




                                                                                        
           
                 




















                                                              
                                        



                   




                                                                                          

                                                             
















                                                                            

   
defmodule LSG.IRC.TxtHandler do
  alias IRC.UserTrack
  require Logger

  @moduledoc """
  # [txt](/irc/txt)

  * **!txt**: liste des fichiers et statistiques.
  Les fichiers avec une `*` sont vérrouillés.
  [Voir sur le web](/irc/txt).

  * **!`FICHIER`**: lis aléatoirement une ligne du fichier `FICHIER`.
  * **!`FICHIER` `<chiffre>`**: lis la ligne `<chiffre>` du fichier `FICHIER`.
  * **!`FILE` `<recherche>`**: recherche une ligne contenant `<recherche>` dans `FICHIER`.

  * **+txt `<file`>**: crée le fichier `<file>`.
  * **+`FICHIER` `<texte>`**: ajoute une ligne `<texte>` dans le fichier `FICHIER`.
  * **-`FICHIER` `<chiffre>`**: supprime la ligne `<chiffre>` du fichier `FICHIER`.

  * **+txtro, +txtrw**. op seulement. active/désactive le mode lecture seule.
  * **+txtlock `<fichier>`, -txtlock `<fichier>`**. op seulement. active/désactive le verrouillage d'un fichier.
  """

  def short_irc_doc, do: "!txt https://sys.115ans.net/irc/txt "
  def irc_doc, do: @moduledoc

  def start_link(client) do
    GenServer.start_link(__MODULE__, [client])
  end

  defstruct client: nil, triggers: %{}, rw: true, locks: nil, markov: nil

  def init([client]) do
    dets_locks_filename = (LSG.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist
    {:ok, locks} = :dets.open_file(dets_locks_filename, [])
    {:ok, markov} = ExChain.MarkovModel.start_link
    state = %__MODULE__{client: client, locks: locks, markov: markov}
    ExIRC.Client.add_handler(client, self())
    {:ok, %__MODULE__{state | triggers: load()}}
  end

  def handle_info({:received, "!reload", _, chan}, state) do
    {:noreply, %__MODULE__{state | triggers: load()}}
  end

  ## -- ADMIN RO/RW
  def handle_info({:received, "+txtrw", %ExIRC.SenderInfo{nick: nick}, chan}, state = %__MODULE__{rw: false}) do
    if UserTrack.operator?(chan, nick) do
      say(state, chan, "txt: écriture réactivée")
      {:noreply, %__MODULE__{state | rw: true}}
    else
      {:noreply, state}
    end
  end

  def handle_info({:received, "+txtro", %ExIRC.SenderInfo{nick: nick}, chan}, state = %__MODULE__{rw: true}) do
    if UserTrack.operator?(chan, nick) do
      say(state, chan, "txt: écriture désactivée")
      {:noreply, %__MODULE__{state | rw: false}}
    else
      {:noreply, state}
    end
  end

  def handle_info({:received, "+txtro", _, _}, state) do
    {:noreply, state}
  end
  def handle_info({:received, "+txtrw", _, _}, state) do
    {:noreply, state}
  end

  ## -- ADMIN LOCKS
  def handle_info({:received, "+txtlock " <> trigger, %ExIRC.SenderInfo{nick: nick}, chan}, state) do
    with \
      {trigger, _} <- clean_trigger(trigger),
      true <- UserTrack.operator?(chan, nick)
    do
      :dets.insert(state.locks, {trigger})
      say(state, chan, "txt: #{trigger} verrouillé")
    end
    {:noreply, state}
  end

  def handle_info({:received, "-txtlock " <> trigger, %ExIRC.SenderInfo{nick: nick}, chan}, state) do
    with \
      {trigger, _} <- clean_trigger(trigger),
      true <- UserTrack.operator?(chan, nick),
      true <- :dets.member(state.locks, trigger)
    do
      :dets.delete(state.locks, trigger)
      say(state, chan, "txt: #{trigger} déverrouillé")
    end
    {:noreply, state}
  end

  # -- ADD

  def handle_info({:received, "!txt", _, chan}, state) do
    map = Enum.map(state.triggers, fn({key, data}) ->
      locked? = case :dets.lookup(state.locks, key) do
        [{trigger}] -> "*"
        _ -> ""
      end

      "#{key}: #{to_string(Enum.count(data))}#{locked?}"
    end)
    total = Enum.reduce(state.triggers, 0, fn({_, data}, acc) ->
      acc + Enum.count(data)
    end)
    detail = Enum.join(map, ", ")
    total = ". total: #{Enum.count(state.triggers)} fichiers, #{to_string(total)} lignes. Détail: https://sys.115ans.net/irc/txt"

    ro = if !state.rw, do: " (lecture seule activée)", else: ""

    (detail<>total<>ro)
    |> String.codepoints
    |> Enum.chunk_every(440)
    |> Enum.map(&Enum.join/1)
    |> Enum.map(fn(line) -> ExIRC.Client.msg(state.client, :privmsg, chan, line) end)
    {:noreply, state}
  end

  def handle_info({:received, "~txt", _, chan}, state) do
    case ExChain.SentenceGenerator.create_filtered_sentence(state.markov) do
      {:ok, line, _, _} ->
        ExIRC.Client.msg(state.client, :privmsg, chan, line)
      error ->
        Logger.error "Txt Markov error: "<>inspect error
    end
    {:noreply, state}
  end
  def handle_info({:received, "~txt "<>complete, _, chan}, state) do
    case ExChain.SentenceGenerator.complete_sentence(state.markov, complete) do
      {line, _} ->
        ExIRC.Client.msg(state.client, :privmsg, chan, line)
      error ->
        Logger.error "Txt Markov error: "<>inspect error
    end
    {:noreply, state}
  end

  def handle_info({:received, "!"<>trigger, _, chan}, state) do
    {trigger, opts} = clean_trigger(trigger)
    line = get_random(state.triggers, trigger, opts)
    if line do
      ExIRC.Client.msg(state.client, :privmsg, chan, line)
    end
    {:noreply, state}
  end

  def handle_info({:received, "+txt "<>trigger, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
    with \
      {trigger, _} <- clean_trigger(trigger),
      true <- can_write?(state, chan, sender, trigger),
      :ok <- create_file(trigger)
    do
      ExIRC.Client.msg(state.client, :privmsg, chan, "#{trigger}.txt créé. Ajouter: `+#{trigger} …` ; Lire: `!#{trigger}`")
      {:noreply, %__MODULE__{state | triggers: load()}}
    else
      _ -> {:noreply, state}
    end
  end

  def handle_info({:received, "+"<>trigger_and_content, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
    with \
      {trigger, _} <- clean_trigger(trigger_and_content),
      true <- can_write?(state, chan, sender, trigger),
      {:ok, idx} <- add(state.triggers, trigger_and_content)
    do
      ExIRC.Client.msg(state.client, :privmsg, chan, "#{nick}: ajouté à #{trigger}. (#{idx})")
      {:noreply, %__MODULE__{state | triggers: load()}}
    else
      _ -> {:noreply, state}
    end
  end

  def handle_info({:received, "-"<>trigger_and_id, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
    with \
      [trigger, id] <- String.split(trigger_and_id, " ", parts: 2),
      {trigger, _} = clean_trigger(trigger),
      true <- can_write?(state, chan, sender, trigger),
      data <- Map.get(state.triggers, trigger),
      {id, ""} <- Integer.parse(id),
      {text, _id} <- Enum.find(data, fn({_, idx}) -> id-1 == idx end)
    do
      data = data |> Enum.into(Map.new)
      data = Map.delete(data, text)
      ExIRC.Client.msg(state.client, :privmsg, chan, "#{trigger}.txt##{id} supprimée: #{text}")
      dump(trigger, data)
      {:noreply, %__MODULE__{state | triggers: load()}}
    else
      error ->
        IO.inspect("error " <> inspect(error))
        {:noreply, state}
    end
  end

  def handle_info(:reload_markov, state=%__MODULE__{triggers: triggers, markov: markov}) do
    all_data = triggers
    |> Enum.map(fn({_, data}) ->
      for {line, _idx} <- data, do: line
    end)
    |> List.flatten

    for l <- all_data, do: IO.puts(l)

    populate = ExChain.MarkovModel.populate_model(markov, all_data)
    IO.puts "populated markov: #{inspect(populate)}"
    {:noreply, state}
  end

  def handle_info(msg, state) do
    {:noreply, state}
  end

  def terminate(_reason, state) do
    if state.locks do
      :dets.sync(state.locks)
      :dets.close(state.locks)
    end
    :ok
  end

  # Load/Reloads text files from disk
  defp load() do
    triggers = Path.wildcard(directory() <> "/*.txt")
    |> Enum.reduce(%{}, fn(path, m) ->
      file = Path.basename(path)
      [key, "txt"] = String.split(file, ".", parts: 2)
      data = directory() <> file
      |> File.read!
      |> String.split("\n")
      |> Enum.reject(fn(line) ->
        cond do
          line == "" -> true
          !line -> true
          true -> false
        end
      end)
      |> Enum.with_index
      Map.put(m, key, data)
    end)
    |> Enum.sort
    |> Enum.into(Map.new)

    send(self(), :reload_markov)
    triggers
  end

  defp dump(trigger, data) do
    data = data
    |> Enum.sort_by(fn({_, idx}) -> idx end)
    |> Enum.map(fn({text, _}) -> text end)
    |> Enum.join("\n")
    File.write!(directory() <> "/" <> trigger <> ".txt", data<>"\n", [])
  end

  defp get_random(triggers, trigger, []) do
    if data = Map.get(triggers, trigger) do
      {data, _idx} = Enum.random(data)
      data
    else
      nil
    end
  end

  defp get_random(triggers, trigger, [opt]) do
    arg = case Integer.parse(opt) do
      {pos, ""} -> {:index, pos}
      {_pos, _some_string} -> {:grep, opt}
      _error -> {:grep, opt}
    end
    get_with_param(triggers, trigger, arg)
  end

  defp get_with_param(triggers, trigger, {:index, pos}) do
    data = Map.get(triggers, trigger, %{})
    case Enum.find(data, fn({_, index}) -> index+1 == pos end) do
      {text, _} -> text
      _ -> nil
    end
  end

  defp get_with_param(triggers, trigger, {:grep, query}) do
    data = Map.get(triggers, trigger, %{})
    regex = Regex.compile!("#{query}", "i")
    out = Enum.filter(data, fn({txt, _}) -> Regex.match?(regex, txt) end)
    |> Enum.map(fn({txt, _}) -> txt end)
    if !Enum.empty?(out) do
      Enum.random(out)
    end
  end

  defp create_file(name) do
    File.touch!(directory() <> "/" <> name <> ".txt")
    :ok
  end

  defp add(triggers, trigger_and_content) do
    case String.split(trigger_and_content, " ", parts: 2) do
      [trigger, content] ->
        {trigger, _} = clean_trigger(trigger)
        if Map.has_key?(triggers, trigger) do
          File.write!(directory() <> "/" <> trigger <> ".txt", content<>"\n", [:append])
          idx = Enum.count(triggers[trigger])+1
          {:ok, idx}
        else
          :error
        end
      _ -> :error
    end
  end

  # fixme: this is definitely the ugliest thing i've ever done
  defp clean_trigger(trigger) do
    [trigger | opts] = trigger
    |> String.strip
    |> String.split(" ", parts: 2)

    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")
    |> String.replace(~r/[^a-z0-9]/, "")

    {trigger, opts}
  end

  defp directory() do
    Application.get_env(:lsg, :data_path) <> "/irc.txt/"
  end

  defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, channel, sender, trigger) do
    admin? = IRC.admin?(sender)
    operator? = IRC.UserTrack.operator?(channel, sender.nick)
    locked? = case :dets.lookup(locks, trigger) do
      [{trigger}] -> true
      _ -> false
    end
    unlocked? = if rw? == false, do: false, else: !locked?
    can? = admin? || operator? || unlocked?

    if !can? do
      reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé"
      say(state, channel, "#{sender.nick}: permission refusée (#{reason})")
    end
    can?
  end

  defp say(%__MODULE__{client: client}, chan, message) do
    ExIRC.Client.msg(client, :privmsg, chan, message)
  end

end