summaryrefslogblamecommitdiff
path: root/lib/irc/user_track.ex
blob: 3f144d5cccd2afdb3c99d4fe97ac4e6bd5f3599a (plain) (tree)
1
2
3
4
5
6
7
                          



                           
                            
                                               











                                                


                                 
                                                               







                                           






















                                                                                             



                                  




                                     
                                                                                                                                                   

                                      
                                                                                                                                                                                       

       
                   

                                                                                                                                                                                                                


       
                                               

                                                                                                                        
                                                           



                                    


                                                





                                      
                                                        

                                                                                                                        
                                                              






                                                    



                                                                                                                                                
                                                                         


                                    

                                              



              



                                  



                                                                                                                        
                                                           



                                        
                                                                                                                                      
                           
                                                                                                                        













                                                                      
                                                                                                         
                              




           








                                       

                                            






                                                                   
                                  
                                                                                   



                                
        
                                                                      
                                                








                                                                                                                                                                  





                                                                                                          

     

                                      
                                                                                                  
                                         

                            
                                                          

                                                                                                                 
                                                                                                                                                         
 
                                            





                                                                 




                                           

                                                                                                                       

        

     













                                                                                                             
                                                                      
        
                                                                 
                                         







                                                             
       

     

                                                
                                             
                                                                       
                                                               
                                                                                            
                                         

                                                                                                                                         


       

                                                                 






                                                                                     
                                                                                                                                                     


       
               









                                                            
                                                  
                                                   


                                              
                                                                                                                                 
          
                                                                                                                                            




                               
                                

                                                          



                                                                                                                                

                             


       







                                                                       


                                              
   
