diff options
Diffstat (limited to 'lib/nola_plugins/txt.ex')
-rw-r--r-- | lib/nola_plugins/txt.ex | 556 |
1 files changed, 0 insertions, 556 deletions
diff --git a/lib/nola_plugins/txt.ex b/lib/nola_plugins/txt.ex deleted file mode 100644 index f6e71a3..0000000 --- a/lib/nola_plugins/txt.ex +++ /dev/null @@ -1,556 +0,0 @@ -defmodule Nola.Plugins.Txt do - alias IRC.UserTrack - require Logger - - @moduledoc """ - # [txt]({{context_path}}/txt) - - * **.txt**: liste des fichiers et statistiques. - Les fichiers avec une `*` sont vérrouillés. - [Voir sur le web]({{context_path}}/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__, [], name: __MODULE__) - end - - defstruct triggers: %{}, rw: true, locks: nil, markov_handler: nil, markov: nil - - def random(file) do - GenServer.call(__MODULE__, {:random, file}) - end - - def reply_random(message, file) do - if line = random(file) do - line - |> format_line(nil, message) - |> message.replyfun.() - - line - end - end - - def init([]) do - dets_locks_filename = (Nola.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist - {:ok, locks} = :dets.open_file(dets_locks_filename, []) - markov_handler = Keyword.get(Application.get_env(:nola, __MODULE__, []), :markov_handler, Nola.Plugins.Txt.Markov.Native) - {:ok, markov} = markov_handler.start_link() - {:ok, _} = Registry.register(IRC.PubSub, "triggers", [plugin: __MODULE__]) - {: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?(msg.network, 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?(msg.network, 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.network, 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.network, 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}) -> - ignore? = String.contains?(key, ".") - locked? = case :dets.lookup(state.locks, key) do - [{trigger}] -> "*" - _ -> "" - end - - unless ignore?, do: "#{key}: #{to_string(Enum.count(data))}#{locked?}" - end) - |> Enum.filter(& &1) - 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.(format_line(line, "#{source}: ", msg)) - end - {:noreply, state} - end - - def handle_info({:irc, :trigger, "txt", msg = %{trigger: %{type: :bang, args: args}}}, state) do - grep = Enum.join(args, " ") - |> String.downcase - |> :unicode.characters_to_nfd_binary() - - result = with_stateful_results(msg, {:bang,"txt",msg.network,msg.channel,grep}, fn() -> - Enum.reduce(state.triggers, [], fn({trigger, data}, acc) -> - if !String.contains?(trigger, ".") do - Enum.reduce(data, acc, fn({l, _}, acc) -> - [{trigger, l} | acc] - end) - else - acc - end - end) - |> Enum.filter(fn({_, line}) -> - line - |> String.downcase() - |> :unicode.characters_to_nfd_binary() - |> String.contains?(grep) - end) - |> Enum.shuffle() - end) - - if result do - {source, line} = result - msg.replyfun.(["#{source}: " | line]) - end - {:noreply, state} - end - - def with_stateful_results(msg, key, initfun) do - me = self() - scope = {msg.network, msg.channel || msg.sender.nick} - key = {__MODULE__, me, scope, key} - with_stateful_results(key, initfun) - end - - def with_stateful_results(key, initfun) do - pid = case :global.whereis_name(key) do - :undefined -> - start_stateful_results(key, initfun.()) - pid -> pid - end - if pid, do: wait_stateful_results(key, initfun, pid) - end - - def start_stateful_results(key, []) do - nil - end - - def start_stateful_results(key, list) do - me = self() - {pid, _} = spawn_monitor(fn() -> - Process.monitor(me) - stateful_results(me, list) - end) - :yes = :global.register_name(key, pid) - pid - end - - def wait_stateful_results(key, initfun, pid) do - send(pid, :get) - receive do - {:stateful_results, line} -> - line - {:DOWN, _ref, :process, ^pid, reason} -> - with_stateful_results(key, initfun) - after - 5000 -> - nil - end - end - - defp stateful_results(owner, []) do - send(owner, :empty) - :ok - end - - @stateful_results_expire :timer.minutes(30) - defp stateful_results(owner, [line | rest] = acc) do - receive do - :get -> - send(owner, {:stateful_results, line}) - stateful_results(owner, rest) - {:DOWN, _ref, :process, ^owner, _} -> - :ok - after - @stateful_results_expire -> :ok - end - 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, m = %{trigger: %{type: :query, args: opts}}}, state) do - {trigger, _} = clean_trigger(trigger) - if Map.get(state.triggers, trigger) do - url = if m.channel do - NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, m.network, NolaWeb.format_chan(m.channel), trigger) - else - NolaWeb.Router.Helpers.irc_url(NolaWeb.Endpoint, :txt, trigger) - end - m.replyfun.("-> #{url}") - end - {:noreply, state} - end - - def handle_info({:irc, :trigger, trigger, msg = %{trigger: %{type: :bang, args: opts}}}, state) do - {trigger, _} = clean_trigger(trigger) - line = get_random(msg, state.triggers, trigger, String.trim(Enum.join(opts, " "))) - if line do - msg.replyfun.(format_line(line, nil, msg)) - 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 - {:error, {:jaro, string, idx}} -> - msg.replyfun.("#{msg.sender.nick}: doublon #{trigger}##{idx}: #{string}") - error -> - Logger.debug("txt add failed: #{inspect error}") - {: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 - {:noreply, state} - end - - def handle_call({:random, file}, _from, state) do - random = get_random(nil, state.triggers, file, []) - {:reply, random, 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 = String.replace(file, ".txt", "") - 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(msg, triggers, trigger, []) do - if data = Map.get(triggers, trigger) do - {data, _idx} = Enum.random(data) - data - else - nil - end - end - - defp get_random(msg, 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(msg, triggers, trigger, arg) - end - - defp get_with_param(msg, 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(msg, triggers, trigger, {:grep, query}) do - out = with_stateful_results(msg, {:grep, trigger, query}, fn() -> - data = Map.get(triggers, trigger, %{}) - regex = Regex.compile!("#{query}", "i") - Enum.filter(data, fn({txt, _}) -> Regex.match?(regex, txt) end) - |> Enum.map(fn({txt, _}) -> txt end) - |> Enum.shuffle() - end) - if out, do: out - 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 - jaro = Enum.find(triggers[trigger], fn({string, idx}) -> String.jaro_distance(content, string) > 0.9 end) - - if jaro do - {string, idx} = jaro - {:error, {:jaro, string, idx}} - else - File.write!(directory() <> "/" <> trigger <> ".txt", content<>"\n", [:append]) - idx = Enum.count(triggers[trigger])+1 - {:ok, idx} - end - else - {:error, :notxt} - end - _ -> {:error, :badarg} - 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 - |> :unicode.characters_to_nfd_binary() - |> String.replace(~r/[^a-z0-9._]/, "") - |> String.trim(".") - |> String.trim("_") - - {trigger, opts} - end - - def format_line(line, prefix, msg) do - prefix = unless(prefix, do: "", else: prefix) - prefix <> line - |> String.split("\\\\") - |> Enum.map(fn(line) -> - String.split(line, "\\\\\\\\") - end) - |> List.flatten() - |> Enum.map(fn(line) -> - String.trim(line) - |> Tmpl.render(msg) - end) - end - - def directory() do - Application.get_env(:nola, :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?(msg.network, 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 |