defmodule Nola.UserTrack do @moduledoc """ User Track DB & Utilities """ @ets Nola.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 || Nola.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: Nola.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 Nola.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: Nola.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 Nola.Membership.touch(user.account, sender.network, channel) end Storage.op(fn(ets) -> :ets.insert(ets, User.to_tuple(user)) end) Nola.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(%Nola.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: Nola.Membership.touch(account, network, chan) if !m.account do {:ok, %Nola.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 Nola.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)) Nola.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 Nola.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)) Nola.Irc.Connection.publish_event({network, channel}, %{type: :part, user_id: user.id, account_id: user.account, reason: nil}) else Nola.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 Nola.Membership.touch(user.account, sender.network, channel) end Nola.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