diff options
Diffstat (limited to 'lib/lsg_irc/txt_plugin.ex')
-rw-r--r-- | lib/lsg_irc/txt_plugin.ex | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/lib/lsg_irc/txt_plugin.ex b/lib/lsg_irc/txt_plugin.ex new file mode 100644 index 0000000..6b7edbd --- /dev/null +++ b/lib/lsg_irc/txt_plugin.ex @@ -0,0 +1,428 @@ +defmodule LSG.IRC.TxtPlugin 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). + + * **!txt**: lis aléatoirement une ligne dans tous les fichiers. + * **!txt `<recherche>`**: recherche une ligne dans tous les fichiers. + + * **~txt**: essaie de générer une phrase (markov). + * **~txt `<début>`**: essaie de générer une phrase commencant par `<debut>`. + + * **!`FICHIER`**: lis aléatoirement une ligne du fichier `FICHIER`. + * **!`FICHIER` `<chiffre>`**: lis la ligne `<chiffre>` du fichier `FICHIER`. + * **!`FICHIER` `<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`. + + * **-txtrw, +txtrw**. op seulement. active/désactive le mode lecture seule. + * **+txtlock `<fichier>`, -txtlock `<fichier>`**. op seulement. active/désactive le verrouillage d'un fichier. + + Insérez `\\\\` pour faire un saut de ligne. + """ + + def short_irc_doc, do: "!txt https://sys.115ans.net/irc/txt " + def irc_doc, do: @moduledoc + + def start_link() do + GenServer.start_link(__MODULE__, []) + end + + defstruct triggers: %{}, rw: true, locks: nil, markov_handler: nil, markov: nil + + def init([]) do + dets_locks_filename = (LSG.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist + {:ok, locks} = :dets.open_file(dets_locks_filename, []) + markov_handler = Keyword.get(Application.get_env(:lsg, __MODULE__, []), :markov_handler, LSG.IRC.TxtPlugin.Markov.Native) + {:ok, markov} = markov_handler.start_link() + {:ok, _} = Registry.register(IRC.PubSub, "triggers", []) + {:ok, %__MODULE__{locks: locks, markov_handler: markov_handler, markov: markov, triggers: load()}} + end + + def handle_info({:received, "!reload", _, chan}, state) do + {:noreply, %__MODULE__{state | triggers: load()}} + end + + # + # ADMIN: RW/RO + # + + def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :plus}}}, state = %{rw: false}) do + if channel && UserTrack.operator?(channel, msg.sender.nick) do + msg.replyfun.("txt: écriture réactivée") + {:noreply, %__MODULE__{state | rw: true}} + else + {:noreply, state} + end + end + + def handle_info({:irc, :trigger, "txtrw", msg = %{channel: channel, trigger: %{type: :minus}}}, state = %{rw: true}) do + if channel && UserTrack.operator?(channel, msg.sender.nick) do + msg.replyfun.("txt: écriture désactivée") + {:noreply, %__MODULE__{state | rw: false}} + else + {:noreply, state} + end + end + + # + # ADMIN: LOCKS + # + + def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do + with \ + {trigger, _} <- clean_trigger(trigger), + true <- UserTrack.operator?(msg.channel, msg.sender.nick) + do + :dets.insert(state.locks, {trigger}) + msg.replyfun.("txt: #{trigger} verrouillé") + end + {:noreply, state} + end + + def handle_info({:irc, :trigger, "txtlock", msg = %{trigger: %{type: :minus, args: [trigger]}}}, state) do + with \ + {trigger, _} <- clean_trigger(trigger), + true <- UserTrack.operator?(msg.channel, msg.sender.nick), + true <- :dets.member(state.locks, trigger) + do + :dets.delete(state.locks, trigger) + msg.replyfun.("txt: #{trigger} déverrouillé") + end + {:noreply, state} + end + + # + # FILE LIST + # + + def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :dot}}}, 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) + |> msg.replyfun.() + {:noreply, state} + end + + # + # GLOBAL: RANDOM + # + + def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: []}}}, state) do + result = Enum.reduce(state.triggers, [], fn({trigger, data}, acc) -> + Enum.reduce(data, acc, fn({l, _}, acc) -> + [{trigger, l} | acc] + end) + end) + |> Enum.shuffle() + + if !Enum.empty?(result) do + {source, line} = Enum.random(result) + msg.replyfun.("#{source}: #{line}") + end + {:noreply, state} + end + + def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: args}}}, state) do + grep = Enum.join(args, " ") + result = Enum.reduce(state.triggers, [], fn({trigger, data}, acc) -> + Enum.reduce(data, acc, fn({l, _}, acc) -> + [{trigger, l} | acc] + end) + end) + |> Enum.filter(fn({_, line}) -> String.contains?(String.downcase(line), String.downcase(grep)) end) + |> Enum.shuffle() + + if !Enum.empty?(result) do + {source, line} = Enum.random(result) + msg.replyfun.("#{source}: #{line}") + end + {:noreply, state} + end + + # + # GLOBAL: MARKOV + # + + def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: []}}}, state) do + case state.markov_handler.sentence(state.markov) do + {:ok, line} -> + msg.replyfun.(line) + error -> + Logger.error "Txt Markov error: "<>inspect error + end + {:noreply, state} + end + + def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :tilde, args: complete}}}, state) do + complete = Enum.join(complete, " ") + case state.markov_handler.complete_sentence(complete, state.markov) do + {:ok, line} -> + msg.replyfun.(line) + error -> + Logger.error "Txt Markov error: "<>inspect error + end + {:noreply, state} + end + + # + # TXT CREATE + # + + def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :plus, args: [trigger]}}}, state) do + with \ + {trigger, _} <- clean_trigger(trigger), + true <- can_write?(state, msg, trigger), + :ok <- create_file(trigger) + do + msg.replyfun.("#{trigger}.txt créé. Ajouter: `+#{trigger} …` ; Lire: `!#{trigger}`") + {:noreply, %__MODULE__{state | triggers: load()}} + else + _ -> {:noreply, state} + end + end + + # + # TXT: RANDOM + # + + def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :bang, args: []}}}, state) do + {trigger, opts} = clean_trigger(trigger) + line = get_random(state.triggers, trigger, opts) + if line do + msg.replyfun.(line) + end + {:noreply, state} + end + + # + # TXT: ADD + # + + def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :plus, args: content}}}, state) do + with \ + true <- can_write?(state, msg, trigger), + {:ok, idx} <- add(state.triggers, msg.text) + do + msg.replyfun.("#{msg.sender.nick}: ajouté à #{trigger}. (#{idx})") + {:noreply, %__MODULE__{state | triggers: load()}} + else + _ -> + {:noreply, state} + end + end + + # + # TXT: DELETE + # + + def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :minus, args: [id]}}}, state) do + with \ + true <- can_write?(state, msg, 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) + msg.replyfun.("#{msg.sender.nick}: #{trigger}.txt##{id} supprimée: #{text}") + dump(trigger, data) + {:noreply, %__MODULE__{state | triggers: load()}} + else + _ -> + {:noreply, state} + end + end + + def handle_info(:reload_markov, state=%__MODULE__{triggers: triggers, markov: markov}) do + state.markov_handler.reload(state.triggers, state.markov) + {:noreply, state} + end + + def handle_info(msg, state) do + IO.puts "txt unhandled #{inspect msg}" + {: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 + + def directory() do + Application.get_env(:lsg, :data_path) <> "/irc.txt/" + end + + defp can_write?(%{rw: rw?, locks: locks}, msg = %{channel: nil, sender: sender}, trigger) do + admin? = IRC.admin?(sender) + locked? = case :dets.lookup(locks, trigger) do + [{trigger}] -> true + _ -> false + end + unlocked? = if rw? == false, do: false, else: !locked? + + can? = unlocked? || admin? + + if !can? do + reason = if !rw?, do: "lecture seule", else: "fichier vérrouillé" + msg.replyfun.("#{sender.nick}: permission refusée (#{reason})") + end + can? + end + + defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, msg = %{channel: channel, sender: 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é" + msg.replyfun.("#{sender.nick}: permission refusée (#{reason})") + end + can? + end + +end |