defmodule LSG.IRC.TxtHandler 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).
* **!`FICHIER`**: lis aléatoirement une ligne du fichier `FICHIER`.
* **!`FICHIER` `<chiffre>`**: lis la ligne `<chiffre>` du fichier `FICHIER`.
* **!`FILE` `<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`.
* **+txtro, +txtrw**. op seulement. active/désactive le mode lecture seule.
* **+txtlock `<fichier>`, -txtlock `<fichier>`**. op seulement. active/désactive le verrouillage d'un fichier.
"""
def short_irc_doc, do: "!txt https://sys.115ans.net/irc/txt "
def irc_doc, do: @moduledoc
def start_link(client) do
GenServer.start_link(__MODULE__, [client])
end
defstruct client: nil, triggers: %{}, rw: true, locks: nil, markov: nil
def init([client]) do
dets_locks_filename = (LSG.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist
{:ok, locks} = :dets.open_file(dets_locks_filename, [])
{:ok, markov} = ExChain.MarkovModel.start_link
state = %__MODULE__{client: client, locks: locks, markov: markov}
ExIRC.Client.add_handler(client, self())
{:ok, %__MODULE__{state | triggers: load()}}
end
def handle_info({:received, "!reload", _, chan}, state) do
{:noreply, %__MODULE__{state | triggers: load()}}
end
## -- ADMIN RO/RW
def handle_info({:received, "+txtrw", %ExIRC.SenderInfo{nick: nick}, chan}, state = %__MODULE__{rw: false}) do
if UserTrack.operator?(chan, nick) do
say(state, chan, "txt: écriture réactivée")
{:noreply, %__MODULE__{state | rw: true}}
else
{:noreply, state}
end
end
def handle_info({:received, "+txtro", %ExIRC.SenderInfo{nick: nick}, chan}, state = %__MODULE__{rw: true}) do
if UserTrack.operator?(chan, nick) do
say(state, chan, "txt: écriture désactivée")
{:noreply, %__MODULE__{state | rw: false}}
else
{:noreply, state}
end
end
def handle_info({:received, "+txtro", _, _}, state) do
{:noreply, state}
end
def handle_info({:received, "+txtrw", _, _}, state) do
{:noreply, state}
end
## -- ADMIN LOCKS
def handle_info({:received, "+txtlock " <> trigger, %ExIRC.SenderInfo{nick: nick}, chan}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- UserTrack.operator?(chan, nick)
do
:dets.insert(state.locks, {trigger})
say(state, chan, "txt: #{trigger} verrouillé")
end
{:noreply, state}
end
def handle_info({:received, "-txtlock " <> trigger, %ExIRC.SenderInfo{nick: nick}, chan}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- UserTrack.operator?(chan, nick),
true <- :dets.member(state.locks, trigger)
do
:dets.delete(state.locks, trigger)
say(state, chan, "txt: #{trigger} déverrouillé")
end
{:noreply, state}
end
# -- ADD
def handle_info({:received, "!txt", _, chan}, 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)
|> String.codepoints
|> Enum.chunk_every(440)
|> Enum.map(&Enum.join/1)
|> Enum.map(fn(line) -> ExIRC.Client.msg(state.client, :privmsg, chan, line) end)
{:noreply, state}
end
def handle_info({:received, "~txt", _, chan}, state) do
case ExChain.SentenceGenerator.create_filtered_sentence(state.markov) do
{:ok, line, _, _} ->
ExIRC.Client.msg(state.client, :privmsg, chan, line)
error ->
Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
def handle_info({:received, "~txt "<>complete, _, chan}, state) do
case ExChain.SentenceGenerator.complete_sentence(state.markov, complete) do
{line, _} ->
ExIRC.Client.msg(state.client, :privmsg, chan, line)
error ->
Logger.error "Txt Markov error: "<>inspect error
end
{:noreply, state}
end
def handle_info({:received, "!"<>trigger, _, chan}, state) do
{trigger, opts} = clean_trigger(trigger)
line = get_random(state.triggers, trigger, opts)
if line do
ExIRC.Client.msg(state.client, :privmsg, chan, line)
end
{:noreply, state}
end
def handle_info({:received, "+txt "<>trigger, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
with \
{trigger, _} <- clean_trigger(trigger),
true <- can_write?(state, chan, sender, trigger),
:ok <- create_file(trigger)
do
ExIRC.Client.msg(state.client, :privmsg, chan, "#{trigger}.txt créé. Ajouter: `+#{trigger} …` ; Lire: `!#{trigger}`")
{:noreply, %__MODULE__{state | triggers: load()}}
else
_ -> {:noreply, state}
end
end
def handle_info({:received, "+"<>trigger_and_content, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
with \
{trigger, _} <- clean_trigger(trigger_and_content),
true <- can_write?(state, chan, sender, trigger),
{:ok, idx} <- add(state.triggers, trigger_and_content)
do
ExIRC.Client.msg(state.client, :privmsg, chan, "#{nick}: ajouté à #{trigger}. (#{idx})")
{:noreply, %__MODULE__{state | triggers: load()}}
else
_ -> {:noreply, state}
end
end
def handle_info({:received, "-"<>trigger_and_id, sender=%ExIRC.SenderInfo{nick: nick}, chan}, state) do
with \
[trigger, id] <- String.split(trigger_and_id, " ", parts: 2),
{trigger, _} = clean_trigger(trigger),
true <- can_write?(state, chan, sender, 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)
ExIRC.Client.msg(state.client, :privmsg, chan, "#{trigger}.txt##{id} supprimée: #{text}")
dump(trigger, data)
{:noreply, %__MODULE__{state | triggers: load()}}
else
error ->
IO.inspect("error " <> inspect(error))
{:noreply, state}
end
end
def handle_info(:reload_markov, state=%__MODULE__{triggers: triggers, markov: markov}) do
all_data = triggers
|> Enum.map(fn({_, data}) ->
for {line, _idx} <- data, do: line
end)
|> List.flatten
for l <- all_data, do: IO.puts(l)
populate = ExChain.MarkovModel.populate_model(markov, all_data)
IO.puts "populated markov: #{inspect(populate)}"
{:noreply, state}
end
def handle_info(msg, state) do
{: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
defp directory() do
Application.get_env(:lsg, :data_path) <> "/irc.txt/"
end
defp can_write?(state = %__MODULE__{rw: rw?, locks: locks}, channel, 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é"
say(state, channel, "#{sender.nick}: permission refusée (#{reason})")
end
can?
end
defp say(%__MODULE__{client: client}, chan, message) do
ExIRC.Client.msg(client, :privmsg, chan, message)
end
end