summaryrefslogblamecommitdiff
path: root/lib/irc/user_track.ex
blob: 5e1c3a337fef17b7c0a88ef1b8cdd797d599581a (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, %{}}]

    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}
    end

    #tuple size: 11
    def from_tuple({id, network, account, _downcased_nick, nick, nicks, username, host, realname, privs, last_active}) 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}
    end
  end

  def find_by_account(%IRC.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}}
       ], [:"$_"]}
    ]
    for obj <- :ets.select(@ets, spec), do: User.from_tuple(obj)
  end

  def find_by_account(network, nil) do
    nil
  end

  def find_by_account(network, %IRC.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 | _] ->
        results
        |> Enum.sort_by(fn({_, _, _, _, _, _, _, _, _, _, actives}) ->
          Map.get(actives, nil)
        end, {:desc, NaiveDateTime})
        |> List.first
        |> User.from_tuple()
      _ -> 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}) ->
      Storage.op(fn(ets) ->
        :ets.insert(@ets, {id, net, new_id, downcased_nick, nick, nicks, username, host, realname, privs, active})
      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(sender = %{nick: nick}) do
  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{network: sender.network, nick: nick, username: uname, host: host, privileges: %{channel => privileges}}

      account = IRC.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)
  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 || IRC.Account.lookup(user)}
    else
      user = %User{network: network, nick: nick, privileges: %{}}
      account = IRC.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 = IRC.Account.lookup(user)
      user = %User{user | nick: new_nick, nicks: [old_nick|user.nicks]}
      account = IRC.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))
    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))
    end
  end

  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))
      else
        Storage.delete(user.id)
      end
    end
  end

  def quitted(sender) 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
      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

end