diff options
author | href <href@random.sh> | 2018-05-02 19:03:35 +0200 |
---|---|---|
committer | href <href@random.sh> | 2018-05-02 19:04:28 +0200 |
commit | a47dc245808921309f58e8b1c4b6fa028b2df073 (patch) | |
tree | 1842d413f2a43d3759b439417f053052b878a1f7 /lib/irc | |
parent | … (diff) |
meh
Diffstat (limited to 'lib/irc')
-rw-r--r-- | lib/irc/connection_handler.ex | 36 | ||||
-rw-r--r-- | lib/irc/login_handler.ex | 25 | ||||
-rw-r--r-- | lib/irc/pubsub_handler.ex | 123 | ||||
-rw-r--r-- | lib/irc/user_track.ex | 154 | ||||
-rw-r--r-- | lib/irc/user_track_handler.ex | 93 |
5 files changed, 431 insertions, 0 deletions
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 |