summaryrefslogtreecommitdiff
path: root/lib/irc/puppet_connection.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/irc/puppet_connection.ex')
-rw-r--r--lib/irc/puppet_connection.ex238
1 files changed, 238 insertions, 0 deletions
diff --git a/lib/irc/puppet_connection.ex b/lib/irc/puppet_connection.ex
new file mode 100644
index 0000000..91a26b3
--- /dev/null
+++ b/lib/irc/puppet_connection.ex
@@ -0,0 +1,238 @@
+defmodule IRC.PuppetConnection do
+ require Logger
+ @min_backoff :timer.seconds(5)
+ @max_backoff :timer.seconds(2*60)
+ @max_idle :timer.hours(12)
+ @env Mix.env
+
+ defmodule Supervisor do
+ use DynamicSupervisor
+
+ def start_link() do
+ DynamicSupervisor.start_link(__MODULE__, [], name: __MODULE__)
+ end
+
+ def start_child(%IRC.Account{id: account_id}, %IRC.Connection{id: connection_id}) do
+ spec = %{id: {account_id, connection_id}, start: {IRC.PuppetConnection, :start_link, [account_id, connection_id]}, 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 whereis(account = %IRC.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}) do
+ {:global, name} = name(account_id, connection_id)
+ case :global.whereis_name(name) do
+ :undefined -> nil
+ pid -> pid
+ end
+ end
+
+ def send_message(account = %IRC.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}, channel, text) do
+ GenServer.cast(name(account_id, connection_id), {:send_message, self(), channel, text})
+ end
+
+ def start_and_send_message(account = %IRC.Account{id: account_id}, connection = %IRC.Connection{id: connection_id}, channel, text) do
+ {:global, name} = name(account_id, connection_id)
+ pid = whereis(account, connection)
+ pid = if !pid do
+ case IRC.PuppetConnection.Supervisor.start_child(account, connection) do
+ {:ok, pid} -> pid
+ {:error, {:already_started, pid}} -> pid
+ end
+ else
+ pid
+ end
+ GenServer.cast(pid, {:send_message, self(), channel, text})
+ end
+
+ def start(account = %IRC.Account{}, connection = %IRC.Connection{}) do
+ IRC.PuppetConnection.Supervisor.start_child(account, connection)
+ end
+
+ def start_link(account_id, connection_id) do
+ GenServer.start_link(__MODULE__, [account_id, connection_id], name: name(account_id, connection_id))
+ end
+
+ def name(account_id, connection_id) do
+ {:global, {PuppetConnection, account_id, connection_id}}
+ end
+
+ def init([account_id, connection_id]) do
+ account = %IRC.Account{} = IRC.Account.get(account_id)
+ connection = %IRC.Connection{} = IRC.Connection.lookup(connection_id)
+ Logger.metadata(puppet_conn: account.id <> "@" <> connection.id)
+ backoff = :backoff.init(@min_backoff, @max_backoff)
+ |> :backoff.type(:jitter)
+ idle = :erlang.send_after(@max_idle, self, :idle)
+ {:ok, %{client: nil, backoff: backoff, idle: idle, connected: false, buffer: [], channels: [], connection_id: connection_id, account_id: account_id, connected_server: nil, connected_port: nil, network: connection.network}, {:continue, :connect}}
+ end
+
+ def handle_continue(:connect, state) do
+ #ipv6 = if @env == :prod do
+ # subnet = Nola.Subnet.assign(state.account_id)
+ # IRC.Account.put_meta(IRC.Account.get(state.account_id), "subnet", subnet)
+ # ip = Pfx.host(subnet, 1)
+ # {:ok, ipv6} = :inet_parse.ipv6_address(to_charlist(ip))
+ # System.cmd("add-ip6", [ip])
+ # ipv6
+ #end
+
+ conn = IRC.Connection.lookup(state.connection_id)
+ client_opts = []
+ |> Keyword.put(:network, conn.network)
+ 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
+
+ base_opts = [
+ {:nodelay, true}
+ ]
+
+ #{ip, opts} = case {ipv6, :inet_res.resolve(to_charlist(conn.host), :in, :aaaa)} do
+ # {ipv6, {:ok, {:dns_rec, _dns_header, _query, rrs = [{:dns_rr, _, _, _, _, _, _, _, _, _} | _], _, _}}} ->
+ # ip = rrs
+ # |> Enum.map(fn({:dns_rr, _, :aaaa, :in, _, _, ipv6, _, _, _}) -> ipv6 end)
+ # |> Enum.shuffle()
+ # |> List.first()
+
+ # opts = [
+ # :inet6,
+ # {:ifaddr, ipv6}
+ # ]
+ # {ip, opts}
+ # _ ->
+ {ip, opts} = {to_charlist(conn.host), []}
+ #end
+
+ conn_fun = if conn.tls, do: :connect_ssl!, else: :connect!
+ apply(ExIRC.Client, conn_fun, [client, ip, conn.port, base_opts ++ opts])
+
+ {:noreply, %{state | client: client}}
+ end
+
+ def handle_continue(:connected, state) do
+ state = Enum.reduce(Enum.reverse(state.buffer), state, fn(b, state) ->
+ {:noreply, state} = handle_cast(b, state)
+ state
+ end)
+ {:noreply, %{state | buffer: []}}
+ end
+
+ def handle_cast(cast = {:send_message, _pid, _channel, _text}, state = %{connected: false, buffer: buffer}) do
+ {:noreply, %{state | buffer: [cast | buffer]}}
+ end
+
+ def handle_cast({:send_message, pid, channel, text}, state = %{connected: true}) do
+ channels = if !Enum.member?(state.channels, channel) do
+ ExIRC.Client.join(state.client, channel)
+ [channel | state.channels]
+ else
+ state.channels
+ end
+ ExIRC.Client.msg(state.client, :privmsg, channel, text)
+
+ meta = %{puppet: true, from: pid}
+ account = IRC.Account.get(state.account_id)
+ nick = make_nick(state)
+ sender = %ExIRC.SenderInfo{network: state.network, nick: suffix_nick(nick), user: nick, host: "puppet."}
+ reply_fun = fn(text) ->
+ IRC.Connection.broadcast_message(state.network, channel, text)
+ end
+ message = %IRC.Message{id: FlakeId.get(), at: NaiveDateTime.utc_now(), text: text, network: state.network, account: account, sender: sender, channel: channel, replyfun: reply_fun, trigger: IRC.Connection.extract_trigger(text), meta: meta}
+ message = case IRC.UserTrack.messaged(message) do
+ :ok -> message
+ {:ok, message} -> message
+ end
+ IRC.Connection.publish(message, ["#{message.network}/#{channel}:messages"])
+
+ idle = if length(state.buffer) == 0 do
+ :erlang.cancel_timer(state.idle)
+ :erlang.send_after(@max_idle, self(), :idle)
+ else
+ state.idle
+ end
+
+ {:noreply, %{state | idle: idle, channels: channels}}
+ end
+
+ def handle_info(:idle, state) do
+ ExIRC.Client.quit(state.client, "Puppet was idle for too long")
+ ExIRC.Client.stop!(state.client)
+ {:stop, :normal, state}
+ 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 | connected: false, backoff: backoff}}
+ end
+
+ def handle_info(:connect, state) do
+ {:noreply, state, {:continue, :connect}}
+ 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)
+ base_nick = make_nick(state)
+ ExIRC.Client.logon(state.client, "", suffix_nick(base_nick), base_nick, "#{base_nick}'s puppet")
+ {: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)
+ # Create an UserTrack entry for the client so it's authenticated to the right account_id already.
+ IRC.UserTrack.connected(state.network, suffix_nick(make_nick(state)), make_nick(state), "puppet.", state.account_id, %{puppet: true})
+ {:noreply, %{state | backoff: backoff}}
+ end
+
+ # ISUP
+ def handle_info({:isup, network}, state) do
+ {:noreply, %{state | network: network, connected: true}, {:continue, :connected}}
+ end
+
+ # Been kicked
+ def handle_info({:kicked, _sender, chan, _reason}, state) do
+ {:noreply, %{state | channels: state.channels -- [chan]}}
+ end
+
+ def handle_info(_info, state) do
+ {:noreply, state}
+ end
+
+ def make_nick(state) do
+ account = IRC.Account.get(state.account_id)
+ user = IRC.UserTrack.find_by_account(state.network, account)
+ base_nick = if(user, do: user.nick, else: account.name)
+ clean_nick = case String.split(base_nick, ":", parts: 2) do
+ ["@"<>nick, _] -> nick
+ [nick] -> nick
+ end
+ clean_nick
+ end
+
+ if Mix.env == :dev do
+ def suffix_nick(nick), do: "#{nick}[d]"
+ else
+ def suffix_nick(nick), do: "#{nick}[p]"
+ end
+
+end