From 50c6a09ff64cb081b27a0c30790b86873449d172 Mon Sep 17 00:00:00 2001 From: href Date: Sat, 17 Feb 2018 21:21:42 +0100 Subject: :) --- lib/lsg_irc/admin_handler.ex | 32 ++++ lib/lsg_irc/base_handler.ex | 37 ++++ lib/lsg_irc/calc_handler.ex | 38 +++++ lib/lsg_irc/connection_handler.ex | 17 +- lib/lsg_irc/dice_handler.ex | 10 +- lib/lsg_irc/handler.ex | 24 +++ lib/lsg_irc/kick_roulette_handler.ex | 29 ++++ lib/lsg_irc/last_fm_handler.ex | 15 +- lib/lsg_irc/np_handler.ex | 4 +- lib/lsg_irc/text_trigger_handler.ex | 219 ------------------------ lib/lsg_irc/txt_handler.ex | 318 +++++++++++++++++++++++++++++++++++ lib/lsg_irc/user_track.ex | 150 +++++++++++++++++ lib/lsg_irc/user_track_handler.ex | 93 ++++++++++ lib/lsg_irc/youtube_handler.ex | 4 +- 14 files changed, 743 insertions(+), 247 deletions(-) create mode 100644 lib/lsg_irc/admin_handler.ex create mode 100644 lib/lsg_irc/base_handler.ex create mode 100644 lib/lsg_irc/calc_handler.ex create mode 100644 lib/lsg_irc/handler.ex create mode 100644 lib/lsg_irc/kick_roulette_handler.ex delete mode 100644 lib/lsg_irc/text_trigger_handler.ex create mode 100644 lib/lsg_irc/txt_handler.ex create mode 100644 lib/lsg_irc/user_track.ex create mode 100644 lib/lsg_irc/user_track_handler.ex (limited to 'lib/lsg_irc') diff --git a/lib/lsg_irc/admin_handler.ex b/lib/lsg_irc/admin_handler.ex new file mode 100644 index 0000000..27d35c3 --- /dev/null +++ b/lib/lsg_irc/admin_handler.ex @@ -0,0 +1,32 @@ +defmodule LSG.IRC.AdminHandler do + @moduledoc """ + # admin + + !op + op; requiert admin + """ + + def irc_doc, do: nil + + def start_link(client) do + GenServer.start_link(__MODULE__, [client]) + end + + def init([client]) do + ExIRC.Client.add_handler client, self + {:ok, client} + end + + def handle_info({:received, "!op", sender, chan}, client) do + if LSG.IRC.admin?(sender) do + ExIRC.Client.mode(client, chan, "+o", sender.nick) + end + {:noreply, client} + end + + def handle_info(msg, client) do + IO.inspect(msg) + {:noreply, client} + end + +end diff --git a/lib/lsg_irc/base_handler.ex b/lib/lsg_irc/base_handler.ex new file mode 100644 index 0000000..9145936 --- /dev/null +++ b/lib/lsg_irc/base_handler.ex @@ -0,0 +1,37 @@ +defmodule LSG.IRC.BaseHandler do + + use LSG.IRC.Handler + + def irc_doc, do: nil + + def start_link(client) do + GenServer.start_link(__MODULE__, [client]) + end + + def init([client]) do + ExIRC.Client.add_handler client, self + {:ok, client} + end + + def handle_info({:received, "!help", %ExIRC.SenderInfo{nick: nick}, chan}, client) do + url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index) + ExIRC.Client.msg(client, :privmsg, chan, "#{nick}: #{url}") + {:noreply, client} + end + + def handle_info({:received, "version", %ExIRC.SenderInfo{nick: nick}}, client) do + {:ok, vsn} = :application.get_key(:lsg, :vsn) + ver = List.to_string(vsn) + url = LSGWeb.Router.Helpers.irc_url(LSGWeb.Endpoint, :index) + version = "v#{ver} ; #{url} ; source: https://git.yt/115ans/sys" + ExIRC.Client.msg(client, :privmsg, nick, version) + {:noreply, client} + end + + def handle_info(msg, client) do + IO.inspect(msg) + {:noreply, client} + end + +end + diff --git a/lib/lsg_irc/calc_handler.ex b/lib/lsg_irc/calc_handler.ex new file mode 100644 index 0000000..0f74ac9 --- /dev/null +++ b/lib/lsg_irc/calc_handler.ex @@ -0,0 +1,38 @@ +defmodule LSG.IRC.CalcHandler do + @moduledoc """ + # calc + + * **!calc ``**: évalue l'expression mathématique ``. + """ + + def irc_doc, do: @moduledoc + + def start_link(client) do + GenServer.start_link(__MODULE__, [client]) + end + + def init([client]) do + ExIRC.Client.add_handler client, self + {:ok, client} + end + + def handle_info({:received, "!calc "<>expr, %ExIRC.SenderInfo{nick: nick}, chan}, client) do + IO.inspect "HAZ CALC " <>inspect(expr) + result = try do + case Abacus.eval(expr) do + {:ok, result} -> result + error -> inspect(error) + end + rescue + error -> "#{error.message}" + end + ExIRC.Client.msg(client, :privmsg, chan, "#{nick}: #{expr} = #{result}") + {:noreply, client} + end + + def handle_info(msg, client) do + {:noreply, client} + end + +end + diff --git a/lib/lsg_irc/connection_handler.ex b/lib/lsg_irc/connection_handler.ex index 337fe00..8d07e58 100644 --- a/lib/lsg_irc/connection_handler.ex +++ b/lib/lsg_irc/connection_handler.ex @@ -1,19 +1,20 @@ defmodule LSG.IRC.ConnectionHandler do defmodule State do - defstruct host: "irc.quakenet.org", - port: 6667, - pass: "", - nick: "`115ans", - user: "115ans", - name: "https://sys.115ans.net/irc", - client: nil + defstruct [:host, :port, :pass, :nick, :name, :user, :client] end def start_link(client) do - GenServer.start_link(__MODULE__, [%State{client: client}]) + irc = Application.get_env(:lsg, :irc)[:irc] + host = irc[:host] + port = irc[:port] + nick = irc[:nick] + user = irc[:user] + name = irc[:name] + GenServer.start_link(__MODULE__, [%State{client: client, host: host, port: port, nick: nick, user: user, name: name}]) end def init([state]) do + IO.puts inspect(state) ExIRC.Client.add_handler state.client, self ExIRC.Client.connect! state.client, state.host, state.port {:ok, state} diff --git a/lib/lsg_irc/dice_handler.ex b/lib/lsg_irc/dice_handler.ex index b865100..b07b59b 100644 --- a/lib/lsg_irc/dice_handler.ex +++ b/lib/lsg_irc/dice_handler.ex @@ -4,14 +4,14 @@ defmodule LSG.IRC.DiceHandler do @moduledoc """ # dice - !dice [6 | faces] [1 | rolls] - roll X times a dice of X faces. + * **!dice `[1 | lancés]` `[6 | faces]`**: lance une ou plusieurs fois un dé de 6 ou autre faces """ @default_faces 6 @default_rolls 1 @max_rolls 50 + def short_irc_doc, do: "!dice (jeter un dé)" defstruct client: nil, dets: nil def irc_doc, do: @moduledoc @@ -31,9 +31,9 @@ defmodule LSG.IRC.DiceHandler do end def handle_info({:received, "!dice "<>params, sender, chan}, state) do - {faces, rolls} = case String.split(params, " ", parts: 2) do - [faces, rolls] -> {faces, rolls} - [faces] -> {faces, "1"} + {rolls, faces} = case String.split(params, " ", parts: 2) do + [faces, rolls] -> {rolls, faces} + [rolls] -> {rolls, @default_faces} end to_integer = fn(string, default) -> diff --git a/lib/lsg_irc/handler.ex b/lib/lsg_irc/handler.ex new file mode 100644 index 0000000..19c0945 --- /dev/null +++ b/lib/lsg_irc/handler.ex @@ -0,0 +1,24 @@ +defmodule LSG.IRC.Handler do + defmacro __using__(_) do + quote do + alias LSG.IRC + alias LSG.IRC.UserTrack + alias ExIRC.Client + require LSG.IRC.Handler + import LSG.IRC.Handler + end + end + + def privmsg(client, {nil, %ExIRC.SenderInfo{nick: nick}}, message) do + privmsg(client, nick, message) + end + + def privmsg(client, {channel, _}, message) do + privmsg(client, channel, message) + end + + def privmsg(client, target, message) when is_binary(target) do + ExIRC.Client.msg(client, :privmsg, target, message) + end + +end diff --git a/lib/lsg_irc/kick_roulette_handler.ex b/lib/lsg_irc/kick_roulette_handler.ex new file mode 100644 index 0000000..7bfa90b --- /dev/null +++ b/lib/lsg_irc/kick_roulette_handler.ex @@ -0,0 +1,29 @@ +defmodule LSG.IRC.KickRouletteHandler do + @moduledoc """ + # kick roulette + + * **!kick** (à peu près une chance sur 5) + """ + + def irc_doc, do: @moduledoc + def start_link(client) do + GenServer.start_link(__MODULE__, [client]) + end + + def init([client]) do + ExIRC.Client.add_handler client, self + {:ok, client} + end + + def handle_info({:received, "!kick", sender, chan}, client) do + if 5 == :crypto.rand_uniform(1, 6) do + ExIRC.Client.kick(client, chan, sender.nick, "perdu") + end + {:noreply, client} + end + + def handle_info(msg, client) do + {:noreply, client} + end + +end diff --git a/lib/lsg_irc/last_fm_handler.ex b/lib/lsg_irc/last_fm_handler.ex index ad6dae9..6ecdab9 100644 --- a/lib/lsg_irc/last_fm_handler.ex +++ b/lib/lsg_irc/last_fm_handler.ex @@ -4,14 +4,9 @@ defmodule LSG.IRC.LastFmHandler do @moduledoc """ # last.fm - !lastfm [nick|username] - say what nick/specified username is listening on lastfm; if last.fm username is known (via +lastfm). - !lastfmall - say what known users (+lastfm) are listening - +lastfm - links the nick who use the command to last.fm account. - -lastfm - unlinks the nick's previously set last.fm username. + * **!lastfm `[nick|username]`** + * **!lastfmall** + * **+lastfm ``, -lastfm** """ defstruct client: nil, dets: nil @@ -24,7 +19,7 @@ defmodule LSG.IRC.LastFmHandler do def init([client]) do ExIRC.Client.add_handler(client, self()) - dets_filename = Application.get_env(:lsg, __MODULE__)[:dets_path] + dets_filename = (LSG.data_path() <> "/" <> "lastfm.dets") |> String.to_charlist {:ok, dets} = :dets.open_file(dets_filename, []) {:ok, %__MODULE__{client: client, dets: dets}} end @@ -128,7 +123,7 @@ defmodule LSG.IRC.LastFmHandler do end defp lookup_nick(username, state) do - case :dets.match(state.dets, {:'$1', username}) do + case :dets.match(state.dets, {:'$1', String.downcase(username)}) do [[match]] -> match [[match] | _many] -> match _ -> username diff --git a/lib/lsg_irc/np_handler.ex b/lib/lsg_irc/np_handler.ex index b198cbc..5f724d4 100644 --- a/lib/lsg_irc/np_handler.ex +++ b/lib/lsg_irc/np_handler.ex @@ -2,10 +2,10 @@ defmodule LSG.IRC.NpHandler do @moduledoc """ # np - !np - now playing on 115ans.net + * **!np** chanson/émission actuellement sur le stream de 115ans.net """ + def short_irc_doc, do: "!np (en ce moment sur 115ans)" def irc_doc, do: @moduledoc def start_link(client) do GenServer.start_link(__MODULE__, [client]) diff --git a/lib/lsg_irc/text_trigger_handler.ex b/lib/lsg_irc/text_trigger_handler.ex deleted file mode 100644 index e8331f5..0000000 --- a/lib/lsg_irc/text_trigger_handler.ex +++ /dev/null @@ -1,219 +0,0 @@ -defmodule LSG.IRC.TxtHandler do - @moduledoc """ - # [txt](/irc/txt) - - !txt - statistics, file list - +txt - create new - - !FILE - read a random line from the file - !FILE - read line # from the file - !FILE - return a phrase from file who matches - +FILE - add in FILE - -FILE - remove line in FILE at index - """ - - def irc_doc, do: @moduledoc - - def start_link(client) do - GenServer.start_link(__MODULE__, [client]) - end - - defstruct client: nil, triggers: %{} - - def init([client]) do - state = %__MODULE__{client: client} - 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 - - def handle_info({:received, "!txt", _, chan}, state) do - map = Enum.map(state.triggers, fn({key, data}) -> - "#{key}: #{to_string(Enum.count(data))}" - 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" - (detail<>total) - |> 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, "!"<>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, _, chan}, state) do - {trigger, _} = clean_trigger(trigger) - if 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, _, chan}, state) do - if idx = add(state.triggers, trigger_and_content) do - ExIRC.Client.msg(state.client, :privmsg, chan, "ajouté. (#{idx})") - {:noreply, %__MODULE__{state | triggers: load()}} - else - {:noreply, state} - end - end - - def handle_info({:received, "-"<>trigger_and_id, _, chan}, state) do - with \ - [trigger, id] <- String.split(trigger_and_id, " ", parts: 2), - {trigger, _} = clean_trigger(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(msg, state) do - {:noreply, state} - end - - # Load/Reloads text files from disk - defp load() do - dir = env()[:directory] - Path.wildcard(dir <> "/*.txt") - |> Enum.reduce(%{}, fn(path, m) -> - file = Path.basename(path) - [key, "txt"] = String.split(file, ".", parts: 2) - data = dir <> 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) - 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!(env()[: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!(env()[:directory] <> "/" <> name <> ".txt") - true - 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!(env()[:directory] <> "/" <> trigger <> ".txt", content<>"\n", [:append]) - Enum.count(triggers[trigger])+1 - end - _ -> false - 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 env(), do: Application.get_env(:lsg, __MODULE__) - -end diff --git a/lib/lsg_irc/txt_handler.ex b/lib/lsg_irc/txt_handler.ex new file mode 100644 index 0000000..f946092 --- /dev/null +++ b/lib/lsg_irc/txt_handler.ex @@ -0,0 +1,318 @@ +defmodule LSG.IRC.TxtHandler do + alias LSG.IRC.UserTrack + + @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` ``**: lis la ligne `` du fichier `FICHIER`. + * **!`FILE` ``**: recherche une ligne contenant `` dans `FICHIER`. + + * **+txt `**: crée le fichier ``. + * **+`FICHIER` ``**: ajoute une ligne `` dans le fichier `FICHIER`. + * **-`FICHIER` ``**: supprime la ligne `` du fichier `FICHIER`. + + * **+txtro, +txtrw**. op seulement. active/désactive le mode lecture seule. + * **+txtlock ``, -txtlock ``**. 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 + + def init([client]) do + dets_locks_filename = (LSG.data_path() <> "/" <> "txtlocks.dets") |> String.to_charlist + {:ok, locks} = :dets.open_file(dets_locks_filename, []) + state = %__MODULE__{client: client, locks: locks} + 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, "!"<>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(msg, state) do + {:noreply, state} + end + + # Load/Reloads text files from disk + defp load() do + 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) + 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? = LSG.IRC.admin?(sender) + operator? = LSG.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 diff --git a/lib/lsg_irc/user_track.ex b/lib/lsg_irc/user_track.ex new file mode 100644 index 0000000..b67b9f6 --- /dev/null +++ b/lib/lsg_irc/user_track.ex @@ -0,0 +1,150 @@ +defmodule LSG.IRC.UserTrack do + @moduledoc """ + User Track DB & Utilities + """ + + @ets LSG.IRC.UserTrack.Storage + # {uuid, nick, nicks, privilege_map} + # Privilege map: + # %{"#channel" => [:operator, :voice] + defmodule Storage do + + def delete(id) do + op(fn(ets) -> :ets.delete(ets, id) end) + end + + def insert(tuple) do + op(fn(ets) -> :ets.insert(ets, tuple) end) + end + + def op(fun) do + GenServer.call(__MODULE__, {:op, fun}) + end + + def start_link do + GenServer.start_link(__MODULE__, [], [name: __MODULE__]) + end + + def init([]) do + ets = :ets.new(__MODULE__, [:set, :named_table, :protected, {:read_concurrency, true}]) + {:ok, ets} + end + + def handle_call({:op, fun}, _from, ets) do + returned = try do + {:ok, fun.(ets)} + rescue + rescued -> {:error, rescued} + catch + rescued -> {:error, rescued} + end + {:reply, returned, ets} + end + end + + defmodule Id, do: use EntropyString + + defmodule User do + defstruct [:id, :nick, :nicks, :username, :host, :realname, :privileges] + + def to_tuple(u = %__MODULE__{}) do + {u.id || LSG.IRC.UserTrack.Id.large_id, u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges} + end + + def from_tuple({id, nick, nicks, username, host, realname, privs}) do + %__MODULE__{id: id, nick: nick, nicks: nicks, username: username, realname: realname, privileges: privs} + end + end + + def find_by_nick(nick) do + case :ets.match(@ets, {:'$1', nick, :_, :_, :_, :_, :_}) do + [[id]] -> lookup(id) + _ -> nil + end + end + + def to_list, do: :ets.tab2list(@ets) + + def lookup(id) do + case :ets.lookup(@ets, id) do + [] -> nil + [tuple] -> User.from_tuple(tuple) + end + end + + def operator?(channel, nick) do + if user = find_by_nick(nick) do + privs = Map.get(user.privileges, channel, []) + Enum.member?(privs, :admin) || Enum.member?(privs, :operator) + else + false + end + end + + def joined(c, s), do: joined(c,s,[]) + + def joined(channel, sender=%{nick: nick, user: uname, host: host}, privileges) do + privileges = if LSG.IRC.admin?(sender) do + privileges ++ [:admin] + else privileges end + user = if user = find_by_nick(nick) do + %User{user | username: uname, host: host, privileges: Map.put(user.privileges || %{}, channel, privileges)} + else + %User{nick: nick, username: uname, host: host, privileges: %{channel => privileges}} + end + + Storage.op(fn(ets) -> + :ets.insert(ets, User.to_tuple(user)) + end) + end + + def joined(channel, nick, privileges) do + user = if user = find_by_nick(nick) do + %User{user | privileges: Map.put(user.privileges, channel, privileges)} + else + %User{nick: nick, privileges: %{channel => privileges}} + end + + Storage.op(fn(ets) -> + :ets.insert(ets, User.to_tuple(user)) + end) + end + + def renamed(old_nick, new_nick) do + if user = find_by_nick(old_nick) do + user = %User{user | nick: new_nick, nicks: [old_nick|user.nicks]} + Storage.insert(User.to_tuple(user)) + end + end + + def change_privileges(channel, nick, {add, remove}) do + if user = find_by_nick(nick) do + privs = Map.get(user.privileges, channel) + + privs = Enum.reduce(add, privs, fn(priv, acc) -> [priv|acc] end) + privs = Enum.reduce(remove, privs, fn(priv, acc) -> List.delete(acc, priv) end) + + user = %User{user | privileges: Map.put(user.privileges, channel, privs)} + Storage.insert(User.to_tuple(user)) + end + end + + def parted(channel, nick) do + if user = find_by_nick(nick) do + privs = Map.delete(user.privileges, channel) + if Enum.count(privs) > 0 do + user = %User{user | privileges: privs} + Storage.insert(User.to_tuple(user)) + else + Storage.delete(user.id) + end + end + end + + def quitted(sender) do + if user = find_by_nick(sender.nick) do + Storage.delete(user.id) + end + end + +end diff --git a/lib/lsg_irc/user_track_handler.ex b/lib/lsg_irc/user_track_handler.ex new file mode 100644 index 0000000..d167af5 --- /dev/null +++ b/lib/lsg_irc/user_track_handler.ex @@ -0,0 +1,93 @@ +defmodule LSG.IRC.UserTrackHandler do + @moduledoc """ + # User Track Handler + + This handlers keeps track of users presence and privileges. + + Planned API: + + UserTrackHandler.operator?(%ExIRC.Sender{nick: "href", …}, "#channel") :: boolean + + """ + + def irc_doc, do: nil + + def start_link(client) do + GenServer.start_link(__MODULE__, [client]) + end + + defstruct client: nil, ets: nil + + def init([client]) do + ExIRC.Client.add_handler client, self + {:ok, %__MODULE__{client: client}} + end + + def handle_info({:joined, channel}, state) do + ExIRC.Client.who(state.client, channel) + {:noreply, state} + end + + def handle_info({:who, channel, whos}, state) do + Enum.map(whos, fn(who = %ExIRC.Who{nick: nick, operator?: operator}) -> + priv = if operator, do: [:operator], else: [] + LSG.IRC.UserTrack.joined(channel, who, priv) + end) + {:noreply, state} + end + + def handle_info({:quit, _reason, sender}, state) do + LSG.IRC.UserTrack.quitted(sender) + {:noreply, state} + end + + def handle_info({:joined, channel, sender}, state) do + LSG.IRC.UserTrack.joined(channel, sender, []) + {:noreply, state} + end + + def handle_info({:kicked, nick, _by, channel, _reason}, state) do + parted(channel, nick) + {:noreply, state} + end + + def handle_info({:parted, channel, %ExIRC.SenderInfo{nick: nick}}, state) do + parted(channel, nick) + {:noreply, state} + end + + def handle_info({:mode, [channel, mode, nick]}, state) do + mode(channel, nick, mode) + {:noreply, state} + end + + def handle_info({:nick_changed, old_nick, new_nick}, state) do + rename(old_nick, new_nick) + {:noreply, state} + end + + def handle_info(msg, state) do + {:noreply, state} + end + + defp parted(channel, nick) do + LSG.IRC.UserTrack.parted(channel, nick) + :ok + end + + defp mode(channel, nick, "+o") do + LSG.IRC.UserTrack.change_privileges(channel, nick, {[:operator], []}) + :ok + end + + defp mode(channel, nick, "-o") do + LSG.IRC.UserTrack.change_privileges(channel, nick, {[], [:operator]}) + :ok + end + + defp rename(old, new) do + LSG.IRC.UserTrack.renamed(old, new) + :ok + end + +end diff --git a/lib/lsg_irc/youtube_handler.ex b/lib/lsg_irc/youtube_handler.ex index 769f220..e7eadfc 100644 --- a/lib/lsg_irc/youtube_handler.ex +++ b/lib/lsg_irc/youtube_handler.ex @@ -4,9 +4,7 @@ defmodule LSG.IRC.YouTubeHandler do @moduledoc """ # youtube - !youtube - !yt - cherche sur youtube (seulement le premier résultat). + * **!yt ``**, !youtube ``: retourne le premier résultat de la `` YouTube """ defstruct client: nil, dets: nil -- cgit v1.2.3