From a47dc245808921309f58e8b1c4b6fa028b2df073 Mon Sep 17 00:00:00 2001 From: href Date: Wed, 2 May 2018 19:03:35 +0200 Subject: meh --- lib/irc.ex | 34 +++++++ lib/irc/connection_handler.ex | 36 +++++++ lib/irc/login_handler.ex | 25 +++++ lib/irc/pubsub_handler.ex | 123 ++++++++++++++++++++++++ lib/irc/user_track.ex | 154 ++++++++++++++++++++++++++++++ lib/irc/user_track_handler.ex | 93 ++++++++++++++++++ lib/lsg/application.ex | 2 +- lib/lsg_irc.ex | 24 ++--- lib/lsg_irc/admin_handler.ex | 15 ++- lib/lsg_irc/base_handler.ex | 2 - lib/lsg_irc/calc_handler.ex | 38 -------- lib/lsg_irc/calc_plugin.ex | 38 ++++++++ lib/lsg_irc/connection_handler.ex | 37 ------- lib/lsg_irc/dice_handler.ex | 1 + lib/lsg_irc/handler.ex | 24 ----- lib/lsg_irc/kick_roulette_handler.ex | 2 +- lib/lsg_irc/last_fm_handler.ex | 8 ++ lib/lsg_irc/login_handler.ex | 25 ----- lib/lsg_irc/quatre_cent_vingt_plugin.ex | 105 ++++++++++++++++++++ lib/lsg_irc/txt_handler.ex | 14 ++- lib/lsg_irc/user_track.ex | 150 ----------------------------- lib/lsg_irc/user_track_handler.ex | 93 ------------------ lib/lsg_irc/wikipedia_plugin.ex | 90 +++++++++++++++++ lib/lsg_irc/youtube_handler.ex | 2 +- lib/lsg_web/controllers/irc_controller.ex | 2 +- 25 files changed, 744 insertions(+), 393 deletions(-) create mode 100644 lib/irc.ex create mode 100644 lib/irc/connection_handler.ex create mode 100644 lib/irc/login_handler.ex create mode 100644 lib/irc/pubsub_handler.ex create mode 100644 lib/irc/user_track.ex create mode 100644 lib/irc/user_track_handler.ex delete mode 100644 lib/lsg_irc/calc_handler.ex create mode 100644 lib/lsg_irc/calc_plugin.ex delete mode 100644 lib/lsg_irc/connection_handler.ex delete mode 100644 lib/lsg_irc/handler.ex delete mode 100644 lib/lsg_irc/login_handler.ex create mode 100644 lib/lsg_irc/quatre_cent_vingt_plugin.ex delete mode 100644 lib/lsg_irc/user_track.ex delete mode 100644 lib/lsg_irc/user_track_handler.ex create mode 100644 lib/lsg_irc/wikipedia_plugin.ex (limited to 'lib') diff --git a/lib/irc.ex b/lib/irc.ex new file mode 100644 index 0000000..6ba819c --- /dev/null +++ b/lib/irc.ex @@ -0,0 +1,34 @@ +defmodule IRC do + + defmodule Message do + defstruct [:text, + :sender, + :channel, + :trigger, + :replyfun] + end + defmodule Trigger do + defstruct [:type, :trigger, :args] + end + + def register(key) do + case Registry.register(IRC.PubSub, key, []) do + {:ok, _} -> :ok + error -> error + end + end + + def admin?(%Message{sender: sender}), do: admin?(sender) + + def admin?(%{nick: nick, user: user, host: host}) do + for {n, u, h} <- Application.get_env(:lsg, :irc, [])[:admins]||[] do + admin_part_match?(n, nick) && admin_part_match?(u, user) && admin_part_match?(h, host) + end + |> Enum.any? + end + + defp admin_part_match?(:_, _), do: true + defp admin_part_match?(a, a), do: true + defp admin_part_match?(_, _), do: false + +end diff --git a/lib/irc/connection_handler.ex b/lib/irc/connection_handler.ex new file mode 100644 index 0000000..1c335f2 --- /dev/null +++ b/lib/irc/connection_handler.ex @@ -0,0 +1,36 @@ +defmodule IRC.ConnectionHandler do + defmodule State do + defstruct [:host, :port, :pass, :nick, :name, :user, :client] + end + + def start_link(client) do + 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 + ExIRC.Client.add_handler state.client, self + ExIRC.Client.connect! state.client, state.host, state.port + {:ok, state} + end + + def handle_info({:connected, server, port}, state) do + debug "Connected to #{server}:#{port}" + ExIRC.Client.logon state.client, state.pass, state.nick, state.user, state.name + {:noreply, state} + end + + # Catch-all for messages you don't care about + def handle_info(msg, state) do + {:noreply, state} + end + + defp debug(msg) do + IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset() + end +end diff --git a/lib/irc/login_handler.ex b/lib/irc/login_handler.ex new file mode 100644 index 0000000..fdec852 --- /dev/null +++ b/lib/irc/login_handler.ex @@ -0,0 +1,25 @@ +defmodule IRC.LoginHandler do + def start_link(client) do + GenServer.start_link(__MODULE__, [client]) + end + + def init([client]) do + ExIRC.Client.add_handler client, self + {:ok, {client, ["#lsg", "#lsgtest"]}} + end + + def handle_info(:logged_in, state = {client, channels}) do + debug "Logged in to server" + channels |> Enum.map(&ExIRC.Client.join client, &1) + {:noreply, state} + end + + # Catch-all for messages you don't care about + def handle_info(_msg, state) do + {:noreply, state} + end + + defp debug(msg) do + IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset() + end +end diff --git a/lib/irc/pubsub_handler.ex b/lib/irc/pubsub_handler.ex new file mode 100644 index 0000000..3e78d7b --- /dev/null +++ b/lib/irc/pubsub_handler.ex @@ -0,0 +1,123 @@ +defmodule IRC.PubSubHandler do + @moduledoc """ + # IRC PubSub + + Provides a nicer abstraction over ExIRC's handlers. + + ## PubSub topics + + * `message` -- all messages (including triggers) + * `message:private` -- all messages without a channel + * `message:#CHANNEL` -- all messages within `#CHANNEL` + * `triggers` -- all triggers + * `trigger:TRIGGER` -- any message with a trigger `TRIGGER` + + ## Replying to %IRC.Message{} + + Each `IRC.Message` comes with a dedicated `replyfun`, to which you only have to pass either: + + """ + def irc_doc, do: nil + + def start_link(client) do + GenServer.start_link(__MODULE__, [client], [name: __MODULE__]) + end + + def init([client]) do + ExIRC.Client.add_handler(client, self()) + {:ok, client} + end + + @triggers %{ + "!" => :bang, + "+" => :plus, + "-" => :minus, + "?" => :query, + "." => :dot, + } + + def handle_info({:received, text, sender, chan}, client) do + reply_fun = fn(text) -> irc_reply(client, {chan, sender}, text) end + message = %IRC.Message{text: text, sender: sender, channel: chan, replyfun: reply_fun, trigger: extract_trigger(text)} + publish(message, ["message:#{chan}"]) + {:noreply, client} + end + + def handle_info({:received, text, sender}, client) do + reply_fun = fn(text) -> irc_reply(client, {sender.nick, sender}, text) end + message = %IRC.Message{text: text, sender: sender, replyfun: reply_fun, trigger: extract_trigger(text)} + publish(message, ["message:private"]) + {:noreply, client} + end + + def handle_info(unhandled, client) do + IO.puts inspect(unhandled) + {:noreply, client} + end + + defp publish(pub), do: publish(pub, []) + + defp publish(m = %IRC.Message{trigger: nil}, keys) do + dispatch(["message"] ++ keys, {:irc, :text, m}) + end + + defp publish(m = %IRC.Message{trigger: t = %IRC.Trigger{trigger: trigger}}, keys) do + dispatch(["message", "triggers", "trigger:"<>trigger]++keys, {:irc, :trigger, trigger, m}) + end + + defp dispatch(key, content) when is_binary(key), do: dispatch([key], content) + defp dispatch(keys, content) when is_list(keys) do + IO.puts "dispatching to #{inspect(keys)} --> #{inspect content}" + for key <- keys do + spawn(fn() -> Registry.dispatch(IRC.PubSub, key, fn h -> + for {pid, _} <- h, do: send(pid, content) + end) end) + end + end + + # + # Triggers + # + + + for {trigger, name} <- @triggers do + defp extract_trigger(unquote(trigger)<>text) do + text = String.strip(text) + [trigger | args] = String.split(text, " ") + %IRC.Trigger{type: unquote(name), trigger: trigger, args: args} + end + end + + defp extract_trigger(_), do: nil + + # + # IRC Replies + # + + # irc_reply(ExIRC.Client pid, {channel or nick, ExIRC.Sender}, binary | replies + # replies :: {:kick, reason} | {:kick, nick, reason} | {:mode, mode, nick} + defp irc_reply(client, {target, _}, text) when is_binary(text) do + ExIRC.Client.msg(client, :privmsg, target, text) + end + + defp irc_reply(client, {target, %{nick: nick}}, {:kick, reason}) do + ExIRC.Client.kick(client, target, nick, reason) + end + + defp irc_reply(client, {target, _}, {:kick, nick, reason}) do + ExIRC.Client.kick(client, target, nick, reason) + end + + defp irc_reply(client, {target, %{nick: nick}}, {:mode, mode}) do + ExIRC.Client.mode(client, target, mode, nick) + end + + defp irc_reply(client, target, {:mode, mode, nick}) do + ExIRC.Client.mode(client, target, mode, nick) + end + + defp irc_reply(client, target, {:channel_mode, mode}) do + ExIRC.Client.mode(client, target, mode) + end + +end diff --git a/lib/irc/user_track.ex b/lib/irc/user_track.ex new file mode 100644 index 0000000..2614d98 --- /dev/null +++ b/lib/irc/user_track.ex @@ -0,0 +1,154 @@ +defmodule IRC.UserTrack do + @moduledoc """ + User Track DB & Utilities + """ + + @ets 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 + + def terminate(_reason, ets) do + :ok + 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 || 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 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/irc/user_track_handler.ex b/lib/irc/user_track_handler.ex new file mode 100644 index 0000000..0ae802a --- /dev/null +++ b/lib/irc/user_track_handler.ex @@ -0,0 +1,93 @@ +defmodule 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: [] + IRC.UserTrack.joined(channel, who, priv) + end) + {:noreply, state} + end + + def handle_info({:quit, _reason, sender}, state) do + IRC.UserTrack.quitted(sender) + {:noreply, state} + end + + def handle_info({:joined, channel, sender}, state) do + 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 + IRC.UserTrack.parted(channel, nick) + :ok + end + + defp mode(channel, nick, "+o") do + IRC.UserTrack.change_privileges(channel, nick, {[:operator], []}) + :ok + end + + defp mode(channel, nick, "-o") do + IRC.UserTrack.change_privileges(channel, nick, {[], [:operator]}) + :ok + end + + defp rename(old, new) do + IRC.UserTrack.renamed(old, new) + :ok + end + +end diff --git a/lib/lsg/application.ex b/lib/lsg/application.ex index 23db221..cc4c120 100644 --- a/lib/lsg/application.ex +++ b/lib/lsg/application.ex @@ -12,7 +12,7 @@ defmodule LSG.Application do supervisor(LSGWeb.Endpoint, []), # Start your own worker by calling: LSG.Worker.start_link(arg1, arg2, arg3) # worker(LSG.Worker, [arg1, arg2, arg3]), - worker(Registry, [[keys: :duplicate, name: LSG.BroadcastRegistry]]), + worker(Registry, [[keys: :duplicate, name: LSG.BroadcastRegistry]], id: :registry_broadcast), worker(LSG.IcecastAgent, []), worker(LSG.Icecast, []), ] ++ LSG.IRC.application_childs diff --git a/lib/lsg_irc.ex b/lib/lsg_irc.ex index b988e04..482cb5d 100644 --- a/lib/lsg_irc.ex +++ b/lib/lsg_irc.ex @@ -4,26 +4,22 @@ defmodule LSG.IRC do {:ok, irc_client} = ExIRC.start_link! import Supervisor.Spec [ - worker(LSG.IRC.UserTrack.Storage, []), - worker(LSG.IRC.ConnectionHandler, [irc_client]), - worker(LSG.IRC.LoginHandler, [irc_client]), - worker(LSG.IRC.UserTrackHandler, [irc_client]), + worker(Registry, [[keys: :duplicate, name: IRC.PubSub]], id: :registry_irc), + worker(IRC.UserTrack.Storage, []), + worker(IRC.ConnectionHandler, [irc_client]), + worker(IRC.LoginHandler, [irc_client]), + worker(IRC.UserTrackHandler, [irc_client]), + worker(IRC.PubSubHandler, [irc_client], [name: :irc_pub_sub]), ] ++ for handler <- Application.get_env(:lsg, :irc)[:handlers] do - worker(handler, [irc_client]) + worker(handler, [irc_client], [name: handler]) end - end - - def admin?(%{nick: nick, user: user, host: host}) do - for {n, u, h} <- Application.get_env(:lsg, :irc, [])[:admins]||[] do - admin_part_match?(n, nick) && admin_part_match?(u, user) && admin_part_match?(h, host) + ++ + for plugin <- Application.get_env(:lsg, :irc)[:plugins] do + worker(plugin, [], [name: plugin]) end - |> Enum.any? end - defp admin_part_match?(:_, _), do: true - defp admin_part_match?(a, a), do: true - defp admin_part_match?(_, _), do: false end diff --git a/lib/lsg_irc/admin_handler.ex b/lib/lsg_irc/admin_handler.ex index 27d35c3..fab4dbc 100644 --- a/lib/lsg_irc/admin_handler.ex +++ b/lib/lsg_irc/admin_handler.ex @@ -14,18 +14,27 @@ defmodule LSG.IRC.AdminHandler do def init([client]) do ExIRC.Client.add_handler client, self + :ok = IRC.register("op") {:ok, client} end - def handle_info({:received, "!op", sender, chan}, client) do - if LSG.IRC.admin?(sender) do + def handle_info({:irc, :trigger, "op", m = %IRC.Message{trigger: %IRC.Trigger{type: :bang}, sender: sender}}, client) do + if IRC.admin?(sender) do + m.replyfun.({:mode, "+o"}) + else + m.replyfun.({:kick, "non"}) + end + {:noreply, client} + end + + def handle_info({:joined, chan, sender}, client) do + if 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 diff --git a/lib/lsg_irc/base_handler.ex b/lib/lsg_irc/base_handler.ex index 9145936..35b2ade 100644 --- a/lib/lsg_irc/base_handler.ex +++ b/lib/lsg_irc/base_handler.ex @@ -1,7 +1,5 @@ defmodule LSG.IRC.BaseHandler do - use LSG.IRC.Handler - def irc_doc, do: nil def start_link(client) do diff --git a/lib/lsg_irc/calc_handler.ex b/lib/lsg_irc/calc_handler.ex deleted file mode 100644 index 0f74ac9..0000000 --- a/lib/lsg_irc/calc_handler.ex +++ /dev/null @@ -1,38 +0,0 @@ -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/calc_plugin.ex b/lib/lsg_irc/calc_plugin.ex new file mode 100644 index 0000000..6e4e30c --- /dev/null +++ b/lib/lsg_irc/calc_plugin.ex @@ -0,0 +1,38 @@ +defmodule LSG.IRC.CalcPlugin do + @moduledoc """ + # calc + + * **!calc ``**: évalue l'expression mathématique ``. + """ + + def irc_doc, do: @moduledoc + + def start_link() do + GenServer.start_link(__MODULE__, []) + end + + def init(_) do + {:ok, _} = Registry.register(IRC.PubSub, "trigger:calc", []) + {:ok, nil} + end + + def handle_info({:irc, :trigger, "calc", message = %IRC.Message{trigger: %IRC.Trigger{type: :bang, args: expr_list}}}, state) do + expr = Enum.join(expr_list, " ") + result = try do + case Abacus.eval(expr) do + {:ok, result} -> result + error -> inspect(error) + end + rescue + error -> "#{error.message}" + end + message.replyfun.("#{message.sender.nick}: #{expr} = #{result}") + {:noreply, state} + end + + def handle_info(msg, state) do + {:noreply, state} + end + +end + diff --git a/lib/lsg_irc/connection_handler.ex b/lib/lsg_irc/connection_handler.ex deleted file mode 100644 index 8d07e58..0000000 --- a/lib/lsg_irc/connection_handler.ex +++ /dev/null @@ -1,37 +0,0 @@ -defmodule LSG.IRC.ConnectionHandler do - defmodule State do - defstruct [:host, :port, :pass, :nick, :name, :user, :client] - end - - def start_link(client) do - 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} - end - - def handle_info({:connected, server, port}, state) do - debug "Connected to #{server}:#{port}" - ExIRC.Client.logon state.client, state.pass, state.nick, state.user, state.name - {:noreply, state} - end - - # Catch-all for messages you don't care about - def handle_info(msg, state) do - {:noreply, state} - end - - defp debug(msg) do - IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset() - end -end diff --git a/lib/lsg_irc/dice_handler.ex b/lib/lsg_irc/dice_handler.ex index b07b59b..7ff7b4d 100644 --- a/lib/lsg_irc/dice_handler.ex +++ b/lib/lsg_irc/dice_handler.ex @@ -22,6 +22,7 @@ defmodule LSG.IRC.DiceHandler do def init([client]) do ExIRC.Client.add_handler(client, self()) + {:ok, _} = Registry.register(IRC.PubSub, "dice", []) {:ok, %__MODULE__{client: client}} end diff --git a/lib/lsg_irc/handler.ex b/lib/lsg_irc/handler.ex deleted file mode 100644 index 19c0945..0000000 --- a/lib/lsg_irc/handler.ex +++ /dev/null @@ -1,24 +0,0 @@ -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 index 3591b5e..ece1b95 100644 --- a/lib/lsg_irc/kick_roulette_handler.ex +++ b/lib/lsg_irc/kick_roulette_handler.ex @@ -18,7 +18,7 @@ defmodule LSG.IRC.KickRouletteHandler do def handle_info({:received, "!kick", sender, chan}, client) do if 5 == :crypto.rand_uniform(1, 6) do spawn(fn() -> - :timer.sleep(:crypto.rand_uniform(500, 15_000)) + :timer.sleep(:crypto.rand_uniform(200, 10_000)) ExIRC.Client.kick(client, chan, sender.nick, "perdu") end) end diff --git a/lib/lsg_irc/last_fm_handler.ex b/lib/lsg_irc/last_fm_handler.ex index fe767b1..4eef24e 100644 --- a/lib/lsg_irc/last_fm_handler.ex +++ b/lib/lsg_irc/last_fm_handler.ex @@ -64,6 +64,14 @@ defmodule LSG.IRC.LastFmHandler do {:noreply, state} end + def terminate(_reason, state) do + if state.dets do + :dets.sync(state.dets) + :dets.close(state.dets) + end + :ok + end + defp irc_now_playing(nick_or_user, chan, state) do nick_or_user = String.strip(nick_or_user) username = case :dets.lookup(state.dets, String.downcase(nick_or_user)) do diff --git a/lib/lsg_irc/login_handler.ex b/lib/lsg_irc/login_handler.ex deleted file mode 100644 index f989b40..0000000 --- a/lib/lsg_irc/login_handler.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule LSG.IRC.LoginHandler do - def start_link(client) do - GenServer.start_link(__MODULE__, [client]) - end - - def init([client]) do - ExIRC.Client.add_handler client, self - {:ok, {client, ["#lsg", "#lsgtest"]}} - end - - def handle_info(:logged_in, state = {client, channels}) do - debug "Logged in to server" - channels |> Enum.map(&ExIRC.Client.join client, &1) - {:noreply, state} - end - - # Catch-all for messages you don't care about - def handle_info(_msg, state) do - {:noreply, state} - end - - defp debug(msg) do - IO.puts IO.ANSI.yellow() <> msg <> IO.ANSI.reset() - end -end diff --git a/lib/lsg_irc/quatre_cent_vingt_plugin.ex b/lib/lsg_irc/quatre_cent_vingt_plugin.ex new file mode 100644 index 0000000..579858e --- /dev/null +++ b/lib/lsg_irc/quatre_cent_vingt_plugin.ex @@ -0,0 +1,105 @@ +defmodule LSG.IRC.QuatreCentVingtPlugin do + require Logger + + @moduledoc """ + # 420 + + * **!420**: recorde un nouveau 420. + * **!420 pseudo**: stats du pseudo. + """ + + @achievements %{ + 1 => ["[le premier… il faut bien commencer un jour]"], + 10 => ["T'en es seulement à 10 ? ╭∩╮(Ο_Ο)╭∩╮"], + 42 => ["Bravo, et est-ce que autant de pétards t'on aidés à trouver la Réponse ? ٩(- ̮̮̃-̃)۶ [42]"], + 100 => ["°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸ 100 °º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸"], + 115 => [" ۜ\(סּںסּَ` )/ۜ 115!!"] + } + + @emojis [ + "\\o/", + "~o~", + "~~o∞~~", + "*\\o/*", + "**\\o/**", + "*ô*", + ] + + @coeffs Range.new(1, 10) + + def irc_doc, do: @moduledoc + + def start_link, do: GenServer.start_link(__MODULE__, []) + + def init(_) do + for coeff <- @coeffs do + {:ok, _} = Registry.register(IRC.PubSub, "trigger:#{420*coeff}", []) + end + dets_filename = (LSG.data_path() <> "/" <> "420.dets") |> String.to_charlist + {:ok, dets} = :dets.open_file(dets_filename, [{:type,:bag}]) + {:ok, dets} + end + + for coeff <- @coeffs do + qvc = to_string(420 * coeff) + def handle_info({:irc, :trigger, unquote(qvc), m = %IRC.Message{trigger: %IRC.Trigger{args: [], type: :bang}}}, dets) do + {count, last} = get_statistics_for_nick(dets, m.sender.nick) + count = count + unquote(coeff) + text = achievement_text(count) + now = DateTime.to_unix(DateTime.utc_now())-1 # this is ugly + for i <- Range.new(1, unquote(coeff)) do + :ok = :dets.insert(dets, {String.downcase(m.sender.nick), now+i}) + end + last_s = if last do + last_s = format_relative_timestamp(last) + " (le dernier était #{last_s})" + else + "" + end + m.replyfun.("#{m.sender.nick} 420 +#{unquote(coeff)} #{text}#{last_s}") + {:noreply, dets} + end + end + + def handle_info({:irc, :trigger, "420", m = %IRC.Message{trigger: %IRC.Trigger{args: [nick], type: :bang}}}, dets) do + text = case get_statistics_for_nick(dets, nick) do + {0, _} -> "#{nick} n'a jamais !420 ... honte à lui." + {count, last} -> + last_s = format_relative_timestamp(last) + "#{nick} 420: total #{count}, le dernier #{last_s}" + end + m.replyfun.(text) + {:noreply, dets} + end + + defp format_relative_timestamp(timestamp) do + alias Timex.Format.DateTime.Formatters + alias Timex.Timezone + date = timestamp + |> DateTime.from_unix! + |> Timezone.convert("Europe/Paris") + + {:ok, relative} = Formatters.Relative.relative_to(date, Timex.now("Europe/Paris"), "{relative}", "fr") + {:ok, detail} = Formatters.Default.lformat(date, " ({h24}:{m})", "fr") + + relative <> detail + end + + defp get_statistics_for_nick(dets, nick) do + qvc = :dets.lookup(dets, String.downcase(nick)) |> Enum.sort + count = Enum.reduce(qvc, 0, fn(_, acc) -> acc + 1 end) + {_, last} = List.last(qvc) || {nil, nil} + {count, last} + end + + @achievements_keys Map.keys(@achievements) + defp achievement_text(count) when count in @achievements_keys do + Enum.random(Map.get(@achievements, count)) + end + + defp achievement_text(count) do + emoji = Enum.random(@emojis) + "#{emoji} [#{count}]" + end + +end diff --git a/lib/lsg_irc/txt_handler.ex b/lib/lsg_irc/txt_handler.ex index efe2b68..032c11c 100644 --- a/lib/lsg_irc/txt_handler.ex +++ b/lib/lsg_irc/txt_handler.ex @@ -1,5 +1,5 @@ defmodule LSG.IRC.TxtHandler do - alias LSG.IRC.UserTrack + alias IRC.UserTrack require Logger @moduledoc """ @@ -213,6 +213,14 @@ defmodule LSG.IRC.TxtHandler 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") @@ -331,8 +339,8 @@ defmodule LSG.IRC.TxtHandler do 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) + admin? = IRC.admin?(sender) + operator? = IRC.UserTrack.operator?(channel, sender.nick) locked? = case :dets.lookup(locks, trigger) do [{trigger}] -> true _ -> false diff --git a/lib/lsg_irc/user_track.ex b/lib/lsg_irc/user_track.ex deleted file mode 100644 index b67b9f6..0000000 --- a/lib/lsg_irc/user_track.ex +++ /dev/null @@ -1,150 +0,0 @@ -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 deleted file mode 100644 index d167af5..0000000 --- a/lib/lsg_irc/user_track_handler.ex +++ /dev/null @@ -1,93 +0,0 @@ -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/wikipedia_plugin.ex b/lib/lsg_irc/wikipedia_plugin.ex new file mode 100644 index 0000000..4ee95b1 --- /dev/null +++ b/lib/lsg_irc/wikipedia_plugin.ex @@ -0,0 +1,90 @@ +defmodule LSG.IRC.WikipediaPlugin do + require Logger + + @moduledoc """ + # wikipédia + + * **!wp ``**: retourne le premier résultat de la `` Wikipedia + * **!wp**: un article Wikipédia au hasard + """ + + def irc_doc, do: @moduledoc + + def start_link() do + GenServer.start_link(__MODULE__, []) + end + + def init(_) do + {:ok, _} = Registry.register(IRC.PubSub, "trigger:wp", []) + {:ok, nil} + end + + def handle_info({:irc, :trigger, "wp", message = %IRC.Message{trigger: %IRC.Trigger{args: []}}}, state) do + irc_random(message) + {:noreply, state} + end + def handle_info({:irc, :trigger, "wp", message = %IRC.Message{trigger: %IRC.Trigger{args: args}}}, state) do + irc_search(Enum.join(args, " "), message) + {:noreply, state} + end + + def handle_info(info, state) do + {:noreply, state} + end + + defp irc_search("", message), do: irc_random(message) + defp irc_search(query, message) do + params = %{ + "action" => "query", + "list" => "search", + "srsearch" => String.strip(query), + "srlimit" => 1, + } + case query_wikipedia(params) do + {:ok, %{"query" => %{"search" => [item | _]}}} -> + title = item["title"] + url = "https://fr.wikipedia.org/wiki/" <> String.replace(title, " ", "_") + msg = "Wikipédia: #{title} — #{url}" + message.replyfun.(msg) + _ -> + nil + end + end + + defp irc_random(message) do + params = %{ + "action" => "query", + "generator" => "random", + "grnnamespace" => 0, + "prop" => "info" + } + case query_wikipedia(params) do + {:ok, %{"query" => %{"pages" => map = %{}}}} -> + [{_, item}] = Map.to_list(map) + title = item["title"] + url = "https://fr.wikipedia.org/wiki/" <> String.replace(title, " ", "_") + msg = "Wikipédia: #{title} — #{url}" + message.replyfun.(msg) + _ -> + nil + end + end + + defp query_wikipedia(params) do + url = "https://fr.wikipedia.org/w/api.php" + params = params + |> Map.put("format", "json") + |> Map.put("utf8", "") + + case HTTPoison.get(url, [], params: params) do + {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> Jason.decode(body) + {:ok, %HTTPoison.Response{status_code: 400, body: body}} -> + Logger.error "Wikipedia HTTP 400: #{inspect body}" + {:error, "http 400"} + error -> + Logger.error "Wikipedia http error: #{inspect error}" + {:error, "http client error"} + end + end + +end diff --git a/lib/lsg_irc/youtube_handler.ex b/lib/lsg_irc/youtube_handler.ex index e7eadfc..51584a2 100644 --- a/lib/lsg_irc/youtube_handler.ex +++ b/lib/lsg_irc/youtube_handler.ex @@ -7,7 +7,7 @@ defmodule LSG.IRC.YouTubeHandler do * **!yt ``**, !youtube ``: retourne le premier résultat de la `` YouTube """ - defstruct client: nil, dets: nil + defstruct client: nil def irc_doc, do: @moduledoc diff --git a/lib/lsg_web/controllers/irc_controller.ex b/lib/lsg_web/controllers/irc_controller.ex index bff5476..af7fff1 100644 --- a/lib/lsg_web/controllers/irc_controller.ex +++ b/lib/lsg_web/controllers/irc_controller.ex @@ -3,7 +3,7 @@ defmodule LSGWeb.IrcController do def index(conn, _) do doc = LSG.IRC.TxtHandler.irc_doc() - commands = for mod <- Application.get_env(:lsg, :irc)[:handlers] do + commands = for mod <- (Application.get_env(:lsg, :irc)[:plugins] ++ Application.get_env(:lsg, :irc)[:handlers]) do mod.irc_doc() end |> Enum.reject(fn(i) -> i == nil end) -- cgit v1.2.3