summaryrefslogtreecommitdiff
path: root/lib/irc
diff options
context:
space:
mode:
authorhref <href@random.sh>2018-05-02 19:03:35 +0200
committerhref <href@random.sh>2018-05-02 19:04:28 +0200
commita47dc245808921309f58e8b1c4b6fa028b2df073 (patch)
tree1842d413f2a43d3759b439417f053052b878a1f7 /lib/irc
parent (diff)
meh
Diffstat (limited to 'lib/irc')
-rw-r--r--lib/irc/connection_handler.ex36
-rw-r--r--lib/irc/login_handler.ex25
-rw-r--r--lib/irc/pubsub_handler.ex123
-rw-r--r--lib/irc/user_track.ex154
-rw-r--r--lib/irc/user_track_handler.ex93
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