summaryrefslogtreecommitdiff
path: root/lib/nola_plugins/txt.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/nola_plugins/txt.ex')
-rw-r--r--lib/nola_plugins/txt.ex556
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