diff options
Diffstat (limited to 'lib/irc/connection.ex')
-rw-r--r-- | lib/irc/connection.ex | 521 |
1 files changed, 0 insertions, 521 deletions
diff --git a/lib/irc/connection.ex b/lib/irc/connection.ex deleted file mode 100644 index 86d8279..0000000 --- a/lib/irc/connection.ex +++ /dev/null @@ -1,521 +0,0 @@ -defmodule IRC.Connection do - require Logger - use Ecto.Schema - - @moduledoc """ - # IRC Connection - - Provides a nicer abstraction over ExIRC's handlers. - - ## Start connections - - ``` - IRC.Connection.start_link(host: "irc.random.sh", port: 6697, nick: "pouetbot", channels: ["#dev"]) - - ## PubSub topics - - * `account` -- accounts change - * {:account_change, old_account_id, new_account_id} # Sent when account merged - * {:accounts, [{:account, network, channel, nick, account_id}] # Sent on bot join - * {:account, network, nick, account_id} # Sent on user join - * `message` -- aill messages (without 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 - - @min_backoff :timer.seconds(5) - @max_backoff :timer.seconds(2*60) - - embedded_schema do - field :network, :string - field :host, :string - field :port, :integer - field :nick, :string - field :user, :string - field :name, :string - field :pass, :string - field :tls, :boolean, default: false - field :channels, {:array, :string}, default: [] - end - - defmodule Supervisor do - use DynamicSupervisor - - def start_link() do - DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__) - end - - def start_child(%IRC.Connection{} = conn) do - spec = %{id: conn.id, start: {IRC.Connection, :start_link, [conn]}, restart: :transient} - DynamicSupervisor.start_child(__MODULE__, spec) - end - - @impl true - def init(_init_arg) do - DynamicSupervisor.init( - strategy: :one_for_one, - max_restarts: 10, - max_seconds: 1 - ) - end - end - - - def changeset(params) do - import Ecto.Changeset - - %__MODULE__{id: EntropyString.large_id()} - |> cast(params, [:network, :host, :port, :nick, :user, :name, :pass, :channels, :tls]) - |> validate_required([:host, :port, :nick, :user, :name]) - |> apply_action(:insert) - end - - def to_tuple(%__MODULE__{} = conn) do - {conn.id, conn.network, conn.host, conn.port, conn.nick, conn.user, conn.name, conn.pass, conn.tls, conn.channels, nil} - end - - def from_tuple({id, network, host, port, nick, user, name, pass, tls, channels, _}) do - %__MODULE__{id: id, network: network, host: host, port: port, nick: nick, user: user, name: name, pass: pass, tls: tls, channels: channels} - end - - ## -- MANAGER API - - def setup() do - :dets.open_file(dets(), []) - end - - def dets(), do: to_charlist(Nola.data_path("/connections.dets")) - - def lookup(id) do - case :dets.lookup(dets(), id) do - [object | _] -> from_tuple(object) - _ -> nil - end - end - - def connections() do - :dets.foldl(fn(object, acc) -> [from_tuple(object) | acc] end, [], dets()) - end - - def start_all() do - for conn <- connections(), do: {conn, IRC.Connection.Supervisor.start_child(conn)} - end - - def get_network(network, channel \\ nil) do - spec = [{{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_}, - [{:==, :"$1", {:const, network}}], [:"$_"]}] - results = Enum.map(:dets.select(dets(), spec), fn(object) -> from_tuple(object) end) - if channel do - Enum.find(results, fn(conn) -> Enum.member?(conn.channels, channel) end) - else - List.first(results) - end - end - - def get_host_nick(host, port, nick) do - spec = [{{:_, :_, :"$1", :"$2", :"$3", :_, :_, :_, :_, :_, :_}, - [{:andalso, - {:andalso, {:==, :"$1", {:const, host}}, {:==, :"$2", {:const, port}}}, - {:==, :"$3", {:const, nick}}}], - [:"$_"]} - ] - case :dets.select(dets(), spec) do - [object] -> from_tuple(object) - [] -> nil - end - end - - def delete_connection(%__MODULE__{id: id} = conn) do - :dets.delete(dets(), id) - stop_connection(conn) - :ok - end - - def start_connection(%__MODULE__{} = conn) do - IRC.Connection.Supervisor.start_child(conn) - end - - def stop_connection(%__MODULE__{id: id}) do - case :global.whereis_name(id) do - pid when is_pid(pid) -> - GenServer.stop(pid, :normal) - _ -> :error - end - end - - def add_connection(opts) do - case changeset(opts) do - {:ok, conn} -> - if existing = get_host_nick(conn.host, conn.port, conn.nick) do - {:error, {:existing, conn}} - else - :dets.insert(dets(), to_tuple(conn)) - IRC.Connection.Supervisor.start_child(conn) - end - error -> error - end - end - - def update_connection(connection) do - :dets.insert(dets(), to_tuple(connection)) - end - - def start_link(conn) do - GenServer.start_link(__MODULE__, [conn], name: {:global, conn.id}) - end - - def broadcast_message(net, chan, message) do - dispatch("conn", {:broadcast, net, chan, message}, IRC.ConnectionPubSub) - end - def broadcast_message(list, message) when is_list(list) do - for {net, chan} <- list do - broadcast_message(net, chan, message) - end - end - - def privmsg(channel, line) do - GenServer.cast(__MODULE__, {:privmsg, channel, line}) - end - - def init([conn]) do - Logger.metadata(conn: conn.id) - backoff = :backoff.init(@min_backoff, @max_backoff) - |> :backoff.type(:jitter) - {:ok, %{client: nil, backoff: backoff, conn: conn, connected_server: nil, connected_port: nil, network: conn.network}, {:continue, :connect}} - end - - @triggers %{ - "!" => :bang, - "+" => :plus, - "-" => :minus, - "?" => :query, - "." => :dot, - "~" => :tilde, - "@" => :at, - "++" => :plus_plus, - "--" => :minus_minus, - "!!" => :bang_bang, - "??" => :query_query, - ".." => :dot_dot, - "~~" => :tilde_tilde, - "@@" => :at_at - } - - def handle_continue(:connect, state) do - client_opts = [] - |> Keyword.put(:network, state.conn.network) - {:ok, _} = Registry.register(IRC.ConnectionPubSub, "conn", []) - client = if state.client && Process.alive?(state.client) do - Logger.info("Reconnecting client") - state.client - else - Logger.info("Connecting") - {:ok, client} = ExIRC.Client.start_link(debug: false) - ExIRC.Client.add_handler(client, self()) - client - end - - opts = [{:nodelay, true}] - conn_fun = if state.conn.tls, do: :connect_ssl!, else: :connect! - apply(ExIRC.Client, conn_fun, [client, to_charlist(state.conn.host), state.conn.port, opts]) - - {:noreply, %{state | client: client}} - end - - def handle_info(:disconnected, state) do - {delay, backoff} = :backoff.fail(state.backoff) - Logger.info("#{inspect(self())} Disconnected -- reconnecting in #{inspect delay}ms") - Process.send_after(self(), :connect, delay) - {:noreply, %{state | backoff: backoff}} - end - - def handle_info(:connect, state) do - {:noreply, state, {:continue, :connect}} - end - - def handle_cast({:privmsg, channel, line}, state) do - irc_reply(state, {channel, nil}, line) - {:noreply, state} - end - - # Connection successful - def handle_info({:connected, server, port}, state) do - Logger.info("#{inspect(self())} Connected to #{inspect(server)}:#{port} #{inspect state}") - {_, backoff} = :backoff.succeed(state.backoff) - ExIRC.Client.logon(state.client, state.conn.pass || "", state.conn.nick, state.conn.user, state.conn.name) - {:noreply, %{state | backoff: backoff, connected_server: server, connected_port: port}} - end - - # Logon successful - def handle_info(:logged_in, state) do - Logger.info("#{inspect(self())} Logged in") - {_, backoff} = :backoff.succeed(state.backoff) - Enum.map(state.conn.channels, &ExIRC.Client.join(state.client, &1)) - {:noreply, %{state | backoff: backoff}} - end - - # ISUP - def handle_info({:isup, network}, state) when is_binary(network) do - IRC.UserTrack.clear_network(state.network) - if network != state.network do - Logger.warn("Possibly misconfigured network: #{network} != #{state.network}") - end - {:noreply, state} - end - - # Been kicked - def handle_info({:kicked, _sender, chan, _reason}, state) do - ExIRC.Client.join(state.client, chan) - {:noreply, state} - end - - # Received something in a channel - def handle_info({:received, text, sender, chan}, state) do - user = if user = IRC.UserTrack.find_by_nick(state.network, sender.nick) do - user - else - Logger.error("Could not lookup user for message: #{inspect {state.network, chan, sender.nick}}") - user = IRC.UserTrack.joined(chan, sender, []) - ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user.. - user - end - if !user do - ExIRC.Client.who(state.client, chan) # Rewho everything in case of need ? We shouldn't not know that user.. - Logger.error("Could not lookup user nor create it for message: #{inspect {state.network, chan, sender.nick}}") - else - if !Map.get(user.options, :puppet) do - reply_fun = fn(text) -> irc_reply(state, {chan, sender}, text) end - account = IRC.Account.lookup(sender) - message = %IRC.Message{id: FlakeId.get(), transport: :irc, at: NaiveDateTime.utc_now(), text: text, network: state.network, - account: account, sender: sender, channel: chan, replyfun: reply_fun, - trigger: extract_trigger(text)} - message = case IRC.UserTrack.messaged(message) do - :ok -> message - {:ok, message} -> message - end - publish(message, ["#{message.network}/#{chan}:messages"]) - end - end - {:noreply, state} - end - - # Received a private message - def handle_info({:received, text, sender}, state) do - reply_fun = fn(text) -> irc_reply(state, {sender.nick, sender}, text) end - account = IRC.Account.lookup(sender) - message = %IRC.Message{id: FlakeId.get(), transport: :irc, text: text, network: state.network, at: NaiveDateTime.utc_now(), - account: account, sender: sender, replyfun: reply_fun, trigger: extract_trigger(text)} - message = case IRC.UserTrack.messaged(message) do - :ok -> message - {:ok, message} -> message - end - publish(message, ["messages:private", "#{message.network}/#{account.id}:messages"]) - {:noreply, state} - end - - ## -- Broadcast - def handle_info({:broadcast, net, account = %IRC.Account{}, message}, state) do - if net == state.conn.network do - user = IRC.UserTrack.find_by_account(net, account) - if user do - irc_reply(state, {user.nick, nil}, message) - end - end - {:noreply, state} - end - def handle_info({:broadcast, net, chan, message}, state) do - if net == state.conn.network && Enum.member?(state.conn.channels, chan) do - irc_reply(state, {chan, nil}, message) - end - {:noreply, state} - end - - ## -- UserTrack - - def handle_info({:joined, channel}, state) do - ExIRC.Client.who(state.client, channel) - {:noreply, state} - end - - def handle_info({:who, channel, whos}, state) do - accounts = Enum.map(whos, fn(who = %ExIRC.Who{nick: nick, operator?: operator}) -> - priv = if operator, do: [:operator], else: [] - # Don't touch -- on WHO the bot joined, not the users. - IRC.UserTrack.joined(channel, who, priv, false) - account = IRC.Account.lookup(who) - if account do - {:account, who.network, channel, who.nick, account.id} - end - end) - |> Enum.filter(fn(x) -> x end) - dispatch("account", {:accounts, accounts}) - {:noreply, state} - end - - def handle_info({:quit, reason, sender}, state) do - IRC.UserTrack.quitted(sender, reason) - {:noreply, state} - end - - def handle_info({:joined, channel, sender}, state) do - IRC.UserTrack.joined(channel, sender, []) - account = IRC.Account.lookup(sender) - if account do - dispatch("account", {:account, sender.network, channel, sender.nick, account.id}) - end - {:noreply, state} - end - - def handle_info({:kicked, nick, _by, channel, _reason}, state) do - IRC.UserTrack.parted(state.network, channel, nick) - {:noreply, state} - end - - def handle_info({:parted, channel, %ExIRC.SenderInfo{nick: nick}}, state) do - IRC.UserTrack.parted(state.network, channel, nick) - {:noreply, state} - end - - def handle_info({:mode, [channel, mode, nick]}, state) do - track_mode(state.network, channel, nick, mode) - {:noreply, state} - end - - def handle_info({:nick_changed, old_nick, new_nick}, state) do - IRC.UserTrack.renamed(state.network, old_nick, new_nick) - {:noreply, state} - end - - def handle_info(unhandled, client) do - Logger.debug("unhandled: #{inspect unhandled}") - {:noreply, client} - end - - def publish(pub), do: publish(pub, []) - - def publish(m = %IRC.Message{trigger: nil}, keys) do - dispatch(["messages"] ++ keys, {:irc, :text, m}) - end - - def publish(m = %IRC.Message{trigger: t = %IRC.Trigger{trigger: trigger}}, keys) do - dispatch(["triggers", "#{m.network}/#{m.channel}:triggers", "trigger:"<>trigger], {:irc, :trigger, trigger, m}) - end - - def publish_event(net, event = %{type: _}) when is_binary(net) do - event = event - |> Map.put(:at, NaiveDateTime.utc_now()) - |> Map.put(:network, net) - dispatch("#{net}:events", {:irc, :event, event}) - end - def publish_event({net, chan}, event = %{type: type}) do - event = event - |> Map.put(:at, NaiveDateTime.utc_now()) - |> Map.put(:network, net) - |> Map.put(:channel, chan) - dispatch("#{net}/#{chan}:events", {:irc, :event, event}) - end - - def dispatch(keys, content, sub \\ IRC.PubSub) - - def dispatch(key, content, sub) when is_binary(key), do: dispatch([key], content, sub) - def dispatch(keys, content, sub) when is_list(keys) do - Logger.debug("dispatch #{inspect keys} = #{inspect content}") - for key <- keys do - spawn(fn() -> Registry.dispatch(sub, key, fn h -> - for {pid, _} <- h, do: send(pid, content) - end) end) - end - end - - # - # Triggers - # - - def triggers, do: @triggers - - for {trigger, name} <- @triggers do - def extract_trigger(unquote(trigger)<>text) do - text = String.strip(text) - [trigger | args] = String.split(text, " ") - %IRC.Trigger{type: unquote(name), trigger: String.downcase(trigger), args: args} - end - end - - def 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(state = %{client: client, network: network}, {target, _}, text) when is_binary(text) or is_list(text) do - lines = IRC.splitlong(text) - |> Enum.map(fn(x) -> if(is_list(x), do: x, else: String.split(x, "\n")) end) - |> List.flatten() - outputs = for line <- lines do - ExIRC.Client.msg(client, :privmsg, target, line) - {:irc, :out, %IRC.Message{id: FlakeId.get(), transport: :irc, network: network, - channel: target, text: line, sender: %ExIRC.SenderInfo{nick: state.conn.nick}, at: NaiveDateTime.utc_now(), meta: %{self: true}}} - end - for f <- outputs, do: dispatch(["irc:outputs", "#{network}/#{target}:outputs"], f) - end - - defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:kick, reason}) do - ExIRC.Client.kick(client, target, nick, reason) - end - - defp irc_reply(%{client: client}, {target, _}, {:kick, nick, reason}) do - ExIRC.Client.kick(client, target, nick, reason) - end - - defp irc_reply(%{client: client}, {target, %{nick: nick}}, {:mode, mode}) do - ExIRC.Client.mode(%{client: client}, target, mode, nick) - end - - defp irc_reply(%{client: client}, target, {:mode, mode, nick}) do - ExIRC.Client.mode(client, target, mode, nick) - end - - defp irc_reply(%{client: client}, target, {:channel_mode, mode}) do - ExIRC.Client.mode(client, target, mode) - end - - defp track_mode(network, channel, nick, "+o") do - IRC.UserTrack.change_privileges(network, channel, nick, {[:operator], []}) - :ok - end - - defp track_mode(network, channel, nick, "-o") do - IRC.UserTrack.change_privileges(network, channel, nick, {[], [:operator]}) - :ok - end - - defp track_mode(network, channel, nick, "+v") do - IRC.UserTrack.change_privileges(network, channel, nick, {[:voice], []}) - :ok - end - - defp track_mode(network, channel, nick, "-v") do - IRC.UserTrack.change_privileges(network, channel, nick, {[], [:voice]}) - :ok - end - - defp track_mode(network, channel, nick, mode) do - Logger.warn("Unhandled track_mode: #{inspect {nick, mode}}") - :ok - end - - defp server(%{conn: %{host: host, port: port}}) do - host <> ":" <> to_string(port) - end - -end |