diff options
Diffstat (limited to 'lib/nola/account.ex')
-rw-r--r-- | lib/nola/account.ex | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/lib/nola/account.ex b/lib/nola/account.ex new file mode 100644 index 0000000..cd424ef --- /dev/null +++ b/lib/nola/account.ex @@ -0,0 +1,263 @@ +defmodule Nola.Account do + alias IRC.UserTrack.User + + @moduledoc """ + Account registry.... + + Maps a network predicate: + * `{net, {:nick, nickname}}` + * `{net, {:account, account}}` + * `{net, {:mask, user@host}}` + to an unique identifier, that can be shared over multiple networks. + + If a predicate cannot be found for an existing account, a new account will be made in the database. + + To link two existing accounts from different network onto a different one, a merge operation is provided. + + """ + + # FIXME: Ensure uniqueness of name? + + @derive {Poison.Encoder, except: [:token]} + defstruct [:id, :name, :token] + @type t :: %__MODULE__{id: id(), name: String.t()} + @type id :: String.t() + + defimpl Inspect, for: __MODULE__ do + import Inspect.Algebra + + def inspect(%{id: id, name: name}, opts) do + concat(["#Nola.Account[", id, " ", name, "]"]) + end + end + + def file(base) do + to_charlist(Nola.data_path() <> "/account_#{base}.dets") + end + + defp from_struct(%__MODULE__{id: id, name: name, token: token}) do + {id, name, token} + end + + defp from_tuple({id, name, token}) do + %__MODULE__{id: id, name: name, token: token} + end + + def start_link() do + GenServer.start_link(__MODULE__, [], [name: __MODULE__]) + end + + def init(_) do + {:ok, accounts} = :dets.open_file(file("db"), []) + {:ok, meta} = :dets.open_file(file("meta"), []) + {:ok, predicates} = :dets.open_file(file("predicates"), [{:type, :set}]) + {:ok, %{accounts: accounts, meta: meta, predicates: predicates}} + end + + def get(id) do + case :dets.lookup(file("db"), id) do + [account] -> from_tuple(account) + _ -> nil + end + end + + def get_by_name(name) do + spec = [{{:_, :"$1", :_}, [{:==, :"$1", {:const, name}}], [:"$_"]}] + case :dets.select(file("db"), spec) do + [account] -> from_tuple(account) + _ -> nil + end + end + + def get_meta(%__MODULE__{id: id}, key, default \\ nil) do + case :dets.lookup(file("meta"), {id, key}) do + [{_, value}] -> (value || default) + _ -> default + end + end + + @spec find_meta_accounts(String.t()) :: [{account :: t(), value :: String.t()}, ...] + @doc "Find all accounts that have a meta of `key`." + def find_meta_accounts(key) do + spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$2", {:const, key}}], [{{:"$1", :"$3"}}]}] + for {id, val} <- :dets.select(file("meta"), spec), do: {get(id), val} + end + + @doc "Find an account given a specific meta `key` and `value`." + @spec find_meta_account(String.t(), String.t()) :: t() | nil + def find_meta_account(key, value) do + #spec = [{{{:"$1", :"$2"}, :"$3"}, [:andalso, {:==, :"$2", {:const, key}}, {:==, :"$3", {:const, value}}], [:"$1"]}] + spec = [{{{:"$1", :"$2"}, :"$3"}, [{:andalso, {:==, :"$2", {:const, key}}, {:==, {:const, value}, :"$3"}}], [:"$1"]}] + case :dets.select(file("meta"), spec) do + [id] -> get(id) + _ -> nil + end + end + + def get_all_meta(%__MODULE__{id: id}) do + spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$1", {:const, id}}], [{{:"$2", :"$3"}}]}] + :dets.select(file("meta"), spec) + end + + def put_user_meta(account = %__MODULE__{}, key, value) do + put_meta(account, "u:"<>key, value) + end + + def put_meta(%__MODULE__{id: id}, key, value) do + :dets.insert(file("meta"), {{id, key}, value}) + end + + def delete_meta(%__MODULE__{id: id}, key) do + :dets.delete(file("meta"), {id, key}) + end + + def all_accounts() do + :dets.traverse(file("db"), fn(obj) -> {:continue, from_tuple(obj)} end) + end + + def all_predicates() do + :dets.traverse(file("predicates"), fn(obj) -> {:continue, obj} end) + end + + def all_meta() do + :dets.traverse(file("meta"), fn(obj) -> {:continue, obj} end) + end + + def merge_account(old_id, new_id) do + if old_id != new_id do + spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, old_id}}], [:"$1"]}] + predicates = :dets.select(file("predicates"), spec) + for pred <- predicates, do: :ok = :dets.insert(file("predicates"), {pred, new_id}) + spec = [{{{:"$1", :"$2"}, :"$3"}, [{:==, :"$1", {:const, old_id}}], [{{:"$2", :"$3"}}]}] + metas = :dets.select(file("meta"), spec) + for {k,v} <- metas do + :dets.delete(file("meta"), {{old_id, k}}) + :ok = :dets.insert(file("meta"), {{new_id, k}, v}) + end + :dets.delete(file("db"), old_id) + IRC.Membership.merge_account(old_id, new_id) + IRC.UserTrack.merge_account(old_id, new_id) + IRC.Connection.dispatch("account", {:account_change, old_id, new_id}) + IRC.Connection.dispatch("conn", {:account_change, old_id, new_id}) + end + :ok + end + + @doc "Find an account by a logged in user" + def find_by_nick(network, nick) do + do_lookup(%ExIRC.SenderInfo{nick: nick, network: network}, false) + end + + @doc "Always find an account by nickname, even if offline. Uses predicates and then account name." + def find_always_by_nick(network, chan, nick) do + with \ + nil <- find_by_nick(network, nick), + nil <- do_lookup(%User{network: network, nick: nick}, false), + nil <- get_by_name(nick) + do + nil + else + %__MODULE__{} = account -> + memberships = IRC.Membership.of_account(account) + if Enum.any?(memberships, fn({net, ch}) -> (net == network) or (chan && chan == ch) end) do + account + else + nil + end + end + end + + def find(something) do + do_lookup(something, false) + end + + def lookup(something, make_default \\ true) do + account = do_lookup(something, make_default) + if account && Map.get(something, :nick) do + IRC.Connection.dispatch("account", {:account_auth, Map.get(something, :nick), account.id}) + end + account + end + + def handle_info(_, state) do + {:noreply, state} + end + + def handle_cast(_, state) do + {:noreply, state} + end + + def handle_call(_, _, state) do + {:noreply, state} + end + + def terminate(_, state) do + for {_, dets} <- state do + :dets.sync(dets) + :dets.close(dets) + end + end + + defp do_lookup(message = %IRC.Message{account: account_id}, make_default) when is_binary(account_id) do + get(account_id) + end + + defp do_lookup(sender = %ExIRC.Who{}, make_default) do + if user = IRC.UserTrack.find_by_nick(sender) do + lookup(user, make_default) + else + #FIXME this will never work with continued lookup by other methods as Who isn't compatible + lookup_by_nick(sender, :dets.lookup(file("predicates"), {sender.network,{:nick, sender.nick}}), make_default) + end + end + + defp do_lookup(sender = %ExIRC.SenderInfo{}, make_default) do + lookup(IRC.UserTrack.find_by_nick(sender), make_default) + end + + defp do_lookup(user = %User{account: id}, make_default) when is_binary(id) do + get(id) + end + + defp do_lookup(user = %User{network: server, nick: nick}, make_default) do + lookup_by_nick(user, :dets.lookup(file("predicates"), {server,{:nick, nick}}), make_default) + end + + defp do_lookup(nil, _) do + nil + end + + defp lookup_by_nick(_, [{_, id}], _make_default) do + get(id) + end + + defp lookup_by_nick(user, _, make_default) do + #authenticate_by_host(user) + if make_default, do: new_account(user), else: nil + end + + def new_account(nick) do + id = EntropyString.large_id() + :dets.insert(file("db"), {id, nick, EntropyString.token()}) + get(id) + end + + def new_account(%{nick: nick, network: server}) do + id = EntropyString.large_id() + :dets.insert(file("db"), {id, nick, EntropyString.token()}) + :dets.insert(file("predicates"), {{server, {:nick, nick}}, id}) + get(id) + end + + def update_account_name(account = %__MODULE__{id: id}, name) do + account = %__MODULE__{account | name: name} + :dets.insert(file("db"), from_struct(account)) + get(id) + end + + def get_predicates(%__MODULE__{} = account) do + spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}] + :dets.select(file("predicates"), spec) + end + +end |