Page Menu
Home
Phabricator
Search
Configure Global Search
Log In
Files
F665047
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
17 KB
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
rNOLA Nola
Event Timeline
Log In to Comment