summaryrefslogtreecommitdiff
path: root/lib/lsg_irc
diff options
context:
space:
mode:
authorhref <href@random.sh>2018-02-17 21:21:42 +0100
committerhref <href@random.sh>2018-02-17 21:21:42 +0100
commit50c6a09ff64cb081b27a0c30790b86873449d172 (patch)
treeba1bebfc7e367f169276692e0ac02709d62d6516 /lib/lsg_irc
parenttxt: fix against malicious filenames (aka 'fuck you shiv') (diff)
:)
Diffstat (limited to 'lib/lsg_irc')
-rw-r--r--lib/lsg_irc/admin_handler.ex32
-rw-r--r--lib/lsg_irc/base_handler.ex37
-rw-r--r--lib/lsg_irc/calc_handler.ex38
-rw-r--r--lib/lsg_irc/connection_handler.ex17
-rw-r--r--lib/lsg_irc/dice_handler.ex10
-rw-r--r--lib/lsg_irc/handler.ex24
-rw-r--r--lib/lsg_irc/kick_roulette_handler.ex29
-rw-r--r--lib/lsg_irc/last_fm_handler.ex15
-rw-r--r--lib/lsg_irc/np_handler.ex4
-rw-r--r--lib/lsg_irc/txt_handler.ex (renamed from lib/lsg_irc/text_trigger_handler.ex)175
-rw-r--r--lib/lsg_irc/user_track.ex150
-rw-r--r--lib/lsg_irc/user_track_handler.ex93
-rw-r--r--lib/lsg_irc/youtube_handler.ex4
13 files changed, 562 insertions, 66 deletions
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 `<expression>`**: évalue l'expression mathématique `<expression>`.
+ """
+
+ 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 <username>
- links the nick who use the command to <username> last.fm account.
- -lastfm
- unlinks the nick's previously set last.fm username.
+ * **!lastfm `[nick|username]`**
+ * **!lastfmall**
+ * **+lastfm `<username last.fm>`, -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/txt_handler.ex
index e8331f5..f946092 100644
--- a/lib/lsg_irc/text_trigger_handler.ex
+++ b/lib/lsg_irc/txt_handler.ex
@@ -1,34 +1,38 @@
defmodule LSG.IRC.TxtHandler do
+ alias LSG.IRC.UserTrack
+
@moduledoc """
# [txt](/irc/txt)
- !txt
- statistics, file list
- +txt <file>
- create new <file>
-
- !FILE
- read a random line from the file
- !FILE <index>
- read line #<index> from the file
- !FILE <query>
- return a phrase from file who matches <query>
- +FILE <text>
- add <text> in FILE
- -FILE <index>
- remove line in FILE at index <index>
+ * **!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: %{}
+ defstruct client: nil, triggers: %{}, rw: true, locks: nil
def init([client]) do
- state = %__MODULE__{client: client}
+ 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
@@ -37,16 +41,76 @@ defmodule LSG.IRC.TxtHandler 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}) ->
- "#{key}: #{to_string(Enum.count(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"
- (detail<>total)
+
+ ro = if !state.rw, do: " (lecture seule activée)", else: ""
+
+ (detail<>total<>ro)
|> String.codepoints
|> Enum.chunk_every(440)
|> Enum.map(&Enum.join/1)
@@ -63,29 +127,37 @@ defmodule LSG.IRC.TxtHandler do
{:noreply, state}
end
- def handle_info({:received, "+txt "<>trigger, _, chan}, state) do
- {trigger, _} = clean_trigger(trigger)
- if create_file(trigger) do
+ 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}
+ _ -> {: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})")
+ 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}
+ _ -> {:noreply, state}
end
end
- def handle_info({:received, "-"<>trigger_and_id, _, chan}, state) do
+ 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)
@@ -102,18 +174,19 @@ defmodule LSG.IRC.TxtHandler do
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")
+ Path.wildcard(directory() <> "/*.txt")
|> Enum.reduce(%{}, fn(path, m) ->
file = Path.basename(path)
[key, "txt"] = String.split(file, ".", parts: 2)
- data = dir <> file
+ data = directory() <> file
|> File.read!
|> String.split("\n")
|> Enum.reject(fn(line) ->
@@ -135,7 +208,7 @@ defmodule LSG.IRC.TxtHandler do
|> Enum.sort_by(fn({_, idx}) -> idx end)
|> Enum.map(fn({text, _}) -> text end)
|> Enum.join("\n")
- File.write!(env()[:directory] <> "/" <> trigger <> ".txt", data<>"\n", [])
+ File.write!(directory() <> "/" <> trigger <> ".txt", data<>"\n", [])
end
defp get_random(triggers, trigger, []) do
@@ -175,8 +248,8 @@ defmodule LSG.IRC.TxtHandler do
end
defp create_file(name) do
- File.touch!(env()[:directory] <> "/" <> name <> ".txt")
- true
+ File.touch!(directory() <> "/" <> name <> ".txt")
+ :ok
end
defp add(triggers, trigger_and_content) do
@@ -184,10 +257,13 @@ defmodule LSG.IRC.TxtHandler 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
+ File.write!(directory() <> "/" <> trigger <> ".txt", content<>"\n", [:append])
+ idx = Enum.count(triggers[trigger])+1
+ {:ok, idx}
+ else
+ :error
end
- _ -> false
+ _ -> :error
end
end
@@ -214,6 +290,29 @@ defmodule LSG.IRC.TxtHandler do
{trigger, opts}
end
- defp env(), do: Application.get_env(:lsg, __MODULE__)
+ 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 <recherche>
- !yt <recherche>
- cherche sur youtube (seulement le premier résultat).
+ * **!yt `<recherche>`**, !youtube `<recherche>`: retourne le premier résultat de la `<recherche>` YouTube
"""
defstruct client: nil, dets: nil