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` ``**: lis la ligne `` du fichier `FICHIER`. * **!`FILE` ``**: recherche une ligne contenant `` dans `FICHIER`. * **+txt `**: crée le fichier ``. * **+`FICHIER` ``**: ajoute une ligne `` dans le fichier `FICHIER`. * **-`FICHIER` ``**: supprime la ligne `` du fichier `FICHIER`. * **+txtro, +txtrw**. op seulement. active/désactive le mode lecture seule. * **+txtlock ``, -txtlock ``**. 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