diff options
Diffstat (limited to 'lib/irc/account.ex')
-rw-r--r-- | lib/irc/account.ex | 451 |
1 files changed, 0 insertions, 451 deletions
diff --git a/lib/irc/account.ex b/lib/irc/account.ex deleted file mode 100644 index 45680f8..0000000 --- a/lib/irc/account.ex +++ /dev/null @@ -1,451 +0,0 @@ -defmodule IRC.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(["#IRC.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 - - defmodule AccountPlugin do - @moduledoc """ - # Account - - * **account** Get current account id and token - * **auth `<account-id>` `<token>`** Authenticate and link the current nickname to an account - * **auth** list authentications methods - * **whoami** list currently authenticated users - * **web** get a one-time login link to web - * **enable-telegram** Link a Telegram account - * **enable-sms** Link a SMS number - * **enable-untappd** Link a Untappd account - * **set-name** set account name - * **setusermeta puppet-nick `<nick>`** Set puppet IRC nickname - """ - - def irc_doc, do: @moduledoc - def start_link(), do: GenServer.start_link(__MODULE__, [], name: __MODULE__) - def init(_) do - {:ok, _} = Registry.register(IRC.PubSub, "messages:private", []) - {:ok, nil} - end - - def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "help"}}, state) do - text = [ - "account: show current account and auth token", - "auth: show authentications methods", - "whoami: list authenticated users", - "set-name <name>: set account name", - "web: login to web", - "enable-sms | disable-sms: enable/change or disable sms", - "enable-telegram: link/change telegram", - "enable-untappd: link untappd account", - "getmeta: show meta datas", - "setusermeta: set user meta", - ] - m.replyfun.(text) - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "auth"}}, state) do - spec = [{{:"$1", :"$2"}, [{:==, :"$2", {:const, account.id}}], [:"$1"]}] - predicates = :dets.select(IRC.Account.file("predicates"), spec) - text = for {net, {key, value}} <- predicates, do: "#{net}: #{to_string(key)}: #{value}" - m.replyfun.(text) - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "whoami"}}, state) do - users = for user <- IRC.UserTrack.find_by_account(m.account) do - chans = Enum.map(user.privileges, fn({chan, _}) -> chan end) - |> Enum.join(" ") - "#{user.network} - #{user.nick}!#{user.username}@#{user.host} - #{chans}" - end - m.replyfun.(users) - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "account"}}, state) do - account = IRC.Account.lookup(m.sender) - text = ["Account Id: #{account.id}", - "Authenticate to this account from another network: \"auth #{account.id} #{account.token}\" to the other bot!"] - m.replyfun.(text) - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{sender: sender, text: "auth"<>_}}, state) do - #account = IRC.Account.lookup(m.sender) - case String.split(m.text, " ") do - ["auth", id, token] -> - join_account(m, id, token) - _ -> - m.replyfun.("Invalid parameters") - end - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{account: account, text: "set-name "<>name}}, state) do - IRC.Account.update_account_name(account, name) - m.replyfun.("Name changed: #{name}") - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{text: "disable-sms"}}, state) do - if IRC.Account.get_meta(m.account, "sms-number") do - IRC.Account.delete_meta(m.account, "sms-number") - m.replfyun.("SMS disabled.") - else - m.replyfun.("SMS already disabled.") - end - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{text: "web"}}, state) do - auth_url = Untappd.auth_url() - login_url = Nola.AuthToken.new_url(m.account.id, nil) - m.replyfun.("-> " <> login_url) - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{text: "enable-sms"}}, state) do - code = String.downcase(EntropyString.small_id()) - IRC.Account.put_meta(m.account, "sms-validation-code", code) - IRC.Account.put_meta(m.account, "sms-validation-target", m.network) - number = Nola.IRC.SmsPlugin.my_number() - text = "To enable or change your number for SMS messaging, please send:" - <> " \"enable #{code}\" to #{number}" - m.replyfun.(text) - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{text: "enable-telegram"}}, state) do - code = String.downcase(EntropyString.small_id()) - IRC.Account.delete_meta(m.account, "telegram-id") - IRC.Account.put_meta(m.account, "telegram-validation-code", code) - IRC.Account.put_meta(m.account, "telegram-validation-target", m.network) - text = "To enable or change your number for telegram messaging, please open #{Nola.Telegram.my_path()} and send:" - <> " \"/enable #{code}\"" - m.replyfun.(text) - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{text: "enable-untappd"}}, state) do - auth_url = Untappd.auth_url() - login_url = Nola.AuthToken.new_url(m.account.id, {:external_redirect, auth_url}) - m.replyfun.(["To link your Untappd account, open this URL:", login_url]) - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{text: "getmeta"<>_}}, state) do - result = case String.split(m.text, " ") do - ["getmeta"] -> - for {k, v} <- IRC.Account.get_all_meta(m.account) do - case k do - "u:"<>key -> "(user) #{key}: #{v}" - key -> "#{key}: #{v}" - end - end - ["getmeta", key] -> - value = IRC.Account.get_meta(m.account, key) - text = if value do - "#{key}: #{value}" - else - "#{key} is not defined" - end - _ -> - "usage: getmeta [key]" - end - m.replyfun.(result) - {:noreply, state} - end - - def handle_info({:irc, :text, m = %IRC.Message{text: "setusermet"<>_}}, state) do - result = case String.split(m.text, " ") do - ["setusermeta", key, value] -> - IRC.Account.put_user_meta(m.account, key, value) - "ok" - _ -> - "usage: setusermeta <key> <value>" - end - m.replyfun.(result) - {:noreply, state} - end - - def handle_info(_, state) do - {:noreply, state} - end - - defp join_account(m, id, token) do - old_account = IRC.Account.lookup(m.sender) - new_account = IRC.Account.get(id) - if new_account && token == new_account.token do - case IRC.Account.merge_account(old_account.id, new_account.id) do - :ok -> - if old_account.id == new_account.id do - m.replyfun.("Already authenticated, but hello") - else - m.replyfun.("Accounts merged!") - end - _ -> m.replyfun.("Something failed :(") - end - else - m.replyfun.("Invalid token") - end - end - - end - -end |