summaryrefslogtreecommitdiff
path: root/lib/nola/account.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/nola/account.ex')
-rw-r--r--lib/nola/account.ex263
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