Page MenuHomePhabricator

No OneTemporary

diff --git a/lib/plugins/txt.ex b/lib/plugins/txt.ex
index b06e5ff..c582e67 100644
--- a/lib/plugins/txt.ex
+++ b/lib/plugins/txt.ex
@@ -1,658 +1,642 @@
defmodule Nola.Plugins.Txt do
alias Nola.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
+ Nola.Plugins.Txt.MarkovPyMarkovify
)
{:ok, markov} = markov_handler.start_link()
{:ok, _} = Registry.register(Nola.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, ", ")
-
link =
NolaWeb.Router.Helpers.irc_url(
NolaWeb.Endpoint,
:txt,
- m.network,
- NolaWeb.format_chan(m.channel)
+ msg.network,
+ NolaWeb.format_chan(msg.channel)
)
total = "#{Enum.count(state.triggers)} fichiers, #{to_string(total)} lignes: #{link}"
ro = if !state.rw, do: " (lecture seule activée)", else: ""
- (detail <> total <> ro)
+ (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 + 1}}
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? = Nola.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? = Nola.Irc.admin?(sender)
operator? = Nola.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

File Metadata

Mime Type
text/x-diff
Expires
Fri, Feb 27, 7:20 AM (17 h, 18 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
87144
Default Alt Text
(17 KB)

Event Timeline