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