summaryrefslogblamecommitdiff
path: root/lib/irc/puppet_connection.ex
blob: 91a26b3c0107d0dfbf462771e3453f6705a17570 (plain) (tree)
1
2
3
4
5
6



                                   
                            
              






















                                                                                                                                             







                                                                                                         
                                                                                                                             
                                                                                           











                                                                                                                                       
       
                                                               

     



                                                                        


















                                                                                                                                                                                                                                                         
                               
                                                    





                                                                                
 











                                                               




                      














                                                                                                                



                                                                             



                                           
                                                                          





                                               
                                                                                                                


                                                  
                                                                                     







                                                           






                                                                                                            
                                                                                                                                                                                                                                                  





                                                                               










                                                         
                                                                   
















                                                                                        
                                                                                              
                                                  

                                                                                                    






                                                                                           

                                                                                                                                         
















                                                                                     
















                                                                
   
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