defmodule IRC.UserTrack do
  @moduledoc """
  User Track DB & Utilities
  """

  @ets IRC.UserTrack.Storage
  # {uuid, network, 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 clear_network(network) do
      op(fn(ets) ->
        spec = [
          {{:_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_, :_},
          [
            {:==, :"$1", {:const, network}}
          ], [:"$_"]}
        ]
        :ets.match_delete(ets, spec)
      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, :account, :network, :nick, {:nicks, []}, :username, :host, :realname, {:privileges, %{}}, {:last_active, %{}}, {:options, %{}}]

    def to_tuple(u = %__MODULE__{}) do
      {u.id || IRC.UserTrack.Id.large_id, u.network, u.account, String.downcase(u.nick), u.nick, u.nicks || [], u.username, u.host, u.realname, u.privileges, u.last_active, u.options}
    end

    #tuple size: 11
    def from_tuple({id, network, account, _downcased_nick, nick, nicks, username, host, realname, privs, last_active, opts}) do
      struct = %__MODULE__{id: id, account: account, network: network, nick: nick, nicks: nicks, username: username, host: host, realname: realname, privileges: privs, last_active: last_active, options: opts}
    end
  end

  def find_by_account(%Nola.Account{id: id}) do
    #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
    spec = [
      {{:_, :_, :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
       [
          {:==, :"$2", {:const, id}}
       ], [:"$_"]}
    ]
    results = :ets.select(@ets, spec)
    |> Enum.filter(& &1)
    for obj <- results, do: User.from_tuple(obj)
  end

  def find_by_account(network, nil) do
    nil
  end

  def find_by_account(network, %Nola.Account{id: id}) do
    #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
    spec = [
      {{:_, :"$1", :"$2", :_, :_, :_, :_, :_, :_, :_, :_, :_},
       [
         {:andalso, {:==, :"$1", {:const, network}},
          {:==, :"$2", {:const, id}}}
       ], [:"$_"]}
    ]
    case :ets.select(@ets, spec) do
      results = [_r | _] ->
        result = results
        |> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> network != "matrix" && net == "matrix" end)
        |> Enum.reject(fn({_, net, _, _, _, _, _, _, _, _, actives, opts}) -> network != "telegram" && net == "telegram" end)
        |> Enum.reject(fn({_, _, _, _, _, _, _, _, _, _, actives, opts}) -> network not in ["matrix", "telegram"] && Map.get(opts, :puppet) end)
        |> Enum.sort_by(fn({_, _, _, _, _, _, _, _, _, _, actives, _}) ->
          Map.get(actives, nil)
        end, {:desc, NaiveDateTime})
        |> List.first

        if result, do: User.from_tuple(result)
      _ -> nil
    end
  end

  def clear_network(network) do
    Storage.clear_network(network)
  end


  def merge_account(old_id, new_id) do
    #iex(15)> :ets.fun2ms(fn(obj = {_, net, acct, _, _, _, _, _, _}) when net == network and acct == account -> obj end)
    spec = [
      {{:_, :_, :"$1", :_, :_, :_, :_, :_, :_, :_, :_, :_},
       [
          {:==, :"$1", {:const, old_id}}
       ], [:"$_"]}
    ]
    Enum.each(:ets.select(@ets, spec), fn({id, net, _, downcased_nick, nick, nicks, username, host, realname, privs, active, opts}) ->
      Storage.op(fn(ets) ->
        :ets.insert(@ets, {id, net, new_id, downcased_nick, nick, nicks, username, host, realname, privs, active, opts})
      end)
    end)
  end

  def find_by_nick(%ExIRC.Who{network: network, nick: nick}) do
    find_by_nick(network, nick)
  end


  def find_by_nick(%ExIRC.SenderInfo{network: network, nick: nick}) do
    find_by_nick(network, nick)
  end

  def find_by_nick(network, nick) do
    case :ets.match(@ets, {:"$1", network, :_, String.downcase(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?(network, channel, nick) do
    if user = find_by_nick(network, nick) do
      privs = Map.get(user.privileges, channel, [])
      Enum.member?(privs, :admin) || Enum.member?(privs, :operator)
    else
      false
    end
  end

  def channel(network, channel) do
    Enum.filter(to_list(), fn({_, network, _, _, _, _, _, _, _, channels, _, _}) ->
      Map.get(channels, channel)
    end)
  end

  # TODO
  def connected(network, nick, user, host, account_id, opts \\ %{}) do
    if account = Nola.Account.get(account_id) do
      user = if user = find_by_nick(network, nick) do
        user
      else
        user = %User{id: IRC.UserTrack.Id.large_id, account: account_id, network: network, nick: nick, username: user, host: host, privileges: %{}, options: opts}
        Storage.op(fn(ets) ->
          :ets.insert(ets, User.to_tuple(user))
        end)
        user
      end

      IRC.Connection.publish_event(network, %{type: :connect, user_id: user.id, account_id: user.account})
      :ok
    else
      :error
    end
  end

  def joined(c, s), do: joined(c,s,[])

  def joined(channel, sender=%{nick: nick, user: uname, host: host}, privileges, touch \\ true) do
    privileges = if IRC.admin?(sender) do
      privileges ++ [:admin]
    else privileges end
    user = if user = find_by_nick(sender.network, nick) do
      %User{user | username: uname, host: host, privileges: Map.put(user.privileges || %{}, channel, privileges)}
    else
      user = %User{id: IRC.UserTrack.Id.large_id, network: sender.network, nick: nick, username: uname, host: host, privileges: %{channel => privileges}}

      account = Nola.Account.lookup(user).id
      user = %User{user | account: account}
    end
    user = touch_struct(user, channel)

    if touch && user.account do
      IRC.Membership.touch(user.account, sender.network, channel)
    end

    Storage.op(fn(ets) ->
      :ets.insert(ets, User.to_tuple(user))
    end)

    IRC.Connection.publish_event({sender.network, channel}, %{type: :join, user_id: user.id, account_id: user.account})

    user
  end

  #def joined(network, channel, nick, privileges) do
  #  user = if user = find_by_nick(network, 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 messaged(%IRC.Message{network: network, account: account, channel: chan, sender: %{nick: nick}} = m) do
    {user, account} = if user = find_by_nick(network, nick) do
      {touch_struct(user, chan), account || Nola.Account.lookup(user)}
    else
      user = %User{network: network, nick: nick, privileges: %{}}
      account = Nola.Account.lookup(user)
      {%User{user | account: account.id}, account}
    end
    Storage.insert(User.to_tuple(user))
    if chan, do: IRC.Membership.touch(account, network, chan)
    if !m.account do
      {:ok, %IRC.Message{m | account: account}}
    else
      :ok
    end
  end

  def renamed(network, old_nick, new_nick) do
    if user = find_by_nick(network, old_nick) do
      old_account = Nola.Account.lookup(user)
      user = %User{user | nick: new_nick, nicks: [old_nick|user.nicks]}
      account = Nola.Account.lookup(user, false) || old_account
      user = %User{user | nick: new_nick, account: account.id, nicks: [old_nick|user.nicks]}
      Storage.insert(User.to_tuple(user))
      channels = for {channel, _} <- user.privileges, do: channel
      IRC.Connection.publish_event(network, %{type: :nick, user_id: user.id, account_id: account.id, nick: new_nick, old_nick: old_nick})
    end
  end

  def change_privileges(network, channel, nick, {add, remove}) do
    if user = find_by_nick(network, 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))
      IRC.Connection.publish_event({network, channel}, %{type: :privileges, user_id: user.id, account_id: user.account, added: add, removed: remove})
    end
  end

  # XXX: Reason
  def parted(channel, %{network: network, nick: nick}) do
    parted(network, channel, nick)
  end

  def parted(network, channel, nick) do
    if user = find_by_nick(network, nick) do
      if user.account do
        IRC.Membership.touch(user.account, network, channel)
      end

      privs = Map.delete(user.privileges, channel)
      lasts = Map.delete(user.last_active, channel)
      if Enum.count(privs) > 0 do
        user = %User{user | privileges: privs}
        Storage.insert(User.to_tuple(user))
        IRC.Connection.publish_event({network, channel}, %{type: :part, user_id: user.id, account_id: user.account, reason: nil})
      else
        IRC.Connection.publish_event(network, %{type: :quit, user_id: user.id, account_id: user.account, reason: "Left all known channels"})
        Storage.delete(user.id)
      end
    end
  end

  def quitted(sender, reason) do
    if user = find_by_nick(sender.network, sender.nick) do
      if user.account do
        for {channel, _} <- user.privileges do
          IRC.Membership.touch(user.account, sender.network, channel)
        end
        IRC.Connection.publish_event(sender.network, %{type: :quit, user_id: user.id, account_id: user.account, reason: reason})
      end
      Storage.delete(user.id)
    end
  end

  defp touch_struct(user = %User{last_active: last_active}, channel) do
    now = NaiveDateTime.utc_now()
    last_active = last_active
    |> Map.put(channel, now)
    |> Map.put(nil, now)
    %User{user | last_active: last_active}
  end

  defp userchans(%{privileges: privileges}) do
    for({chan, _} <- privileges, do: chan)
  end
end