diff --git a/lib/irc/account.ex b/lib/irc/account.ex
index 56019df..28b5141 100644
--- a/lib/irc/account.ex
+++ b/lib/irc/account.ex
@@ -1,441 +1,445 @@
 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(LSG.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
 
   defp 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
-    * **enable-sms** Link a SMS number
+    * **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 = LSG.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 = LSG.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 #{LSG.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 = LSG.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